Merge "[ColorScheme] Make color scheme immutable" into androidx-main
diff --git a/activity/activity/lint-baseline.xml b/activity/activity/lint-baseline.xml
index c33c4d5..6c10a57 100644
--- a/activity/activity/lint-baseline.xml
+++ b/activity/activity/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="cli" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="NewApi"
@@ -28,40 +28,4 @@
             file="src/main/java/androidx/activity/result/PickVisualMediaRequest.kt"/>
     </issue>
 
-    <issue
-        id="NewApi"
-        message="Field requires API level 19 (current min is 14): `VideoOnly`"
-        errorLine1="            ActivityResultContracts.PickVisualMedia.VideoOnly"
-        errorLine2="                                                    ~~~~~~~~~">
-        <location
-            file="src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 19 (current min is 14): `VideoOnly`"
-        errorLine1="        assertThat(request.mediaType).isEqualTo(ActivityResultContracts.PickVisualMedia.VideoOnly)"
-        errorLine2="                                                                                        ~~~~~~~~~">
-        <location
-            file="src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 19 (current min is 14): `VideoOnly`"
-        errorLine1="        val request = PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly)"
-        errorLine2="                                                                                     ~~~~~~~~~">
-        <location
-            file="src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 19 (current min is 14): `VideoOnly`"
-        errorLine1="        assertThat(request.mediaType).isEqualTo(ActivityResultContracts.PickVisualMedia.VideoOnly)"
-        errorLine2="                                                                                        ~~~~~~~~~">
-        <location
-            file="src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt"/>
-    </issue>
-
 </issues>
diff --git a/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml b/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
index ac48471..7155089 100644
--- a/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
+++ b/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-alpha05" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-alpha05)" variant="all" version="8.0.0-alpha05">
+<issues format="6" by="lint 8.2.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-beta01)" variant="all" version="8.2.0-beta01">
 
     <issue
         id="ExperimentalAnnotationRetention"
@@ -327,6 +327,15 @@
 
     <issue
         id="UnsafeOptInUsageError"
+        message="This declaration is opt-in and its usage should be marked with `@sample.optin.ExperimentalJavaAnnotation` or `@OptIn(markerClass = sample.optin.ExperimentalJavaAnnotation.class)`"
+        errorLine1="        player.accessor"
+        errorLine2="               ~~~~~~~~">
+        <location
+            file="src/main/java/sample/optin/RegressionTestKotlin298322402.kt"/>
+    </issue>
+
+    <issue
+        id="UnsafeOptInUsageError"
         message="This declaration is opt-in and its usage should be marked with `@sample.kotlin.ExperimentalJavaAnnotation` or `@OptIn(markerClass = sample.kotlin.ExperimentalJavaAnnotation.class)`"
         errorLine1="        AnnotatedJavaClass experimentalObject = new AnnotatedJavaClass();"
         errorLine2="                                                ~~~~~~~~~~~~~~~~~~~~~~~~">
diff --git a/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/optin/AnnotatedJavaMembers.java b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/optin/AnnotatedJavaMembers.java
index 82955e3..88faa0f 100644
--- a/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/optin/AnnotatedJavaMembers.java
+++ b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/optin/AnnotatedJavaMembers.java
@@ -38,6 +38,11 @@
         return -1;
     }
 
+    @ExperimentalJavaAnnotation
+    public int getAccessor() {
+        return -1;
+    }
+
     public int getFieldWithSetMarker() {
         return mFieldWithSetMarker;
     }
diff --git a/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/optin/RegressionTestKotlin298322402.kt b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/optin/RegressionTestKotlin298322402.kt
new file mode 100644
index 0000000..5a8eb40
--- /dev/null
+++ b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/optin/RegressionTestKotlin298322402.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+@file:Suppress("unused")
+
+package sample.optin
+
+internal class RegressionTestKotlin298322402 {
+    fun testMethod(player: AnnotatedJavaMembers) {
+        player.accessor
+    }
+}
diff --git a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
index ef20bed..54cb668 100644
--- a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
+++ b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
@@ -35,12 +35,14 @@
 import com.intellij.psi.PsiAnnotation
 import com.intellij.psi.PsiClass
 import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiComment
 import com.intellij.psi.PsiElement
 import com.intellij.psi.PsiField
 import com.intellij.psi.PsiMethod
 import com.intellij.psi.PsiModifier
 import com.intellij.psi.PsiModifierListOwner
 import com.intellij.psi.PsiPackage
+import com.intellij.psi.PsiWhiteSpace
 import com.intellij.psi.impl.source.PsiClassReferenceType
 import com.intellij.psi.search.GlobalSearchScope
 import com.intellij.psi.util.PsiTreeUtil
@@ -643,24 +645,38 @@
             else -> throw IllegalArgumentException("Unsupported element type")
         }
 
-        // If the element has a modifier list, e.g. not an anonymous class or lambda, then place the
-        // insertion point at the beginning of the modifiers list. This ensures that we don't insert
-        // the annotation in an invalid position, such as after the "public" or "fun" keywords. We
-        // also don't want to place it on the element range itself, since that would place it before
-        // the comments.
-        val elementLocation = context.getLocation(element.modifierList ?: element as UElement)
+        // If the element can include modifiers, e.g. not an anonymous class or lambda, find the
+        // where the list should start. This ensures that we don't insert the annotation in an
+        // invalid position, such as after the `public` or `fun` keywords. We also don't want to
+        // place it on the element range itself, since that would place it before the comments.
+        val elementSourcePsi = element.sourcePsi
+        val elementForInsert = if (elementSourcePsi is PsiModifierListOwner) {
+            elementSourcePsi.asIterable().firstOrNull { child ->
+                child !is PsiWhiteSpace && child !is PsiComment
+            } ?: throw IllegalArgumentException("Failed to locate element declaration")
+        } else {
+            element
+        }
 
         return fix()
-            .replace()
             .name("Add '$annotation' annotation to $elementLabel")
-            .range(elementLocation)
-            .beginning()
-            .shortenNames()
-            .with("$annotation ")
+            .annotate(annotation, true)
+            .range(context.getLocation(elementForInsert))
             .build()
     }
 
     /**
+     * Returns an iterable of child elements.
+     */
+    private fun PsiElement.asIterable(): Iterable<PsiElement> = object : Iterable<PsiElement> {
+        override fun iterator(): Iterator<PsiElement> = object : Iterator<PsiElement> {
+            private var current = firstChild
+            override fun hasNext(): Boolean = current != null
+            override fun next(): PsiElement = current.apply { current = nextSibling }
+        }
+    }
+
+    /**
      * Reports an issue and trims indentation on the [message].
      */
     private fun report(
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt
index 25567ce..21e170e 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt
@@ -72,52 +72,40 @@
         val expectedFix = """
 Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 25: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTime.class)' annotation to 'getDateUnsafe':
 @@ -24 +24
--     int getDateUnsafe() {
-+     @androidx.annotation.OptIn(markerClass = ExperimentalDateTime.class) int getDateUnsafe() {
++     @androidx.annotation.OptIn(markerClass = ExperimentalDateTime.class)
 Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 25: Add '@sample.experimental.ExperimentalDateTime' annotation to 'getDateUnsafe':
 @@ -24 +24
--     int getDateUnsafe() {
-+     @ExperimentalDateTime int getDateUnsafe() {
++     @ExperimentalDateTime
 Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 25: Add '@sample.experimental.ExperimentalDateTime' annotation to containing class 'UseJavaExperimentalFromJava':
 @@ -19 +19
-- @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
-+ @ExperimentalDateTime @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
++ @ExperimentalDateTime
 Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 26: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTime.class)' annotation to 'getDateUnsafe':
 @@ -24 +24
--     int getDateUnsafe() {
-+     @androidx.annotation.OptIn(markerClass = ExperimentalDateTime.class) int getDateUnsafe() {
++     @androidx.annotation.OptIn(markerClass = ExperimentalDateTime.class)
 Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 26: Add '@sample.experimental.ExperimentalDateTime' annotation to 'getDateUnsafe':
 @@ -24 +24
--     int getDateUnsafe() {
-+     @ExperimentalDateTime int getDateUnsafe() {
++     @ExperimentalDateTime
 Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 26: Add '@sample.experimental.ExperimentalDateTime' annotation to containing class 'UseJavaExperimentalFromJava':
 @@ -19 +19
-- @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
-+ @ExperimentalDateTime @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
++ @ExperimentalDateTime
 Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 53: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalLocation.class)' annotation to 'getDateExperimentalLocationUnsafe':
 @@ -50 +50
--     @ExperimentalDateTime
-+     @androidx.annotation.OptIn(markerClass = ExperimentalLocation.class) @ExperimentalDateTime
++     @androidx.annotation.OptIn(markerClass = ExperimentalLocation.class)
 Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 53: Add '@sample.experimental.ExperimentalLocation' annotation to 'getDateExperimentalLocationUnsafe':
 @@ -50 +50
--     @ExperimentalDateTime
-+     @ExperimentalLocation @ExperimentalDateTime
++     @ExperimentalLocation
 Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 53: Add '@sample.experimental.ExperimentalLocation' annotation to containing class 'UseJavaExperimentalFromJava':
 @@ -19 +19
-- @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
-+ @ExperimentalLocation @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
++ @ExperimentalLocation
 Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 54: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalLocation.class)' annotation to 'getDateExperimentalLocationUnsafe':
 @@ -50 +50
--     @ExperimentalDateTime
-+     @androidx.annotation.OptIn(markerClass = ExperimentalLocation.class) @ExperimentalDateTime
++     @androidx.annotation.OptIn(markerClass = ExperimentalLocation.class)
 Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 54: Add '@sample.experimental.ExperimentalLocation' annotation to 'getDateExperimentalLocationUnsafe':
 @@ -50 +50
--     @ExperimentalDateTime
-+     @ExperimentalLocation @ExperimentalDateTime
++     @ExperimentalLocation
 Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 54: Add '@sample.experimental.ExperimentalLocation' annotation to containing class 'UseJavaExperimentalFromJava':
 @@ -19 +19
-- @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
-+ @ExperimentalLocation @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
++ @ExperimentalLocation
         """.trimIndent()
         /* ktlint-enable max-line-length */
 
@@ -189,53 +177,41 @@
 
         val expectedFix = """
 Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 29: Add '@androidx.annotation.OptIn(sample.experimental.ExperimentalDateTime::class)' annotation to 'getDateUnsafe':
-@@ -1 +1
-- /*
-+ @androidx.annotation.OptIn(ExperimentalDateTime::class) /*
+@@ -25 +25
++     @androidx.annotation.OptIn(ExperimentalDateTime::class)
 Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 29: Add '@sample.experimental.ExperimentalDateTime' annotation to 'getDateUnsafe':
-@@ -1 +1
-- /*
-+ @ExperimentalDateTime /*
+@@ -25 +25
++     @ExperimentalDateTime
 Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 29: Add '@sample.experimental.ExperimentalDateTime' annotation to containing class 'UseJavaExperimentalFromKt':
-@@ -23 +23
-- @Suppress("unused", "MemberVisibilityCanBePrivate")
-+ @ExperimentalDateTime @Suppress("unused", "MemberVisibilityCanBePrivate")
+@@ -1 +1
++ @ExperimentalDateTime
 Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 30: Add '@androidx.annotation.OptIn(sample.experimental.ExperimentalDateTime::class)' annotation to 'getDateUnsafe':
-@@ -1 +1
-- /*
-+ @androidx.annotation.OptIn(ExperimentalDateTime::class) /*
+@@ -25 +25
++     @androidx.annotation.OptIn(ExperimentalDateTime::class)
 Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 30: Add '@sample.experimental.ExperimentalDateTime' annotation to 'getDateUnsafe':
-@@ -1 +1
-- /*
-+ @ExperimentalDateTime /*
+@@ -25 +25
++     @ExperimentalDateTime
 Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 30: Add '@sample.experimental.ExperimentalDateTime' annotation to containing class 'UseJavaExperimentalFromKt':
-@@ -23 +23
-- @Suppress("unused", "MemberVisibilityCanBePrivate")
-+ @ExperimentalDateTime @Suppress("unused", "MemberVisibilityCanBePrivate")
+@@ -1 +1
++ @ExperimentalDateTime
 Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 57: Add '@androidx.annotation.OptIn(sample.experimental.ExperimentalLocation::class)' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -54 +54
--     @ExperimentalDateTime
-+     @androidx.annotation.OptIn(ExperimentalLocation::class) @ExperimentalDateTime
+@@ -51 +51
++     @androidx.annotation.OptIn(ExperimentalLocation::class)
 Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 57: Add '@sample.experimental.ExperimentalLocation' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -54 +54
--     @ExperimentalDateTime
-+     @ExperimentalLocation @ExperimentalDateTime
+@@ -51 +51
++     @ExperimentalLocation
 Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 57: Add '@sample.experimental.ExperimentalLocation' annotation to containing class 'UseJavaExperimentalFromKt':
-@@ -23 +23
-- @Suppress("unused", "MemberVisibilityCanBePrivate")
-+ @ExperimentalLocation @Suppress("unused", "MemberVisibilityCanBePrivate")
+@@ -1 +1
++ @ExperimentalLocation
 Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 58: Add '@androidx.annotation.OptIn(sample.experimental.ExperimentalLocation::class)' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -54 +54
--     @ExperimentalDateTime
-+     @androidx.annotation.OptIn(ExperimentalLocation::class) @ExperimentalDateTime
+@@ -51 +51
++     @androidx.annotation.OptIn(ExperimentalLocation::class)
 Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 58: Add '@sample.experimental.ExperimentalLocation' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -54 +54
--     @ExperimentalDateTime
-+     @ExperimentalLocation @ExperimentalDateTime
+@@ -51 +51
++     @ExperimentalLocation
 Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 58: Add '@sample.experimental.ExperimentalLocation' annotation to containing class 'UseJavaExperimentalFromKt':
-@@ -23 +23
-- @Suppress("unused", "MemberVisibilityCanBePrivate")
-+ @ExperimentalLocation @Suppress("unused", "MemberVisibilityCanBePrivate")
+@@ -1 +1
++ @ExperimentalLocation
         """.trimIndent()
         /* ktlint-enable max-line-length */
 
@@ -286,100 +262,76 @@
         val expectedFix = """
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 25: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTimeKt.class)' annotation to 'getDateUnsafe':
 @@ -24 +24
--     int getDateUnsafe() {
-+     @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) int getDateUnsafe() {
++     @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class)
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 25: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to 'getDateUnsafe':
 @@ -24 +24
--     int getDateUnsafe() {
-+     @ExperimentalDateTimeKt int getDateUnsafe() {
++     @ExperimentalDateTimeKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 25: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to containing class 'UseKtExperimentalFromJava':
 @@ -19 +19
-- @SuppressWarnings({"unused", "WeakerAccess"})
-+ @ExperimentalDateTimeKt @SuppressWarnings({"unused", "WeakerAccess"})
++ @ExperimentalDateTimeKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 26: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTimeKt.class)' annotation to 'getDateUnsafe':
 @@ -24 +24
--     int getDateUnsafe() {
-+     @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) int getDateUnsafe() {
++     @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class)
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 26: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to 'getDateUnsafe':
 @@ -24 +24
--     int getDateUnsafe() {
-+     @ExperimentalDateTimeKt int getDateUnsafe() {
++     @ExperimentalDateTimeKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 26: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to containing class 'UseKtExperimentalFromJava':
 @@ -19 +19
-- @SuppressWarnings({"unused", "WeakerAccess"})
-+ @ExperimentalDateTimeKt @SuppressWarnings({"unused", "WeakerAccess"})
++ @ExperimentalDateTimeKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 54: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalLocationKt.class)' annotation to 'getDateExperimentalLocationUnsafe':
 @@ -51 +51
--     @ExperimentalDateTimeKt
-+     @androidx.annotation.OptIn(markerClass = ExperimentalLocationKt.class) @ExperimentalDateTimeKt
++     @androidx.annotation.OptIn(markerClass = ExperimentalLocationKt.class)
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 54: Add '@sample.experimental.ExperimentalLocationKt' annotation to 'getDateExperimentalLocationUnsafe':
 @@ -51 +51
--     @ExperimentalDateTimeKt
-+     @ExperimentalLocationKt @ExperimentalDateTimeKt
++     @ExperimentalLocationKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 54: Add '@sample.experimental.ExperimentalLocationKt' annotation to containing class 'UseKtExperimentalFromJava':
 @@ -19 +19
-- @SuppressWarnings({"unused", "WeakerAccess"})
-+ @ExperimentalLocationKt @SuppressWarnings({"unused", "WeakerAccess"})
++ @ExperimentalLocationKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 55: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalLocationKt.class)' annotation to 'getDateExperimentalLocationUnsafe':
 @@ -51 +51
--     @ExperimentalDateTimeKt
-+     @androidx.annotation.OptIn(markerClass = ExperimentalLocationKt.class) @ExperimentalDateTimeKt
++     @androidx.annotation.OptIn(markerClass = ExperimentalLocationKt.class)
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 55: Add '@sample.experimental.ExperimentalLocationKt' annotation to 'getDateExperimentalLocationUnsafe':
 @@ -51 +51
--     @ExperimentalDateTimeKt
-+     @ExperimentalLocationKt @ExperimentalDateTimeKt
++     @ExperimentalLocationKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 55: Add '@sample.experimental.ExperimentalLocationKt' annotation to containing class 'UseKtExperimentalFromJava':
 @@ -19 +19
-- @SuppressWarnings({"unused", "WeakerAccess"})
-+ @ExperimentalLocationKt @SuppressWarnings({"unused", "WeakerAccess"})
++ @ExperimentalLocationKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 88: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTimeKt.class)' annotation to 'regressionTestStaticUsage':
 @@ -87 +87
--     void regressionTestStaticUsage() {
-+     @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) void regressionTestStaticUsage() {
++     @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class)
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 88: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to 'regressionTestStaticUsage':
 @@ -87 +87
--     void regressionTestStaticUsage() {
-+     @ExperimentalDateTimeKt void regressionTestStaticUsage() {
++     @ExperimentalDateTimeKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 88: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to containing class 'UseKtExperimentalFromJava':
 @@ -19 +19
-- @SuppressWarnings({"unused", "WeakerAccess"})
-+ @ExperimentalDateTimeKt @SuppressWarnings({"unused", "WeakerAccess"})
++ @ExperimentalDateTimeKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 89: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTimeKt.class)' annotation to 'regressionTestStaticUsage':
 @@ -87 +87
--     void regressionTestStaticUsage() {
-+     @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) void regressionTestStaticUsage() {
++     @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class)
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 89: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to 'regressionTestStaticUsage':
 @@ -87 +87
--     void regressionTestStaticUsage() {
-+     @ExperimentalDateTimeKt void regressionTestStaticUsage() {
++     @ExperimentalDateTimeKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 89: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to containing class 'UseKtExperimentalFromJava':
 @@ -19 +19
-- @SuppressWarnings({"unused", "WeakerAccess"})
-+ @ExperimentalDateTimeKt @SuppressWarnings({"unused", "WeakerAccess"})
++ @ExperimentalDateTimeKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 96: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTimeKt.class)' annotation to 'regressionTestInlineUsage':
 @@ -95 +95
--     void regressionTestInlineUsage() {
-+     @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) void regressionTestInlineUsage() {
++     @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class)
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 96: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to 'regressionTestInlineUsage':
 @@ -95 +95
--     void regressionTestInlineUsage() {
-+     @ExperimentalDateTimeKt void regressionTestInlineUsage() {
++     @ExperimentalDateTimeKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 96: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to containing class 'UseKtExperimentalFromJava':
 @@ -19 +19
-- @SuppressWarnings({"unused", "WeakerAccess"})
-+ @ExperimentalDateTimeKt @SuppressWarnings({"unused", "WeakerAccess"})
++ @ExperimentalDateTimeKt
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 97: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTime.class)' annotation to 'regressionTestInlineUsage':
 @@ -95 +95
--     void regressionTestInlineUsage() {
-+     @androidx.annotation.OptIn(markerClass = ExperimentalDateTime.class) void regressionTestInlineUsage() {
++     @androidx.annotation.OptIn(markerClass = ExperimentalDateTime.class)
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 97: Add '@sample.experimental.ExperimentalDateTime' annotation to 'regressionTestInlineUsage':
 @@ -95 +95
--     void regressionTestInlineUsage() {
-+     @ExperimentalDateTime void regressionTestInlineUsage() {
++     @ExperimentalDateTime
 Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 97: Add '@sample.experimental.ExperimentalDateTime' annotation to containing class 'UseKtExperimentalFromJava':
 @@ -19 +19
-- @SuppressWarnings({"unused", "WeakerAccess"})
-+ @ExperimentalDateTime @SuppressWarnings({"unused", "WeakerAccess"})
++ @ExperimentalDateTime
         """.trimIndent()
         /* ktlint-enable max-line-length */
 
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt
index 99ca8e3..f26e382 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt
@@ -23,7 +23,6 @@
 import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
 import com.android.tools.lint.checks.infrastructure.TestLintResult
 import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
-import com.android.tools.lint.checks.infrastructure.TestMode
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -39,7 +38,6 @@
                 *testFiles
             )
             .issues(*ExperimentalDetector.ISSUES.toTypedArray())
-            .testModes(TestMode.PARTIAL)
             .run()
     }
 
@@ -444,6 +442,38 @@
         check(*input).expect(expected)
     }
 
+    @Test
+    fun regressionTestKotlin298322402() {
+        val input = arrayOf(
+            javaSample("sample.optin.ExperimentalJavaAnnotation"),
+            javaSample("sample.optin.AnnotatedJavaMembers"),
+            ktSample("sample.optin.RegressionTestKotlin298322402")
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/sample/optin/RegressionTestKotlin298322402.kt:22: Error: This declaration is opt-in and its usage should be marked with @sample.optin.ExperimentalJavaAnnotation or @OptIn(markerClass = sample.optin.ExperimentalJavaAnnotation.class) [UnsafeOptInUsageError]
+        player.accessor
+               ~~~~~~~~
+1 errors, 0 warnings
+        """.trimIndent()
+
+        val expectedFix = """
+Fix for src/sample/optin/RegressionTestKotlin298322402.kt line 22: Add '@androidx.annotation.OptIn(sample.optin.ExperimentalJavaAnnotation::class)' annotation to 'testMethod':
+@@ -21 +21
++     @androidx.annotation.OptIn(ExperimentalJavaAnnotation::class)
+Fix for src/sample/optin/RegressionTestKotlin298322402.kt line 22: Add '@sample.optin.ExperimentalJavaAnnotation' annotation to 'testMethod':
+@@ -21 +21
++     @ExperimentalJavaAnnotation
+Fix for src/sample/optin/RegressionTestKotlin298322402.kt line 22: Add '@sample.optin.ExperimentalJavaAnnotation' annotation to containing class 'RegressionTestKotlin298322402':
+@@ -1 +1
++ @ExperimentalJavaAnnotation
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input).expectFixDiffs(expectedFix).expect(expected)
+    }
+
     /* ktlint-disable max-line-length */
     companion object {
         /**
diff --git a/appcompat/appcompat/lint-baseline.xml b/appcompat/appcompat/lint-baseline.xml
index 35c6cb0..d2600ce 100644
--- a/appcompat/appcompat/lint-baseline.xml
+++ b/appcompat/appcompat/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-beta02" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta02)" variant="all" version="8.1.0-beta02">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="NewApi"
@@ -75,6 +75,24 @@
 
     <issue
         id="NewApi"
+        message="Call requires API level 17 (current min is 14): `setLocalNightMode`"
+        errorLine1="        delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES"
+        errorLine2="                 ~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/appcompat/app/NightModeLocalBeforeAttachBaseActivity.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Field requires API level 17 (current min is 14): `setLocalNightMode`"
+        errorLine1="        delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES"
+        errorLine2="                 ~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/appcompat/app/NightModeLocalBeforeAttachBaseActivity.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Call requires API level 17 (current min is 16): `android.content.res.Configuration#setLayoutDirection`"
         errorLine1="        configuration.setLayoutDirection(locale)"
         errorLine2="                      ~~~~~~~~~~~~~~~~~~">
@@ -93,6 +111,15 @@
 
     <issue
         id="NewApi"
+        message="Call requires API level 17 (current min is 16): `android.content.res.Configuration#getLayoutDirection`"
+        errorLine1="        assertEquals(TextUtils.getLayoutDirectionFromLocale(CUSTOM_LOCALE), config.layoutDirection)"
+        errorLine2="                                                                                   ~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/appcompat/app/NightModeRtlTestUtilsRegressionTestCase.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Call requires API level 17 (current min is 16): `android.text.TextUtils#getLayoutDirectionFromLocale`"
         errorLine1="        assertEquals(TextUtils.getLayoutDirectionFromLocale(CUSTOM_LOCALE), config.layoutDirection)"
         errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index c46429c..c2eac42 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -112,8 +112,10 @@
         mAppSearchDir = mTemporaryFolder.newFolder();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
@@ -498,7 +500,8 @@
         InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
-                mAppSearchDir, new UnlimitedLimitConfig(), new DefaultIcingOptionsConfig(),
+                mAppSearchDir, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()),
                 initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
 
         // Check recovery state
@@ -728,8 +731,10 @@
                 (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 mockVisibilityChecker);
@@ -902,8 +907,10 @@
                 (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 mockVisibilityChecker);
@@ -2861,8 +2868,10 @@
         // That document should be visible even from another instance.
         AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
@@ -2931,8 +2940,10 @@
         // Only the second document should be retrievable from another instance.
         AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
@@ -3008,8 +3019,10 @@
         // Only the second document should be retrievable from another instance.
         AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
@@ -3120,8 +3133,7 @@
         // Create a new mAppSearchImpl with a lower limit
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
-                mTemporaryFolder.newFolder(),
-                new LimitConfig() {
+                mTemporaryFolder.newFolder(), new AppSearchConfigImpl(new LimitConfig() {
                     @Override
                     public int getMaxDocumentSizeBytes() {
                         return 80;
@@ -3136,8 +3148,7 @@
                     public int getMaxSuggestionCount() {
                         return Integer.MAX_VALUE;
                     }
-                },
-                new DefaultIcingOptionsConfig(),
+                }, new DefaultIcingOptionsConfig()),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
@@ -3201,8 +3212,7 @@
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
         mAppSearchImpl = AppSearchImpl.create(
-                tempFolder,
-                new LimitConfig() {
+                tempFolder, new AppSearchConfigImpl(new LimitConfig() {
                     @Override
                     public int getMaxDocumentSizeBytes() {
                         return 80;
@@ -3217,8 +3227,7 @@
                     public int getMaxSuggestionCount() {
                         return Integer.MAX_VALUE;
                     }
-                },
-                new DefaultIcingOptionsConfig(),
+                }, new DefaultIcingOptionsConfig()),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
@@ -3260,8 +3269,7 @@
         // Close and reinitialize AppSearchImpl
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
-                tempFolder,
-                new LimitConfig() {
+                tempFolder, new AppSearchConfigImpl(new LimitConfig() {
                     @Override
                     public int getMaxDocumentSizeBytes() {
                         return 80;
@@ -3276,8 +3284,7 @@
                     public int getMaxSuggestionCount() {
                         return Integer.MAX_VALUE;
                     }
-                },
-                new DefaultIcingOptionsConfig(),
+                }, new DefaultIcingOptionsConfig()),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
@@ -3299,8 +3306,7 @@
         // Create a new mAppSearchImpl with a lower limit
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
-                mTemporaryFolder.newFolder(),
-                new LimitConfig() {
+                mTemporaryFolder.newFolder(), new AppSearchConfigImpl(new LimitConfig() {
                     @Override
                     public int getMaxDocumentSizeBytes() {
                         return Integer.MAX_VALUE;
@@ -3315,8 +3321,7 @@
                     public int getMaxSuggestionCount() {
                         return Integer.MAX_VALUE;
                     }
-                },
-                new DefaultIcingOptionsConfig(),
+                }, new DefaultIcingOptionsConfig()),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
@@ -3414,8 +3419,7 @@
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
         mAppSearchImpl = AppSearchImpl.create(
-                tempFolder,
-                new LimitConfig() {
+                tempFolder, new AppSearchConfigImpl(new LimitConfig() {
                     @Override
                     public int getMaxDocumentSizeBytes() {
                         return Integer.MAX_VALUE;
@@ -3430,8 +3434,7 @@
                     public int getMaxSuggestionCount() {
                         return Integer.MAX_VALUE;
                     }
-                },
-                new DefaultIcingOptionsConfig(),
+                }, new DefaultIcingOptionsConfig()),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
@@ -3512,8 +3515,7 @@
         // Reinitialize to make sure packages are parsed correctly on init
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
-                tempFolder,
-                new LimitConfig() {
+                tempFolder, new AppSearchConfigImpl(new LimitConfig() {
                     @Override
                     public int getMaxDocumentSizeBytes() {
                         return Integer.MAX_VALUE;
@@ -3528,8 +3530,7 @@
                     public int getMaxSuggestionCount() {
                         return Integer.MAX_VALUE;
                     }
-                },
-                new DefaultIcingOptionsConfig(),
+                }, new DefaultIcingOptionsConfig()),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
@@ -3571,8 +3572,7 @@
         // Create a new mAppSearchImpl with a lower limit
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
-                mTemporaryFolder.newFolder(),
-                new LimitConfig() {
+                mTemporaryFolder.newFolder(), new AppSearchConfigImpl(new LimitConfig() {
                     @Override
                     public int getMaxDocumentSizeBytes() {
                         return Integer.MAX_VALUE;
@@ -3587,8 +3587,7 @@
                     public int getMaxSuggestionCount() {
                         return Integer.MAX_VALUE;
                     }
-                },
-                new DefaultIcingOptionsConfig(),
+                }, new DefaultIcingOptionsConfig()),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
@@ -3726,8 +3725,7 @@
         // Create a new mAppSearchImpl with a lower limit
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
-                mTemporaryFolder.newFolder(),
-                new LimitConfig() {
+                mTemporaryFolder.newFolder(), new AppSearchConfigImpl(new LimitConfig() {
                     @Override
                     public int getMaxDocumentSizeBytes() {
                         return Integer.MAX_VALUE;
@@ -3742,8 +3740,7 @@
                     public int getMaxSuggestionCount() {
                         return Integer.MAX_VALUE;
                     }
-                },
-                new DefaultIcingOptionsConfig(),
+                }, new DefaultIcingOptionsConfig()),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
@@ -3811,8 +3808,7 @@
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
         mAppSearchImpl = AppSearchImpl.create(
-                tempFolder,
-                new LimitConfig() {
+                tempFolder, new AppSearchConfigImpl(new LimitConfig() {
                     @Override
                     public int getMaxDocumentSizeBytes() {
                         return Integer.MAX_VALUE;
@@ -3827,8 +3823,7 @@
                     public int getMaxSuggestionCount() {
                         return Integer.MAX_VALUE;
                     }
-                },
-                new DefaultIcingOptionsConfig(),
+                }, new DefaultIcingOptionsConfig()),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
@@ -3870,8 +3865,7 @@
         // Reinitialize to make sure replacements are correctly accounted for by init
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
-                tempFolder,
-                new LimitConfig() {
+                tempFolder, new AppSearchConfigImpl(new LimitConfig() {
                     @Override
                     public int getMaxDocumentSizeBytes() {
                         return Integer.MAX_VALUE;
@@ -3886,8 +3880,7 @@
                     public int getMaxSuggestionCount() {
                         return Integer.MAX_VALUE;
                     }
-                },
-                new DefaultIcingOptionsConfig(),
+                }, new DefaultIcingOptionsConfig()),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
@@ -3918,8 +3911,7 @@
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
         mAppSearchImpl = AppSearchImpl.create(
-                tempFolder,
-                new LimitConfig() {
+                tempFolder, new AppSearchConfigImpl(new LimitConfig() {
                     @Override
                     public int getMaxDocumentSizeBytes() {
                         return Integer.MAX_VALUE;
@@ -3934,8 +3926,7 @@
                     public int getMaxSuggestionCount() {
                         return 2;
                     }
-                },
-                new DefaultIcingOptionsConfig(),
+                }, new DefaultIcingOptionsConfig()),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
@@ -4036,8 +4027,10 @@
                 (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 mockVisibilityChecker);
@@ -4087,8 +4080,10 @@
                 (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 mockVisibilityChecker);
@@ -4136,8 +4131,10 @@
                 (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 mockVisibilityChecker);
@@ -4187,8 +4184,10 @@
                         callerAccess.getCallingPackageName().equals("visiblePackage");
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 mockVisibilityChecker);
@@ -4530,8 +4529,10 @@
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
@@ -4569,8 +4570,10 @@
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
@@ -4600,8 +4603,10 @@
                 (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 mockVisibilityChecker);
@@ -4696,8 +4701,10 @@
                         -> prefixedSchema.endsWith("VisibleType");
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 mockVisibilityChecker);
@@ -4783,8 +4790,10 @@
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/null,
                 ALWAYS_OPTIMIZE,
                 rejectChecker);
@@ -4885,8 +4894,10 @@
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/null,
                 ALWAYS_OPTIMIZE,
                 visibilityChecker);
@@ -4945,8 +4956,10 @@
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/null,
                 ALWAYS_OPTIMIZE,
                 rejectChecker);
@@ -5271,8 +5284,10 @@
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/null,
                 ALWAYS_OPTIMIZE,
                 visibilityChecker);
@@ -5427,8 +5442,10 @@
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/null,
                 ALWAYS_OPTIMIZE,
                 visibilityChecker);
@@ -5514,8 +5531,10 @@
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/null,
                 ALWAYS_OPTIMIZE,
                 visibilityChecker);
@@ -5605,8 +5624,10 @@
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/null,
                 ALWAYS_OPTIMIZE,
                 visibilityChecker);
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
index c9828f5..22afe67 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
@@ -75,8 +75,10 @@
     public void setUp() throws Exception {
         mAppSearchImpl = AppSearchImpl.create(
                 mTemporaryFolder.newFolder(),
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
@@ -151,6 +153,10 @@
         final int nativeDocumentSize = 7;
         final int nativeNumTokensIndexed = 8;
         final boolean nativeExceededMaxNumTokens = true;
+        final int nativeTermIndexLatencyMillis = 9;
+        final int nativeIntegerIndexLatencyMillis = 10;
+        final int nativeQualifiedIdJoinIndexLatencyMillis = 11;
+        final int nativeLiteIndexSortLatencyMillis = 12;
         PutDocumentStatsProto nativePutDocumentStats = PutDocumentStatsProto.newBuilder()
                 .setLatencyMs(nativeLatencyMillis)
                 .setDocumentStoreLatencyMs(nativeDocumentStoreLatencyMillis)
@@ -160,6 +166,10 @@
                 .setTokenizationStats(PutDocumentStatsProto.TokenizationStats.newBuilder()
                         .setNumTokensIndexed(nativeNumTokensIndexed)
                         .build())
+                .setTermIndexLatencyMs(nativeTermIndexLatencyMillis)
+                .setIntegerIndexLatencyMs(nativeIntegerIndexLatencyMillis)
+                .setQualifiedIdJoinIndexLatencyMs(nativeQualifiedIdJoinIndexLatencyMillis)
+                .setLiteIndexSortLatencyMs(nativeLiteIndexSortLatencyMillis)
                 .build();
         PutDocumentStats.Builder pBuilder = new PutDocumentStats.Builder(PACKAGE_NAME, DATABASE);
 
@@ -174,6 +184,14 @@
                 nativeIndexMergeLatencyMillis);
         assertThat(pStats.getNativeDocumentSizeBytes()).isEqualTo(nativeDocumentSize);
         assertThat(pStats.getNativeNumTokensIndexed()).isEqualTo(nativeNumTokensIndexed);
+        assertThat(pStats.getNativeTermIndexLatencyMillis()).isEqualTo(
+                nativeTermIndexLatencyMillis);
+        assertThat(pStats.getNativeIntegerIndexLatencyMillis()).isEqualTo(
+                nativeIntegerIndexLatencyMillis);
+        assertThat(pStats.getNativeQualifiedIdJoinIndexLatencyMillis()).isEqualTo(
+                nativeQualifiedIdJoinIndexLatencyMillis);
+        assertThat(pStats.getNativeLiteIndexSortLatencyMillis()).isEqualTo(
+                nativeLiteIndexSortLatencyMillis);
     }
 
     @Test
@@ -350,8 +368,10 @@
         InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
         AppSearchImpl appSearchImpl = AppSearchImpl.create(
                 mTemporaryFolder.newFolder(),
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 initStatsBuilder,
                 ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
@@ -381,8 +401,10 @@
 
         AppSearchImpl appSearchImpl = AppSearchImpl.create(
                 folder,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
@@ -419,7 +441,8 @@
         // Create another appsearchImpl on the same folder
         InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
         appSearchImpl = AppSearchImpl.create(
-                folder, new UnlimitedLimitConfig(), new DefaultIcingOptionsConfig(),
+                folder, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()),
                 initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
         InitializeStats iStats = initStatsBuilder.build();
 
@@ -446,7 +469,8 @@
         final File folder = mTemporaryFolder.newFolder();
 
         AppSearchImpl appSearchImpl = AppSearchImpl.create(
-                folder, new UnlimitedLimitConfig(), new DefaultIcingOptionsConfig(),
+                folder, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
 
         List<AppSearchSchema> schemas = ImmutableList.of(
@@ -485,7 +509,8 @@
         // Create another appsearchImpl on the same folder
         InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
         appSearchImpl = AppSearchImpl.create(
-                folder, new UnlimitedLimitConfig(), new DefaultIcingOptionsConfig(),
+                folder, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()),
                 initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
         InitializeStats iStats = initStatsBuilder.build();
 
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java
index 6840a98..dfbc317 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java
@@ -51,8 +51,10 @@
     public void setUp() throws Exception {
         mAppSearchImpl = AppSearchImpl.create(
                 mTemporaryFolder.newFolder(),
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
     }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
index fa3647b..df0a1f4 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
@@ -19,6 +19,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.localstorage.AppSearchConfigImpl;
+import androidx.appsearch.localstorage.DefaultIcingOptionsConfig;
+import androidx.appsearch.localstorage.UnlimitedLimitConfig;
 
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.PropertyConfigProto;
@@ -63,7 +66,7 @@
                     SCHEMA_PROTO_2);
 
     @Test
-    public void testDocumentProtoConvert() {
+    public void testDocumentProtoConvert() throws Exception {
         GenericDocument document =
                 new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
                         SCHEMA_TYPE_1)
@@ -115,7 +118,8 @@
 
         GenericDocument convertedGenericDocument =
                 GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
-                        SCHEMA_MAP);
+                        SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                                new DefaultIcingOptionsConfig()));
         DocumentProto convertedDocumentProto =
                 GenericDocumentToProtoConverter.toDocumentProto(document);
 
@@ -124,7 +128,7 @@
     }
 
     @Test
-    public void testConvertDocument_whenPropertyHasEmptyList() {
+    public void testConvertDocument_whenPropertyHasEmptyList() throws Exception {
         // Build original GenericDocument
         GenericDocument document =
                 new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
@@ -212,7 +216,8 @@
         // Convert to the other type and check if they are matched.
         GenericDocument convertedGenericDocument =
                 GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
-                        schemaMap);
+                        schemaMap, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                                new DefaultIcingOptionsConfig()));
         DocumentProto convertedDocumentProto =
                 GenericDocumentToProtoConverter.toDocumentProto(document);
         assertThat(convertedDocumentProto).isEqualTo(documentProto);
@@ -220,7 +225,7 @@
     }
 
     @Test
-    public void testConvertDocument_whenNestedDocumentPropertyHasEmptyList() {
+    public void testConvertDocument_whenNestedDocumentPropertyHasEmptyList() throws Exception {
         // Build original nested document in type 1 and outer document in type2
         GenericDocument nestedDocument =
                 new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
@@ -340,10 +345,84 @@
         // Convert to the other type and check if they are matched.
         GenericDocument convertedGenericDocument =
                 GenericDocumentToProtoConverter.toGenericDocument(outerDocumentProto, PREFIX,
-                        schemaMap);
+                        schemaMap, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                                new DefaultIcingOptionsConfig()));
         DocumentProto convertedDocumentProto =
                 GenericDocumentToProtoConverter.toDocumentProto(outerDocument);
         assertThat(convertedDocumentProto).isEqualTo(outerDocumentProto);
         assertThat(convertedGenericDocument).isEqualTo(outerDocument);
     }
+
+    @Test
+    public void testConvertDocument_withParentTypes() throws Exception {
+        // Create a type with a parent type.
+        SchemaTypeConfigProto schemaProto1 = SchemaTypeConfigProto.newBuilder()
+                .setSchemaType(PREFIX + SCHEMA_TYPE_1)
+                .addParentTypes(PREFIX + SCHEMA_TYPE_2)
+                .build();
+        Map<String, SchemaTypeConfigProto> schemaMap =
+                ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, schemaProto1, PREFIX + SCHEMA_TYPE_2,
+                        SCHEMA_PROTO_2);
+
+        // Create a document proto for the above type.
+        DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
+                .setUri("id1")
+                .setSchema(SCHEMA_TYPE_1)
+                .setCreationTimestampMs(5L)
+                .setScore(1)
+                .setTtlMs(1L)
+                .setNamespace("namespace");
+        HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>();
+        propertyProtoMap.put("longKey1",
+                PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L));
+        propertyProtoMap.put("doubleKey1",
+                PropertyProto.newBuilder().setName("doubleKey1").addDoubleValues(1.0));
+        for (Map.Entry<String, PropertyProto.Builder> entry : propertyProtoMap.entrySet()) {
+            documentProtoBuilder.addProperties(entry.getValue());
+        }
+        DocumentProto documentProto = documentProtoBuilder.build();
+
+        // Check if the parent types list is properly wrapped, either as a property or a meta field.
+        GenericDocument expectedDocWithParentAsMetaField =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
+                        SCHEMA_TYPE_1)
+                        .setParentTypes(Collections.singletonList(SCHEMA_TYPE_2))
+                        .setCreationTimestampMillis(5L)
+                        .setScore(1)
+                        .setTtlMillis(1L)
+                        .setPropertyLong("longKey1", 1L)
+                        .setPropertyDouble("doubleKey1", 1.0)
+                        .build();
+        GenericDocument expectedDocWithParentAsSyntheticProperty =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
+                        SCHEMA_TYPE_1)
+                        .setPropertyString(
+                                GenericDocument.PARENT_TYPES_SYNTHETIC_PROPERTY, SCHEMA_TYPE_2)
+                        .setCreationTimestampMillis(5L)
+                        .setScore(1)
+                        .setTtlMillis(1L)
+                        .setPropertyLong("longKey1", 1L)
+                        .setPropertyDouble("doubleKey1", 1.0)
+                        .build();
+
+        GenericDocument actualDocWithParentAsMetaField =
+                GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
+                        schemaMap, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                                new DefaultIcingOptionsConfig(),
+                                /* storeParentInfoAsSyntheticProperty= */ false));
+        GenericDocument actualDocWithParentAsSyntheticProperty =
+                GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
+                        schemaMap, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                                new DefaultIcingOptionsConfig(),
+                                /* storeParentInfoAsSyntheticProperty= */ true));
+
+        assertThat(actualDocWithParentAsMetaField).isEqualTo(expectedDocWithParentAsMetaField);
+        assertThat(actualDocWithParentAsMetaField).isNotEqualTo(
+                expectedDocWithParentAsSyntheticProperty);
+
+        assertThat(actualDocWithParentAsSyntheticProperty).isEqualTo(
+                expectedDocWithParentAsSyntheticProperty);
+        assertThat(actualDocWithParentAsSyntheticProperty).isNotEqualTo(
+                expectedDocWithParentAsMetaField);
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
index 59bdf03..11e0e8e 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
@@ -25,6 +25,9 @@
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchConfigImpl;
+import androidx.appsearch.localstorage.DefaultIcingOptionsConfig;
+import androidx.appsearch.localstorage.UnlimitedLimitConfig;
 import androidx.appsearch.localstorage.util.PrefixUtil;
 
 import com.google.android.icing.proto.DocumentProto;
@@ -45,6 +48,8 @@
         final String id = "id";
         final String namespace = prefix + "namespace";
         final String schemaType = prefix + "schema";
+        final AppSearchConfigImpl config = new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                new DefaultIcingOptionsConfig());
 
         // Building the SearchResult received from query.
         DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
@@ -78,20 +83,22 @@
 
         removePrefixesFromDocument(documentProtoBuilder);
         removePrefixesFromDocument(joinedDocProtoBuilder);
-        SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.toSearchResultPage(searchResultProto, schemaMap);
+        SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
+                searchResultProto, schemaMap, config);
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult result = searchResultPage.getResults().get(0);
         assertThat(result.getPackageName()).isEqualTo("com.package.foo");
         assertThat(result.getDatabaseName()).isEqualTo("databaseName");
         assertThat(result.getGenericDocument()).isEqualTo(
                 GenericDocumentToProtoConverter.toGenericDocument(
-                        documentProtoBuilder.build(), prefix, schemaMap.get(prefix)));
+                        documentProtoBuilder.build(), prefix, schemaMap.get(prefix),
+                        config));
 
         assertThat(result.getJoinedResults()).hasSize(1);
         assertThat(result.getJoinedResults().get(0).getGenericDocument()).isEqualTo(
                 GenericDocumentToProtoConverter.toGenericDocument(
-                        joinedDocProtoBuilder.build(), prefix, schemaMap.get(prefix)));
+                        joinedDocProtoBuilder.build(), prefix, schemaMap.get(prefix),
+                        config));
     }
 
     @Test
@@ -140,8 +147,10 @@
                 ImmutableMap.of(schemaType, schemaTypeConfigProto));
 
         removePrefixesFromDocument(documentProtoBuilder);
-        Exception e = assertThrows(AppSearchException.class, () ->
-                SearchResultToProtoConverter.toSearchResultPage(searchResultProto, schemaMap));
+        Exception e = assertThrows(AppSearchException.class,
+                () -> SearchResultToProtoConverter.toSearchResultPage(searchResultProto, schemaMap,
+                        new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                                new DefaultIcingOptionsConfig())));
         assertThat(e.getMessage())
                 .isEqualTo("Nesting joined results within joined results not allowed.");
     }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
index de56b69..bc4d274 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
@@ -25,6 +25,7 @@
 
 import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.DefaultIcingOptionsConfig;
 import androidx.appsearch.localstorage.IcingOptionsConfig;
@@ -71,8 +72,10 @@
     public void setUp() throws Exception {
         mAppSearchImpl = AppSearchImpl.create(
                 mTemporaryFolder.newFolder(),
-                new UnlimitedLimitConfig(),
-                mDefaultIcingOptionsConfig,
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        mDefaultIcingOptionsConfig
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
@@ -496,6 +499,58 @@
     }
 
     @Test
+    public void testToSearchSpecProto_propertyFilter_withJoinSpec_packageFilter() throws Exception {
+        String personPrefix = PrefixUtil.createPrefix("contacts", "database");
+        String actionPrefix = PrefixUtil.createPrefix("aiai", "database");
+
+        SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+                personPrefix, ImmutableMap.of(personPrefix + "Person", configProto),
+                actionPrefix, ImmutableMap.of(actionPrefix + "ContactAction", configProto));
+        Map<String, Set<String>> namespaceMap = ImmutableMap.of(
+                personPrefix, ImmutableSet.of(personPrefix + "namespaceA"),
+                actionPrefix, ImmutableSet.of(actionPrefix + "namespaceA"));
+
+        SearchSpec nestedSearchSpec = new SearchSpec.Builder()
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, 10)
+                .addFilterProperties("ContactAction", ImmutableList.of("type"))
+                .build();
+
+        // Create a JoinSpec object and set it in the converter
+        JoinSpec joinSpec = new JoinSpec.Builder("childPropertyExpression")
+                .setNestedSearch("nestedQuery", nestedSearchSpec)
+                .build();
+
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setJoinSpec(joinSpec)
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, 10)
+                .addFilterProperties("Person", ImmutableList.of("name"))
+                .build();
+
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+                /*queryExpression=*/"query",
+                searchSpec,
+                /*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
+                namespaceMap,
+                schemaMap,
+                mDefaultIcingOptionsConfig);
+
+        SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+
+        assertThat(searchSpecProto.getTypePropertyFiltersCount()).isEqualTo(1);
+        assertThat(searchSpecProto.getTypePropertyFilters(0).getSchemaType()).isEqualTo(
+                "contacts$database/Person");
+        assertThat(searchSpecProto.getTypePropertyFilters(0).getPaths(0)).isEqualTo("name");
+
+        SearchSpecProto nestedSearchSpecProto =
+                searchSpecProto.getJoinSpec().getNestedSpec().getSearchSpec();
+        assertThat(nestedSearchSpecProto.getTypePropertyFiltersCount()).isEqualTo(1);
+        assertThat(nestedSearchSpecProto.getTypePropertyFilters(0).getSchemaType()).isEqualTo(
+                "aiai$database/ContactAction");
+        assertThat(nestedSearchSpecProto.getTypePropertyFilters(0).getPaths(0)).isEqualTo("type");
+    }
+
+    @Test
     public void testToResultSpecProto_weight_withJoinSpec_packageFilter() throws Exception {
         String personPrefix = PrefixUtil.createPrefix("contacts", "database");
         String actionPrefix = PrefixUtil.createPrefix("aiai", "database");
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
index 9e3b559..026424f 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
@@ -21,6 +21,9 @@
 import androidx.appsearch.app.PropertyPath;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
+import androidx.appsearch.localstorage.AppSearchConfigImpl;
+import androidx.appsearch.localstorage.DefaultIcingOptionsConfig;
+import androidx.appsearch.localstorage.UnlimitedLimitConfig;
 import androidx.appsearch.localstorage.util.PrefixUtil;
 
 import com.google.android.icing.proto.DocumentProto;
@@ -92,7 +95,8 @@
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
                 searchResultProto,
-                SCHEMA_MAP);
+                SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()));
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult.MatchInfo match = searchResultPage.getResults().get(0).getMatchInfos().get(0);
         assertThat(match.getPropertyPath()).isEqualTo(propertyKeyString);
@@ -132,7 +136,8 @@
 
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
                 searchResultProto,
-                SCHEMA_MAP);
+                SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()));
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getMatchInfos()).isEmpty();
     }
@@ -188,7 +193,8 @@
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
                 searchResultProto,
-                SCHEMA_MAP);
+                SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()));
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatchInfos().get(0);
         assertThat(match1.getPropertyPath()).isEqualTo("senderName");
@@ -274,7 +280,8 @@
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
                 searchResultProto,
-                SCHEMA_MAP);
+                SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()));
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatchInfos().get(0);
         assertThat(match1.getPropertyPath()).isEqualTo("sender.name");
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
index c395e31..667dd86 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
@@ -86,6 +86,10 @@
         final int nativeDocumentSize = 7;
         final int nativeNumTokensIndexed = 8;
         final boolean nativeExceededMaxNumTokens = true;
+        final int nativeTermIndexLatencyMillis = 9;
+        final int nativeIntegerIndexLatencyMillis = 10;
+        final int nativeQualifiedIdJoinIndexLatencyMillis = 11;
+        final int nativeLiteIndexSortLatencyMillis = 12;
         final PutDocumentStats.Builder pStatsBuilder =
                 new PutDocumentStats.Builder(TEST_PACKAGE_NAME, TEST_DATA_BASE)
                         .setStatusCode(TEST_STATUS_CODE)
@@ -97,7 +101,12 @@
                         .setNativeIndexLatencyMillis(nativeIndexLatencyMillis)
                         .setNativeIndexMergeLatencyMillis(nativeIndexMergeLatencyMillis)
                         .setNativeDocumentSizeBytes(nativeDocumentSize)
-                        .setNativeNumTokensIndexed(nativeNumTokensIndexed);
+                        .setNativeNumTokensIndexed(nativeNumTokensIndexed)
+                        .setNativeTermIndexLatencyMillis(nativeTermIndexLatencyMillis)
+                        .setNativeIntegerIndexLatencyMillis(nativeIntegerIndexLatencyMillis)
+                        .setNativeQualifiedIdJoinIndexLatencyMillis(
+                                nativeQualifiedIdJoinIndexLatencyMillis)
+                        .setNativeLiteIndexSortLatencyMillis(nativeLiteIndexSortLatencyMillis);
 
         final PutDocumentStats pStats = pStatsBuilder.build();
 
@@ -118,6 +127,14 @@
                 nativeIndexMergeLatencyMillis);
         assertThat(pStats.getNativeDocumentSizeBytes()).isEqualTo(nativeDocumentSize);
         assertThat(pStats.getNativeNumTokensIndexed()).isEqualTo(nativeNumTokensIndexed);
+        assertThat(pStats.getNativeTermIndexLatencyMillis()).isEqualTo(
+                nativeTermIndexLatencyMillis);
+        assertThat(pStats.getNativeIntegerIndexLatencyMillis()).isEqualTo(
+                nativeIntegerIndexLatencyMillis);
+        assertThat(pStats.getNativeQualifiedIdJoinIndexLatencyMillis()).isEqualTo(
+                nativeQualifiedIdJoinIndexLatencyMillis);
+        assertThat(pStats.getNativeLiteIndexSortLatencyMillis()).isEqualTo(
+                nativeLiteIndexSortLatencyMillis);
     }
 
     @Test
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
index 167ed1c..d516e81 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
@@ -31,6 +31,7 @@
 import androidx.appsearch.app.InternalSetSchemaResponse;
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.DefaultIcingOptionsConfig;
 import androidx.appsearch.localstorage.OptimizeStrategy;
@@ -127,8 +128,10 @@
 
         // Persist to disk and re-open the AppSearchImpl
         appSearchImplInV0.close();
-        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
+                new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()), /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
         VisibilityDocument actualDocument1 = new VisibilityDocument(
@@ -193,8 +196,10 @@
                         .build())
                 .build();
         // Set deprecated visibility schema version 0 into AppSearchImpl.
-        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
+                new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()), /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
         InternalSetSchemaResponse internalSetSchemaResponse = appSearchImpl.setSchema(
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
index 0ca32c0..56086d4 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
@@ -26,6 +26,7 @@
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.DefaultIcingOptionsConfig;
 import androidx.appsearch.localstorage.OptimizeStrategy;
@@ -71,8 +72,10 @@
         byte[] sha256CertBar = new byte[32];
 
         // Create AppSearchImpl with visibility document version 1;
-        AppSearchImpl appSearchImplInV1 = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+        AppSearchImpl appSearchImplInV1 = AppSearchImpl.create(mFile,
+                new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()), /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
         InternalSetSchemaResponse internalSetSchemaResponse = appSearchImplInV1.setSchema(
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
@@ -119,8 +122,10 @@
 
         // Persist to disk and re-open the AppSearchImpl
         appSearchImplInV1.close();
-        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
+                new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()), /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
 
         VisibilityDocument actualDocument = new VisibilityDocument(
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java
index 99a1ffe..721b3ad 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java
@@ -25,6 +25,7 @@
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.VisibilityDocument;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.DefaultIcingOptionsConfig;
 import androidx.appsearch.localstorage.OptimizeStrategy;
@@ -60,8 +61,10 @@
         mAppSearchDir = mTemporaryFolder.newFolder();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 /*initStatsBuilder=*/ null,
                 ALWAYS_OPTIMIZE,
                 /*visibilityChecker=*/null);
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
index 120a025..9b71dde 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
@@ -67,6 +67,8 @@
             case Features.SCHEMA_ADD_PARENT_TYPE:
                 // fall through
             case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
+                // fall through
+            case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
                 return true;
             default:
                 return false;
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfig.java
new file mode 100644
index 0000000..811f04d
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfig.java
@@ -0,0 +1,34 @@
+/*
+ * 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.appsearch.localstorage;
+
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+
+/**
+ * An interface that wraps AppSearch configurations required to create {@link AppSearchImpl}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface AppSearchConfig extends IcingOptionsConfig, LimitConfig {
+
+    /**
+     * Whether to store {@link GenericDocument}'s parent types as a synthetic property. If not,
+     * the list of parent types will be wrapped as a meta field in {@link GenericDocument}, in a
+     * similar way as namespace, id, creationTimestamp, etc.
+     */
+    boolean shouldStoreParentInfoAsSyntheticProperty();
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfigImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfigImpl.java
new file mode 100644
index 0000000..c29800c
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfigImpl.java
@@ -0,0 +1,129 @@
+/*
+ * 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.
+ */
+// @exportToFramework:copyToPath(testing/testutils/src/android/app/appsearch/testutil/external/AppSearchConfigImpl.java)
+package androidx.appsearch.localstorage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * An implementation of AppSearchConfig that returns configurations based what is specified in
+ * constructor.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class AppSearchConfigImpl implements AppSearchConfig {
+    private final LimitConfig mLimitConfig;
+    private final IcingOptionsConfig mIcingOptionsConfig;
+    private final boolean mStoreParentInfoAsSyntheticProperty;
+
+    public AppSearchConfigImpl(@NonNull LimitConfig limitConfig,
+            @NonNull IcingOptionsConfig icingOptionsConfig) {
+        this(limitConfig, icingOptionsConfig, false);
+    }
+
+    public AppSearchConfigImpl(@NonNull LimitConfig limitConfig,
+            @NonNull IcingOptionsConfig icingOptionsConfig,
+            boolean storeParentInfoAsSyntheticProperty) {
+        mLimitConfig = limitConfig;
+        mIcingOptionsConfig = icingOptionsConfig;
+        mStoreParentInfoAsSyntheticProperty = storeParentInfoAsSyntheticProperty;
+    }
+
+    @Override
+    public int getMaxTokenLength() {
+        return mIcingOptionsConfig.getMaxTokenLength();
+    }
+
+    @Override
+    public int getIndexMergeSize() {
+        return mIcingOptionsConfig.getIndexMergeSize();
+    }
+
+    @Override
+    public boolean getDocumentStoreNamespaceIdFingerprint() {
+        return mIcingOptionsConfig.getDocumentStoreNamespaceIdFingerprint();
+    }
+
+    @Override
+    public float getOptimizeRebuildIndexThreshold() {
+        return mIcingOptionsConfig.getOptimizeRebuildIndexThreshold();
+    }
+
+    @Override
+    public int getCompressionLevel() {
+        return mIcingOptionsConfig.getCompressionLevel();
+    }
+
+    @Override
+    public boolean getAllowCircularSchemaDefinitions() {
+        return mIcingOptionsConfig.getAllowCircularSchemaDefinitions();
+    }
+
+    @Override
+    public boolean getUseReadOnlySearch() {
+        return mIcingOptionsConfig.getUseReadOnlySearch();
+    }
+
+    @Override
+    public boolean getUsePreMappingWithFileBackedVector() {
+        return mIcingOptionsConfig.getUsePreMappingWithFileBackedVector();
+    }
+
+    @Override
+    public boolean getUsePersistentHashMap() {
+        return mIcingOptionsConfig.getUsePersistentHashMap();
+    }
+
+    @Override
+    public int getMaxPageBytesLimit() {
+        return mIcingOptionsConfig.getMaxPageBytesLimit();
+    }
+
+    @Override
+    public int getIntegerIndexBucketSplitThreshold() {
+        return mIcingOptionsConfig.getIntegerIndexBucketSplitThreshold();
+    }
+
+    @Override
+    public boolean getLiteIndexSortAtIndexing() {
+        return mIcingOptionsConfig.getLiteIndexSortAtIndexing();
+    }
+
+    @Override
+    public int getLiteIndexSortSize() {
+        return mIcingOptionsConfig.getLiteIndexSortSize();
+    }
+
+    @Override
+    public int getMaxDocumentSizeBytes() {
+        return mLimitConfig.getMaxDocumentSizeBytes();
+    }
+
+    @Override
+    public int getMaxDocumentCount() {
+        return mLimitConfig.getMaxDocumentCount();
+    }
+
+    @Override
+    public int getMaxSuggestionCount() {
+        return mLimitConfig.getMaxSuggestionCount();
+    }
+
+    @Override
+    public boolean shouldStoreParentInfoAsSyntheticProperty() {
+        return mStoreParentInfoAsSyntheticProperty;
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index c819c45..68fa3b7 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -181,8 +181,7 @@
 
     private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock();
     private final OptimizeStrategy mOptimizeStrategy;
-    private final LimitConfig mLimitConfig;
-    private final IcingOptionsConfig mIcingOptionsConfig;
+    private final AppSearchConfig mConfig;
 
     @GuardedBy("mReadWriteLock")
     @VisibleForTesting
@@ -268,14 +267,13 @@
     @NonNull
     public static AppSearchImpl create(
             @NonNull File icingDir,
-            @NonNull LimitConfig limitConfig,
-            @NonNull IcingOptionsConfig icingOptionsConfig,
+            @NonNull AppSearchConfig config,
             @Nullable InitializeStats.Builder initStatsBuilder,
             @NonNull OptimizeStrategy optimizeStrategy,
             @Nullable VisibilityChecker visibilityChecker)
             throws AppSearchException {
-        return new AppSearchImpl(icingDir, limitConfig, icingOptionsConfig, initStatsBuilder,
-                optimizeStrategy, visibilityChecker);
+        return new AppSearchImpl(icingDir, config, initStatsBuilder, optimizeStrategy,
+                visibilityChecker);
     }
 
     /**
@@ -283,15 +281,13 @@
      */
     private AppSearchImpl(
             @NonNull File icingDir,
-            @NonNull LimitConfig limitConfig,
-            @NonNull IcingOptionsConfig icingOptionsConfig,
+            @NonNull AppSearchConfig config,
             @Nullable InitializeStats.Builder initStatsBuilder,
             @NonNull OptimizeStrategy optimizeStrategy,
             @Nullable VisibilityChecker visibilityChecker)
             throws AppSearchException {
         Preconditions.checkNotNull(icingDir);
-        mLimitConfig = Preconditions.checkNotNull(limitConfig);
-        mIcingOptionsConfig = Preconditions.checkNotNull(icingOptionsConfig);
+        mConfig = Preconditions.checkNotNull(config);
         mOptimizeStrategy = Preconditions.checkNotNull(optimizeStrategy);
         mVisibilityCheckerLocked = visibilityChecker;
 
@@ -301,19 +297,21 @@
             // than once. It's unnecessary and can be a costly operation.
             IcingSearchEngineOptions options = IcingSearchEngineOptions.newBuilder()
                     .setBaseDir(icingDir.getAbsolutePath())
-                    .setMaxTokenLength(icingOptionsConfig.getMaxTokenLength())
-                    .setIndexMergeSize(icingOptionsConfig.getIndexMergeSize())
+                    .setMaxTokenLength(mConfig.getMaxTokenLength())
+                    .setIndexMergeSize(mConfig.getIndexMergeSize())
                     .setDocumentStoreNamespaceIdFingerprint(
-                            icingOptionsConfig.getDocumentStoreNamespaceIdFingerprint())
+                            mConfig.getDocumentStoreNamespaceIdFingerprint())
                     .setOptimizeRebuildIndexThreshold(
-                            icingOptionsConfig.getOptimizeRebuildIndexThreshold())
-                    .setCompressionLevel(icingOptionsConfig.getCompressionLevel())
+                            mConfig.getOptimizeRebuildIndexThreshold())
+                    .setCompressionLevel(mConfig.getCompressionLevel())
                     .setAllowCircularSchemaDefinitions(
-                            icingOptionsConfig.getAllowCircularSchemaDefinitions())
-                    .setPreMappingFbv(icingOptionsConfig.getUsePreMappingWithFileBackedVector())
-                    .setUsePersistentHashMap(icingOptionsConfig.getUsePersistentHashMap())
+                            mConfig.getAllowCircularSchemaDefinitions())
+                    .setPreMappingFbv(mConfig.getUsePreMappingWithFileBackedVector())
+                    .setUsePersistentHashMap(mConfig.getUsePersistentHashMap())
                     .setIntegerIndexBucketSplitThreshold(
-                            icingOptionsConfig.getIntegerIndexBucketSplitThreshold())
+                            mConfig.getIntegerIndexBucketSplitThreshold())
+                    .setLiteIndexSortAtIndexing(mConfig.getLiteIndexSortAtIndexing())
+                    .setLiteIndexSortSize(mConfig.getLiteIndexSortSize())
                     .build();
             LogUtil.piiTrace(TAG, "Constructing IcingSearchEngine, request", options);
             mIcingSearchEngineLocked = new IcingSearchEngine(options);
@@ -1092,12 +1090,12 @@
     private int enforceLimitConfigLocked(String packageName, String newDocUri, int newDocSize)
             throws AppSearchException {
         // Limits check: size of document
-        if (newDocSize > mLimitConfig.getMaxDocumentSizeBytes()) {
+        if (newDocSize > mConfig.getMaxDocumentSizeBytes()) {
             throw new AppSearchException(
                     AppSearchResult.RESULT_OUT_OF_SPACE,
                     "Document \"" + newDocUri + "\" for package \"" + packageName
                             + "\" serialized to " + newDocSize + " bytes, which exceeds "
-                            + "limit of " + mLimitConfig.getMaxDocumentSizeBytes() + " bytes");
+                            + "limit of " + mConfig.getMaxDocumentSizeBytes() + " bytes");
         }
 
         // Limits check: number of documents
@@ -1108,7 +1106,7 @@
         } else {
             newDocumentCount = oldDocumentCount + 1;
         }
-        if (newDocumentCount > mLimitConfig.getMaxDocumentCount()) {
+        if (newDocumentCount > mConfig.getMaxDocumentCount()) {
             // Our management of mDocumentCountMapLocked doesn't account for document
             // replacements, so our counter might have overcounted if the app has replaced docs.
             // Rebuild the counter from StorageInfo in case this is so.
@@ -1123,12 +1121,12 @@
                 newDocumentCount = oldDocumentCount + 1;
             }
         }
-        if (newDocumentCount > mLimitConfig.getMaxDocumentCount()) {
+        if (newDocumentCount > mConfig.getMaxDocumentCount()) {
             // Now we really can't fit it in, even accounting for replacements.
             throw new AppSearchException(
                     AppSearchResult.RESULT_OUT_OF_SPACE,
                     "Package \"" + packageName + "\" exceeded limit of "
-                            + mLimitConfig.getMaxDocumentCount() + " documents. Some documents "
+                            + mConfig.getMaxDocumentCount() + " documents. Some documents "
                             + "must be removed to index additional ones.");
         }
 
@@ -1193,7 +1191,7 @@
             Map<String, SchemaTypeConfigProto> schemaTypeMap =
                     Preconditions.checkNotNull(mSchemaMapLocked.get(prefix));
             return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
-                    prefix, schemaTypeMap);
+                    prefix, schemaTypeMap, mConfig);
         } finally {
             mReadWriteLock.readLock().unlock();
         }
@@ -1235,7 +1233,7 @@
             Map<String, SchemaTypeConfigProto> schemaTypeMap =
                     Preconditions.checkNotNull(mSchemaMapLocked.get(prefix));
             return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
-                    prefix, schemaTypeMap);
+                    prefix, schemaTypeMap, mConfig);
         } finally {
             mReadWriteLock.readLock().unlock();
         }
@@ -1346,7 +1344,7 @@
             SearchSpecToProtoConverter searchSpecToProtoConverter =
                     new SearchSpecToProtoConverter(queryExpression, searchSpec,
                             Collections.singleton(prefix), mNamespaceMapLocked, mSchemaMapLocked,
-                            mIcingOptionsConfig);
+                            mConfig);
             if (searchSpecToProtoConverter.hasNothingToSearch()) {
                 // there is nothing to search over given their search filters, so we can return an
                 // empty SearchResult and skip sending request to Icing.
@@ -1446,7 +1444,7 @@
             }
             SearchSpecToProtoConverter searchSpecToProtoConverter =
                     new SearchSpecToProtoConverter(queryExpression, searchSpec, prefixFilters,
-                            mNamespaceMapLocked, mSchemaMapLocked, mIcingOptionsConfig);
+                            mNamespaceMapLocked, mSchemaMapLocked, mConfig);
             // Remove those inaccessible schemas.
             searchSpecToProtoConverter.removeInaccessibleSchemaFilter(
                     callerAccess, mVisibilityStoreLocked, mVisibilityCheckerLocked);
@@ -1501,7 +1499,7 @@
         long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
         // Rewrite search result before we return.
         SearchResultPage searchResultPage = SearchResultToProtoConverter
-                .toSearchResultPage(searchResultProto, mSchemaMapLocked);
+                .toSearchResultPage(searchResultProto, mSchemaMapLocked, mConfig);
         if (sStatsBuilder != null) {
             sStatsBuilder.setRewriteSearchResultLatencyMillis(
                     (int) (SystemClock.elapsedRealtime()
@@ -1568,12 +1566,12 @@
                         "suggestionQueryExpression cannot be empty.");
             }
             if (searchSuggestionSpec.getMaximumResultCount()
-                    > mLimitConfig.getMaxSuggestionCount()) {
+                    > mConfig.getMaxSuggestionCount()) {
                 throw new AppSearchException(
                         AppSearchResult.RESULT_INVALID_ARGUMENT,
                         "Trying to get " + searchSuggestionSpec.getMaximumResultCount()
                                 + " suggestion results, which exceeds limit of "
-                                + mLimitConfig.getMaxSuggestionCount());
+                                + mConfig.getMaxSuggestionCount());
             }
 
             String prefix = createPrefix(packageName, databaseName);
@@ -1697,7 +1695,7 @@
             long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
             // Rewrite search result before we return.
             SearchResultPage searchResultPage = SearchResultToProtoConverter
-                    .toSearchResultPage(searchResultProto, mSchemaMapLocked);
+                    .toSearchResultPage(searchResultProto, mSchemaMapLocked, mConfig);
             if (sStatsBuilder != null) {
                 sStatsBuilder.setRewriteSearchResultLatencyMillis(
                         (int) (SystemClock.elapsedRealtime()
@@ -1912,7 +1910,7 @@
             SearchSpecToProtoConverter searchSpecToProtoConverter =
                     new SearchSpecToProtoConverter(queryExpression, searchSpec,
                             Collections.singleton(prefix), mNamespaceMapLocked, mSchemaMapLocked,
-                            mIcingOptionsConfig);
+                            mConfig);
             if (searchSpecToProtoConverter.hasNothingToSearch()) {
                 // there is nothing to search over given their search filters, so we can return
                 // early and skip sending request to Icing.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
index 6fb135a..652fc94 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
@@ -64,7 +64,13 @@
                 .setNativeIndexMergeLatencyMillis(fromNativeStats.getIndexMergeLatencyMs())
                 .setNativeDocumentSizeBytes(fromNativeStats.getDocumentSize())
                 .setNativeNumTokensIndexed(
-                        fromNativeStats.getTokenizationStats().getNumTokensIndexed());
+                        fromNativeStats.getTokenizationStats().getNumTokensIndexed())
+                .setNativeTermIndexLatencyMillis(fromNativeStats.getTermIndexLatencyMs())
+                .setNativeIntegerIndexLatencyMillis(fromNativeStats.getIntegerIndexLatencyMs())
+                .setNativeQualifiedIdJoinIndexLatencyMillis(
+                        fromNativeStats.getQualifiedIdJoinIndexLatencyMs())
+                .setNativeLiteIndexSortLatencyMillis(
+                        fromNativeStats.getLiteIndexSortLatencyMs());
     }
 
     /**
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/DefaultIcingOptionsConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/DefaultIcingOptionsConfig.java
index ac9742d..4201c4e 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/DefaultIcingOptionsConfig.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/DefaultIcingOptionsConfig.java
@@ -78,4 +78,14 @@
     public int getIntegerIndexBucketSplitThreshold() {
         return DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD;
     }
+
+    @Override
+    public boolean getLiteIndexSortAtIndexing() {
+        return DEFAULT_LITE_INDEX_SORT_AT_INDEXING;
+    }
+
+    @Override
+    public int getLiteIndexSortSize() {
+        return DEFAULT_LITE_INDEX_SORT_SIZE;
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
index 0f929f4..418569a 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
@@ -62,6 +62,14 @@
      */
     int DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD = 65536;
 
+    boolean DEFAULT_LITE_INDEX_SORT_AT_INDEXING = false;
+
+    /**
+     * The default sort threshold for the lite index when sort at indexing is enabled.
+     * 8192 is picked based on Icing microbenchmarks (icing-search-engine_benchmarks.cc).
+     */
+    int DEFAULT_LITE_INDEX_SORT_SIZE = 8192;   // 8Kib
+
     /**
      * The maximum allowable token length. All tokens in excess of this size will be truncated to
      * max_token_length before being indexed.
@@ -175,4 +183,27 @@
      * list).
      */
     int getIntegerIndexBucketSplitThreshold();
+
+    /**
+     * Flag for {@link com.google.android.icing.proto.IcingSearchEngineOptions}.
+     *
+     * <p>Whether Icing should sort and merge its lite index HitBuffer unsorted tail at indexing
+     * time.
+     *
+     * <p>If set to true, the HitBuffer will be sorted at indexing time after exceeding the sort
+     * threshold. If false, the HifBuffer will be sorted at querying time, before the first query
+     * after inserting new elements into the HitBuffer.
+     */
+    boolean getLiteIndexSortAtIndexing();
+
+    /**
+     * Flag for {@link com.google.android.icing.proto.IcingSearchEngineOptions}.
+     *
+     * <p>Size (in bytes) at which Icing's lite index should sort and merge the HitBuffer's
+     * unsorted tail into the sorted head for sorting at indexing time. Size specified here is
+     * unsorted tail section.
+     *
+     * <p>Setting a lower sort size reduces querying latency at the expense of indexing latency.
+     */
+    int getLiteIndexSortSize();
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index 86c825a..3d62ed9 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -339,8 +339,10 @@
         AppSearchImpl.syncLoggingLevelToIcing();
         mAppSearchImpl = AppSearchImpl.create(
                 icingDir,
-                new UnlimitedLimitConfig(),
-                new DefaultIcingOptionsConfig(),
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new DefaultIcingOptionsConfig()
+                ),
                 initStatsBuilder,
                 new JetpackOptimizeStrategy(),
                 /*visibilityChecker=*/null);
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
index e6a7506..56c52b3 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -20,6 +20,11 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchConfig;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
 import com.google.android.icing.proto.DocumentProto;
@@ -28,9 +33,13 @@
 import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.protobuf.ByteString;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
 
 /**
  * Translates a {@link GenericDocument} into a {@link DocumentProto}.
@@ -132,7 +141,8 @@
     @NonNull
     public static GenericDocument toGenericDocument(@NonNull DocumentProtoOrBuilder proto,
             @NonNull String prefix,
-            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap) {
+            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap,
+            @NonNull AppSearchConfig config) throws AppSearchException {
         Preconditions.checkNotNull(proto);
         GenericDocument.Builder<?> documentBuilder =
                 new GenericDocument.Builder<>(proto.getNamespace(), proto.getUri(),
@@ -141,6 +151,16 @@
                         .setTtlMillis(proto.getTtlMs())
                         .setCreationTimestampMillis(proto.getCreationTimestampMs());
         String prefixedSchemaType = prefix + proto.getSchema();
+        List<String> parentSchemaTypes = getUnprefixedParentSchemaTypes(
+                prefixedSchemaType, schemaTypeMap);
+        if (!parentSchemaTypes.isEmpty()) {
+            if (config.shouldStoreParentInfoAsSyntheticProperty()) {
+                documentBuilder.setPropertyString(GenericDocument.PARENT_TYPES_SYNTHETIC_PROPERTY,
+                        parentSchemaTypes.toArray(new String[0]));
+            } else {
+                documentBuilder.setParentTypes(parentSchemaTypes);
+            }
+        }
 
         for (int i = 0; i < proto.getPropertiesCount(); i++) {
             PropertyProto property = proto.getProperties(i);
@@ -179,7 +199,7 @@
                 GenericDocument[] values = new GenericDocument[property.getDocumentValuesCount()];
                 for (int j = 0; j < values.length; j++) {
                     values[j] = toGenericDocument(property.getDocumentValues(j), prefix,
-                            schemaTypeMap);
+                            schemaTypeMap, config);
                 }
                 documentBuilder.setPropertyDocument(name, values);
             } else {
@@ -192,6 +212,66 @@
         return documentBuilder.build();
     }
 
+    /**
+     * Get the list of unprefixed parent type names of {@code prefixedSchemaType}.
+     *
+     * <p>It's guaranteed that child types always appear before parent types in the list.
+     */
+    // TODO(b/290389974): Consider caching the result based prefixedSchemaType, and reset the
+    //  cache whenever a new setSchema is called.
+    @NonNull
+    private static List<String> getUnprefixedParentSchemaTypes(
+            @NonNull String prefixedSchemaType,
+            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap) throws AppSearchException {
+        // Please note that neither DFS nor BFS order is guaranteed to always put child types
+        // before parent types (due to the diamond problem), so a topological sorting algorithm
+        // is required.
+        Map<String, Integer> inDegreeMap = new ArrayMap<>();
+        collectParentTypeInDegrees(prefixedSchemaType, schemaTypeMap,
+                /* visited= */new ArraySet<>(), inDegreeMap);
+
+        List<String> result = new ArrayList<>();
+        Queue<String> queue = new ArrayDeque<>();
+        // prefixedSchemaType is the only type that has zero in-degree at this point.
+        queue.add(prefixedSchemaType);
+        while (!queue.isEmpty()) {
+            SchemaTypeConfigProto currentSchema = Preconditions.checkNotNull(
+                    schemaTypeMap.get(queue.poll()));
+            for (int i = 0; i < currentSchema.getParentTypesCount(); ++i) {
+                String prefixedParentType = currentSchema.getParentTypes(i);
+                int parentInDegree =
+                        Preconditions.checkNotNull(inDegreeMap.get(prefixedParentType)) - 1;
+                inDegreeMap.put(prefixedParentType, parentInDegree);
+                if (parentInDegree == 0) {
+                    result.add(PrefixUtil.removePrefix(prefixedParentType));
+                    queue.add(prefixedParentType);
+                }
+            }
+        }
+        return result;
+    }
+
+    private static void collectParentTypeInDegrees(
+            @NonNull String prefixedSchemaType,
+            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap,
+            @NonNull Set<String> visited, @NonNull Map<String, Integer> inDegreeMap) {
+        if (visited.contains(prefixedSchemaType)) {
+            return;
+        }
+        visited.add(prefixedSchemaType);
+        SchemaTypeConfigProto schema =
+                Preconditions.checkNotNull(schemaTypeMap.get(prefixedSchemaType));
+        for (int i = 0; i < schema.getParentTypesCount(); ++i) {
+            String prefixedParentType = schema.getParentTypes(i);
+            Integer parentInDegree = inDegreeMap.get(prefixedParentType);
+            if (parentInDegree == null) {
+                parentInDegree = 0;
+            }
+            inDegreeMap.put(prefixedParentType, parentInDegree + 1);
+            collectParentTypeInDegrees(prefixedParentType, schemaTypeMap, visited, inDegreeMap);
+        }
+    }
+
     private static void setEmptyProperty(@NonNull String propertyName,
             @NonNull GenericDocument.Builder<?> documentBuilder,
             @NonNull SchemaTypeConfigProto schema) {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
index b5f0b97..e21213f 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
@@ -101,7 +101,11 @@
                         .setValueType(
                                 convertJoinableValueTypeToProto(
                                         stringProperty.getJoinableValueType()))
+                        // @exportToFramework:startStrip()
+                        // Do not call this in framework as it will populate the proto field and
+                        // fail comparison tests.
                         .setPropagateDelete(stringProperty.getDeletionPropagation())
+                        // @exportToFramework:endStrip()
                         .build();
                 builder.setJoinableConfig(joinableConfig);
             }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index b26954b..8652cca 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -29,6 +29,7 @@
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchConfig;
 import androidx.core.util.Preconditions;
 
 import com.google.android.icing.proto.DocumentProto;
@@ -60,13 +61,14 @@
      */
     @NonNull
     public static SearchResultPage toSearchResultPage(@NonNull SearchResultProto proto,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)
+            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull AppSearchConfig config)
             throws AppSearchException {
         Bundle bundle = new Bundle();
         bundle.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, proto.getNextPageToken());
         ArrayList<Bundle> resultBundles = new ArrayList<>(proto.getResultsCount());
         for (int i = 0; i < proto.getResultsCount(); i++) {
-            SearchResult result = toUnprefixedSearchResult(proto.getResults(i), schemaMap);
+            SearchResult result = toUnprefixedSearchResult(proto.getResults(i), schemaMap, config);
             resultBundles.add(result.getBundle());
         }
         bundle.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
@@ -85,8 +87,8 @@
     @NonNull
     private static SearchResult toUnprefixedSearchResult(
             @NonNull SearchResultProto.ResultProto proto,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)
-            throws AppSearchException {
+            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull AppSearchConfig config) throws AppSearchException {
 
         DocumentProto.Builder documentBuilder = proto.getDocument().toBuilder();
         String prefix = removePrefixesFromDocument(documentBuilder);
@@ -94,7 +96,7 @@
                 Preconditions.checkNotNull(schemaMap.get(prefix));
         GenericDocument document =
                 GenericDocumentToProtoConverter.toGenericDocument(documentBuilder, prefix,
-                        schemaTypeMap);
+                        schemaTypeMap, config);
         SearchResult.Builder builder =
                 new SearchResult.Builder(getPackageName(prefix), getDatabaseName(prefix))
                         .setGenericDocument(document).setRankingSignal(proto.getScore());
@@ -116,7 +118,7 @@
                         "Nesting joined results within joined results not allowed.");
             }
 
-            builder.addJoinedResult(toUnprefixedSearchResult(joinedResultProto, schemaMap));
+            builder.addJoinedResult(toUnprefixedSearchResult(joinedResultProto, schemaMap, config));
         }
         return builder.build();
     }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
index b7d648b..981ea944 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
@@ -291,6 +291,27 @@
                 .addAllSchemaTypeFilters(mTargetPrefixedSchemaFilters)
                 .setUseReadOnlySearch(mIcingOptionsConfig.getUseReadOnlySearch());
 
+        // Convert type property filter map into type property mask proto.
+        for (Map.Entry<String, List<String>> entry :
+                mSearchSpec.getFilterProperties().entrySet()) {
+            if (entry.getKey().equals(SearchSpec.SCHEMA_TYPE_WILDCARD)) {
+                protoBuilder.addTypePropertyFilters(TypePropertyMask.newBuilder()
+                        .setSchemaType(SearchSpec.SCHEMA_TYPE_WILDCARD)
+                        .addAllPaths(entry.getValue())
+                        .build());
+            } else {
+                for (String prefix : mCurrentSearchSpecPrefixFilters) {
+                    String prefixedSchemaType = prefix + entry.getKey();
+                    if (mTargetPrefixedSchemaFilters.contains(prefixedSchemaType)) {
+                        protoBuilder.addTypePropertyFilters(TypePropertyMask.newBuilder()
+                                .setSchemaType(prefixedSchemaType)
+                                .addAllPaths(entry.getValue())
+                                .build());
+                    }
+                }
+            }
+        }
+
         @SearchSpec.TermMatch int termMatchCode = mSearchSpec.getTermMatch();
         TermMatchType.Code termMatchCodeProto = TermMatchType.Code.forNumber(termMatchCode);
         if (termMatchCodeProto == null || termMatchCodeProto.equals(TermMatchType.Code.UNKNOWN)) {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
index 65c5923..15c6e32 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
@@ -66,6 +66,21 @@
     /** Number of tokens added to the index. */
     private final int mNativeNumTokensIndexed;
 
+    /**
+     * Time used to index all indexable string terms in the document. It does not include the
+     * time to merge indices.
+     */
+    private final int mNativeTermIndexLatencyMillis;
+
+    /** Time used to index all indexable integers in the document. */
+    private final int mNativeIntegerIndexLatencyMillis;
+
+    /** Time used to index all qualified id join strings in the document. */
+    private final int mNativeQualifiedIdJoinIndexLatencyMillis;
+
+    /** Time used to sort and merge the lite index's hit buffer. */
+    private final int mNativeLiteIndexSortLatencyMillis;
+
     PutDocumentStats(@NonNull Builder builder) {
         Preconditions.checkNotNull(builder);
         mPackageName = builder.mPackageName;
@@ -80,6 +95,10 @@
         mNativeIndexMergeLatencyMillis = builder.mNativeIndexMergeLatencyMillis;
         mNativeDocumentSizeBytes = builder.mNativeDocumentSizeBytes;
         mNativeNumTokensIndexed = builder.mNativeNumTokensIndexed;
+        mNativeTermIndexLatencyMillis = builder.mNativeTermIndexLatencyMillis;
+        mNativeIntegerIndexLatencyMillis = builder.mNativeIntegerIndexLatencyMillis;
+        mNativeQualifiedIdJoinIndexLatencyMillis = builder.mNativeQualifiedIdJoinIndexLatencyMillis;
+        mNativeLiteIndexSortLatencyMillis = builder.mNativeLiteIndexSortLatencyMillis;
     }
 
     /** Returns calling package name. */
@@ -145,6 +164,26 @@
         return mNativeNumTokensIndexed;
     }
 
+    /** Returns time spent on term indexing, in milliseconds. */
+    public int getNativeTermIndexLatencyMillis() {
+        return mNativeTermIndexLatencyMillis;
+    }
+
+    /** Returns time spent on integer indexing, in milliseconds. */
+    public int getNativeIntegerIndexLatencyMillis() {
+        return mNativeIntegerIndexLatencyMillis;
+    }
+
+    /** Returns time spent on qualified id join indexing, in milliseconds. */
+    public int getNativeQualifiedIdJoinIndexLatencyMillis() {
+        return mNativeQualifiedIdJoinIndexLatencyMillis;
+    }
+
+    /** Returns time spent sorting and merging the lite index, in milliseconds. */
+    public int getNativeLiteIndexSortLatencyMillis() {
+        return mNativeLiteIndexSortLatencyMillis;
+    }
+
     /** Builder for {@link PutDocumentStats}. */
     public static class Builder {
         @NonNull
@@ -162,6 +201,10 @@
         int mNativeIndexMergeLatencyMillis;
         int mNativeDocumentSizeBytes;
         int mNativeNumTokensIndexed;
+        int mNativeTermIndexLatencyMillis;
+        int mNativeIntegerIndexLatencyMillis;
+        int mNativeQualifiedIdJoinIndexLatencyMillis;
+        int mNativeLiteIndexSortLatencyMillis;
 
         /** Builder for {@link PutDocumentStats} */
         public Builder(@NonNull String packageName, @NonNull String database) {
@@ -253,6 +296,39 @@
             return this;
         }
 
+        /** Sets the native term indexing time, in millis. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setNativeTermIndexLatencyMillis(int nativeTermIndexLatencyMillis) {
+            mNativeTermIndexLatencyMillis = nativeTermIndexLatencyMillis;
+            return this;
+        }
+
+        /** Sets the native integer indexing time, in millis. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setNativeIntegerIndexLatencyMillis(int nativeIntegerIndexLatencyMillis) {
+            mNativeIntegerIndexLatencyMillis = nativeIntegerIndexLatencyMillis;
+            return this;
+        }
+
+        /** Sets the native qualified id indexing time, in millis. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setNativeQualifiedIdJoinIndexLatencyMillis(
+                int nativeQualifiedIdJoinIndexLatencyMillis) {
+            mNativeQualifiedIdJoinIndexLatencyMillis = nativeQualifiedIdJoinIndexLatencyMillis;
+            return this;
+        }
+
+        /** Sets the native lite index sort latency, in millis. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setNativeLiteIndexSortLatencyMillis(int nativeLiteIndexSortLatencyMillis) {
+            mNativeLiteIndexSortLatencyMillis = nativeLiteIndexSortLatencyMillis;
+            return this;
+        }
+
         /**
          * Creates a new {@link PutDocumentStats} object from the contents of this
          * {@link Builder} instance.
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
index ed3a8f6..4095f32 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
@@ -74,6 +74,9 @@
                 // fall through
             case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
                 // TODO(b/289150947) : Update when feature is ready in service-appsearch.
+                // fall through
+            case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
+                // TODO(b/296088047) : Update when feature is ready in service-appsearch.
                 return false;
             default:
                 return false;
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
index f87ab48..3775ab7 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
@@ -24,6 +24,8 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.core.util.Preconditions;
 
+import java.util.Arrays;
+
 /**
  * Translates between Platform and Jetpack versions of {@link GenericDocument}.
  *
@@ -113,6 +115,15 @@
                 .setCreationTimestampMillis(platformDocument.getCreationTimestampMillis());
         for (String propertyName : platformDocument.getPropertyNames()) {
             Object property = platformDocument.getProperty(propertyName);
+            if (propertyName.equals(GenericDocument.PARENT_TYPES_SYNTHETIC_PROPERTY)) {
+                if (!(property instanceof String[])) {
+                    throw new IllegalStateException(
+                            String.format("Parents list must be of String[] type, but got %s",
+                                    property.getClass().toString()));
+                }
+                jetpackBuilder.setParentTypes(Arrays.asList((String[]) property));
+                continue;
+            }
             if (property instanceof String[]) {
                 jetpackBuilder.setPropertyString(propertyName, (String[]) property);
             } else if (property instanceof long[]) {
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
index 4cb82c1..47bff5a 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
@@ -127,6 +127,12 @@
             }
             ApiHelperForU.setJoinSpec(platformBuilder, jetpackSearchSpec.getJoinSpec());
         }
+
+        if (!jetpackSearchSpec.getFilterProperties().isEmpty()) {
+            // TODO(b/296088047): Remove this once property filters become available.
+            throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+                    + " is not available on this AppSearch implementation.");
+        }
         return platformBuilder.build();
     }
 
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
index 2441e57d..a2a3e53 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
@@ -71,6 +71,34 @@
             case Features.LIST_FILTER_QUERY_LANGUAGE:
                 // TODO(b/208654892) : Update to reflect support in Android U+ once this feature is
                 //  synced over into service-appsearch.
+                // fall through
+            case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
+                // TODO(b/258715421) : Update to reflect support in Android U+ once this feature is
+                //  synced over into service-appsearch.
+                // fall through
+            case Features.SEARCH_SUGGESTION:
+                // TODO(b/227356108) : Update to reflect support in Android U+ once this feature is
+                //  synced over into service-appsearch.
+                // fall through
+            case Features.SCHEMA_SET_DELETION_PROPAGATION:
+                // TODO(b/268521214) : Update to reflect support in Android U+ once this feature is
+                //  synced over into service-appsearch.
+                // fall through
+            case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
+                // TODO(b/280698121) : Update to reflect support in Android U+ once this feature is
+                //  synced over into service-appsearch.
+                // fall through
+            case Features.SCHEMA_ADD_PARENT_TYPE:
+                // TODO(b/269295094) : Update to reflect support in Android U+ once this feature is
+                //  synced over into service-appsearch.
+                // fall through
+            case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
+                // TODO(b/289150947) : Update to reflect support in Android U+ once this feature is
+                //  synced over into service-appsearch.
+                // fall through
+            case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
+                // TODO(b/296088047) : Update to reflect support in Android U+ once this feature is
+                //  synced over into service-appsearch.
                 return false;
             default:
                 return false; // AppSearch features in U+, absent in GMSCore AppSearch.
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java
index 8922bd4..1b7701b 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java
@@ -98,6 +98,12 @@
                     + "AppSearch implementation.");
         }
 
+        if (!jetpackSearchSpec.getFilterProperties().isEmpty()) {
+            // TODO(b/296088047): Remove this once property filters become available.
+            throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+                    + " is not available on this AppSearch implementation.");
+        }
+
         return gmsBuilder.build();
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
index af87d4d..690eb0f 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
@@ -45,8 +45,10 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
 public abstract class AnnotationProcessorTestBase {
@@ -1324,6 +1326,8 @@
 
         Place place = Place.createPlace("id1", "namespace", 2000, "place_loc");
         GenericDocument placeGeneric = GenericDocument.fromDocumentClass(place);
+        placeGeneric = placeGeneric.toBuilder().setParentTypes(
+                Collections.singletonList("InterfaceRoot")).build();
         assertThat(placeGeneric.getId()).isEqualTo("id1");
         assertThat(placeGeneric.getNamespace()).isEqualTo("namespace");
         assertThat(placeGeneric.getCreationTimestampMillis()).isEqualTo(2000);
@@ -1337,6 +1341,8 @@
                 .setOrganizationDescription("organization_dec")
                 .build();
         GenericDocument organizationGeneric = GenericDocument.fromDocumentClass(organization);
+        organizationGeneric = organizationGeneric.toBuilder().setParentTypes(
+                Collections.singletonList("InterfaceRoot")).build();
         assertThat(organizationGeneric.getId()).isEqualTo("id2");
         assertThat(organizationGeneric.getNamespace()).isEqualTo("namespace");
         assertThat(organizationGeneric.getCreationTimestampMillis()).isEqualTo(3000);
@@ -1347,6 +1353,10 @@
         Business business = Business.createBusiness("id3", "namespace", 4000, "business_loc",
                 "business_dec", "business_name");
         GenericDocument businessGeneric = GenericDocument.fromDocumentClass(business);
+        // At runtime, business is type of BusinessImpl. As a result, the list of parent types
+        // for it should contain Business.
+        businessGeneric = businessGeneric.toBuilder().setParentTypes(new ArrayList<>(
+                Arrays.asList("Business", "Place", "Organization", "InterfaceRoot"))).build();
         assertThat(businessGeneric.getId()).isEqualTo("id3");
         assertThat(businessGeneric.getNamespace()).isEqualTo("namespace");
         assertThat(businessGeneric.getCreationTimestampMillis()).isEqualTo(4000);
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
index c2022c1..b835033 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
@@ -29,6 +29,8 @@
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
 import androidx.appsearch.testutil.AppSearchEmail;
+import androidx.appsearch.util.DocumentIdUtil;
+import androidx.test.core.app.ApplicationProvider;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -37,6 +39,9 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
@@ -205,6 +210,592 @@
         assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
     }
 
+    // TODO(b/296088047): move to CTS once the APIs it uses are public
+    @Test
+    public void testQuery_typePropertyFilters() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example subject with some body")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Query with type property filters {"Email", ["subject", "to"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        // Only email2 should be returned because email1 doesn't have the term "body" in subject
+        // or to fields
+        assertThat(documents).containsExactly(email2);
+    }
+
+    // TODO(b/296088047): move to CTS once the APIs it uses are public
+    @Test
+    public void testQuery_typePropertyFiltersWithDifferentSchemaTypes() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"Email": ["subject", "to"], "Note": ["body"]}. Note
+        // schema has body in its property filter but Email schema doesn't.
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .addFilterProperties("Note", ImmutableList.of("body"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        // Only the note document should be returned because the email property filter doesn't
+        // allow searching in the body.
+        assertThat(documents).containsExactly(note);
+    }
+
+    // TODO(b/296088047): move to CTS once the APIs it uses are public
+    @Test
+    public void testQuery_typePropertyFiltersWithWildcard() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example subject with some body")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"*": ["subject", "title"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
+                        ImmutableList.of("subject", "title"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        // The wildcard property filter will apply to both the Email and Note schema. The email
+        // document should be returned since it has the term "body" in its subject property. The
+        // note document should not be returned since it doesn't have the term "body" in the title
+        // property (subject property is not applicable for Note schema)
+        assertThat(documents).containsExactly(email);
+    }
+
+    // TODO(b/296088047): move to CTS once the APIs it uses are public
+    @Test
+    public void testQuery_typePropertyFiltersWithWildcardAndExplicitSchema() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example subject with some body")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"*": ["subject", "title"], "Note": ["body"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
+                        ImmutableList.of("subject", "title"))
+                .addFilterProperties("Note", ImmutableList.of("body"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        // The wildcard property filter will only apply to the Email schema since Note schema has
+        // its own explicit property filter specified. The email document should be returned since
+        // it has the term "body" in its subject property. The note document should also be returned
+        // since it has the term "body" in the body property.
+        assertThat(documents).containsExactly(email, note);
+    }
+
+    // TODO(b/296088047): move to CTS once the APIs it uses are public
+    @Test
+    public void testQuery_typePropertyFiltersNonExistentType() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example subject with some body")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"NonExistentType": ["to", "title"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties("NonExistentType", ImmutableList.of("to", "title"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        // The supplied property filters don't apply to either schema types. Both the documents
+        // should be returned since the term "body" is present in at least one of their properties.
+        assertThat(documents).containsExactly(email, note);
+    }
+
+    // TODO(b/296088047): move to CTS once the APIs it uses are public
+    @Test
+    public void testQuery_typePropertyFiltersEmpty() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"email": []}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        // The email document should not be returned since the property filter doesn't allow
+        // searching any property.
+        assertThat(documents).containsExactly(note);
+    }
+
+    // TODO(b/296088047): move to CTS once the APIs it uses are public
+    @Test
+    public void testQueryWithJoin_typePropertyFiltersOnNestedSpec() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // A full example of how join might be used with property filters in join spec
+        AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
+                .addProperty(new StringPropertyConfig.Builder("entityId")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setJoinableValueType(StringPropertyConfig
+                                .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("note")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("viewType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
+                        .build()).get();
+
+        // Index 2 email documents
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        // Index 2 viewAction documents, one for email1 and the other for email2
+        String qualifiedId1 =
+                DocumentIdUtil.createQualifiedId(
+                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+                        "namespace", "id1");
+        String qualifiedId2 =
+                DocumentIdUtil.createQualifiedId(
+                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+                        "namespace", "id2");
+        GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
+                .setPropertyString("entityId", qualifiedId1)
+                .setPropertyString("note", "Viewed email on Monday")
+                .setPropertyString("viewType", "Stared").build();
+        GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
+                .setPropertyString("entityId", qualifiedId2)
+                .setPropertyString("note", "Viewed email on Tuesday")
+                .setPropertyString("viewType", "Viewed").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
+                                viewAction1, viewAction2)
+                        .build()));
+
+        // The nested search spec only allows searching the viewType property for viewAction
+        // schema type. It also specifies a property filter for Email schema.
+        SearchSpec nestedSearchSpec =
+                new SearchSpec.Builder()
+                        .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
+                        .addFilterProperties(AppSearchEmail.SCHEMA_TYPE,
+                                ImmutableList.of("subject"))
+                        .build();
+
+        // Search for the term "Viewed" in join spec
+        JoinSpec js = new JoinSpec.Builder("entityId")
+                .setNestedSearch("Viewed", nestedSearchSpec)
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .build();
+
+        SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+                .setJoinSpec(js)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+
+        List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+        // Both email docs are returned, email2 comes first because it has higher number of
+        // joined documents. The property filters for Email schema specified in the nested search
+        // specs don't apply to the outer query (otherwise none of the email documents would have
+        // been returned).
+        assertThat(sr).hasSize(2);
+
+        // Email2 has a viewAction document viewAction2 that satisfies the property filters in
+        // the join spec, so it should be present in the joined results.
+        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
+        assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
+        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+        assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
+
+        // Email1 has a viewAction document viewAction1 but it doesn't satisfy the property filters
+        // in the join spec, so it should not be present in the joined results.
+        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
+        assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
+        assertThat(sr.get(1).getJoinedResults()).isEmpty();
+    }
+
+    // TODO(b/296088047): move to CTS once the APIs it uses are public
+    @Test
+    public void testQueryWithJoin_typePropertyFiltersOnOuterSpec() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // A full example of how join might be used with property filters in join spec
+        AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
+                .addProperty(new StringPropertyConfig.Builder("entityId")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setJoinableValueType(StringPropertyConfig
+                                .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("note")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("viewType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
+                        .build()).get();
+
+        // Index 2 email documents
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        // Index 2 viewAction documents, one for email1 and the other for email2
+        String qualifiedId1 =
+                DocumentIdUtil.createQualifiedId(
+                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+                        "namespace", "id1");
+        String qualifiedId2 =
+                DocumentIdUtil.createQualifiedId(
+                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+                        "namespace", "id2");
+        GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
+                .setPropertyString("entityId", qualifiedId1)
+                .setPropertyString("note", "Viewed email on Monday")
+                .setPropertyString("viewType", "Stared").build();
+        GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
+                .setPropertyString("entityId", qualifiedId2)
+                .setPropertyString("note", "Viewed email on Tuesday")
+                .setPropertyString("viewType", "Viewed").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
+                                viewAction1, viewAction2)
+                        .build()));
+
+        // The nested search spec doesn't specify any property filters.
+        SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
+
+        // Search for the term "Viewed" in join spec
+        JoinSpec js = new JoinSpec.Builder("entityId")
+                .setNestedSearch("Viewed", nestedSearchSpec)
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .build();
+
+        // Outer search spec adds property filters for both Email and ViewAction schema
+        SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+                .setJoinSpec(js)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body"))
+                .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
+                .build());
+
+        List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+        // Both email docs are returned as they both satisfy the property filters for Email, email2
+        // comes first because it has higher id lexicographically.
+        assertThat(sr).hasSize(2);
+
+        // Email2 has a viewAction document viewAction2 that satisfies the property filters in
+        // the outer spec (although those property filters are irrelevant for joined documents),
+        // it should be present in the joined results.
+        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
+        assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
+        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+        assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
+
+        // Email1 has a viewAction document viewAction1 that doesn't satisfy the property filters
+        // in the outer spec, but property filters in the outer spec should not apply on joined
+        // documents, so viewAction1 should be present in the joined results.
+        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
+        assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
+        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+        assertThat(sr.get(1).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
+    }
+
+    // TODO(b/296088047): move to CTS once the APIs it uses are public
+    @Test
+    public void testQuery_typePropertyFiltersNotSupported() throws Exception {
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Query with type property filters {"Email", ["subject", "to"]} and verify that unsupported
+        // exception is thrown
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .build();
+        UnsupportedOperationException exception =
+                assertThrows(UnsupportedOperationException.class,
+                        () -> mDb1.search("body", searchSpec));
+        assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+                + " is not available on this AppSearch implementation.");
+    }
+
     // TODO(b/268521214): Move test to cts once deletion propagation is available in framework.
     @Test
     public void testGetSchema_joinableValueType() throws Exception {
@@ -704,4 +1295,371 @@
                                 + " is not available on this"
                                 + " AppSearch implementation.");
     }
+
+    @Test
+    public void testQuery_typeFilterWithPolymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+
+        // Schema registration
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema artistSchema =
+                new AppSearchSchema.Builder("Artist")
+                        .addParentType("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchema)
+                                .addSchemas(artistSchema)
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .build())
+                .get();
+
+        // Index some documents
+        GenericDocument personDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setPropertyString("name", "Foo")
+                        .build();
+        GenericDocument artistDoc =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setPropertyString("name", "Foo")
+                        .build();
+        AppSearchEmail emailDoc =
+                new AppSearchEmail.Builder("namespace", "id3")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("Foo")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(personDoc, artistDoc, emailDoc)
+                                .build()));
+        GenericDocument artistDocWithParent = artistDoc.toBuilder().setParentTypes(
+                Collections.singletonList("Person")).build();
+
+        // Query for the documents
+        SearchResults searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(3);
+        assertThat(documents).containsExactly(personDoc, artistDocWithParent, emailDoc);
+
+        // Query with a filter for the "Person" type should also include the "Artist" type.
+        searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .addFilterSchemas("Person")
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(2);
+        assertThat(documents).containsExactly(personDoc, artistDocWithParent);
+
+        // Query with a filters for the "Artist" type should not include the "Person" type.
+        searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .addFilterSchemas("Artist")
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents).containsExactly(artistDocWithParent);
+    }
+
+    @Test
+    public void testQuery_projectionWithPolymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+
+        // Schema registration
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("emailAddress")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema artistSchema =
+                new AppSearchSchema.Builder("Artist")
+                        .addParentType("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("emailAddress")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("company")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchema)
+                                .addSchemas(artistSchema)
+                                .build())
+                .get();
+
+        // Index two documents
+        GenericDocument personDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Person")
+                        .setPropertyString("emailAddress", "person@gmail.com")
+                        .build();
+        GenericDocument artistDoc =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Artist")
+                        .setPropertyString("emailAddress", "artist@gmail.com")
+                        .setPropertyString("company", "Company")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(personDoc, artistDoc)
+                                .build()));
+
+        // Query with type property paths {"Person", ["name"]}, {"Artist", ["emailAddress"]}
+        // This will be expanded to paths {"Person", ["name"]}, {"Artist", ["name", "emailAddress"]}
+        // via polymorphism.
+        SearchResults searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addProjection("Person", ImmutableList.of("name"))
+                                .addProjection("Artist", ImmutableList.of("emailAddress"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The person document should have been returned with only the "name" property. The artist
+        // document should have been returned with all of its properties.
+        GenericDocument expectedPerson =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Person")
+                        .build();
+        GenericDocument expectedArtist =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setParentTypes(Collections.singletonList("Person"))
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Artist")
+                        .setPropertyString("emailAddress", "artist@gmail.com")
+                        .build();
+        assertThat(documents).containsExactly(expectedPerson, expectedArtist);
+    }
+
+    @Test
+    public void testQuery_indexBasedOnParentTypePolymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+
+        // Schema registration
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema artistSchema =
+                new AppSearchSchema.Builder("Artist")
+                        .addParentType("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("company")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema messageSchema =
+                new AppSearchSchema.Builder("Message")
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "sender", "Person")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setShouldIndexNestedProperties(true)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchema)
+                                .addSchemas(artistSchema)
+                                .addSchemas(messageSchema)
+                                .build())
+                .get();
+
+        // Index some an artistDoc and a messageDoc
+        GenericDocument artistDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "Artist")
+                        .setPropertyString("name", "Foo")
+                        .setPropertyString("company", "Bar")
+                        .build();
+        GenericDocument messageDoc =
+                new GenericDocument.Builder<>("namespace", "id2", "Message")
+                        // sender is defined as a Person, which accepts an Artist because Artist <:
+                        // Person.
+                        // However, indexing will be based on what's defined in Person, so the
+                        // "company"
+                        // property in artistDoc cannot be used to search this messageDoc.
+                        .setPropertyDocument("sender", artistDoc)
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(artistDoc, messageDoc)
+                                .build()));
+        GenericDocument expectedArtistDoc = artistDoc.toBuilder().setParentTypes(
+                Collections.singletonList("Person")).build();
+        GenericDocument expectedMessageDoc = messageDoc.toBuilder().setPropertyDocument("sender",
+                expectedArtistDoc).build();
+
+        // Query for the documents
+        SearchResults searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(2);
+        assertThat(documents).containsExactly(expectedArtistDoc, expectedMessageDoc);
+
+        // The "company" property in artistDoc cannot be used to search messageDoc.
+        searchResults =
+                mDb1.search(
+                        "Bar",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents).containsExactly(expectedArtistDoc);
+    }
+
+    @Test
+    public void testQuery_parentTypeListIsTopologicalOrder() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        // Create the following subtype relation graph, where
+        // 1. A's direct parents are B and C.
+        // 2. B's direct parent is D.
+        // 3. C's direct parent is B and D.
+        // DFS order from A: [A, B, D, C]. Not acceptable because B and D appear before C.
+        // BFS order from A: [A, B, C, D]. Not acceptable because B appears before C.
+        // Topological order (all subtypes appear before supertypes) from A: [A, C, B, D].
+        AppSearchSchema schemaA =
+                new AppSearchSchema.Builder("A")
+                        .addParentType("B")
+                        .addParentType("C")
+                        .build();
+        AppSearchSchema schemaB =
+                new AppSearchSchema.Builder("B")
+                        .addParentType("D")
+                        .build();
+        AppSearchSchema schemaC =
+                new AppSearchSchema.Builder("C")
+                        .addParentType("B")
+                        .addParentType("D")
+                        .build();
+        AppSearchSchema schemaD =
+                new AppSearchSchema.Builder("D")
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(schemaA)
+                                .addSchemas(schemaB)
+                                .addSchemas(schemaC)
+                                .addSchemas(schemaD)
+                                .build())
+                .get();
+
+        // Index some documents
+        GenericDocument docA =
+                new GenericDocument.Builder<>("namespace", "id1", "A")
+                        .build();
+        GenericDocument docB =
+                new GenericDocument.Builder<>("namespace", "id2", "B")
+                        .build();
+        GenericDocument docC =
+                new GenericDocument.Builder<>("namespace", "id3", "C")
+                        .build();
+        GenericDocument docD =
+                new GenericDocument.Builder<>("namespace", "id4", "D")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(docA, docB, docC, docD)
+                                .build()));
+
+        GenericDocument expectedDocA =
+                docA.toBuilder().setParentTypes(
+                        new ArrayList<>(Arrays.asList("C", "B", "D"))).build();
+        GenericDocument expectedDocB =
+                docB.toBuilder().setParentTypes(
+                        Collections.singletonList("D")).build();
+        GenericDocument expectedDocC =
+                docC.toBuilder().setParentTypes(
+                        new ArrayList<>(Arrays.asList("B", "D"))).build();
+        // Query for the documents
+        SearchResults searchResults = mDb1.search("", new SearchSpec.Builder().build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(4);
+        assertThat(documents).containsExactly(expectedDocA, expectedDocB, expectedDocC, docD);
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
index 8197731..8161c1c 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
@@ -18,13 +18,12 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertThrows;
-
 import android.os.Bundle;
 import android.os.Parcel;
 
 import org.junit.Test;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 
 /** Tests for private APIs of {@link GenericDocument}. */
@@ -68,46 +67,42 @@
     }
 
     @Test
-    public void testPropertyParcel_onePropertySet_success() {
-        String[] stringValues = {"a", "b"};
-        long[] longValues = {1L, 2L};
-        double[] doubleValues = {1.0, 2.0};
-        boolean[] booleanValues = {true, false};
-        byte[][] bytesValues = {new byte[1]};
-        Bundle[] bundleValues = {new Bundle()};
+    public void testRecreateFromParcelWithParentTypes() {
+        GenericDocument inDoc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setParentTypes(new ArrayList<>(Arrays.asList("Class1", "Class2")))
+                .setScore(42)
+                .setPropertyString("propString", "Hello")
+                .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
+                .setPropertyDocument(
+                        "propDocument",
+                        new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                                .setPropertyString("propString", "Goodbye")
+                                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                                .build())
+                .build();
 
-        assertThat(new PropertyParcel.Builder("name").setStringValues(
-                stringValues).build().getStringValues()).isEqualTo(
-                Arrays.copyOf(stringValues, stringValues.length));
-        assertThat(new PropertyParcel.Builder("name").setLongValues(
-                longValues).build().getLongValues()).isEqualTo(
-                Arrays.copyOf(longValues, longValues.length));
-        assertThat(new PropertyParcel.Builder("name").setDoubleValues(
-                doubleValues).build().getDoubleValues()).isEqualTo(
-                Arrays.copyOf(doubleValues, doubleValues.length));
-        assertThat(new PropertyParcel.Builder("name").setBooleanValues(
-                booleanValues).build().getBooleanValues()).isEqualTo(
-                Arrays.copyOf(booleanValues, booleanValues.length));
-        assertThat(new PropertyParcel.Builder("name").setBytesValues(
-                bytesValues).build().getBytesValues()).isEqualTo(
-                Arrays.copyOf(bytesValues, bytesValues.length));
-        assertThat(new PropertyParcel.Builder("name").setDocumentValues(
-                bundleValues).build().getDocumentValues()).isEqualTo(
-                Arrays.copyOf(bundleValues, bundleValues.length));
-    }
+        // Serialize the document
+        Parcel inParcel = Parcel.obtain();
+        inParcel.writeBundle(inDoc.getBundle());
+        byte[] data = inParcel.marshall();
+        inParcel.recycle();
 
-    @Test
-    public void testPropertyParcel_moreThanOnePropertySet_exceptionThrown() {
-        String[] stringValues = {"a", "b"};
-        long[] longValues = {1L, 2L};
-        PropertyParcel.Builder propertyParcelBuilder =
-                new PropertyParcel.Builder("name")
-                        .setStringValues(stringValues)
-                        .setLongValues(longValues);
+        // Deserialize the document
+        Parcel outParcel = Parcel.obtain();
+        outParcel.unmarshall(data, 0, data.length);
+        outParcel.setDataPosition(0);
+        Bundle outBundle = outParcel.readBundle();
+        outParcel.recycle();
 
-        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
-                () -> propertyParcelBuilder.build());
-
-        assertThat(exception.getMessage()).contains("One and only one type array");
+        // Compare results
+        GenericDocument outDoc = new GenericDocument(outBundle);
+        assertThat(inDoc).isEqualTo(outDoc);
+        assertThat(outDoc.getParentTypes()).isEqualTo(Arrays.asList("Class1", "Class2"));
+        assertThat(outDoc.getPropertyString("propString")).isEqualTo("Hello");
+        assertThat(outDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
+        assertThat(outDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
+                .isEqualTo("Goodbye");
+        assertThat(outDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
+                .isEqualTo(new byte[][]{{3, 4}});
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
index 106c244..ed00e5a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
@@ -18,10 +18,17 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+
 import android.os.Bundle;
 
+import com.google.common.collect.ImmutableList;
+
 import org.junit.Test;
 
+import java.util.List;
+import java.util.Map;
+
 /** Tests for private APIs of {@link SearchSpec}. */
 public class SearchSpecInternalTest {
 
@@ -82,4 +89,38 @@
         assertThat(searchSpec3.getEnabledFeatures()).containsExactly(
                 Features.VERBATIM_SEARCH, Features.LIST_FILTER_QUERY_LANGUAGE);
     }
+
+    // TODO(b/296088047): move to CTS once the APIs it uses are public
+    @Test
+    public void testGetPropertyFiltersTypePropertyMasks() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addFilterProperties("TypeA", ImmutableList.of("field1", "field2.subfield2"))
+                .addFilterProperties("TypeB", ImmutableList.of("field7"))
+                .addFilterProperties("TypeC", ImmutableList.of())
+                .build();
+
+        Map<String, List<String>> typePropertyPathMap = searchSpec.getFilterProperties();
+        assertThat(typePropertyPathMap.keySet())
+                .containsExactly("TypeA", "TypeB", "TypeC");
+        assertThat(typePropertyPathMap.get("TypeA")).containsExactly("field1", "field2.subfield2");
+        assertThat(typePropertyPathMap.get("TypeB")).containsExactly("field7");
+        assertThat(typePropertyPathMap.get("TypeC")).isEmpty();
+    }
+
+    // TODO(b/296088047): move to CTS once the APIs it uses are public
+    @Test
+    public void testBuilder_throwsException_whenTypePropertyFilterNotInSchemaFilter() {
+        SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addFilterSchemas("Schema1", "Schema2")
+                .addFilterPropertyPaths("Schema3", ImmutableList.of(
+                        new PropertyPath("field1"), new PropertyPath("field2.subfield2")));
+
+        IllegalStateException exception =
+                assertThrows(IllegalStateException.class, searchSpecBuilder::build);
+        assertThat(exception.getMessage())
+                .isEqualTo("The schema: Schema3 exists in the property filter but doesn't"
+                        + " exist in the schema filter.");
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/safeparcel/GenericDocumentParcelTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/safeparcel/GenericDocumentParcelTest.java
new file mode 100644
index 0000000..47df245
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/safeparcel/GenericDocumentParcelTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.appsearch.app.safeparcel;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/** Tests for {@link androidx.appsearch.app.GenericDocument} related SafeParcels. */
+public class GenericDocumentParcelTest {
+    @Test
+    public void testPropertyParcel_onePropertySet_success() {
+        String[] stringValues = {"a", "b"};
+        long[] longValues = {1L, 2L};
+        double[] doubleValues = {1.0, 2.0};
+        boolean[] booleanValues = {true, false};
+        byte[][] bytesValues = {new byte[1]};
+        GenericDocumentParcel[] docValues = {(new GenericDocumentParcel.Builder(
+                "namespace", "id", "schemaType")).build()};
+
+        assertThat(new PropertyParcel.Builder("name").setStringValues(
+                stringValues).build().getStringValues()).isEqualTo(
+                Arrays.copyOf(stringValues, stringValues.length));
+        assertThat(new PropertyParcel.Builder("name").setLongValues(
+                longValues).build().getLongValues()).isEqualTo(
+                Arrays.copyOf(longValues, longValues.length));
+        assertThat(new PropertyParcel.Builder("name").setDoubleValues(
+                doubleValues).build().getDoubleValues()).isEqualTo(
+                Arrays.copyOf(doubleValues, doubleValues.length));
+        assertThat(new PropertyParcel.Builder("name").setBooleanValues(
+                booleanValues).build().getBooleanValues()).isEqualTo(
+                Arrays.copyOf(booleanValues, booleanValues.length));
+        assertThat(new PropertyParcel.Builder("name").setBytesValues(
+                bytesValues).build().getBytesValues()).isEqualTo(
+                Arrays.copyOf(bytesValues, bytesValues.length));
+        assertThat(new PropertyParcel.Builder("name").setDocumentValues(
+                docValues).build().getDocumentValues()).isEqualTo(
+                Arrays.copyOf(docValues, docValues.length));
+    }
+
+    @Test
+    public void testPropertyParcel_moreThanOnePropertySet_exceptionThrown() {
+        String[] stringValues = {"a", "b"};
+        long[] longValues = {1L, 2L};
+        PropertyParcel.Builder propertyParcelBuilder =
+                new PropertyParcel.Builder("name")
+                        .setStringValues(stringValues)
+                        .setLongValues(longValues);
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> propertyParcelBuilder.build());
+
+        assertThat(exception.getMessage()).contains("One and only one type array");
+    }
+
+    @Test
+    public void testGenericDocumentParcel_propertiesGeneratedCorrectly() {
+        GenericDocumentParcel.Builder builder =
+                new GenericDocumentParcel.Builder(
+                        /*namespace=*/ "namespace",
+                        /*id=*/ "id",
+                        /*schemaType=*/ "schemaType");
+        long[] longArray = new long[]{1L, 2L, 3L};
+        String[] stringArray = new String[]{"hello", "world", "!"};
+        builder.putInPropertyMap(/*name=*/ "longArray", /*values=*/ longArray);
+        builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
+        GenericDocumentParcel genericDocumentParcel = builder.build();
+
+        PropertyParcel[] properties = genericDocumentParcel.getProperties();
+        Map<String, PropertyParcel> propertyMap = genericDocumentParcel.getPropertyMap();
+        PropertyParcel longArrayProperty = new PropertyParcel.Builder(
+                /*name=*/ "longArray").setLongValues(longArray).build();
+        PropertyParcel stringArrayProperty = new PropertyParcel.Builder(
+                /*name=*/ "stringArray").setStringValues(stringArray).build();
+
+        assertThat(properties).asList().containsExactly(longArrayProperty, stringArrayProperty);
+        assertThat(propertyMap).containsExactly("longArray", longArrayProperty,
+                "stringArray", stringArrayProperty);
+    }
+
+    @Test
+    public void testGenericDocumentParcel_buildFromAnotherDocumentParcelCorrectly() {
+        GenericDocumentParcel.Builder builder =
+                new GenericDocumentParcel.Builder(
+                        /*namespace=*/ "namespace",
+                        /*id=*/ "id",
+                        /*schemaType=*/ "schemaType");
+        long[] longArray = new long[]{1L, 2L, 3L};
+        String[] stringArray = new String[]{"hello", "world", "!"};
+        builder.putInPropertyMap(/*name=*/ "longArray", /*values=*/ longArray);
+        builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
+        GenericDocumentParcel genericDocumentParcel = builder.build();
+
+        GenericDocumentParcel genericDocumentParcelCopy =
+                new GenericDocumentParcel.Builder(genericDocumentParcel).build();
+
+        assertThat(genericDocumentParcelCopy.getNamespace()).isEqualTo(
+                genericDocumentParcel.getNamespace());
+        assertThat(genericDocumentParcelCopy.getId()).isEqualTo(genericDocumentParcel.getId());
+        assertThat(genericDocumentParcelCopy.getSchemaType()).isEqualTo(
+                genericDocumentParcel.getSchemaType());
+        assertThat(genericDocumentParcelCopy.getCreationTimestampMillis()).isEqualTo(
+                genericDocumentParcel.getCreationTimestampMillis());
+        assertThat(genericDocumentParcelCopy.getTtlMillis()).isEqualTo(
+                genericDocumentParcel.getTtlMillis());
+        assertThat(genericDocumentParcelCopy.getScore()).isEqualTo(
+                genericDocumentParcel.getScore());
+        // Check it is a copy.
+        assertThat(genericDocumentParcelCopy).isNotSameInstanceAs(genericDocumentParcel);
+        assertThat(genericDocumentParcelCopy.getProperties()).isEqualTo(
+                genericDocumentParcel.getProperties());
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
index 84661ff..4118c45 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
@@ -327,7 +327,6 @@
                 + "    {\n"
                 + "      name: \"document\",\n"
                 + "      shouldIndexNestedProperties: true,\n"
-                + "      indexableNestedProperties: [],\n"
                 + "      schemaType: \"builtin:Email\",\n"
                 + "      cardinality: CARDINALITY_REPEATED,\n"
                 + "      dataType: DATA_TYPE_DOCUMENT,\n"
@@ -408,7 +407,10 @@
                 + "  ]\n"
                 + "}";
 
-        assertThat(schemaString).isEqualTo(expectedString);
+        String[] lines = expectedString.split("\n");
+        for (String line : lines) {
+            assertThat(schemaString).contains(line);
+        }
     }
 
     @Test
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index 7bde81f..ccfaa65 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -5761,295 +5761,6 @@
     }
 
     @Test
-    public void testQuery_typeFilterWithPolymorphism() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
-
-        // Schema registration
-        AppSearchSchema personSchema =
-                new AppSearchSchema.Builder("Person")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("name")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        AppSearchSchema artistSchema =
-                new AppSearchSchema.Builder("Artist")
-                        .addParentType("Person")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("name")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder()
-                                .addSchemas(personSchema)
-                                .addSchemas(artistSchema)
-                                .addSchemas(AppSearchEmail.SCHEMA)
-                                .build())
-                .get();
-
-        // Index some documents
-        GenericDocument personDoc =
-                new GenericDocument.Builder<>("namespace", "id1", "Person")
-                        .setPropertyString("name", "Foo")
-                        .build();
-        GenericDocument artistDoc =
-                new GenericDocument.Builder<>("namespace", "id2", "Artist")
-                        .setPropertyString("name", "Foo")
-                        .build();
-        AppSearchEmail emailDoc =
-                new AppSearchEmail.Builder("namespace", "id3")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("Foo")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder()
-                                .addGenericDocuments(personDoc, artistDoc, emailDoc)
-                                .build()));
-
-        // Query for the documents
-        SearchResults searchResults =
-                mDb1.search(
-                        "Foo",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(3);
-        assertThat(documents).containsExactly(personDoc, artistDoc, emailDoc);
-
-        // Query with a filter for the "Person" type should also include the "Artist" type.
-        searchResults =
-                mDb1.search(
-                        "Foo",
-                        new SearchSpec.Builder()
-                                .addFilterSchemas("Person")
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(2);
-        assertThat(documents).containsExactly(personDoc, artistDoc);
-
-        // Query with a filters for the "Artist" type should not include the "Person" type.
-        searchResults =
-                mDb1.search(
-                        "Foo",
-                        new SearchSpec.Builder()
-                                .addFilterSchemas("Artist")
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        assertThat(documents).containsExactly(artistDoc);
-    }
-
-    @Test
-    public void testQuery_projectionWithPolymorphism() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
-
-        // Schema registration
-        AppSearchSchema personSchema =
-                new AppSearchSchema.Builder("Person")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("name")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("emailAddress")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        AppSearchSchema artistSchema =
-                new AppSearchSchema.Builder("Artist")
-                        .addParentType("Person")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("name")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("emailAddress")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("company")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder()
-                                .addSchemas(personSchema)
-                                .addSchemas(artistSchema)
-                                .build())
-                .get();
-
-        // Index two documents
-        GenericDocument personDoc =
-                new GenericDocument.Builder<>("namespace", "id1", "Person")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("name", "Foo Person")
-                        .setPropertyString("emailAddress", "person@gmail.com")
-                        .build();
-        GenericDocument artistDoc =
-                new GenericDocument.Builder<>("namespace", "id2", "Artist")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("name", "Foo Artist")
-                        .setPropertyString("emailAddress", "artist@gmail.com")
-                        .setPropertyString("company", "Company")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder()
-                                .addGenericDocuments(personDoc, artistDoc)
-                                .build()));
-
-        // Query with type property paths {"Person", ["name"]}, {"Artist", ["emailAddress"]}
-        // This will be expanded to paths {"Person", ["name"]}, {"Artist", ["name", "emailAddress"]}
-        // via polymorphism.
-        SearchResults searchResults =
-                mDb1.search(
-                        "Foo",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .addProjection("Person", ImmutableList.of("name"))
-                                .addProjection("Artist", ImmutableList.of("emailAddress"))
-                                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-
-        // The person document should have been returned with only the "name" property. The artist
-        // document should have been returned with all of its properties.
-        GenericDocument expectedPerson =
-                new GenericDocument.Builder<>("namespace", "id1", "Person")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("name", "Foo Person")
-                        .build();
-        GenericDocument expectedArtist =
-                new GenericDocument.Builder<>("namespace", "id2", "Artist")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("name", "Foo Artist")
-                        .setPropertyString("emailAddress", "artist@gmail.com")
-                        .build();
-        assertThat(documents).containsExactly(expectedPerson, expectedArtist);
-    }
-
-    @Test
-    public void testQuery_indexBasedOnParentTypePolymorphism() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
-
-        // Schema registration
-        AppSearchSchema personSchema =
-                new AppSearchSchema.Builder("Person")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("name")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        AppSearchSchema artistSchema =
-                new AppSearchSchema.Builder("Artist")
-                        .addParentType("Person")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("name")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("company")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        AppSearchSchema messageSchema =
-                new AppSearchSchema.Builder("Message")
-                        .addProperty(
-                                new AppSearchSchema.DocumentPropertyConfig.Builder(
-                                        "sender", "Person")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setShouldIndexNestedProperties(true)
-                                        .build())
-                        .build();
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder()
-                                .addSchemas(personSchema)
-                                .addSchemas(artistSchema)
-                                .addSchemas(messageSchema)
-                                .build())
-                .get();
-
-        // Index some an artistDoc and a messageDoc
-        GenericDocument artistDoc =
-                new GenericDocument.Builder<>("namespace", "id1", "Artist")
-                        .setPropertyString("name", "Foo")
-                        .setPropertyString("company", "Bar")
-                        .build();
-        GenericDocument messageDoc =
-                new GenericDocument.Builder<>("namespace", "id2", "Message")
-                        // sender is defined as a Person, which accepts an Artist because Artist <:
-                        // Person.
-                        // However, indexing will be based on what's defined in Person, so the
-                        // "company"
-                        // property in artistDoc cannot be used to search this messageDoc.
-                        .setPropertyDocument("sender", artistDoc)
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder()
-                                .addGenericDocuments(artistDoc, messageDoc)
-                                .build()));
-
-        // Query for the documents
-        SearchResults searchResults =
-                mDb1.search(
-                        "Foo",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(2);
-        assertThat(documents).containsExactly(artistDoc, messageDoc);
-
-        // The "company" property in artistDoc cannot be used to search messageDoc.
-        searchResults =
-                mDb1.search(
-                        "Bar",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        assertThat(documents).containsExactly(artistDoc);
-    }
-
-    @Test
     public void testSetSchema_indexableNestedPropsList() throws Exception {
         assumeTrue(
                 mDb1.getFeatures()
@@ -6704,4 +6415,42 @@
         assertThat(outDocuments).hasSize(1);
         assertThat(outDocuments).containsExactly(org2);
     }
+
+    @Test
+    public void testSetSchema_toString_containsIndexableNestedPropsList() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures()
+                        .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
+
+        AppSearchSchema emailSchema =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "sender", "Person")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                                        .setShouldIndexNestedProperties(false)
+                                        .addIndexableNestedProperties(
+                                                Arrays.asList(
+                                                        "name", "worksFor.name", "worksFor.notes"))
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "recipient", "Person")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                                        .setShouldIndexNestedProperties(true)
+                                        .build())
+                        .build();
+        String expectedIndexableNestedPropertyMessage =
+                "indexableNestedProperties: [name, worksFor.notes, worksFor.name]";
+
+        assertThat(emailSchema.toString()).contains(expectedIndexableNestedPropertyMessage);
+
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/DocumentIdUtilCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/DocumentIdUtilCtsTest.java
index 89d2180..f3cb395 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/DocumentIdUtilCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/DocumentIdUtilCtsTest.java
@@ -23,6 +23,7 @@
 
 import org.junit.Test;
 
+/*@exportToFramework:SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)*/
 public class DocumentIdUtilCtsTest {
 
     @Test
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/JoinSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/JoinSpecCtsTest.java
index a58b892..c04d448 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/JoinSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/JoinSpecCtsTest.java
@@ -23,6 +23,7 @@
 
 import org.junit.Test;
 
+/*@exportToFramework:SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)*/
 public class JoinSpecCtsTest {
 
     @Test
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PropertyPathCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PropertyPathCtsTest.java
index b7ed1f8..69a94d3 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PropertyPathCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PropertyPathCtsTest.java
@@ -17,7 +17,6 @@
 package androidx.appsearch.cts.app;
 
 import static androidx.appsearch.app.PropertyPath.PathSegment.NON_REPEATED_CARDINALITY;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import androidx.appsearch.app.PropertyPath;
@@ -30,6 +29,7 @@
 import java.util.Iterator;
 import java.util.List;
 
+/*@exportToFramework:SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)*/
 public class PropertyPathCtsTest {
     @Test
     public void testPropertyPathInvalid() {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
index 426d01a..24d0153 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
@@ -17,7 +17,6 @@
 package androidx.appsearch.cts.app;
 
 import static com.google.common.truth.Truth.assertThat;
-
 import static org.junit.Assert.assertThrows;
 
 import androidx.appsearch.app.PropertyPath;
@@ -107,6 +106,7 @@
     }
 
     @Test
+    /*@exportToFramework:SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)*/
     public void testJoinedDocument() {
         AppSearchEmail email = new AppSearchEmail.Builder("namespace1", "id1")
                 .setBody("Hello World.")
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionResultCtsTest.java
index 66fb2ad..675b507 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionResultCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionResultCtsTest.java
@@ -22,6 +22,7 @@
 
 import org.junit.Test;
 
+/*@exportToFramework:SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)*/
 public class SearchSuggestionResultCtsTest {
     @Test
     public void testBuildDefaultSearchSuggestionResult() {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java
index 69979c3..d3dd5c3 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java
@@ -17,7 +17,6 @@
 package androidx.appsearch.cts.app;
 
 import static com.google.common.truth.Truth.assertThat;
-
 import static org.junit.Assert.assertThrows;
 
 import androidx.appsearch.app.SearchSuggestionSpec;
@@ -26,6 +25,7 @@
 
 import org.junit.Test;
 
+/*@exportToFramework:SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)*/
 public class SearchSuggestionSpecCtsTest {
     @Test
     public void testBuildDefaultSearchSuggestionSpec() throws Exception {
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
index 6a198be..012c789 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -16,6 +16,7 @@
 package androidx.appsearch.app;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 
 /**
  * A class that encapsulates all features that are only supported in certain cases (e.g. only on
@@ -110,13 +111,22 @@
      */
     String SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA = "SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA";
 
-    /** Feature for {@link #isFeatureSupported(String)}. This feature covers
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
      * {@link SearchSpec.Builder#setPropertyWeights}.
      */
     String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";
 
     /**
      * Feature for {@link #isFeatureSupported(String)}. This feature covers
+     * {@link SearchSpec.Builder#addFilterProperties}.
+     * @exportToFramework:hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
      * {@link SearchSpec.Builder#setRankingStrategy(String)}.
      */
     String SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION = "SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION";
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index 6dcecce..cdb3004 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -77,6 +77,10 @@
     private static final String TTL_MILLIS_FIELD = "ttlMillis";
     private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";
     private static final String NAMESPACE_FIELD = "namespace";
+    private static final String PARENT_TYPES_FIELD = "parentTypes";
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static final String PARENT_TYPES_SYNTHETIC_PROPERTY = "$$__AppSearch__parentTypes";
 
     /**
      * The maximum number of indexed properties a document can have.
@@ -190,6 +194,22 @@
     }
 
     /**
+     * Returns the list of parent types of the {@link GenericDocument}'s type.
+     *
+     * <p>It is guaranteed that child types appear before parent types in the list.
+     * <!--@exportToFramework:hide-->
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Nullable
+    public List<String> getParentTypes() {
+        List<String> result = mBundle.getStringArrayList(PARENT_TYPES_FIELD);
+        if (result == null) {
+            return null;
+        }
+        return Collections.unmodifiableList(result);
+    }
+
+    /**
      * Returns the creation timestamp of the {@link GenericDocument}, in milliseconds.
      *
      * <p>The value is in the {@link System#currentTimeMillis} time base.
@@ -979,6 +999,10 @@
         builder.append("id: \"").append(getId()).append("\",\n");
         builder.append("score: ").append(getScore()).append(",\n");
         builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
+        List<String> parentTypes = getParentTypes();
+        if (parentTypes != null) {
+            builder.append("parentTypes: ").append(parentTypes).append("\n");
+        }
         builder
                 .append("creationTimestampMillis: ")
                 .append(getCreationTimestampMillis())
@@ -1170,6 +1194,23 @@
         }
 
         /**
+         * Sets the list of parent types of the {@link GenericDocument}'s type.
+         *
+         * <p>Child types must appear before parent types in the list.
+         * <!--@exportToFramework:hide-->
+         */
+        @CanIgnoreReturnValue
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @NonNull
+        public BuilderType setParentTypes(@NonNull List<String> parentTypes) {
+            Preconditions.checkNotNull(parentTypes);
+            resetIfBuilt();
+            mBundle.putStringArrayList(GenericDocument.PARENT_TYPES_FIELD,
+                    new ArrayList<>(parentTypes));
+            return mBuilderTypeInstance;
+        }
+
+        /**
          * Sets the score of the {@link GenericDocument}.
          *
          * <p>The score is a query-independent measure of the document's quality, relative to
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyParcel.java
deleted file mode 100644
index 7e6fa88..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyParcel.java
+++ /dev/null
@@ -1,321 +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.appsearch.app;
-
-
-import android.os.Bundle;
-import android.os.Parcel;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.appsearch.safeparcel.AbstractSafeParcelable;
-import androidx.appsearch.safeparcel.SafeParcelable;
-import androidx.appsearch.safeparcel.stub.StubCreators.PropertyParcelCreator;
-import androidx.appsearch.util.BundleUtil;
-
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * A {@link SafeParcelable} to hold the value of a property in {@code GenericDocument#mProperties}.
- *
- * <p>This resembles PropertyProto in IcingLib.
- *
- * @exportToFramework:hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SafeParcelable.Class(creator = "PropertyParcelCreator")
-public final class PropertyParcel extends AbstractSafeParcelable {
-    @NonNull public static final PropertyParcelCreator CREATOR = new PropertyParcelCreator();
-
-    @NonNull
-    @Field(id = 1, getter = "getPropertyName")
-    private final String mPropertyName;
-
-    @Nullable
-    @Field(id = 2, getter = "getStringValues")
-    private final String[] mStringValues;
-
-    @Nullable
-    @Field(id = 3, getter = "getLongValues")
-    private final long[] mLongValues;
-
-    @Nullable
-    @Field(id = 4, getter = "getDoubleValues")
-    private final double[] mDoubleValues;
-
-    @Nullable
-    @Field(id = 5, getter = "getBooleanValues")
-    private final boolean[] mBooleanValues;
-
-    @Nullable
-    @Field(id = 6, getter = "getBytesValues")
-    private final byte[][] mBytesValues;
-
-    // TODO(b/24205844) Change it to GenericDocumentParcel once it is added.
-    @Nullable
-    @Field(id = 7, getter = "getDocumentValues")
-    private final Bundle[] mDocumentValues;
-
-    @Nullable private Integer mHashCode;
-
-    @Constructor
-    PropertyParcel(
-            @Param(id = 1) @NonNull String propertyName,
-            @Param(id = 2) @Nullable String[] stringValues,
-            @Param(id = 3) @Nullable long[] longValues,
-            @Param(id = 4) @Nullable double[] doubleValues,
-            @Param(id = 5) @Nullable boolean[] booleanValues,
-            @Param(id = 6) @Nullable byte[][] bytesValues,
-            @Param(id = 7) @Nullable Bundle[] documentValues) {
-        mPropertyName = Objects.requireNonNull(propertyName);
-        mStringValues = stringValues;
-        mLongValues = longValues;
-        mDoubleValues = doubleValues;
-        mBooleanValues = booleanValues;
-        mBytesValues = bytesValues;
-        mDocumentValues = documentValues;
-        checkOnlyOneArrayCanBeSet();
-    }
-
-    /** Returns the name of the property. */
-    @NonNull
-    public String getPropertyName() {
-        return mPropertyName;
-    }
-
-    /** Returns {@code String} values in an array. */
-    @Nullable
-    public String[] getStringValues() {
-        return mStringValues;
-    }
-
-    /** Returns {@code long} values in an array. */
-    @Nullable
-    public long[] getLongValues() {
-        return mLongValues;
-    }
-
-    /** Returns {@code double} values in an array. */
-    @Nullable
-    public double[] getDoubleValues() {
-        return mDoubleValues;
-    }
-
-    /** Returns {@code boolean} values in an array. */
-    @Nullable
-    public boolean[] getBooleanValues() {
-        return mBooleanValues;
-    }
-
-    /** Returns a two-dimension {@code byte} array. */
-    @Nullable
-    public byte[][] getBytesValues() {
-        return mBytesValues;
-    }
-
-    /** Returns {@link Bundle} in an array. */
-    @Nullable
-    public Bundle[] getDocumentValues() {
-        return mDocumentValues;
-    }
-
-    /**
-     * Returns the held values in an array for this property.
-     *
-     * <p>Different from other getter methods, this one will return an {@link Object}.
-     */
-    @Nullable
-    public Object getValues() {
-        if (mStringValues != null) {
-            return mStringValues;
-        }
-        if (mLongValues != null) {
-            return mLongValues;
-        }
-        if (mDoubleValues != null) {
-            return mDoubleValues;
-        }
-        if (mBooleanValues != null) {
-            return mBooleanValues;
-        }
-        if (mBytesValues != null) {
-            return mBytesValues;
-        }
-        if (mDocumentValues != null) {
-            return mDocumentValues;
-        }
-        return null;
-    }
-
-    /**
-     * Checks there is one and only one array can be set for the property.
-     *
-     * @throws IllegalArgumentException if 0, or more than 1 arrays are set.
-     */
-    private void checkOnlyOneArrayCanBeSet() {
-        int notNullCount = 0;
-        if (mStringValues != null) {
-            ++notNullCount;
-        }
-        if (mLongValues != null) {
-            ++notNullCount;
-        }
-        if (mDoubleValues != null) {
-            ++notNullCount;
-        }
-        if (mBooleanValues != null) {
-            ++notNullCount;
-        }
-        if (mBytesValues != null) {
-            ++notNullCount;
-        }
-        if (mDocumentValues != null) {
-            ++notNullCount;
-        }
-        if (notNullCount == 0 || notNullCount > 1) {
-            throw new IllegalArgumentException(
-                    "One and only one type array can be set in PropertyParcel");
-        }
-    }
-
-    @Override
-    public int hashCode() {
-        if (mHashCode == null) {
-            int hashCode = 0;
-            if (mStringValues != null) {
-                hashCode = Arrays.hashCode(mStringValues);
-            } else if (mLongValues != null) {
-                hashCode = Arrays.hashCode(mLongValues);
-            } else if (mDoubleValues != null) {
-                hashCode = Arrays.hashCode(mDoubleValues);
-            } else if (mBooleanValues != null) {
-                hashCode = Arrays.hashCode(mBooleanValues);
-            } else if (mBytesValues != null) {
-                hashCode = Arrays.deepHashCode(mBytesValues);
-            } else if (mDocumentValues != null) {
-                // TODO(b/24205844) change those to Arrays.hashCode() as well once we replace
-                //  this Bundle[] with GenericDocumentParcel[].
-                int[] innerHashCodes = new int[mDocumentValues.length];
-                for (int i = 0; i < mDocumentValues.length; ++i) {
-                    innerHashCodes[i] = BundleUtil.deepHashCode(mDocumentValues[i]);
-                }
-                hashCode = Arrays.hashCode(innerHashCodes);
-            }
-            mHashCode = Objects.hash(mPropertyName, hashCode);
-        }
-        return mHashCode;
-    }
-
-    @Override
-    public boolean equals(@Nullable Object other) {
-        if (this == other) {
-            return true;
-        }
-        if (!(other instanceof PropertyParcel)) {
-            return false;
-        }
-        PropertyParcel otherPropertyParcel = (PropertyParcel) other;
-        if (!mPropertyName.equals(otherPropertyParcel.mPropertyName)) {
-            return false;
-        }
-        return Arrays.equals(mStringValues, otherPropertyParcel.mStringValues)
-                && Arrays.equals(mLongValues, otherPropertyParcel.mLongValues)
-                && Arrays.equals(mDoubleValues, otherPropertyParcel.mDoubleValues)
-                && Arrays.equals(mBooleanValues, otherPropertyParcel.mBooleanValues)
-                && Arrays.deepEquals(mBytesValues, otherPropertyParcel.mBytesValues)
-                // TODO(b/24205844) Change it to Arrays.equals once GenericDocumentParcel is added.
-                && BundleUtil.bundleValueEquals(
-                mDocumentValues, otherPropertyParcel.mDocumentValues);
-    }
-
-    /** Builder for {@link PropertyParcel}. */
-    public static final class Builder {
-        private String mPropertyName;
-        private String[] mStringValues;
-        private long[] mLongValues;
-        private double[] mDoubleValues;
-        private boolean[] mBooleanValues;
-        private byte[][] mBytesValues;
-        private Bundle[] mDocumentValues;
-
-        public Builder(@NonNull String propertyName) {
-            mPropertyName = Objects.requireNonNull(propertyName);
-        }
-
-        /** Sets String values. */
-        @NonNull
-        public Builder setStringValues(@NonNull String[] stringValues) {
-            mStringValues = Objects.requireNonNull(stringValues);
-            return this;
-        }
-
-        /** Sets long values. */
-        @NonNull
-        public Builder setLongValues(@NonNull long[] longValues) {
-            mLongValues = Objects.requireNonNull(longValues);
-            return this;
-        }
-
-        /** Sets double values. */
-        @NonNull
-        public Builder setDoubleValues(@NonNull double[] doubleValues) {
-            mDoubleValues = Objects.requireNonNull(doubleValues);
-            return this;
-        }
-
-        /** Sets boolean values. */
-        @NonNull
-        public Builder setBooleanValues(@NonNull boolean[] booleanValues) {
-            mBooleanValues = Objects.requireNonNull(booleanValues);
-            return this;
-        }
-
-        /** Sets a two dimension byte array. */
-        @NonNull
-        public Builder setBytesValues(@NonNull byte[][] bytesValues) {
-            mBytesValues = Objects.requireNonNull(bytesValues);
-            return this;
-        }
-
-        /** Sets document values. */
-        @NonNull
-        public Builder setDocumentValues(@NonNull Bundle[] documentValues) {
-            mDocumentValues = Objects.requireNonNull(documentValues);
-            return this;
-        }
-
-        /** Builds a {@link PropertyParcel}. */
-        @NonNull
-        public PropertyParcel build() {
-            return new PropertyParcel(
-                    mPropertyName,
-                    mStringValues,
-                    mLongValues,
-                    mDoubleValues,
-                    mBooleanValues,
-                    mBytesValues,
-                    mDocumentValues);
-        }
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        PropertyParcelCreator.writeToParcel(this, dest, flags);
-    }
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
index a8ccaf9..f9d87c3 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -55,9 +55,19 @@
      */
     public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
 
+    /**
+     * Schema type to be used in {@link SearchSpec.Builder#addFilterProperties(String, Collection)}
+     * to apply property paths to all results, excepting any types that have had their own, specific
+     * property paths set.
+     * @exportToFramework:hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static final String SCHEMA_TYPE_WILDCARD = "*";
+
     static final String TERM_MATCH_TYPE_FIELD = "termMatchType";
     static final String SCHEMA_FIELD = "schema";
     static final String NAMESPACE_FIELD = "namespace";
+    static final String PROPERTY_FIELD = "property";
     static final String PACKAGE_NAME_FIELD = "packageName";
     static final String NUM_PER_PAGE_FIELD = "numPerPage";
     static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
@@ -268,6 +278,30 @@
     }
 
     /**
+     * Returns the map of schema and target properties to search over.
+     *
+     * <p>If empty, will search over all schema and properties.
+     *
+     * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
+     * by this function, rather than calling it multiple times.
+     *
+     * @exportToFramework:hide
+     */
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public Map<String, List<String>> getFilterProperties() {
+        Bundle typePropertyPathsBundle = Preconditions.checkNotNull(
+                mBundle.getBundle(PROPERTY_FIELD));
+        Set<String> schemas = typePropertyPathsBundle.keySet();
+        Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+        for (String schema : schemas) {
+            typePropertyPathsMap.put(schema, Preconditions.checkNotNull(
+                    typePropertyPathsBundle.getStringArrayList(schema)));
+        }
+        return typePropertyPathsMap;
+    }
+
+    /**
      * Returns the list of namespaces to search over.
      *
      * <p>If empty, the query will search over all namespaces.
@@ -512,6 +546,7 @@
     public static final class Builder {
         private ArrayList<String> mSchemas = new ArrayList<>();
         private ArrayList<String> mNamespaces = new ArrayList<>();
+        private Bundle mTypePropertyFilters = new Bundle();
         private ArrayList<String> mPackageNames = new ArrayList<>();
         private ArraySet<String> mEnabledFeatures = new ArraySet<>();
         private Bundle mProjectionTypePropertyMasks = new Bundle();
@@ -629,6 +664,145 @@
 // @exportToFramework:endStrip()
 
         /**
+         * Adds property paths for the specified type to the property filter of
+         * {@link SearchSpec} Entry. Only returns documents that have matches under
+         * the specified properties. If property paths are added for a type, then only the
+         * properties referred to will be searched for results of that type.
+         *
+         * <p> If a property path that is specified isn't present in a result, it will be ignored
+         * for that result. Property paths cannot be null.
+         *
+         * <p>If no property paths are added for a particular type, then all properties of
+         * results of that type will be searched.
+         *
+         * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc.
+         *
+         * <p>If property paths are added for the
+         * {@link SearchSpec#SCHEMA_TYPE_WILDCARD}, then those property paths will
+         * apply to all results, excepting any types that have their own, specific property paths
+         * set.
+         *
+         * @param schema the {@link AppSearchSchema} that contains the target properties
+         * @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited
+         *                      sequence of property names.
+         *
+         * @exportToFramework:hide
+         */
+         // TODO(b/296088047) unhide from framework when type property filters are made public.
+        @NonNull
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        // @exportToFramework:startStrip()
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
+        // @exportToFramework:endStrip()
+        public Builder addFilterProperties(@NonNull String schema,
+                @NonNull Collection<String> propertyPaths) {
+            Preconditions.checkNotNull(schema);
+            Preconditions.checkNotNull(propertyPaths);
+            resetIfBuilt();
+            ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size());
+            for (String propertyPath : propertyPaths) {
+                Preconditions.checkNotNull(propertyPath);
+                propertyPathsArrayList.add(propertyPath);
+            }
+            mTypePropertyFilters.putStringArrayList(schema, propertyPathsArrayList);
+            return this;
+        }
+
+        /**
+         * Adds property paths for the specified type to the property filter of
+         * {@link SearchSpec} Entry. Only returns documents that have matches under the specified
+         * properties. If property paths are added for a type, then only the properties referred
+         * to will be searched for results of that type.
+         *
+         * @see #addFilterProperties(String, Collection)
+         *
+         * @param schema the {@link AppSearchSchema} that contains the target properties
+         * @param propertyPaths The {@link PropertyPath} to search search over
+         *
+         * @exportToFramework:hide
+         */
+         // TODO(b/296088047) unhide from framework when type property filters are made public.
+        @NonNull
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        // @exportToFramework:startStrip()
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
+        // @exportToFramework:endStrip()
+        public Builder addFilterPropertyPaths(@NonNull String schema,
+                @NonNull Collection<PropertyPath> propertyPaths) {
+            Preconditions.checkNotNull(schema);
+            Preconditions.checkNotNull(propertyPaths);
+            ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size());
+            for (PropertyPath propertyPath : propertyPaths) {
+                propertyPathsArrayList.add(propertyPath.toString());
+            }
+            return addFilterProperties(schema, propertyPathsArrayList);
+        }
+
+
+// @exportToFramework:startStrip()
+        /**
+         * Adds property paths for the specified type to the property filter of
+         * {@link SearchSpec} Entry. Only returns documents that have matches under the specified
+         * properties. If property paths are added for a type, then only the properties referred
+         * to will be searched for results of that type.
+         *
+         * @see #addFilterProperties(String, Collection)
+         *
+         * @param documentClass class annotated with {@link Document}.
+         * @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited
+                                sequence of property names.
+         *
+         */
+        @NonNull
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
+        public Builder addFilterProperties(@NonNull Class<?> documentClass,
+                @NonNull Collection<String> propertyPaths) throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+            Preconditions.checkNotNull(propertyPaths);
+            resetIfBuilt();
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return addFilterProperties(factory.getSchemaName(), propertyPaths);
+        }
+// @exportToFramework:endStrip()
+
+// @exportToFramework:startStrip()
+        /**
+         * Adds property paths for the specified type to the property filter of
+         * {@link SearchSpec} Entry. Only returns documents that have matches under the specified
+         * properties. If property paths are added for a type, then only the properties referred
+         * to will be searched for results of that type.
+         *
+         * @see #addFilterProperties(String, Collection)
+         *
+         * @param documentClass class annotated with {@link Document}.
+         * @param propertyPaths The {@link PropertyPath} to search search over
+         *
+         */
+        @NonNull
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
+        public Builder addFilterPropertyPaths(@NonNull Class<?> documentClass,
+                @NonNull Collection<PropertyPath> propertyPaths) throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+            Preconditions.checkNotNull(propertyPaths);
+            resetIfBuilt();
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return addFilterPropertyPaths(factory.getSchemaName(), propertyPaths);
+        }
+// @exportToFramework:endStrip()
+
+        /**
          * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that
          * have the specified namespaces.
          * <p>If unset, the query will search over all namespaces.
@@ -1478,7 +1652,18 @@
                 }
             }
 
+            Set<String> schemaFilter = new ArraySet<>(mSchemas);
+            if (!mSchemas.isEmpty()) {
+                for (String schema : mTypePropertyFilters.keySet()) {
+                    if (!schemaFilter.contains(schema)) {
+                        throw new IllegalStateException(
+                                "The schema: " + schema + " exists in the property filter but "
+                                        + "doesn't exist in the schema filter.");
+                    }
+                }
+            }
             bundle.putStringArrayList(SCHEMA_FIELD, mSchemas);
+            bundle.putBundle(PROPERTY_FIELD, mTypePropertyFilters);
             bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
             bundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames);
             bundle.putStringArrayList(ENABLED_FEATURES_FIELD, new ArrayList<>(mEnabledFeatures));
@@ -1501,6 +1686,7 @@
         private void resetIfBuilt() {
             if (mBuilt) {
                 mSchemas = new ArrayList<>(mSchemas);
+                mTypePropertyFilters = BundleUtil.deepCopy(mTypePropertyFilters);
                 mNamespaces = new ArrayList<>(mNamespaces);
                 mPackageNames = new ArrayList<>(mPackageNames);
                 mProjectionTypePropertyMasks = BundleUtil.deepCopy(mProjectionTypePropertyMasks);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/GenericDocumentParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/GenericDocumentParcel.java
new file mode 100644
index 0000000..3d92c02
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/GenericDocumentParcel.java
@@ -0,0 +1,494 @@
+/*
+ * 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.appsearch.app.safeparcel;
+
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.GenericDocumentParcelCreator;
+import androidx.collection.ArrayMap;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Holds data for a {@link GenericDocument}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SafeParcelable.Class(creator = "GenericDocumentParcelCreator")
+public final class GenericDocumentParcel extends AbstractSafeParcelable {
+    @NonNull
+    public static final GenericDocumentParcelCreator CREATOR =
+            new GenericDocumentParcelCreator();
+
+    /** The default score of document. */
+    private static final int DEFAULT_SCORE = 0;
+
+    /** The default time-to-live in millisecond of a document, which is infinity. */
+    private static final long DEFAULT_TTL_MILLIS = 0L;
+
+    /** Default but invalid value for {@code mCreationTimestampMillis}. */
+    private static final long INVALID_CREATION_TIMESTAMP_MILLIS = -1L;
+
+    @Field(id = 1, getter = "getNamespace")
+    @NonNull
+    private final String mNamespace;
+
+    @Field(id = 2, getter = "getId")
+    @NonNull
+    private final String mId;
+
+    @Field(id = 3, getter = "getSchemaType")
+    @NonNull
+    private final String mSchemaType;
+
+    @Field(id = 4, getter = "getCreationTimestampMillis")
+    private final long mCreationTimestampMillis;
+
+    @Field(id = 5, getter = "getTtlMillis")
+    private final long mTtlMillis;
+
+    @Field(id = 6, getter = "getScore")
+    private final int mScore;
+
+    /**
+     * Contains all properties in {@link GenericDocument} in a list.
+     *
+     * <p>Unfortunately SafeParcelable doesn't support map type so we have to use a list here.
+     */
+    @Field(id = 7, getter = "getProperties")
+    @NonNull
+    private final PropertyParcel[] mProperties;
+
+    /**
+     * Contains all properties in {@link GenericDocument} to support getting properties via name
+     *
+     * <p>This map is created for quick looking up property by name.
+     */
+    @NonNull
+    private final Map<String, PropertyParcel> mPropertyMap;
+
+    @Nullable
+    private Integer mHashCode;
+
+    /**
+     * The constructor taking the property list, and create map internally from this list.
+     *
+     * <p> This will be used in createFromParcel, so creating the property map can not be avoided
+     * in this constructor.
+     */
+    @Constructor
+    GenericDocumentParcel(
+            @Param(id = 1) @NonNull String namespace,
+            @Param(id = 2) @NonNull String id,
+            @Param(id = 3) @NonNull String schemaType,
+            @Param(id = 4) long creationTimestampMillis,
+            @Param(id = 5) long ttlMillis,
+            @Param(id = 6) int score,
+            @Param(id = 7) @NonNull PropertyParcel[] properties) {
+        this(namespace, id, schemaType, creationTimestampMillis, ttlMillis, score,
+                properties, createPropertyMapFromPropertyArray(properties));
+    }
+
+    /**
+     * A constructor taking both property list and property map.
+     *
+     * <p>Caller needs to make sure property list and property map
+     * matches(map is generated from list, or list generated from map).
+     */
+    GenericDocumentParcel(
+            @NonNull String namespace,
+            @NonNull String id,
+            @NonNull String schemaType,
+            long creationTimestampMillis,
+            long ttlMillis,
+            int score,
+            @NonNull PropertyParcel[] properties,
+            @NonNull Map<String, PropertyParcel> propertyMap) {
+        mNamespace = Objects.requireNonNull(namespace);
+        mId = Objects.requireNonNull(id);
+        mSchemaType = Objects.requireNonNull(schemaType);
+        mCreationTimestampMillis = creationTimestampMillis;
+        mTtlMillis = ttlMillis;
+        mScore = score;
+        mProperties = Objects.requireNonNull(properties);
+        mPropertyMap = Objects.requireNonNull(propertyMap);
+    }
+
+    private static Map<String, PropertyParcel> createPropertyMapFromPropertyArray(
+            @NonNull PropertyParcel[] properties) {
+        Objects.requireNonNull(properties);
+        Map<String, PropertyParcel> propertyMap = new ArrayMap<>(properties.length);
+        for (int i = 0; i < properties.length; ++i) {
+            PropertyParcel property = properties[i];
+            propertyMap.put(property.getPropertyName(), property);
+        }
+        return propertyMap;
+    }
+
+    /** Returns the unique identifier of the {@link GenericDocument}. */
+    @NonNull
+    public String getId() {
+        return mId;
+    }
+
+    /** Returns the namespace of the {@link GenericDocument}. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */
+    @NonNull
+    public String getSchemaType() {
+        return mSchemaType;
+    }
+
+    /** Returns the creation timestamp of the {@link GenericDocument}, in milliseconds. */
+    /*@exportToFramework:CurrentTimeMillisLong*/
+    public long getCreationTimestampMillis() {
+        return mCreationTimestampMillis;
+    }
+
+    /** Returns the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds. */
+    public long getTtlMillis() {
+        return mTtlMillis;
+    }
+
+    /** Returns the score of the {@link GenericDocument}. */
+    public int getScore() {
+        return mScore;
+    }
+
+    /** Returns the names of all properties defined in this document. */
+    @NonNull
+    public Set<String> getPropertyNames() {
+        return mPropertyMap.keySet();
+    }
+
+    /** Returns all the properties the document has. */
+    @NonNull
+    public PropertyParcel[] getProperties() {
+        return mProperties;
+    }
+
+    /** Returns the property map the document has. */
+    @NonNull
+    public Map<String, PropertyParcel> getPropertyMap() {
+        return mPropertyMap;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof GenericDocumentParcel)) {
+            return false;
+        }
+        GenericDocumentParcel otherDocument = (GenericDocumentParcel) other;
+        return mNamespace.equals(otherDocument.mNamespace)
+                && mId.equals(otherDocument.mId)
+                && mSchemaType.equals(otherDocument.mSchemaType)
+                && mTtlMillis == otherDocument.mTtlMillis
+                && mCreationTimestampMillis == otherDocument.mCreationTimestampMillis
+                && mScore == otherDocument.mScore
+                && Arrays.equals(mProperties, otherDocument.mProperties)
+                && Objects.equals(mPropertyMap, otherDocument.mPropertyMap);
+    }
+
+    @Override
+    public int hashCode() {
+        if (mHashCode == null) {
+            mHashCode = Objects.hash(
+                    mNamespace,
+                    mId,
+                    mSchemaType,
+                    mTtlMillis,
+                    mScore,
+                    mCreationTimestampMillis,
+                    Arrays.hashCode(mProperties),
+                    mPropertyMap.hashCode());
+        }
+        return mHashCode;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        GenericDocumentParcelCreator.writeToParcel(this, dest, flags);
+    }
+
+    /** The builder class for {@link GenericDocumentParcel}. */
+    public static final class Builder {
+        private String mNamespace;
+        private String mId;
+        private String mSchemaType;
+        private long mCreationTimestampMillis;
+        private long mTtlMillis;
+        private int mScore;
+        private Map<String, PropertyParcel> mPropertyMap;
+        private boolean mBuilt = false;
+
+        /**
+         * Creates a new {@link GenericDocument.Builder}.
+         *
+         * <p>Document IDs are unique within a namespace.
+         *
+         * <p>The number of namespaces per app should be kept small for efficiency reasons.
+         */
+        public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
+            mNamespace = Objects.requireNonNull(namespace);
+            mId = Objects.requireNonNull(id);
+            mSchemaType = Objects.requireNonNull(schemaType);
+            mCreationTimestampMillis = INVALID_CREATION_TIMESTAMP_MILLIS;
+            mTtlMillis = DEFAULT_TTL_MILLIS;
+            mScore = DEFAULT_SCORE;
+            mPropertyMap = new ArrayMap<>();
+        }
+
+        /**
+         * Creates a new {@link GenericDocumentParcel.Builder} from the given
+         * {@link GenericDocumentParcel}.
+         */
+        Builder(@NonNull GenericDocumentParcel documentSafeParcel) {
+            Objects.requireNonNull(documentSafeParcel);
+
+            mNamespace = documentSafeParcel.mNamespace;
+            mId = documentSafeParcel.mId;
+            mSchemaType = documentSafeParcel.mSchemaType;
+            mCreationTimestampMillis = documentSafeParcel.mCreationTimestampMillis;
+            mTtlMillis = documentSafeParcel.mTtlMillis;
+            mScore = documentSafeParcel.mScore;
+
+            // Create a shallow copy of the map so we won't change the original one.
+            Map<String, PropertyParcel> propertyMap = documentSafeParcel.mPropertyMap;
+            mPropertyMap = new ArrayMap<>(propertyMap.size());
+            for (PropertyParcel value : propertyMap.values()) {
+                mPropertyMap.put(value.getPropertyName(), value);
+            }
+        }
+
+        /**
+         * Sets the app-defined namespace this document resides in, changing the value provided in
+         * the constructor. No special values are reserved or understood by the infrastructure.
+         *
+         * <p>Document IDs are unique within a namespace.
+         *
+         * <p>The number of namespaces per app should be kept small for efficiency reasons.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setNamespace(@NonNull String namespace) {
+            Objects.requireNonNull(namespace);
+            resetIfBuilt();
+            mNamespace = namespace;
+            return this;
+        }
+
+        /**
+         * Sets the ID of this document, changing the value provided in the constructor. No special
+         * values are reserved or understood by the infrastructure.
+         *
+         * <p>Document IDs are unique within a namespace.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setId(@NonNull String id) {
+            Objects.requireNonNull(id);
+            resetIfBuilt();
+            mId = id;
+            return this;
+        }
+
+        /**
+         * Sets the schema type of this document, changing the value provided in the constructor.
+         *
+         * <p>To successfully index a document, the schema type must match the name of an {@link
+         * AppSearchSchema} object previously provided to {@link AppSearchSession#setSchema}.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setSchemaType(@NonNull String schemaType) {
+            Objects.requireNonNull(schemaType);
+            resetIfBuilt();
+            mSchemaType = schemaType;
+            return this;
+        }
+
+        /** Sets the score of the parent {@link GenericDocument}. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setScore(int score) {
+            resetIfBuilt();
+            mScore = score;
+            return this;
+        }
+
+        /**
+         * Sets the creation timestamp of the {@link GenericDocument}, in milliseconds.
+         *
+         * <p>This should be set using a value obtained from the {@link System#currentTimeMillis}
+         * time base.
+         *
+         * <p>If this method is not called, this will be set to the time the object is built.
+         *
+         * @param creationTimestampMillis a creation timestamp in milliseconds.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setCreationTimestampMillis(
+                /*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {
+            resetIfBuilt();
+            mCreationTimestampMillis = creationTimestampMillis;
+            return this;
+        }
+
+        /**
+         * Sets the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
+         *
+         * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of
+         * {@code creationTimestampMillis + ttlMillis}, measured in the {@link
+         * System#currentTimeMillis} time base, the document will be auto-deleted.
+         *
+         * <p>The default value is 0, which means the document is permanent and won't be
+         * auto-deleted until the app is uninstalled or {@link AppSearchSession#remove} is called.
+         *
+         * @param ttlMillis a non-negative duration in milliseconds.
+         * @throws IllegalArgumentException if ttlMillis is negative.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setTtlMillis(long ttlMillis) {
+            if (ttlMillis < 0) {
+                throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
+            }
+            resetIfBuilt();
+            mTtlMillis = ttlMillis;
+            return this;
+        }
+
+        /**
+         * Clears the value for the property with the given name.
+         *
+         * <p>Note that this method does not support property paths.
+         *
+         * @param name The name of the property to clear.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder clearProperty(@NonNull String name) {
+            Objects.requireNonNull(name);
+            resetIfBuilt();
+            mPropertyMap.remove(name);
+            return this;
+        }
+
+        /** puts an array of {@link String} in property map. */
+        @NonNull
+        public Builder putInPropertyMap(@NonNull String name, @NonNull String[] values)
+                throws IllegalArgumentException {
+            mPropertyMap.put(name,
+                    new PropertyParcel.Builder(name).setStringValues(values).build());
+            return this;
+        }
+
+        /** puts an array of boolean in property map. */
+        @NonNull
+        public Builder putInPropertyMap(@NonNull String name, @NonNull boolean[] values) {
+            mPropertyMap.put(name,
+                    new PropertyParcel.Builder(name).setBooleanValues(values).build());
+            return this;
+        }
+
+        /** puts an array of double in property map. */
+        @NonNull
+        public Builder putInPropertyMap(@NonNull String name, @NonNull double[] values) {
+            mPropertyMap.put(name,
+                    new PropertyParcel.Builder(name).setDoubleValues(values).build());
+            return this;
+        }
+
+        /** puts an array of long in property map. */
+        @NonNull
+        public Builder putInPropertyMap(@NonNull String name, @NonNull long[] values) {
+            mPropertyMap.put(name,
+                    new PropertyParcel.Builder(name).setLongValues(values).build());
+            return this;
+        }
+
+        /**
+         * Converts and saves a byte[][] into {@link #mProperties}.
+         */
+        @NonNull
+        public Builder putInPropertyMap(@NonNull String name, @NonNull byte[][] values) {
+            mPropertyMap.put(name,
+                    new PropertyParcel.Builder(name).setBytesValues(values).build());
+            return this;
+        }
+
+        /** puts an array of {@link GenericDocumentParcel} in property map. */
+        @NonNull
+        public Builder putInPropertyMap(@NonNull String name,
+                @NonNull GenericDocumentParcel[] values) {
+            mPropertyMap.put(name,
+                    new PropertyParcel.Builder(name).setDocumentValues(values).build());
+            return this;
+        }
+
+        /** Builds the {@link GenericDocument} object. */
+        @NonNull
+        public GenericDocumentParcel build() {
+            mBuilt = true;
+            // Set current timestamp for creation timestamp by default.
+            if (mCreationTimestampMillis == INVALID_CREATION_TIMESTAMP_MILLIS) {
+                mCreationTimestampMillis = System.currentTimeMillis();
+            }
+            return new GenericDocumentParcel(
+                    mNamespace,
+                    mId,
+                    mSchemaType,
+                    mCreationTimestampMillis,
+                    mTtlMillis,
+                    mScore,
+                    mPropertyMap.values().toArray(new PropertyParcel[0]));
+        }
+
+        void resetIfBuilt() {
+            if (mBuilt) {
+                Map<String, PropertyParcel> propertyMap = mPropertyMap;
+                mPropertyMap = new ArrayMap<>(propertyMap.size());
+                for (PropertyParcel value : propertyMap.values()) {
+                    // PropertyParcel is not deep copied since it is not mutable.
+                    mPropertyMap.put(value.getPropertyName(), value);
+                }
+                mBuilt = false;
+            }
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/PropertyConfigParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/PropertyConfigParcel.java
new file mode 100644
index 0000000..f80a575
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/PropertyConfigParcel.java
@@ -0,0 +1,306 @@
+/*
+ * 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.appsearch.app.safeparcel;
+
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSchema.PropertyConfig.Cardinality;
+import androidx.appsearch.app.AppSearchSchema.PropertyConfig.DataType;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.JoinableValueType;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TokenizerType;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.DocumentIndexingConfigParcelCreator;
+import androidx.appsearch.safeparcel.stub.StubCreators.IntegerIndexingConfigParcelCreator;
+import androidx.appsearch.safeparcel.stub.StubCreators.JoinableConfigParcelCreator;
+import androidx.appsearch.safeparcel.stub.StubCreators.PropertyConfigParcelCreator;
+import androidx.appsearch.safeparcel.stub.StubCreators.StringIndexingConfigParcelCreator;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Class to hold property configuration for one property defined in {@link AppSearchSchema}.
+ *
+ * <p>It is defined as same as PropertyConfigProto for the native code to handle different property
+ * types in one class.
+ *
+ * <p>Currently it can handle String, long, double, boolean, bytes and document type.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SafeParcelable.Class(creator = "PropertyConfigParcelCreator")
+public final class PropertyConfigParcel extends AbstractSafeParcelable {
+    @NonNull
+    public static final PropertyConfigParcelCreator CREATOR = new PropertyConfigParcelCreator();
+
+    @Field(id = 1, getter = "getName")
+    private final String mName;
+
+    @AppSearchSchema.PropertyConfig.DataType
+    @Field(id = 2, getter = "getDataType")
+    private final int mDataType;
+
+    @AppSearchSchema.PropertyConfig.Cardinality
+    @Field(id = 3, getter = "getCardinality")
+    private final int mCardinality;
+
+    @Field(id = 4, getter = "getSchemaType")
+    private final String mSchemaType;
+
+    @Field(id = 5, getter = "getStringIndexingConfigParcel")
+    private final StringIndexingConfigParcel mStringIndexingConfigParcel;
+
+    @Field(id = 6, getter = "getDocumentIndexingConfigParcel")
+    private final DocumentIndexingConfigParcel mDocumentIndexingConfigParcel;
+
+    @Field(id = 7, getter = "getIntegerIndexingConfigParcel")
+    private final IntegerIndexingConfigParcel mIntegerIndexingConfigParcel;
+
+    @Field(id = 8, getter = "getJoinableConfigParcel")
+    private final JoinableConfigParcel mJoinableConfigParcel;
+
+    /** Constructor for {@link PropertyConfigParcel}. */
+    @Constructor
+    public PropertyConfigParcel(
+            @Param(id = 1) @NonNull String name,
+            @Param(id = 2) @DataType int dataType,
+            @Param(id = 3) @Cardinality int cardinality,
+            @Param(id = 4) @Nullable String schemaType,
+            @Param(id = 5) @Nullable StringIndexingConfigParcel stringIndexingConfigParcel,
+            @Param(id = 6) @Nullable DocumentIndexingConfigParcel documentIndexingConfigParcel,
+            @Param(id = 7) @Nullable IntegerIndexingConfigParcel integerIndexingConfigParcel,
+            @Param(id = 8) @Nullable JoinableConfigParcel joinableConfigParcel) {
+        mName = Objects.requireNonNull(name);
+        mDataType = dataType;
+        mCardinality = cardinality;
+        mSchemaType = schemaType;
+        mStringIndexingConfigParcel = stringIndexingConfigParcel;
+        mDocumentIndexingConfigParcel = documentIndexingConfigParcel;
+        mIntegerIndexingConfigParcel = integerIndexingConfigParcel;
+        mJoinableConfigParcel = joinableConfigParcel;
+    }
+
+    /** Gets name for the property. */
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    /** Gets data type for the property. */
+    @DataType
+    public int getDataType() {
+        return mDataType;
+    }
+
+    /** Gets cardinality for the property. */
+    @Cardinality
+    public int getCardinality() {
+        return mCardinality;
+    }
+
+    /** Gets schema type. */
+    @Nullable
+    public String getSchemaType() {
+        return mSchemaType;
+    }
+
+    /** Gets the {@link StringIndexingConfigParcel}. */
+    @Nullable
+    public StringIndexingConfigParcel getStringIndexingConfigParcel() {
+        return mStringIndexingConfigParcel;
+    }
+
+    /** Gets the {@link DocumentIndexingConfigParcel}. */
+    @Nullable
+    public DocumentIndexingConfigParcel getDocumentIndexingConfigParcel() {
+        return mDocumentIndexingConfigParcel;
+    }
+
+    /** Gets the {@link IntegerIndexingConfigParcel}. */
+    @Nullable
+    public IntegerIndexingConfigParcel getIntegerIndexingConfigParcel() {
+        return mIntegerIndexingConfigParcel;
+    }
+
+    /** Gets the {@link JoinableConfigParcel}. */
+    @Nullable
+    public JoinableConfigParcel getJoinableConfigParcel() {
+        return mJoinableConfigParcel;
+    }
+
+    /** Class to hold join configuration for a String type. */
+    @SafeParcelable.Class(creator = "JoinableConfigParcelCreator")
+    public static class JoinableConfigParcel extends AbstractSafeParcelable {
+        @NonNull
+        public static final JoinableConfigParcelCreator CREATOR = new JoinableConfigParcelCreator();
+
+        @JoinableValueType
+        @Field(id = 1, getter = "getJoinableValueType")
+        private final int mJoinableValueType;
+
+        @Field(id = 2, getter = "getDeletionPropagation")
+        private final boolean mDeletionPropagation;
+
+        /** Constructor for {@link JoinableConfigParcel}. */
+        @Constructor
+        public JoinableConfigParcel(
+                @Param(id = 1) @JoinableValueType int joinableValueType,
+                @Param(id = 2) boolean deletionPropagation) {
+            mJoinableValueType = joinableValueType;
+            mDeletionPropagation = deletionPropagation;
+        }
+
+        /** Gets {@link JoinableValueType} of the join. */
+        @JoinableValueType
+        public int getJoinableValueType() {
+            return mJoinableValueType;
+        }
+
+        /** Gets whether delete will be propagated. */
+        public boolean getDeletionPropagation() {
+            return mDeletionPropagation;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            JoinableConfigParcelCreator.writeToParcel(this, dest, flags);
+        }
+    }
+
+    /** Class to hold configuration a string type. */
+    @SafeParcelable.Class(creator = "StringIndexingConfigParcelCreator")
+    public static class StringIndexingConfigParcel extends AbstractSafeParcelable {
+        @NonNull
+        public static final StringIndexingConfigParcelCreator CREATOR =
+                new StringIndexingConfigParcelCreator();
+
+        @AppSearchSchema.StringPropertyConfig.IndexingType
+        @Field(id = 1, getter = "getIndexingType")
+        private final int mIndexingType;
+
+        @TokenizerType
+        @Field(id = 2, getter = "getTokenizerType")
+        private final int mTokenizerType;
+
+        /** Constructor for {@link StringIndexingConfigParcel}. */
+        @Constructor
+        public StringIndexingConfigParcel(
+                @Param(id = 1) @AppSearchSchema.StringPropertyConfig.IndexingType int indexingType,
+                @Param(id = 2) @TokenizerType int tokenizerType) {
+            mIndexingType = indexingType;
+            mTokenizerType = tokenizerType;
+        }
+
+        /** Gets the indexing type for this property. */
+        @AppSearchSchema.StringPropertyConfig.IndexingType
+        public int getIndexingType() {
+            return mIndexingType;
+        }
+
+        /** Gets the tokenization type for this property. */
+        @TokenizerType
+        public int getTokenizerType() {
+            return mTokenizerType;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            StringIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
+        }
+    }
+
+    /** Class to hold configuration for integer property type. */
+    @SafeParcelable.Class(creator = "IntegerIndexingConfigParcelCreator")
+    public static class IntegerIndexingConfigParcel extends AbstractSafeParcelable {
+        @NonNull
+        public static final IntegerIndexingConfigParcelCreator CREATOR =
+                new IntegerIndexingConfigParcelCreator();
+
+        @AppSearchSchema.LongPropertyConfig.IndexingType
+        @Field(id = 1, getter = "getIndexingType")
+        private final int mIndexingType;
+
+        /** Constructor for {@link IntegerIndexingConfigParcel}. */
+        @Constructor
+        public IntegerIndexingConfigParcel(
+                @Param(id = 1) @AppSearchSchema.LongPropertyConfig.IndexingType int indexingType) {
+            mIndexingType = indexingType;
+        }
+
+        /** Gets the indexing type for this integer property. */
+        @AppSearchSchema.LongPropertyConfig.IndexingType
+        public int getIndexingType() {
+            return mIndexingType;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            IntegerIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
+        }
+    }
+
+    /** Class to hold configuration for document property type. */
+    @SafeParcelable.Class(creator = "DocumentIndexingConfigParcelCreator")
+    public static class DocumentIndexingConfigParcel extends AbstractSafeParcelable {
+        @NonNull
+        public static final DocumentIndexingConfigParcelCreator CREATOR =
+                new DocumentIndexingConfigParcelCreator();
+
+        @Field(id = 1, getter = "shouldIndexNestedProperties")
+        private final boolean mIndexNestedProperties;
+
+        @NonNull
+        @Field(id = 2, getter = "getIndexableNestedPropertiesList")
+        private final List<String> mIndexableNestedPropertiesList;
+
+        /** Constructor for {@link DocumentIndexingConfigParcel}. */
+        @Constructor
+        public DocumentIndexingConfigParcel(
+                @Param(id = 1) boolean indexNestedProperties,
+                @Param(id = 2) @NonNull List<String> indexableNestedPropertiesList) {
+            mIndexNestedProperties = indexNestedProperties;
+            mIndexableNestedPropertiesList = Objects.requireNonNull(indexableNestedPropertiesList);
+        }
+
+        /** Nested properties should be indexed. */
+        public boolean shouldIndexNestedProperties() {
+            return mIndexNestedProperties;
+        }
+
+        /** Gets the list for nested property list. */
+        @NonNull
+        public List<String> getIndexableNestedPropertiesList() {
+            return mIndexableNestedPropertiesList;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            DocumentIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
+        }
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        PropertyConfigParcelCreator.writeToParcel(this, dest, flags);
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/PropertyParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/PropertyParcel.java
new file mode 100644
index 0000000..f9034a9
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/safeparcel/PropertyParcel.java
@@ -0,0 +1,310 @@
+/*
+ * 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.appsearch.app.safeparcel;
+
+
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.PropertyParcelCreator;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A {@link SafeParcelable} to hold the value of a property in {@code GenericDocument#mProperties}.
+ *
+ * <p>This resembles PropertyProto in IcingLib.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SafeParcelable.Class(creator = "PropertyParcelCreator")
+public final class PropertyParcel extends AbstractSafeParcelable {
+    @NonNull public static final PropertyParcelCreator CREATOR = new PropertyParcelCreator();
+
+    @NonNull
+    @Field(id = 1, getter = "getPropertyName")
+    private final String mPropertyName;
+
+    @Nullable
+    @Field(id = 2, getter = "getStringValues")
+    private final String[] mStringValues;
+
+    @Nullable
+    @Field(id = 3, getter = "getLongValues")
+    private final long[] mLongValues;
+
+    @Nullable
+    @Field(id = 4, getter = "getDoubleValues")
+    private final double[] mDoubleValues;
+
+    @Nullable
+    @Field(id = 5, getter = "getBooleanValues")
+    private final boolean[] mBooleanValues;
+
+    @Nullable
+    @Field(id = 6, getter = "getBytesValues")
+    private final byte[][] mBytesValues;
+
+    @Nullable
+    @Field(id = 7, getter = "getDocumentValues")
+    private final GenericDocumentParcel[] mDocumentValues;
+
+    @Nullable private Integer mHashCode;
+
+    @Constructor
+    PropertyParcel(
+            @Param(id = 1) @NonNull String propertyName,
+            @Param(id = 2) @Nullable String[] stringValues,
+            @Param(id = 3) @Nullable long[] longValues,
+            @Param(id = 4) @Nullable double[] doubleValues,
+            @Param(id = 5) @Nullable boolean[] booleanValues,
+            @Param(id = 6) @Nullable byte[][] bytesValues,
+            @Param(id = 7) @Nullable GenericDocumentParcel[] documentValues) {
+        mPropertyName = Objects.requireNonNull(propertyName);
+        mStringValues = stringValues;
+        mLongValues = longValues;
+        mDoubleValues = doubleValues;
+        mBooleanValues = booleanValues;
+        mBytesValues = bytesValues;
+        mDocumentValues = documentValues;
+        checkOnlyOneArrayCanBeSet();
+    }
+
+    /** Returns the name of the property. */
+    @NonNull
+    public String getPropertyName() {
+        return mPropertyName;
+    }
+
+    /** Returns {@code String} values in an array. */
+    @Nullable
+    public String[] getStringValues() {
+        return mStringValues;
+    }
+
+    /** Returns {@code long} values in an array. */
+    @Nullable
+    public long[] getLongValues() {
+        return mLongValues;
+    }
+
+    /** Returns {@code double} values in an array. */
+    @Nullable
+    public double[] getDoubleValues() {
+        return mDoubleValues;
+    }
+
+    /** Returns {@code boolean} values in an array. */
+    @Nullable
+    public boolean[] getBooleanValues() {
+        return mBooleanValues;
+    }
+
+    /** Returns a two-dimension {@code byte} array. */
+    @Nullable
+    public byte[][] getBytesValues() {
+        return mBytesValues;
+    }
+
+    /** Returns {@link GenericDocumentParcel}s in an array. */
+    @Nullable
+    public GenericDocumentParcel[] getDocumentValues() {
+        return mDocumentValues;
+    }
+
+    /**
+     * Returns the held values in an array for this property.
+     *
+     * <p>Different from other getter methods, this one will return an {@link Object}.
+     */
+    @Nullable
+    public Object getValues() {
+        if (mStringValues != null) {
+            return mStringValues;
+        }
+        if (mLongValues != null) {
+            return mLongValues;
+        }
+        if (mDoubleValues != null) {
+            return mDoubleValues;
+        }
+        if (mBooleanValues != null) {
+            return mBooleanValues;
+        }
+        if (mBytesValues != null) {
+            return mBytesValues;
+        }
+        if (mDocumentValues != null) {
+            return mDocumentValues;
+        }
+        return null;
+    }
+
+    /**
+     * Checks there is one and only one array can be set for the property.
+     *
+     * @throws IllegalArgumentException if 0, or more than 1 arrays are set.
+     */
+    private void checkOnlyOneArrayCanBeSet() {
+        int notNullCount = 0;
+        if (mStringValues != null) {
+            ++notNullCount;
+        }
+        if (mLongValues != null) {
+            ++notNullCount;
+        }
+        if (mDoubleValues != null) {
+            ++notNullCount;
+        }
+        if (mBooleanValues != null) {
+            ++notNullCount;
+        }
+        if (mBytesValues != null) {
+            ++notNullCount;
+        }
+        if (mDocumentValues != null) {
+            ++notNullCount;
+        }
+        if (notNullCount == 0 || notNullCount > 1) {
+            throw new IllegalArgumentException(
+                    "One and only one type array can be set in PropertyParcel");
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        if (mHashCode == null) {
+            int hashCode = 0;
+            if (mStringValues != null) {
+                hashCode = Arrays.hashCode(mStringValues);
+            } else if (mLongValues != null) {
+                hashCode = Arrays.hashCode(mLongValues);
+            } else if (mDoubleValues != null) {
+                hashCode = Arrays.hashCode(mDoubleValues);
+            } else if (mBooleanValues != null) {
+                hashCode = Arrays.hashCode(mBooleanValues);
+            } else if (mBytesValues != null) {
+                hashCode = Arrays.deepHashCode(mBytesValues);
+            } else if (mDocumentValues != null) {
+                hashCode = Arrays.hashCode(mDocumentValues);
+            }
+            mHashCode = Objects.hash(mPropertyName, hashCode);
+        }
+        return mHashCode;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof PropertyParcel)) {
+            return false;
+        }
+        PropertyParcel otherPropertyParcel = (PropertyParcel) other;
+        if (!mPropertyName.equals(otherPropertyParcel.mPropertyName)) {
+            return false;
+        }
+        return Arrays.equals(mStringValues, otherPropertyParcel.mStringValues)
+                && Arrays.equals(mLongValues, otherPropertyParcel.mLongValues)
+                && Arrays.equals(mDoubleValues, otherPropertyParcel.mDoubleValues)
+                && Arrays.equals(mBooleanValues, otherPropertyParcel.mBooleanValues)
+                && Arrays.deepEquals(mBytesValues, otherPropertyParcel.mBytesValues)
+                && Arrays.equals(mDocumentValues, otherPropertyParcel.mDocumentValues);
+    }
+
+    /** Builder for {@link PropertyParcel}. */
+    public static final class Builder {
+        private String mPropertyName;
+        private String[] mStringValues;
+        private long[] mLongValues;
+        private double[] mDoubleValues;
+        private boolean[] mBooleanValues;
+        private byte[][] mBytesValues;
+        private GenericDocumentParcel[] mDocumentValues;
+
+        public Builder(@NonNull String propertyName) {
+            mPropertyName = Objects.requireNonNull(propertyName);
+        }
+
+        /** Sets String values. */
+        @NonNull
+        public Builder setStringValues(@NonNull String[] stringValues) {
+            mStringValues = Objects.requireNonNull(stringValues);
+            return this;
+        }
+
+        /** Sets long values. */
+        @NonNull
+        public Builder setLongValues(@NonNull long[] longValues) {
+            mLongValues = Objects.requireNonNull(longValues);
+            return this;
+        }
+
+        /** Sets double values. */
+        @NonNull
+        public Builder setDoubleValues(@NonNull double[] doubleValues) {
+            mDoubleValues = Objects.requireNonNull(doubleValues);
+            return this;
+        }
+
+        /** Sets boolean values. */
+        @NonNull
+        public Builder setBooleanValues(@NonNull boolean[] booleanValues) {
+            mBooleanValues = Objects.requireNonNull(booleanValues);
+            return this;
+        }
+
+        /** Sets a two dimension byte array. */
+        @NonNull
+        public Builder setBytesValues(@NonNull byte[][] bytesValues) {
+            mBytesValues = Objects.requireNonNull(bytesValues);
+            return this;
+        }
+
+        /** Sets document values. */
+        @NonNull
+        public Builder setDocumentValues(@NonNull GenericDocumentParcel[] documentValues) {
+            mDocumentValues = Objects.requireNonNull(documentValues);
+            return this;
+        }
+
+        /** Builds a {@link PropertyParcel}. */
+        @NonNull
+        public PropertyParcel build() {
+            return new PropertyParcel(
+                    mPropertyName,
+                    mStringValues,
+                    mLongValues,
+                    mDoubleValues,
+                    mBooleanValues,
+                    mBytesValues,
+                    mDocumentValues);
+        }
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        PropertyParcelCreator.writeToParcel(this, dest, flags);
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
index 63c31df2..c4f9241 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
@@ -16,6 +16,9 @@
 package androidx.appsearch.safeparcel.stub;
 
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.safeparcel.GenericDocumentParcel;
+import androidx.appsearch.app.safeparcel.PropertyConfigParcel;
+import androidx.appsearch.app.safeparcel.PropertyParcel;
 
 /**
  * Stub creators for any classes extending
@@ -32,7 +35,43 @@
     public static class StorageInfoCreator extends AbstractCreator {
     }
 
-    /** Stub creator for {@link androidx.appsearch.app.PropertyParcel}. */
+    /** Stub creator for {@link PropertyParcel}. */
     public static class PropertyParcelCreator extends AbstractCreator {
     }
+
+    /** Stub creator for {@link PropertyConfigParcel}. */
+    public static class PropertyConfigParcelCreator extends AbstractCreator {
+    }
+
+    /**
+     * Stub creator for
+     * {@link PropertyConfigParcel.JoinableConfigParcel}.
+     */
+    public static class JoinableConfigParcelCreator extends AbstractCreator {
+    }
+
+    /**
+     * Stub creator for
+     * {@link PropertyConfigParcel.StringIndexingConfigParcel}.
+     */
+    public static class StringIndexingConfigParcelCreator extends AbstractCreator {
+    }
+
+    /**
+     * Stub creator for
+     * {@link PropertyConfigParcel.IntegerIndexingConfigParcel}.
+     */
+    public static class IntegerIndexingConfigParcelCreator extends AbstractCreator {
+    }
+
+    /**
+     * Stub creator for
+     * {@link PropertyConfigParcel.DocumentIndexingConfigParcel}.
+     */
+    public static class DocumentIndexingConfigParcelCreator extends AbstractCreator {
+    }
+
+    /** Stub creator for {@link GenericDocumentParcel}. */
+    public static class GenericDocumentParcelCreator extends AbstractCreator {
+    }
 }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java
index 712cb2b..9208237 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java
@@ -20,7 +20,6 @@
 import static androidx.appsearch.compiler.IntrospectionHelper.getDocumentAnnotation;
 import static androidx.appsearch.compiler.IntrospectionHelper.getPropertyType;
 import static androidx.appsearch.compiler.IntrospectionHelper.validateIsGetter;
-
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.joining;
 
@@ -360,7 +359,7 @@
         List<? extends AnnotationMirror> annotations =
                 element.getAnnotationMirrors().stream()
                         .filter(ann -> ann.getAnnotationType().toString().startsWith(
-                                DOCUMENT_ANNOTATION_CLASS)).toList();
+                                DOCUMENT_ANNOTATION_CLASS.canonicalName())).toList();
         if (annotations.isEmpty()) {
             return null;
         }
@@ -533,7 +532,7 @@
                 .anyMatch(expectedType -> typeUtils.isSameType(expectedType, target));
         if (!isValid) {
             String error = "@"
-                    + getterOrField.getAnnotation().getSimpleClassName()
+                    + getterOrField.getAnnotation().getClassName().simpleName()
                     + " must only be placed on a getter/field of type "
                     + (allowRepeated ? "or array or collection of " : "")
                     + expectedTypes.stream().map(TypeMirror::toString).collect(joining("|"));
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java
index f15e793..4ea0f1d6 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java
@@ -15,6 +15,8 @@
  */
 package androidx.appsearch.compiler;
 
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_ANNOTATION_PKG;
+import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_SIMPLE_CLASS_NAME;
 import static javax.lang.model.util.ElementFilter.typesIn;
 
 import androidx.annotation.NonNull;
@@ -47,7 +49,7 @@
  *
  * <p>Only plain Java objects and AutoValue Document classes without builders are supported.
  */
-@SupportedAnnotationTypes({IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS})
+@SupportedAnnotationTypes({APPSEARCH_ANNOTATION_PKG + "." + DOCUMENT_ANNOTATION_SIMPLE_CLASS_NAME})
 @SupportedSourceVersion(SourceVersion.RELEASE_8)
 @SupportedOptions({AppSearchCompiler.OUTPUT_DIR_OPTION})
 public class AppSearchCompiler extends BasicAnnotationProcessor {
@@ -80,7 +82,7 @@
 
         @Override
         public ImmutableSet<String> annotations() {
-            return ImmutableSet.of(IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS);
+            return ImmutableSet.of(IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS.canonicalName());
         }
 
         @Override
@@ -88,7 +90,7 @@
                 ImmutableSetMultimap<String, Element> elementsByAnnotation) {
             Set<TypeElement> documentElements =
                     typesIn(elementsByAnnotation.get(
-                            IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS));
+                            IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS.canonicalName()));
 
             ImmutableSet.Builder<Element> nextRound = new ImmutableSet.Builder<>();
             for (TypeElement document : documentElements) {
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java
index 925d0bb..93116a9 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java
@@ -19,8 +19,6 @@
 import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
 import static androidx.appsearch.compiler.IntrospectionHelper.generateClassHierarchy;
 import static androidx.appsearch.compiler.IntrospectionHelper.getDocumentAnnotation;
-import static androidx.appsearch.compiler.IntrospectionHelper.validateIsGetter;
-
 import static java.util.stream.Collectors.groupingBy;
 
 import androidx.annotation.NonNull;
@@ -32,6 +30,7 @@
 import androidx.appsearch.compiler.annotationwrapper.PropertyAnnotation;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumMap;
 import java.util.HashMap;
@@ -41,7 +40,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.Set;
 import java.util.function.Predicate;
 
@@ -53,8 +51,6 @@
 import javax.lang.model.element.Modifier;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.element.VariableElement;
-import javax.lang.model.type.TypeKind;
-import javax.lang.model.type.TypeMirror;
 import javax.lang.model.util.Elements;
 import javax.lang.model.util.Types;
 
@@ -69,9 +65,6 @@
     /** Enumeration of fields that must be handled specially (i.e. are not properties) */
     enum SpecialField {ID, NAMESPACE, CREATION_TIMESTAMP_MILLIS, TTL_MILLIS, SCORE}
 
-    /** Determines how the annotation processor has decided to read the value of a field. */
-    enum ReadKind {FIELD, GETTER}
-
     /** Determines how the annotation processor has decided to write the value of a field. */
     enum WriteKind {FIELD, SETTER, CREATION_METHOD}
 
@@ -89,12 +82,9 @@
     // for AutoValue document.
     // Warning: if you change this to a HashSet, we may choose different getters or setters from
     // run to run, causing the generated code to bounce.
-    private final Set<ExecutableElement> mAllMethods = new LinkedHashSet<>();
+    private final LinkedHashSet<ExecutableElement> mAllMethods;
     // All methods in the builder class, if a builder producer is provided.
-    private final Set<ExecutableElement> mAllBuilderMethods = new LinkedHashSet<>();
-    // Key: Name of the element which is accessed through the getter method.
-    // Value: ExecutableElement of the getter method.
-    private final Map<String, ExecutableElement> mGetterMethods = new HashMap<>();
+    private final LinkedHashSet<ExecutableElement> mAllBuilderMethods;
     // Key: Name of the element whose value is set through the setter method.
     // Value: ExecutableElement of the setter method.
     private final Map<String, ExecutableElement> mSetterMethods = new HashMap<>();
@@ -109,7 +99,6 @@
     // name
     private final Map<String, Element> mPropertyElements = new LinkedHashMap<>();
     private final Map<SpecialField, String> mSpecialFieldNames = new EnumMap<>(SpecialField.class);
-    private final Map<Element, ReadKind> mReadKinds = new HashMap<>();
     private final Map<Element, WriteKind> mWriteKinds = new HashMap<>();
     // Contains the reason why that element couldn't be written either by field or by setter.
     private final Map<Element, ProcessingException> mWriteWhyCreationMethod =
@@ -121,6 +110,15 @@
 
     private final List<AnnotatedGetterOrField> mAnnotatedGettersAndFields;
 
+    @NonNull
+    private final AnnotatedGetterOrField mIdAnnotatedGetterOrField;
+
+    @NonNull
+    private final AnnotatedGetterOrField mNamespaceAnnotatedGetterOrField;
+
+    @NonNull
+    private final Map<AnnotatedGetterOrField, PropertyAccessor> mAccessors;
+
     private DocumentModel(
             @NonNull ProcessingEnvironment env,
             @NonNull TypeElement clazz,
@@ -140,23 +138,24 @@
         mAnnotatedGettersAndFields = scanAnnotatedGettersAndFields(clazz, env);
 
         requireNoDuplicateMetadataProperties();
-        requireGetterOrFieldMatchingPredicate(
+        mIdAnnotatedGetterOrField = requireGetterOrFieldMatchingPredicate(
                 getterOrField -> getterOrField.getAnnotation() == MetadataPropertyAnnotation.ID,
                 /* errorMessage= */"All @Document classes must have exactly one field annotated "
                         + "with @Id");
-        requireGetterOrFieldMatchingPredicate(
+        mNamespaceAnnotatedGetterOrField = requireGetterOrFieldMatchingPredicate(
                 getterOrField ->
                         getterOrField.getAnnotation() == MetadataPropertyAnnotation.NAMESPACE,
                 /* errorMessage= */"All @Document classes must have exactly one field annotated "
                         + "with @Namespace");
 
+        mAllMethods = mHelper.getAllMethods(clazz);
+        mAccessors = inferPropertyAccessors(mAnnotatedGettersAndFields, mAllMethods, mHelper);
+
         // Scan methods and constructors. We will need this info when processing fields to
         // make sure the fields can be get and set.
         Set<ExecutableElement> potentialCreationMethods = extractCreationMethods(clazz);
-        addAllMethods(mClass, mAllMethods);
-        if (mBuilderClass != null) {
-            addAllMethods(mBuilderClass, mAllBuilderMethods);
-        }
+        mAllBuilderMethods = mBuilderClass != null
+                ? mHelper.getAllMethods(mBuilderClass) : new LinkedHashSet<>();
         scanFields(mClass);
         chooseCreationMethod(potentialCreationMethods);
     }
@@ -177,7 +176,7 @@
             boolean isAnnotated = false;
             for (AnnotationMirror annotation : child.getAnnotationMirrors()) {
                 if (annotation.getAnnotationType().toString().equals(
-                        IntrospectionHelper.BUILDER_PRODUCER_CLASS)) {
+                        IntrospectionHelper.BUILDER_PRODUCER_CLASS.canonicalName())) {
                     isAnnotated = true;
                     break;
                 }
@@ -256,7 +255,8 @@
             if (gettersAndFields.size() > 1) {
                 // Can show the error on any of the duplicates. Just pick the first first.
                 throw new ProcessingException(
-                        "Duplicate member annotated with @" + annotation.getSimpleClassName(),
+                        "Duplicate member annotated with @"
+                                + annotation.getClassName().simpleName(),
                         gettersAndFields.get(0).getElement());
             }
         }
@@ -266,16 +266,17 @@
      * Makes sure {@link #mAnnotatedGettersAndFields} contains a getter/field that matches the
      * predicate.
      *
+     * @return The matched getter/field.
      * @throws ProcessingException with the error message if no match.
      */
-    private void requireGetterOrFieldMatchingPredicate(
+    @NonNull
+    private AnnotatedGetterOrField requireGetterOrFieldMatchingPredicate(
             @NonNull Predicate<AnnotatedGetterOrField> predicate,
             @NonNull String errorMessage) throws ProcessingException {
-        Optional<AnnotatedGetterOrField> annotatedGetterOrField =
-                mAnnotatedGettersAndFields.stream().filter(predicate).findFirst();
-        if (annotatedGetterOrField.isEmpty()) {
-            throw new ProcessingException(errorMessage, mClass);
-        }
+        return mAnnotatedGettersAndFields.stream()
+                .filter(predicate)
+                .findFirst()
+                .orElseThrow(() -> new ProcessingException(errorMessage, mClass));
     }
 
     private Set<ExecutableElement> extractCreationMethods(TypeElement typeElement)
@@ -301,24 +302,6 @@
         return Collections.unmodifiableSet(creationMethods);
     }
 
-    private void addAllMethods(TypeElement typeElement, Set<ExecutableElement> allMethods) {
-        for (Element child : typeElement.getEnclosedElements()) {
-            if (child.getKind() == ElementKind.METHOD) {
-                allMethods.add((ExecutableElement) child);
-            }
-        }
-
-        TypeMirror superClass = typeElement.getSuperclass();
-        if (superClass.getKind().equals(TypeKind.DECLARED)) {
-            addAllMethods((TypeElement) mTypeUtil.asElement(superClass), allMethods);
-        }
-        for (TypeMirror implementedInterface : typeElement.getInterfaces()) {
-            if (implementedInterface.getKind().equals(TypeKind.DECLARED)) {
-                addAllMethods((TypeElement) mTypeUtil.asElement(implementedInterface), allMethods);
-            }
-        }
-    }
-
     /**
      * Tries to create an {@link DocumentModel} from the given {@link Element}.
      *
@@ -386,6 +369,35 @@
     }
 
     /**
+     * Returns the getter/field annotated with {@code @Document.Id}.
+     */
+    @NonNull
+    public AnnotatedGetterOrField getIdAnnotatedGetterOrField() {
+        return mIdAnnotatedGetterOrField;
+    }
+
+    /**
+     * Returns the getter/field annotated with {@code @Document.Namespace}.
+     */
+    @NonNull
+    public AnnotatedGetterOrField getNamespaceAnnotatedGetterOrField() {
+        return mNamespaceAnnotatedGetterOrField;
+    }
+
+    /**
+     * Returns the public/package-private accessor for an annotated getter/field (may be private).
+     */
+    @NonNull
+    public PropertyAccessor getAccessor(@NonNull AnnotatedGetterOrField getterOrField) {
+        PropertyAccessor accessor = mAccessors.get(getterOrField);
+        if (accessor == null) {
+            throw new IllegalArgumentException(
+                    "No such getter/field belongs to this DocumentModel: " + getterOrField);
+        }
+        return accessor;
+    }
+
+    /**
      * @deprecated Use {@link #getAnnotatedGettersAndFields()} instead.
      */
     @Deprecated
@@ -400,23 +412,12 @@
     }
 
     @Nullable
-    public ReadKind getElementReadKind(String elementName) {
-        Element element = mAllAppSearchElements.get(elementName);
-        return mReadKinds.get(element);
-    }
-
-    @Nullable
     public WriteKind getElementWriteKind(String elementName) {
         Element element = mAllAppSearchElements.get(elementName);
         return mWriteKinds.get(element);
     }
 
     @Nullable
-    public ExecutableElement getGetterForElement(String elementName) {
-        return mGetterMethods.get(elementName);
-    }
-
-    @Nullable
     public ExecutableElement getSetterForElement(String elementName) {
         return mSetterMethods.get(elementName);
     }
@@ -483,6 +484,26 @@
         return mBuilderClass;
     }
 
+    /**
+     * Infers the {@link PropertyAccessor} for each of the {@link AnnotatedGetterOrField}.
+     *
+     * <p>Each accessor may be the {@link AnnotatedGetterOrField} itself or some other non-private
+     * getter.
+     */
+    @NonNull
+    private static Map<AnnotatedGetterOrField, PropertyAccessor> inferPropertyAccessors(
+            @NonNull List<AnnotatedGetterOrField> annotatedGettersAndFields,
+            @NonNull Collection<ExecutableElement> allMethods,
+            @NonNull IntrospectionHelper helper) throws ProcessingException {
+        Map<AnnotatedGetterOrField, PropertyAccessor> accessors = new HashMap<>();
+        for (AnnotatedGetterOrField getterOrField : annotatedGettersAndFields) {
+            accessors.put(
+                    getterOrField,
+                    PropertyAccessor.infer(getterOrField, allMethods, helper));
+        }
+        return accessors;
+    }
+
     private boolean isFactoryMethod(ExecutableElement method) {
         Set<Modifier> methodModifiers = method.getModifiers();
         return methodModifiers.contains(Modifier.STATIC)
@@ -511,85 +532,85 @@
         // no annotation mirrors -> non-indexable field
         for (AnnotationMirror annotation : childElement.getAnnotationMirrors()) {
             String annotationFq = annotation.getAnnotationType().toString();
-            if (!annotationFq.startsWith(DOCUMENT_ANNOTATION_CLASS) || annotationFq.equals(
-                    BUILDER_PRODUCER_CLASS)) {
+            if (!annotationFq.startsWith(DOCUMENT_ANNOTATION_CLASS.canonicalName())
+                    || annotationFq.equals(BUILDER_PRODUCER_CLASS.canonicalName())) {
                 continue;
             }
             if (childElement.getKind() == ElementKind.CLASS) {
                 continue;
             }
 
-            switch (annotationFq) {
-                case IntrospectionHelper.ID_CLASS:
-                    if (mSpecialFieldNames.containsKey(SpecialField.ID)) {
-                        throw new ProcessingException(
-                                "Class hierarchy contains multiple fields annotated @Id",
-                                childElement);
-                    }
-                    mSpecialFieldNames.put(SpecialField.ID, fieldName);
-                    break;
-                case IntrospectionHelper.NAMESPACE_CLASS:
-                    if (mSpecialFieldNames.containsKey(SpecialField.NAMESPACE)) {
-                        throw new ProcessingException(
-                                "Class hierarchy contains multiple fields annotated @Namespace",
-                                childElement);
-                    }
-                    mSpecialFieldNames.put(SpecialField.NAMESPACE, fieldName);
-                    break;
-                case IntrospectionHelper.CREATION_TIMESTAMP_MILLIS_CLASS:
-                    if (mSpecialFieldNames.containsKey(SpecialField.CREATION_TIMESTAMP_MILLIS)) {
-                        throw new ProcessingException("Class hierarchy contains multiple fields "
-                                + "annotated @CreationTimestampMillis", childElement);
-                    }
-                    mSpecialFieldNames.put(
-                            SpecialField.CREATION_TIMESTAMP_MILLIS, fieldName);
-                    break;
-                case IntrospectionHelper.TTL_MILLIS_CLASS:
-                    if (mSpecialFieldNames.containsKey(SpecialField.TTL_MILLIS)) {
-                        throw new ProcessingException(
-                                "Class hierarchy contains multiple fields annotated @TtlMillis",
-                                childElement);
-                    }
-                    mSpecialFieldNames.put(SpecialField.TTL_MILLIS, fieldName);
-                    break;
-                case IntrospectionHelper.SCORE_CLASS:
-                    if (mSpecialFieldNames.containsKey(SpecialField.SCORE)) {
-                        throw new ProcessingException(
-                                "Class hierarchy contains multiple fields annotated @Score",
-                                childElement);
-                    }
-                    mSpecialFieldNames.put(SpecialField.SCORE, fieldName);
-                    break;
-                default:
-                    PropertyClass propertyClass = getPropertyClass(annotationFq);
-                    if (propertyClass != null) {
-                        // A property must either:
-                        //   1. be unique
-                        //   2. override a property from the Java parent while maintaining the same
-                        //      AppSearch property name
-                        checkFieldTypeForPropertyAnnotation(childElement, propertyClass);
-                        // It's assumed that parent types, in the context of Java's type system,
-                        // are always visited before child types, so existingProperty must come
-                        // from the parent type. To make this assumption valid, the result
-                        // returned by generateClassHierarchy must put parent types before child
-                        // types.
-                        Element existingProperty = mPropertyElements.get(fieldName);
-                        if (existingProperty != null) {
-                            if (!mTypeUtil.isSameType(
-                                    existingProperty.asType(), childElement.asType())) {
-                                throw new ProcessingException(
-                                        "Cannot override a property with a different type",
-                                        childElement);
-                            }
-                            if (!getPropertyName(existingProperty).equals(getPropertyName(
-                                    childElement))) {
-                                throw new ProcessingException(
-                                        "Cannot override a property with a different name",
-                                        childElement);
-                            }
+            if (annotationFq.equals(MetadataPropertyAnnotation.ID.getClassName().canonicalName())) {
+                if (mSpecialFieldNames.containsKey(SpecialField.ID)) {
+                    throw new ProcessingException(
+                            "Class hierarchy contains multiple fields annotated @Id",
+                            childElement);
+                }
+                mSpecialFieldNames.put(SpecialField.ID, fieldName);
+            } else if (annotationFq.equals(
+                    MetadataPropertyAnnotation.NAMESPACE.getClassName().canonicalName())) {
+                if (mSpecialFieldNames.containsKey(SpecialField.NAMESPACE)) {
+                    throw new ProcessingException(
+                            "Class hierarchy contains multiple fields annotated @Namespace",
+                            childElement);
+                }
+                mSpecialFieldNames.put(SpecialField.NAMESPACE, fieldName);
+            } else if (annotationFq.equals(
+                    MetadataPropertyAnnotation.CREATION_TIMESTAMP_MILLIS
+                            .getClassName()
+                            .canonicalName())) {
+                if (mSpecialFieldNames.containsKey(SpecialField.CREATION_TIMESTAMP_MILLIS)) {
+                    throw new ProcessingException("Class hierarchy contains multiple fields "
+                            + "annotated @CreationTimestampMillis", childElement);
+                }
+                mSpecialFieldNames.put(
+                        SpecialField.CREATION_TIMESTAMP_MILLIS, fieldName);
+            } else if (annotationFq.equals(
+                    MetadataPropertyAnnotation.TTL_MILLIS.getClassName().canonicalName())) {
+                if (mSpecialFieldNames.containsKey(SpecialField.TTL_MILLIS)) {
+                    throw new ProcessingException(
+                            "Class hierarchy contains multiple fields annotated @TtlMillis",
+                            childElement);
+                }
+                mSpecialFieldNames.put(SpecialField.TTL_MILLIS, fieldName);
+            } else if (annotationFq.equals(
+                    MetadataPropertyAnnotation.SCORE.getClassName().canonicalName())) {
+                if (mSpecialFieldNames.containsKey(SpecialField.SCORE)) {
+                    throw new ProcessingException(
+                            "Class hierarchy contains multiple fields annotated @Score",
+                            childElement);
+                }
+                mSpecialFieldNames.put(SpecialField.SCORE, fieldName);
+            } else {
+                PropertyClass propertyClass = getPropertyClass(annotationFq);
+                if (propertyClass != null) {
+                    // A property must either:
+                    //   1. be unique
+                    //   2. override a property from the Java parent while maintaining the same
+                    //      AppSearch property name
+                    checkFieldTypeForPropertyAnnotation(childElement, propertyClass);
+                    // It's assumed that parent types, in the context of Java's type system,
+                    // are always visited before child types, so existingProperty must come
+                    // from the parent type. To make this assumption valid, the result
+                    // returned by generateClassHierarchy must put parent types before child
+                    // types.
+                    Element existingProperty = mPropertyElements.get(fieldName);
+                    if (existingProperty != null) {
+                        if (!mTypeUtil.isSameType(
+                                existingProperty.asType(), childElement.asType())) {
+                            throw new ProcessingException(
+                                    "Cannot override a property with a different type",
+                                    childElement);
                         }
-                        mPropertyElements.put(fieldName, childElement);
+                        if (!getPropertyName(existingProperty).equals(getPropertyName(
+                                childElement))) {
+                            throw new ProcessingException(
+                                    "Cannot override a property with a different name",
+                                    childElement);
+                        }
                     }
+                    mPropertyElements.put(fieldName, childElement);
+                }
             }
 
             mAllAppSearchElements.put(fieldName, childElement);
@@ -648,7 +669,7 @@
         mSchemaName = computeSchemaName(hierarchy);
 
         for (Element appSearchField : mAllAppSearchElements.values()) {
-            chooseAccessKinds(appSearchField);
+            chooseWriteKind(appSearchField);
         }
     }
 
@@ -721,27 +742,15 @@
     }
 
     /**
-     * Chooses how to access the given field for read and write, subject to our requirements for all
-     * AppSearch-managed class fields:
+     * Chooses how to write a given field.
      *
-     * <p>For read: visible field, or visible getter
-     *
-     * <p>For write: visible mutable field, or visible setter, or visible creation method
-     * accepting at minimum all fields that aren't mutable and have no visible setter.
-     *
-     * @throws ProcessingException if no access type is possible for the given field
+     * <p>The writing strategy can be one of: visible mutable field, or visible setter, or visible
+     * creation method accepting at minimum all fields that aren't mutable and have no visible
+     * setter.
      */
-    private void chooseAccessKinds(@NonNull Element field)
-            throws ProcessingException {
-        // Choose get access
+    private void chooseWriteKind(@NonNull Element field) {
+        // TODO(b/300114568): Carve out better distinction b/w the different write strategies
         Set<Modifier> modifiers = field.getModifiers();
-        if (modifiers.contains(Modifier.PRIVATE) || field.getKind() == ElementKind.METHOD) {
-            findGetter(field);
-            mReadKinds.put(field, ReadKind.GETTER);
-        } else {
-            mReadKinds.put(field, ReadKind.FIELD);
-        }
-
         // Choose set access
         if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.FINAL)
                 || modifiers.contains(Modifier.STATIC) || field.getKind() == ElementKind.METHOD
@@ -860,54 +869,6 @@
     }
 
     /**
-     * Finds getter function for a private field, or for a property defined by a annotated getter
-     * method, in which case the annotated element itself should be the getter unless it's
-     * private or it takes parameters.
-     */
-    private void findGetter(@NonNull Element element) throws ProcessingException {
-        String elementName = element.getSimpleName().toString();
-        ProcessingException e;
-        if (element.getKind() == ElementKind.METHOD) {
-            e = new ProcessingException(
-                    "Failed to find a suitable getter for element \"" + elementName + "\"",
-                    mAllAppSearchElements.get(elementName));
-        } else {
-            e = new ProcessingException(
-                    "Field cannot be read: it is private and we failed to find a suitable getter "
-                            + "for field \"" + elementName + "\"",
-                    mAllAppSearchElements.get(elementName));
-        }
-
-        for (ExecutableElement method : mAllMethods) {
-            String methodName = method.getSimpleName().toString();
-            String normalizedElementName = getNormalizedElementName(element);
-            // normalizedElementName with first letter capitalized, to be paired with [is] or [get]
-            // prefix
-            String methodNameSuffix = normalizedElementName.substring(0, 1).toUpperCase()
-                    + normalizedElementName.substring(1);
-
-            if (methodName.equals(normalizedElementName)
-                    || methodName.equals("get" + methodNameSuffix)
-                    || (
-                    mHelper.isFieldOfBooleanType(element)
-                            && methodName.equals("is" + methodNameSuffix))
-            ) {
-                List<ProcessingException> errors = validateIsGetter(method);
-                if (!errors.isEmpty()) {
-                    e.addWarnings(errors);
-                    continue;
-                }
-                // Found one!
-                mGetterMethods.put(elementName, method);
-                return;
-            }
-        }
-
-        // Broke out of the loop without finding anything.
-        throw e;
-    }
-
-    /**
      * Finds setter function for a private field, or for a property defined by a annotated getter
      * method.
      */
@@ -1251,11 +1212,9 @@
         }
 
         /**
-         * Returns the serialized name for the corresponding property in the database.
+         * Returns the serialized name that should be used for the property in the database.
          *
-         * <p>Assumes the getter/field is annotated with a {@link DataPropertyAnnotation} to pull
-         * the serialized name out of the annotation
-         * e.g. {@code @Document.StringProperty("serializedName")}.
+         * <p>Assumes the getter/field is annotated with a {@link DataPropertyAnnotation}.
          */
         @NonNull
         private static String getSerializedName(@NonNull AnnotatedGetterOrField getterOrField) {
@@ -1280,13 +1239,12 @@
                 throws ProcessingException {
             PropertyAnnotation existingAnnotation = existingGetterOrField.getAnnotation();
             PropertyAnnotation overriddenAnnotation = overriddenGetterOfField.getAnnotation();
-            if (!existingAnnotation.getQualifiedClassName().equals(
-                    overriddenAnnotation.getQualifiedClassName())) {
+            if (!existingAnnotation.getClassName().equals(overriddenAnnotation.getClassName())) {
                 throw new ProcessingException(
                         ("Property type must stay consistent when overriding annotated members "
                                 + "but changed from @%s -> @%s").formatted(
-                                existingAnnotation.getSimpleClassName(),
-                                overriddenAnnotation.getSimpleClassName()),
+                                existingAnnotation.getClassName().simpleName(),
+                                overriddenAnnotation.getClassName().simpleName()),
                         overriddenGetterOfField.getElement());
             }
         }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
index e0e0b56..071fcd4 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
@@ -16,6 +16,7 @@
 
 package androidx.appsearch.compiler;
 
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_EXCEPTION_CLASS;
 import static androidx.appsearch.compiler.IntrospectionHelper.getDocumentAnnotation;
 import static androidx.appsearch.compiler.IntrospectionHelper.getPropertyType;
 
@@ -81,7 +82,7 @@
                 .returns(classType)
                 .addAnnotation(Override.class)
                 .addParameter(mHelper.getAppSearchClass("GenericDocument"), "genericDoc")
-                .addException(mHelper.getAppSearchExceptionClass());
+                .addException(APPSEARCH_EXCEPTION_CLASS);
 
         unpackSpecialFields(methodBuilder);
 
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
index 420bd35..7e815f3 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
@@ -16,6 +16,7 @@
 package androidx.appsearch.compiler;
 
 import static com.google.auto.common.MoreTypes.asTypeElement;
+import static java.util.stream.Collectors.toCollection;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -30,6 +31,7 @@
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -60,18 +62,31 @@
 public class IntrospectionHelper {
     static final String GEN_CLASS_PREFIX = "$$__AppSearch__";
     static final String APPSEARCH_PKG = "androidx.appsearch.app";
+
+    public static final ClassName APPSEARCH_SCHEMA_CLASS =
+            ClassName.get(APPSEARCH_PKG, "AppSearchSchema");
+
+    static final ClassName PROPERTY_CONFIG_CLASS =
+            APPSEARCH_SCHEMA_CLASS.nestedClass("PropertyConfig");
+
     static final String APPSEARCH_EXCEPTION_PKG = "androidx.appsearch.exceptions";
-    static final String APPSEARCH_EXCEPTION_SIMPLE_NAME = "AppSearchException";
-    public static final String DOCUMENT_ANNOTATION_CLASS = "androidx.appsearch.annotation.Document";
-    static final String ID_CLASS = "androidx.appsearch.annotation.Document.Id";
-    static final String NAMESPACE_CLASS = "androidx.appsearch.annotation.Document.Namespace";
-    static final String CREATION_TIMESTAMP_MILLIS_CLASS =
-            "androidx.appsearch.annotation.Document.CreationTimestampMillis";
-    static final String TTL_MILLIS_CLASS = "androidx.appsearch.annotation.Document"
-            + ".TtlMillis";
-    static final String SCORE_CLASS = "androidx.appsearch.annotation.Document.Score";
-    static final String BUILDER_PRODUCER_CLASS =
-            "androidx.appsearch.annotation.Document.BuilderProducer";
+
+    static final ClassName APPSEARCH_EXCEPTION_CLASS =
+            ClassName.get(APPSEARCH_EXCEPTION_PKG, "AppSearchException");
+
+    public static final String APPSEARCH_ANNOTATION_PKG = "androidx.appsearch.annotation";
+
+    public static final String DOCUMENT_ANNOTATION_SIMPLE_CLASS_NAME = "Document";
+
+    public static final ClassName DOCUMENT_ANNOTATION_CLASS =
+            ClassName.get(APPSEARCH_ANNOTATION_PKG, DOCUMENT_ANNOTATION_SIMPLE_CLASS_NAME);
+
+    public static final ClassName GENERIC_DOCUMENT_CLASS =
+            ClassName.get(APPSEARCH_PKG, "GenericDocument");
+
+    public static final ClassName BUILDER_PRODUCER_CLASS =
+            DOCUMENT_ANNOTATION_CLASS.nestedClass("BuilderProducer");
+
     final TypeMirror mCollectionType;
     final TypeMirror mListType;
     final TypeMirror mStringType;
@@ -125,7 +140,8 @@
         Objects.requireNonNull(element);
         for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
             String annotationFq = annotation.getAnnotationType().toString();
-            if (IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS.equals(annotationFq)) {
+            if (IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS.canonicalName().equals(
+                    annotationFq)) {
                 return annotation;
             }
         }
@@ -210,7 +226,9 @@
      * Creates the name of output class. $$__AppSearch__Foo for Foo, $$__AppSearch__Foo$$__Bar
      * for inner class Foo.Bar.
      */
-    public ClassName getDocumentClassFactoryForClass(String pkg, String className) {
+    @NonNull
+    public static ClassName getDocumentClassFactoryForClass(
+            @NonNull String pkg, @NonNull String className) {
         String genClassName = GEN_CLASS_PREFIX + className.replace(".", "$$__");
         return ClassName.get(pkg, genClassName);
     }
@@ -219,7 +237,8 @@
      * Creates the name of output class. $$__AppSearch__Foo for Foo, $$__AppSearch__Foo$$__Bar
      * for inner class Foo.Bar.
      */
-    public ClassName getDocumentClassFactoryForClass(ClassName clazz) {
+    @NonNull
+    public static ClassName getDocumentClassFactoryForClass(@NonNull ClassName clazz) {
         String className = clazz.canonicalName().substring(clazz.packageName().length() + 1);
         return getDocumentClassFactoryForClass(clazz.packageName(), className);
     }
@@ -228,8 +247,40 @@
         return ClassName.get(APPSEARCH_PKG, clazz, nested);
     }
 
-    public ClassName getAppSearchExceptionClass() {
-        return ClassName.get(APPSEARCH_EXCEPTION_PKG, APPSEARCH_EXCEPTION_SIMPLE_NAME);
+    /**
+     * Returns all the methods within a class, whether inherited or declared directly.
+     */
+    @NonNull
+    public LinkedHashSet<ExecutableElement> getAllMethods(@NonNull TypeElement clazz) {
+        return mEnv.getElementUtils().getAllMembers(clazz).stream()
+                .filter(element -> element.getKind() == ElementKind.METHOD)
+                .map(element -> (ExecutableElement) element)
+                .collect(toCollection(LinkedHashSet::new));
+    }
+
+    /**
+     * Whether a type is the same as {@code long[]}.
+     */
+    public boolean isPrimitiveLongArray(@NonNull TypeMirror type) {
+        return isArrayOf(type, mLongPrimitiveType);
+    }
+
+    /**
+     * Whether a type is the same as {@code double[]}.
+     */
+    public boolean isPrimitiveDoubleArray(@NonNull TypeMirror type) {
+        return isArrayOf(type, mDoublePrimitiveType);
+    }
+
+    /**
+     * Whether a type is the same as {@code boolean[]}.
+     */
+    public boolean isPrimitiveBooleanArray(@NonNull TypeMirror type) {
+        return isArrayOf(type, mBooleanPrimitiveType);
+    }
+
+    private boolean isArrayOf(@NonNull TypeMirror type, @NonNull TypeMirror arrayComponentType) {
+        return mTypeUtils.isSameType(type, mTypeUtils.getArrayType(arrayComponentType));
     }
 
     /**
@@ -284,6 +335,21 @@
         return errors;
     }
 
+    /**
+     * Same as {@link #validateIsGetter} but additionally verifies that the getter returns the
+     * specified type.
+     */
+    @NonNull
+    public List<ProcessingException> validateIsGetterThatReturns(
+            @NonNull ExecutableElement method, @NonNull TypeMirror expectedReturnType) {
+        List<ProcessingException> errors = validateIsGetter(method);
+        if (!mTypeUtils.isSameType(method.getReturnType(), expectedReturnType)) {
+            errors.add(new ProcessingException(
+                    "Getter cannot be used: Does not return " + expectedReturnType, method));
+        }
+        return errors;
+    }
+
     private static void generateClassHierarchyHelper(@NonNull TypeElement leafElement,
             @NonNull TypeElement currentClass, @NonNull Deque<TypeElement> hierarchy,
             @NonNull Set<TypeElement> visited)
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/PropertyAccessor.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/PropertyAccessor.java
new file mode 100644
index 0000000..28b3c05
--- /dev/null
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/PropertyAccessor.java
@@ -0,0 +1,162 @@
+/*
+ * 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.appsearch.compiler;
+
+import static java.util.stream.Collectors.joining;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.compiler.AnnotatedGetterOrField.ElementTypeCategory;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+
+/**
+ * The public/package-private accessor for an {@link AnnotatedGetterOrField}.
+ *
+ * <p>The accessor itself may be a getter or a field.
+ *
+ * <p>May be the {@link AnnotatedGetterOrField} itself or some completely different method in
+ * case the {@link AnnotatedGetterOrField} is private. For example:
+ *
+ * <pre>
+ * {@code
+ * @Document("MyEntity")
+ * class Entity {
+ *     @Document.StringProperty
+ *     private String mName;
+ *
+ *     public String getName();
+ *     //            ^^^^^^^
+ * }
+ * }
+ * </pre>
+ */
+@AutoValue
+public abstract class PropertyAccessor {
+
+    /**
+     * The getter/field element.
+     */
+    @NonNull
+    public abstract Element getElement();
+
+
+    /**
+     * Whether the accessor is a getter.
+     */
+    public boolean isGetter() {
+        return getElement().getKind() == ElementKind.METHOD;
+    }
+
+    /**
+     * Whether the accessor is a field.
+     */
+    public boolean isField() {
+        return getElement().getKind() == ElementKind.FIELD;
+    }
+
+    /**
+     * Infers the {@link PropertyAccessor} for a given {@link AnnotatedGetterOrField}.
+     *
+     * @param neighboringMethods The surrounding methods in the same class as the field. In case
+     *                           the field is private, an appropriate non-private getter can be
+     *                           picked from this list.
+     */
+    @NonNull
+    public static PropertyAccessor infer(
+            @NonNull AnnotatedGetterOrField getterOrField,
+            @NonNull Collection<ExecutableElement> neighboringMethods,
+            @NonNull IntrospectionHelper helper) throws ProcessingException {
+        if (!getterOrField.getElement().getModifiers().contains(Modifier.PRIVATE)) {
+            // Accessible as-is
+            return new AutoValue_PropertyAccessor(getterOrField.getElement());
+        }
+
+        if (getterOrField.isGetter()) {
+            throw new ProcessingException(
+                    "Annotated getter must not be private", getterOrField.getElement());
+        }
+
+        return new AutoValue_PropertyAccessor(
+                findCorrespondingGetter(getterOrField, neighboringMethods, helper));
+    }
+
+    @NonNull
+    private static ExecutableElement findCorrespondingGetter(
+            @NonNull AnnotatedGetterOrField privateField,
+            @NonNull Collection<ExecutableElement> neighboringMethods,
+            @NonNull IntrospectionHelper helper) throws ProcessingException {
+        Set<String> getterNames = getAcceptableGetterNames(privateField, helper);
+        List<ExecutableElement> potentialGetters =
+                neighboringMethods.stream()
+                        .filter(method -> getterNames.contains(method.getSimpleName().toString()))
+                        .toList();
+
+        // Start building the exception for the case where we don't find a suitable getter
+        String potentialSignatures = getterNames.stream()
+                .map(name -> "[public] " + privateField.getJvmType() + " " + name + "()")
+                .collect(joining(" OR "));
+        ProcessingException processingException = new ProcessingException(
+                "Field '%s' cannot be read: it is private and has no suitable getters %s"
+                        .formatted(privateField.getJvmName(), potentialSignatures),
+                privateField.getElement());
+
+        for (ExecutableElement method : potentialGetters) {
+            List<ProcessingException> errors =
+                    helper.validateIsGetterThatReturns(method, privateField.getJvmType());
+            if (!errors.isEmpty()) {
+                processingException.addWarnings(errors);
+                continue;
+            }
+            // found one!
+            return method;
+        }
+
+        throw processingException;
+    }
+
+    @NonNull
+    private static Set<String> getAcceptableGetterNames(
+            @NonNull AnnotatedGetterOrField privateField,
+            @NonNull IntrospectionHelper helper) {
+        // String mMyField -> {myField, getMyField}
+        // boolean mMyField -> {myField, getMyField, isMyField}
+        String normalizedName = privateField.getNormalizedName();
+        Set<String> getterNames = new HashSet<>();
+        getterNames.add(normalizedName);
+        String upperCamelCase = normalizedName.substring(0, 1).toUpperCase()
+                + normalizedName.substring(1);
+        getterNames.add("get" + upperCamelCase);
+        boolean isBooleanField = helper.isFieldOfExactType(
+                privateField.getElement(),
+                helper.mBooleanPrimitiveType,
+                helper.mBooleanBoxType);
+        if (isBooleanField && privateField.getElementTypeCategory() == ElementTypeCategory.SINGLE) {
+            getterNames.add("is" + upperCamelCase);
+        }
+        return getterNames;
+    }
+}
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
index 8473ed4..288a61b 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
@@ -16,9 +16,16 @@
 
 package androidx.appsearch.compiler;
 
-import static androidx.appsearch.compiler.IntrospectionHelper.getPropertyType;
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_EXCEPTION_CLASS;
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_SCHEMA_CLASS;
+import static androidx.appsearch.compiler.IntrospectionHelper.PROPERTY_CONFIG_CLASS;
+import static androidx.appsearch.compiler.IntrospectionHelper.getDocumentClassFactoryForClass;
 
 import androidx.annotation.NonNull;
+import androidx.appsearch.compiler.annotationwrapper.DataPropertyAnnotation;
+import androidx.appsearch.compiler.annotationwrapper.DocumentPropertyAnnotation;
+import androidx.appsearch.compiler.annotationwrapper.LongPropertyAnnotation;
+import androidx.appsearch.compiler.annotationwrapper.StringPropertyAnnotation;
 
 import com.squareup.javapoet.ClassName;
 import com.squareup.javapoet.CodeBlock;
@@ -29,41 +36,45 @@
 import com.squareup.javapoet.TypeSpec;
 import com.squareup.javapoet.WildcardTypeName;
 
+import java.util.Collections;
 import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 import javax.annotation.processing.ProcessingEnvironment;
-import javax.lang.model.element.AnnotationMirror;
-import javax.lang.model.element.Element;
 import javax.lang.model.element.Modifier;
 import javax.lang.model.element.TypeElement;
-import javax.lang.model.type.ArrayType;
-import javax.lang.model.type.DeclaredType;
-import javax.lang.model.type.TypeKind;
 import javax.lang.model.type.TypeMirror;
-import javax.lang.model.util.Types;
 
 /** Generates java code for an {@link androidx.appsearch.app.AppSearchSchema}. */
 class SchemaCodeGenerator {
-    private final ProcessingEnvironment mEnv;
-    private final IntrospectionHelper mHelper;
     private final DocumentModel mModel;
-    private final Set<ClassName> mDependencyDocumentClasses = new LinkedHashSet<>();
+    private final LinkedHashSet<TypeElement> mDependencyDocumentClasses;
 
     public static void generate(
             @NonNull ProcessingEnvironment env,
             @NonNull DocumentModel model,
             @NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
-        new SchemaCodeGenerator(env, model).generate(classBuilder);
+        new SchemaCodeGenerator(model, env).generate(classBuilder);
     }
 
-    private SchemaCodeGenerator(
-            @NonNull ProcessingEnvironment env, @NonNull DocumentModel model) {
-        mEnv = env;
-        mHelper = new IntrospectionHelper(env);
+    private SchemaCodeGenerator(@NonNull DocumentModel model, @NonNull ProcessingEnvironment env) {
         mModel = model;
+        mDependencyDocumentClasses = computeDependencyClasses(model, env);
+    }
+
+    @NonNull
+    private static LinkedHashSet<TypeElement> computeDependencyClasses(
+            @NonNull DocumentModel model,
+            @NonNull ProcessingEnvironment env) {
+        LinkedHashSet<TypeElement> dependencies = new LinkedHashSet<>(model.getParentTypes());
+        for (AnnotatedGetterOrField getterOrField : model.getAnnotatedGettersAndFields()) {
+            if (!(getterOrField.getAnnotation() instanceof DocumentPropertyAnnotation)) {
+                continue;
+            }
+
+            TypeMirror documentClass = getterOrField.getComponentType();
+            dependencies.add((TypeElement) env.getTypeUtils().asElement(documentClass));
+        }
+        return dependencies;
     }
 
     private void generate(@NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
@@ -76,20 +87,18 @@
         classBuilder.addMethod(
                 MethodSpec.methodBuilder("getSchemaName")
                         .addModifiers(Modifier.PUBLIC)
-                        .returns(TypeName.get(mHelper.mStringType))
+                        .returns(String.class)
                         .addAnnotation(Override.class)
                         .addStatement("return SCHEMA_NAME")
                         .build());
 
-        CodeBlock schemaInitializer = createSchemaInitializerGetDocumentTypes();
-
         classBuilder.addMethod(
                 MethodSpec.methodBuilder("getSchema")
                         .addModifiers(Modifier.PUBLIC)
-                        .returns(mHelper.getAppSearchClass("AppSearchSchema"))
+                        .returns(APPSEARCH_SCHEMA_CLASS)
                         .addAnnotation(Override.class)
-                        .addException(mHelper.getAppSearchExceptionClass())
-                        .addStatement("return $L", schemaInitializer)
+                        .addException(APPSEARCH_EXCEPTION_CLASS)
+                        .addStatement("return $L", createSchemaInitializerGetDocumentTypes())
                         .build());
 
         classBuilder.addMethod(createDependencyClassesMethod());
@@ -97,239 +106,280 @@
 
     @NonNull
     private MethodSpec createDependencyClassesMethod() {
-        TypeName setOfClasses = ParameterizedTypeName.get(ClassName.get("java.util", "List"),
+        TypeName listOfClasses = ParameterizedTypeName.get(ClassName.get("java.util", "List"),
                 ParameterizedTypeName.get(ClassName.get(Class.class),
                         WildcardTypeName.subtypeOf(Object.class)));
 
-        TypeName arraySetOfClasses =
+        TypeName arrayListOfClasses =
                 ParameterizedTypeName.get(ClassName.get("java.util", "ArrayList"),
                         ParameterizedTypeName.get(ClassName.get(Class.class),
                                 WildcardTypeName.subtypeOf(Object.class)));
 
         MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("getDependencyDocumentClasses")
                 .addModifiers(Modifier.PUBLIC)
-                .returns(setOfClasses)
+                .returns(listOfClasses)
                 .addAnnotation(Override.class)
-                .addException(mHelper.getAppSearchExceptionClass());
+                .addException(APPSEARCH_EXCEPTION_CLASS);
 
         if (mDependencyDocumentClasses.isEmpty()) {
-            methodBuilder.addStatement("return $T.emptyList()",
-                    ClassName.get("java.util", "Collections"));
+            methodBuilder.addStatement("return $T.emptyList()", ClassName.get(Collections.class));
         } else {
-            methodBuilder.addStatement("$T classSet = new $T()", setOfClasses, arraySetOfClasses);
-            for (ClassName className : mDependencyDocumentClasses) {
-                methodBuilder.addStatement("classSet.add($T.class)", className);
+            methodBuilder.addStatement("$T classSet = new $T()", listOfClasses, arrayListOfClasses);
+            for (TypeElement dependencyType : mDependencyDocumentClasses) {
+                methodBuilder.addStatement("classSet.add($T.class)", ClassName.get(dependencyType));
             }
             methodBuilder.addStatement("return classSet").build();
         }
+
         return methodBuilder.build();
     }
 
     /**
-     * This method accumulates Document-type properties, by calling {@link #createPropertySchema},
-     * and parent types in mDependencyDocumentClasses.
+     * Creates an expr of type {@link androidx.appsearch.app.AppSearchSchema}.
+     *
+     * <p>The AppSearchSchema has parent types and various Document.*Properties set.
      */
     private CodeBlock createSchemaInitializerGetDocumentTypes() throws ProcessingException {
         CodeBlock.Builder codeBlock = CodeBlock.builder()
-                .add("new $T(SCHEMA_NAME)", mHelper.getAppSearchClass("AppSearchSchema", "Builder"))
+                .add("new $T(SCHEMA_NAME)", APPSEARCH_SCHEMA_CLASS.nestedClass("Builder"))
                 .indent();
         for (TypeElement parentType : mModel.getParentTypes()) {
             ClassName parentDocumentFactoryClass =
-                    mHelper.getDocumentClassFactoryForClass(ClassName.get(parentType));
+                    getDocumentClassFactoryForClass(ClassName.get(parentType));
             codeBlock.add("\n.addParentType($T.SCHEMA_NAME)", parentDocumentFactoryClass);
-            mDependencyDocumentClasses.add(ClassName.get(parentType));
         }
-        for (Element property : mModel.getPropertyElements().values()) {
-            codeBlock.add("\n.addProperty($L)", createPropertySchema(property));
+
+        for (AnnotatedGetterOrField getterOrField : mModel.getAnnotatedGettersAndFields()) {
+            if (!(getterOrField.getAnnotation() instanceof DataPropertyAnnotation)) {
+                continue;
+            }
+
+            CodeBlock propertyConfigExpr = createPropertyConfig(
+                    (DataPropertyAnnotation) getterOrField.getAnnotation(), getterOrField);
+            codeBlock.add("\n.addProperty($L)", propertyConfigExpr);
         }
+
         codeBlock.add("\n.build()").unindent();
         return codeBlock.build();
     }
 
-    /** This method accumulates Document-type properties in mDependencyDocumentClasses. */
-    private CodeBlock createPropertySchema(@NonNull Element property)
-            throws ProcessingException {
-        AnnotationMirror annotation = mModel.getPropertyAnnotation(property);
-        Map<String, Object> params = mHelper.getAnnotationParams(annotation);
-
-        // Find the property type
-        Types typeUtil = mEnv.getTypeUtils();
-        TypeMirror propertyType = getPropertyType(property);
-        boolean repeated = false;
-        boolean isPropertyString = false;
-        boolean isPropertyDocument = false;
-        boolean isPropertyLong = false;
-        if (propertyType.getKind() == TypeKind.ERROR) {
-            throw new ProcessingException("Property type unknown to java compiler", property);
-        } else if (typeUtil.isAssignable(
-                typeUtil.erasure(propertyType), mHelper.mCollectionType)) {
-            List<? extends TypeMirror> genericTypes =
-                    ((DeclaredType) propertyType).getTypeArguments();
-            if (genericTypes.isEmpty()) {
-                throw new ProcessingException(
-                        "Property is repeated but has no generic type", property);
-            }
-            propertyType = genericTypes.get(0);
-            repeated = true;
-        } else if (propertyType.getKind() == TypeKind.ARRAY
-                // Byte arrays have a native representation in Icing, so they are not considered a
-                // "repeated" type
-                && !typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)
-                && !typeUtil.isSameType(propertyType, mHelper.mByteBoxArrayType)) {
-            propertyType = ((ArrayType) propertyType).getComponentType();
-            repeated = true;
-
-        }
-        ClassName propertyClass;
-        if (typeUtil.isSameType(propertyType, mHelper.mStringType)) {
-            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "StringPropertyConfig");
-            isPropertyString = true;
-        } else if (typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mIntPrimitiveType)
-                || typeUtil.isSameType(propertyType, mHelper.mLongBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mLongPrimitiveType)) {
-            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "LongPropertyConfig");
-            isPropertyLong = true;
-        } else if (typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mFloatPrimitiveType)
-                || typeUtil.isSameType(propertyType, mHelper.mDoubleBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mDoublePrimitiveType)) {
-            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "DoublePropertyConfig");
-        } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mBooleanPrimitiveType)) {
-            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "BooleanPropertyConfig");
-        } else if (typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)
-                || typeUtil.isSameType(propertyType, mHelper.mByteBoxArrayType)) {
-            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "BytesPropertyConfig");
-        } else {
-            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "DocumentPropertyConfig");
-            isPropertyDocument = true;
-        }
-
-        // Start the builder for the property
-        String propertyName = mModel.getPropertyName(property);
+    /**
+     * Produces an expr for the creating the property's config e.g.
+     *
+     * <pre>
+     * {@code
+     * new StringPropertyConfig.Builder("someProp")
+     *   .setCardinality(StringPropertyConfig.CARDINALITY_REPEATED)
+     *   .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+     *   .build()
+     * }
+     * </pre>
+     */
+    private CodeBlock createPropertyConfig(
+            @NonNull DataPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) throws ProcessingException {
         CodeBlock.Builder codeBlock = CodeBlock.builder();
-        if (isPropertyDocument) {
-            ClassName documentClass = (ClassName) ClassName.get(propertyType);
-            ClassName documentFactoryClass = mHelper.getDocumentClassFactoryForClass(documentClass);
-            codeBlock.add(
-                    "new $T($S, $T.SCHEMA_NAME)",
-                    propertyClass.nestedClass("Builder"),
-                    propertyName,
+        if (annotation.getDataPropertyKind() == DataPropertyAnnotation.Kind.DOCUMENT_PROPERTY) {
+            ClassName documentClass = (ClassName) ClassName.get(getterOrField.getComponentType());
+            ClassName documentFactoryClass = getDocumentClassFactoryForClass(documentClass);
+            codeBlock.add("new $T.Builder($S, $T.SCHEMA_NAME)",
+                    DocumentPropertyAnnotation.CONFIG_CLASS,
+                    annotation.getName(),
                     documentFactoryClass);
-            mDependencyDocumentClasses.add(documentClass);
         } else {
-            codeBlock.add("new $T($S)", propertyClass.nestedClass("Builder"), propertyName);
+            // All other property configs have a single param constructor that just takes the
+            // property's serialized name as input
+            codeBlock.add("new $T.Builder($S)",
+                    annotation.getConfigClassName(), annotation.getName());
         }
-        codeBlock.indent();
+        codeBlock.indent().add(createSetCardinalityExpr(annotation, getterOrField));
+        switch (annotation.getDataPropertyKind()) {
+            case STRING_PROPERTY:
+                StringPropertyAnnotation stringPropertyAnnotation =
+                        (StringPropertyAnnotation) annotation;
+                codeBlock.add(createSetTokenizerTypeExpr(stringPropertyAnnotation, getterOrField))
+                        .add(createSetIndexingTypeExpr(stringPropertyAnnotation, getterOrField))
+                        .add(createSetJoinableValueTypeExpr(
+                                stringPropertyAnnotation, getterOrField));
+                break;
+            case DOCUMENT_PROPERTY:
+                DocumentPropertyAnnotation documentPropertyAnnotation =
+                        (DocumentPropertyAnnotation) annotation;
+                codeBlock.add(createSetShouldIndexNestedPropertiesExpr(documentPropertyAnnotation));
+                break;
+            case LONG_PROPERTY:
+                LongPropertyAnnotation longPropertyAnnotation = (LongPropertyAnnotation) annotation;
+                codeBlock.add(createSetIndexingTypeExpr(longPropertyAnnotation, getterOrField));
+                break;
+            case DOUBLE_PROPERTY: // fall-through
+            case BOOLEAN_PROPERTY: // fall-through
+            case BYTES_PROPERTY:
+                break;
+            default:
+                throw new IllegalStateException("Unhandled annotation: " + annotation);
+        }
+        return codeBlock.add("\n.build()")
+                .unindent()
+                .build();
+    }
 
-        // Find property cardinality
-        ClassName cardinalityEnum;
-        if (repeated) {
-            cardinalityEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "CARDINALITY_REPEATED");
-        } else if (Boolean.parseBoolean(params.get("required").toString())) {
-            cardinalityEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "CARDINALITY_REQUIRED");
+    /**
+     * Creates an expr like {@code .setCardinality(PropertyConfig.CARDINALITY_REPEATED)}.
+     */
+    @NonNull
+    private static CodeBlock createSetCardinalityExpr(
+            @NonNull DataPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) {
+        AnnotatedGetterOrField.ElementTypeCategory typeCategory =
+                getterOrField.getElementTypeCategory();
+        String enumName;
+        switch (typeCategory) {
+            case COLLECTION: // fall-through
+            case ARRAY:
+                enumName = "CARDINALITY_REPEATED";
+                break;
+            case SINGLE:
+                enumName = annotation.isRequired()
+                        ? "CARDINALITY_REQUIRED"
+                        : "CARDINALITY_OPTIONAL";
+                break;
+            default:
+                throw new IllegalStateException("Unhandled type category: " + typeCategory);
+        }
+        return CodeBlock.of("\n.setCardinality($T.$N)", PROPERTY_CONFIG_CLASS, enumName);
+    }
+
+    /**
+     * Creates an expr like {@code .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)}.
+     */
+    @NonNull
+    private static CodeBlock createSetTokenizerTypeExpr(
+            @NonNull StringPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) throws ProcessingException {
+        String enumName;
+        if (annotation.getIndexingType() == 0) { // INDEXING_TYPE_NONE
+            //TODO(b/171857731) remove this hack after apply to Icing lib's change.
+            enumName = "TOKENIZER_TYPE_NONE";
         } else {
-            cardinalityEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "CARDINALITY_OPTIONAL");
-        }
-        codeBlock.add("\n.setCardinality($T)", cardinalityEnum);
-
-        if (isPropertyString) {
-            // Find tokenizer type
-            int tokenizerType = Integer.parseInt(params.get("tokenizerType").toString());
-            if (Integer.parseInt(params.get("indexingType").toString()) == 0) {
-                //TODO(b/171857731) remove this hack after apply to Icing lib's change.
-                tokenizerType = 0;
-            }
-            ClassName tokenizerEnum;
-            if (tokenizerType == 0) {  // TOKENIZER_TYPE_NONE
-                tokenizerEnum = mHelper.getAppSearchClass(
-                        "AppSearchSchema", "StringPropertyConfig", "TOKENIZER_TYPE_NONE");
-            } else if (tokenizerType == 1) {  // TOKENIZER_TYPE_PLAIN
-                tokenizerEnum = mHelper.getAppSearchClass(
-                        "AppSearchSchema", "StringPropertyConfig", "TOKENIZER_TYPE_PLAIN");
-            } else if (tokenizerType == 2) {  // TOKENIZER_TYPE_VERBATIM
-                tokenizerEnum = mHelper.getAppSearchClass(
-                        "AppSearchSchema", "StringPropertyConfig", "TOKENIZER_TYPE_VERBATIM");
-            } else if (tokenizerType == 3) { // TOKENIZER_TYPE_RFC822
-                tokenizerEnum = mHelper.getAppSearchClass(
-                        "AppSearchSchema", "StringPropertyConfig", "TOKENIZER_TYPE_RFC822");
-            } else {
-                throw new ProcessingException("Unknown tokenizer type " + tokenizerType, property);
-            }
-            codeBlock.add("\n.setTokenizerType($T)", tokenizerEnum);
-
-            // Find indexing type
-            int indexingType = Integer.parseInt(params.get("indexingType").toString());
-            ClassName indexingEnum;
-            if (indexingType == 0) {  // INDEXING_TYPE_NONE
-                indexingEnum = mHelper.getAppSearchClass(
-                        "AppSearchSchema", "StringPropertyConfig", "INDEXING_TYPE_NONE");
-            } else if (indexingType == 1) {  // INDEXING_TYPE_EXACT_TERMS
-                indexingEnum = mHelper.getAppSearchClass(
-                        "AppSearchSchema", "StringPropertyConfig", "INDEXING_TYPE_EXACT_TERMS");
-            } else if (indexingType == 2) {  // INDEXING_TYPE_PREFIXES
-                indexingEnum = mHelper.getAppSearchClass(
-                        "AppSearchSchema", "StringPropertyConfig", "INDEXING_TYPE_PREFIXES");
-            } else {
-                throw new ProcessingException("Unknown indexing type " + indexingType, property);
-            }
-            codeBlock.add("\n.setIndexingType($T)", indexingEnum);
-
-            int joinableValueType = Integer.parseInt(params.get("joinableValueType").toString());
-            ClassName joinableEnum;
-            if (joinableValueType == 0) { // JOINABLE_VALUE_TYPE_NONE
-                joinableEnum = mHelper.getAppSearchClass(
-                        "AppSearchSchema", "StringPropertyConfig", "JOINABLE_VALUE_TYPE_NONE");
-
-            } else if (joinableValueType == 1) { // JOINABLE_VALUE_TYPE_QUALIFIED_ID
-                if (repeated) {
+            switch (annotation.getTokenizerType()) {
+                case 0:
+                    enumName = "TOKENIZER_TYPE_NONE";
+                    break;
+                case 1:
+                    enumName = "TOKENIZER_TYPE_PLAIN";
+                    break;
+                case 2:
+                    enumName = "TOKENIZER_TYPE_VERBATIM";
+                    break;
+                case 3:
+                    enumName = "TOKENIZER_TYPE_RFC822";
+                    break;
+                default:
                     throw new ProcessingException(
-                            "Joinable value type " + joinableValueType + " not allowed on repeated "
-                                    + "properties.", property);
-
-                }
-                joinableEnum = mHelper.getAppSearchClass(
-                        "AppSearchSchema", "StringPropertyConfig",
-                        "JOINABLE_VALUE_TYPE_QUALIFIED_ID");
-            } else {
-                throw new ProcessingException(
-                        "Unknown joinable value type " + joinableValueType, property);
+                            "Unknown tokenizer type " + annotation.getTokenizerType(),
+                            getterOrField.getElement());
             }
-            codeBlock.add("\n.setJoinableValueType($T)", joinableEnum);
-
-        } else if (isPropertyDocument) {
-            if (params.containsKey("indexNestedProperties")) {
-                boolean indexNestedProperties = Boolean.parseBoolean(
-                        params.get("indexNestedProperties").toString());
-
-                codeBlock.add("\n.setShouldIndexNestedProperties($L)", indexNestedProperties);
-            }
-        } else if (isPropertyLong) {
-            int indexingType = 0;  // INDEXING_TYPE_NONE
-            if (params.containsKey("indexingType")) {
-                indexingType = Integer.parseInt(params.get("indexingType").toString());
-            }
-
-            ClassName indexingEnum;
-            if (indexingType == 0) {  // INDEXING_TYPE_NONE
-                indexingEnum = mHelper.getAppSearchClass(
-                        "AppSearchSchema", "LongPropertyConfig", "INDEXING_TYPE_NONE");
-            } else if (indexingType == 1) {  // INDEXING_TYPE_RANGE
-                indexingEnum = mHelper.getAppSearchClass(
-                        "AppSearchSchema", "LongPropertyConfig", "INDEXING_TYPE_RANGE");
-            } else {
-                throw new ProcessingException("Unknown indexing type " + indexingType, property);
-            }
-            codeBlock.add("\n.setIndexingType($T)", indexingEnum);
         }
+        return CodeBlock.of("\n.setTokenizerType($T.$N)",
+                StringPropertyAnnotation.CONFIG_CLASS, enumName);
+    }
 
-        // Done!
-        codeBlock.add("\n.build()");
-        codeBlock.unindent();
-        return codeBlock.build();
+    /**
+     * Creates an expr like {@code .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)}.
+     */
+    @NonNull
+    private static CodeBlock createSetIndexingTypeExpr(
+            @NonNull StringPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) throws ProcessingException {
+        String enumName;
+        switch (annotation.getIndexingType()) {
+            case 0:
+                enumName = "INDEXING_TYPE_NONE";
+                break;
+            case 1:
+                enumName = "INDEXING_TYPE_EXACT_TERMS";
+                break;
+            case 2:
+                enumName = "INDEXING_TYPE_PREFIXES";
+                break;
+            default:
+                throw new ProcessingException(
+                        "Unknown indexing type " + annotation.getIndexingType(),
+                        getterOrField.getElement());
+        }
+        return CodeBlock.of("\n.setIndexingType($T.$N)",
+                StringPropertyAnnotation.CONFIG_CLASS, enumName);
+    }
+
+    /**
+     * Creates an expr like {@code .setShouldIndexNestedProperties(true)}.
+     */
+    @NonNull
+    private static CodeBlock createSetShouldIndexNestedPropertiesExpr(
+            @NonNull DocumentPropertyAnnotation annotation) {
+        return CodeBlock.of("\n.setShouldIndexNestedProperties($L)",
+                annotation.shouldIndexNestedProperties());
+    }
+
+    /**
+     * Creates an expr like {@code .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)}.
+     */
+    @NonNull
+    private static CodeBlock createSetIndexingTypeExpr(
+            @NonNull LongPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) throws ProcessingException {
+        String enumName;
+        switch (annotation.getIndexingType()) {
+            case 0:
+                enumName = "INDEXING_TYPE_NONE";
+                break;
+            case 1:
+                enumName = "INDEXING_TYPE_RANGE";
+                break;
+            default:
+                throw new ProcessingException(
+                        "Unknown indexing type " + annotation.getIndexingType(),
+                        getterOrField.getElement());
+        }
+        return CodeBlock.of("\n.setIndexingType($T.$N)",
+                LongPropertyAnnotation.CONFIG_CLASS, enumName);
+    }
+
+    /**
+     * Creates an expr like
+     * {@code .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)}.
+     */
+    @NonNull
+    private static CodeBlock createSetJoinableValueTypeExpr(
+            @NonNull StringPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) throws ProcessingException {
+        String enumName;
+        AnnotatedGetterOrField.ElementTypeCategory typeCategory =
+                getterOrField.getElementTypeCategory();
+        switch (annotation.getJoinableValueType()) {
+            case 0:
+                enumName = "JOINABLE_VALUE_TYPE_NONE";
+                break;
+            case 1:
+                switch (typeCategory) {
+                    case COLLECTION: // fall-through
+                    case ARRAY:
+                        throw new ProcessingException(
+                                "Joinable value type 1 not allowed on repeated properties.",
+                                getterOrField.getElement());
+                    case SINGLE: // fall-through
+                        break;
+                    default:
+                        throw new IllegalStateException("Unhandled cardinality: " + typeCategory);
+                }
+                enumName = "JOINABLE_VALUE_TYPE_QUALIFIED_ID";
+                break;
+            default:
+                throw new ProcessingException(
+                        "Unknown joinable value type " + annotation.getJoinableValueType(),
+                        getterOrField.getElement());
+        }
+        return CodeBlock.of("\n.setJoinableValueType($T.$N)",
+                StringPropertyAnnotation.CONFIG_CLASS, enumName);
     }
 }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
index 2f82106..9aa28a2 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
@@ -16,30 +16,29 @@
 
 package androidx.appsearch.compiler;
 
-import static androidx.appsearch.compiler.IntrospectionHelper.getDocumentAnnotation;
-import static androidx.appsearch.compiler.IntrospectionHelper.getPropertyType;
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_EXCEPTION_CLASS;
+import static androidx.appsearch.compiler.IntrospectionHelper.GENERIC_DOCUMENT_CLASS;
 
 import androidx.annotation.NonNull;
+import androidx.appsearch.compiler.AnnotatedGetterOrField.ElementTypeCategory;
+import androidx.appsearch.compiler.annotationwrapper.DataPropertyAnnotation;
+import androidx.appsearch.compiler.annotationwrapper.DocumentPropertyAnnotation;
+import androidx.appsearch.compiler.annotationwrapper.MetadataPropertyAnnotation;
+import androidx.appsearch.compiler.annotationwrapper.PropertyAnnotation;
 
+import com.squareup.javapoet.ClassName;
 import com.squareup.javapoet.CodeBlock;
 import com.squareup.javapoet.MethodSpec;
 import com.squareup.javapoet.ParameterizedTypeName;
-import com.squareup.javapoet.TypeName;
 import com.squareup.javapoet.TypeSpec;
 import com.squareup.javapoet.WildcardTypeName;
 
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
 import javax.annotation.processing.ProcessingEnvironment;
-import javax.lang.model.element.Element;
 import javax.lang.model.element.Modifier;
 import javax.lang.model.type.ArrayType;
-import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.PrimitiveType;
 import javax.lang.model.type.TypeKind;
 import javax.lang.model.type.TypeMirror;
-import javax.lang.model.util.Types;
 
 /**
  * Generates java code for a translator from an instance of a class annotated with
@@ -47,13 +46,11 @@
  * {@link androidx.appsearch.app.GenericDocument}.
  */
 class ToGenericDocumentCodeGenerator {
-    private final ProcessingEnvironment mEnv;
     private final IntrospectionHelper mHelper;
     private final DocumentModel mModel;
 
     private ToGenericDocumentCodeGenerator(
             @NonNull ProcessingEnvironment env, @NonNull DocumentModel model) {
-        mEnv = env;
         mHelper = new IntrospectionHelper(env);
         mModel = model;
     }
@@ -61,40 +58,54 @@
     public static void generate(
             @NonNull ProcessingEnvironment env,
             @NonNull DocumentModel model,
-            @NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
+            @NonNull TypeSpec.Builder classBuilder) {
         new ToGenericDocumentCodeGenerator(env, model).generate(classBuilder);
     }
 
-    private void generate(TypeSpec.Builder classBuilder) throws ProcessingException {
+    private void generate(TypeSpec.Builder classBuilder) {
         classBuilder.addMethod(createToGenericDocumentMethod());
     }
 
-    private MethodSpec createToGenericDocumentMethod() throws ProcessingException {
+    private MethodSpec createToGenericDocumentMethod() {
         // Method header
-        TypeName classType = TypeName.get(mModel.getClassElement().asType());
         MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("toGenericDocument")
                 .addModifiers(Modifier.PUBLIC)
-                .returns(mHelper.getAppSearchClass("GenericDocument"))
+                .returns(GENERIC_DOCUMENT_CLASS)
                 .addAnnotation(Override.class)
-                .addParameter(classType, "document")
-                .addException(mHelper.getAppSearchExceptionClass());
+                .addParameter(ClassName.get(mModel.getClassElement()), "document")
+                .addException(APPSEARCH_EXCEPTION_CLASS);
 
         // Construct a new GenericDocument.Builder with the namespace, id, and schema type
         methodBuilder.addStatement("$T builder =\nnew $T<>($L, $L, SCHEMA_NAME)",
                 ParameterizedTypeName.get(
-                        mHelper.getAppSearchClass("GenericDocument", "Builder"),
+                        GENERIC_DOCUMENT_CLASS.nestedClass("Builder"),
                         WildcardTypeName.subtypeOf(Object.class)),
-                mHelper.getAppSearchClass("GenericDocument", "Builder"),
-                createAppSearchFieldRead(
-                        mModel.getSpecialFieldName(DocumentModel.SpecialField.NAMESPACE)),
-                createAppSearchFieldRead(
-                        mModel.getSpecialFieldName(DocumentModel.SpecialField.ID)));
+                GENERIC_DOCUMENT_CLASS.nestedClass("Builder"),
+                createReadExpr(mModel.getNamespaceAnnotatedGetterOrField()),
+                createReadExpr(mModel.getIdAnnotatedGetterOrField()));
 
-        setSpecialFields(methodBuilder);
+        // Set metadata properties
+        for (AnnotatedGetterOrField getterOrField : mModel.getAnnotatedGettersAndFields()) {
+            PropertyAnnotation annotation = getterOrField.getAnnotation();
+            if (annotation.getPropertyKind() != PropertyAnnotation.Kind.METADATA_PROPERTY
+                    // Already set in the generated constructor above
+                    || annotation == MetadataPropertyAnnotation.ID
+                    || annotation == MetadataPropertyAnnotation.NAMESPACE) {
+                continue;
+            }
 
-        // Set properties
-        for (Map.Entry<String, Element> entry : mModel.getPropertyElements().entrySet()) {
-            fieldToGenericDoc(methodBuilder, entry.getKey(), entry.getValue());
+            methodBuilder.addCode(codeToCopyIntoGenericDoc(
+                    (MetadataPropertyAnnotation) annotation, getterOrField));
+        }
+
+        // Set data properties
+        for (AnnotatedGetterOrField getterOrField : mModel.getAnnotatedGettersAndFields()) {
+            PropertyAnnotation annotation = getterOrField.getAnnotation();
+            if (annotation.getPropertyKind() != PropertyAnnotation.Kind.DATA_PROPERTY) {
+                continue;
+            }
+            methodBuilder.addCode(codeToCopyIntoGenericDoc(
+                    (DataPropertyAnnotation) annotation, getterOrField));
         }
 
         methodBuilder.addStatement("return builder.build()");
@@ -102,19 +113,52 @@
     }
 
     /**
-     * Converts a field from a document class into a format suitable for one of the
-     * {@link androidx.appsearch.app.GenericDocument.Builder#setProperty} methods.
+     * Returns code that copies the getter/field annotated with a {@link MetadataPropertyAnnotation}
+     * from a document class into a {@code GenericDocument.Builder}.
+     *
+     * <p>Assumes:
+     * <ol>
+     *     <li>There is a document class var in-scope called {@code document}.</li>
+     *     <li>There is {@code GenericDocument.Builder} var in-scope called {@code builder}.</li>
+     *     <li>
+     *         The annotation is not {@link MetadataPropertyAnnotation#ID} or
+     *         {@link MetadataPropertyAnnotation#NAMESPACE}.
+     *     </li>
+     * </ol>
      */
-    private void fieldToGenericDoc(
-            @NonNull MethodSpec.Builder method,
-            @NonNull String fieldName,
-            @NonNull Element property) throws ProcessingException {
+    private CodeBlock codeToCopyIntoGenericDoc(
+            @NonNull MetadataPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) {
+        if (getterOrField.getJvmType() instanceof PrimitiveType) {
+            // Directly set it
+            return CodeBlock.builder()
+                    .addStatement("builder.$N($L)",
+                            annotation.getGenericDocSetterName(), createReadExpr(getterOrField))
+                    .build();
+        }
+        // Boxed type. Need to guard against the case where the value is null at runtime.
+        return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
+    }
+
+    /**
+     * Returns code that copies the getter/field annotated with a {@link DataPropertyAnnotation}
+     * from a document class into a {@code GenericDocument.Builder}.
+     *
+     * <p>Assumes:
+     * <ol>
+     *     <li>There is a document class var in-scope called {@code document}.</li>
+     *     <li>There is {@code GenericDocument.Builder} var in-scope called {@code builder}.</li>
+     * </ol>
+     */
+    private CodeBlock codeToCopyIntoGenericDoc(
+            @NonNull DataPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) {
         // Scenario 1: field is a Collection
         //   1a: CollectionForLoopAssign
-        //       Collection contains boxed Long, Integer, Double, Float, Boolean or byte[].
-        //       We have to pack it into a primitive array of type long[], double[], boolean[],
-        //       or byte[][] by reading each element one-by-one and assigning it. The compiler takes
-        //       care of unboxing.
+        //       Collection contains boxed Long, Integer, Double, Float, Boolean, byte[].
+        //       We have to pack it into a primitive array of type long[], double[], boolean[] or
+        //       byte[][] by reading each element one-by-one and assigning it. The compiler takes
+        //       care of unboxing and widening where necessary.
         //
         //   1b: CollectionCallToArray
         //       Collection contains String or GenericDocument.
@@ -126,20 +170,13 @@
         //       Collection contains a class which is annotated with @Document.
         //       We have to convert this into an array of GenericDocument[], by reading each element
         //       one-by-one and converting it through the standard conversion machinery.
-        //
-        //   1x: Collection contains any other kind of class. This unsupported and compilation
-        //       fails.
-        //       Note: Set<Byte[]>, Set<Byte>, and Set<Set<Byte>> are in this category. We don't
-        //       support such conversions currently, but in principle they are possible and could
-        //       be implemented.
 
         // Scenario 2: field is an Array
         //   2a: ArrayForLoopAssign
-        //       Array is of type Long[], Integer[], int[], Double[], Float[], float[], Boolean[],
-        //       or Byte[].
-        //       We have to pack it into a primitive array of type long[], double[], boolean[] or
-        //       byte[] by reading each element one-by-one and assigning it. The compiler takes care
-        //       of unboxing.
+        //       Array is of type Long[], Integer[], int[], Double[], Float[], float[], Boolean[].
+        //       We have to pack it into a primitive array of type long[], double[], boolean[]
+        //       by reading each element one-by-one and assigning it. The compiler takes care of
+        //       unboxing and widening where necessary.
         //
         //   2b: ArrayUseDirectly
         //       Array is of type String[], long[], double[], boolean[], byte[][] or
@@ -153,15 +190,10 @@
         //
         //   2d: Array is of class byte[]. This is actually a single-valued field as byte arrays are
         //       natively supported by Icing, and is handled as Scenario 3a.
-        //
-        //   2x: Array is of any other kind of class. This unsupported and compilation fails.
-        //       Note: Byte[][] is in this category. We don't support such conversions
-        //       currently, but in principle they are possible and could be implemented.
 
         // Scenario 3: Single valued fields
         //   3a: FieldUseDirectlyWithNullCheck
-        //       Field is of type String, Long, Integer, Double, Float, Boolean, byte[] or
-        //       GenericDocument.
+        //       Field is of type String, Long, Integer, Double, Float, Boolean.
         //       We can use this field directly, after testing for null. The java compiler will box
         //       or unbox as needed.
         //
@@ -173,553 +205,405 @@
         //       Field is of a class which is annotated with @Document.
         //       We have to convert this into a GenericDocument through the standard conversion
         //       machinery.
-        String propertyName = mModel.getPropertyName(property);
-        if (tryConvertFromCollection(method, fieldName, propertyName, property)) {
-            return;
+        ElementTypeCategory typeCategory = getterOrField.getElementTypeCategory();
+        switch (annotation.getDataPropertyKind()) {
+            case STRING_PROPERTY:
+                switch (typeCategory) {
+                    case COLLECTION: // List<String>: 1b
+                        return collectionCallToArray(annotation, getterOrField);
+                    case ARRAY: // String[]: 2b
+                        return arrayUseDirectly(annotation, getterOrField);
+                    case SINGLE: // String: 3a
+                        return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
+                    default:
+                        throw new IllegalStateException("Unhandled type-category: " + typeCategory);
+                }
+            case DOCUMENT_PROPERTY:
+                DocumentPropertyAnnotation docPropAnnotation =
+                        (DocumentPropertyAnnotation) annotation;
+                switch (typeCategory) {
+                    case COLLECTION: // List<Person>: 1c
+                        return collectionForLoopCallToGenericDocument(
+                                docPropAnnotation, getterOrField);
+                    case ARRAY: // Person[]: 2c
+                        return arrayForLoopCallToGenericDocument(docPropAnnotation, getterOrField);
+                    case SINGLE: // Person: 3c
+                        return fieldCallToGenericDocument(docPropAnnotation, getterOrField);
+                    default:
+                        throw new IllegalStateException("Unhandled type-category: " + typeCategory);
+                }
+            case LONG_PROPERTY:
+                switch (typeCategory) {
+                    case COLLECTION: // List<Long>|List<Integer>: 1a
+                        return collectionForLoopAssign(
+                                annotation,
+                                getterOrField,
+                                /* targetArrayComponentType= */mHelper.mLongPrimitiveType);
+                    case ARRAY:
+                        if (mHelper.isPrimitiveLongArray(getterOrField.getJvmType())) {
+                            return arrayUseDirectly(annotation, getterOrField); // long[]: 2b
+                        } else {
+                            // Long[]|Integer[]|int[]: 2a
+                            return arrayForLoopAssign(
+                                    annotation,
+                                    getterOrField,
+                                    /* targetArrayComponentType= */mHelper.mLongPrimitiveType);
+                        }
+                    case SINGLE:
+                        if (getterOrField.getJvmType() instanceof PrimitiveType) {
+                            // long|int: 3b
+                            return fieldUseDirectlyWithoutNullCheck(annotation, getterOrField);
+                        } else {
+                            // Long|Integer: 3a
+                            return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
+                        }
+                    default:
+                        throw new IllegalStateException("Unhandled type-category: " + typeCategory);
+                }
+            case DOUBLE_PROPERTY:
+                switch (typeCategory) {
+                    case COLLECTION: // List<Double>|List<Float>: 1a
+                        return collectionForLoopAssign(
+                                annotation,
+                                getterOrField,
+                                /* targetArrayComponentType= */mHelper.mDoublePrimitiveType);
+                    case ARRAY:
+                        if (mHelper.isPrimitiveDoubleArray(getterOrField.getJvmType())) {
+                            return arrayUseDirectly(annotation, getterOrField); // double[]: 2b
+                        } else {
+                            // Double[]|Float[]|float[]: 2a
+                            return arrayForLoopAssign(
+                                    annotation,
+                                    getterOrField,
+                                    /* targetArrayComponentType= */mHelper.mDoublePrimitiveType);
+                        }
+                    case SINGLE:
+                        if (getterOrField.getJvmType() instanceof PrimitiveType) {
+                            // double|float: 3b
+                            return fieldUseDirectlyWithoutNullCheck(annotation, getterOrField);
+                        } else {
+                            // Double|Float: 3b
+                            return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
+                        }
+                    default:
+                        throw new IllegalStateException("Unhandled type-category: " + typeCategory);
+                }
+            case BOOLEAN_PROPERTY:
+                switch (typeCategory) {
+                    case COLLECTION: // List<Boolean>: 1a
+                        return collectionForLoopAssign(
+                                annotation,
+                                getterOrField,
+                                /* targetArrayComponentType= */mHelper.mBooleanPrimitiveType);
+                    case ARRAY:
+                        if (mHelper.isPrimitiveBooleanArray(getterOrField.getJvmType())) {
+                            return arrayUseDirectly(annotation, getterOrField); // boolean[]: 2b
+                        } else {
+                            // Boolean[]: 2a
+                            return arrayForLoopAssign(
+                                    annotation,
+                                    getterOrField,
+                                    /* targetArrayComponentType= */mHelper.mBooleanPrimitiveType);
+                        }
+                    case SINGLE:
+                        if (getterOrField.getJvmType() instanceof PrimitiveType) {
+                            // boolean: 3b
+                            return fieldUseDirectlyWithoutNullCheck(annotation, getterOrField);
+                        } else {
+                            // Boolean: 3a
+                            return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
+                        }
+                    default:
+                        throw new IllegalStateException("Unhandled type-category: " + typeCategory);
+                }
+            case BYTES_PROPERTY:
+                switch (typeCategory) {
+                    case COLLECTION: // List<byte[]>: 1a
+                        return collectionForLoopAssign(
+                                annotation,
+                                getterOrField,
+                                /* targetArrayComponentType= */mHelper.mBytePrimitiveArrayType);
+                    case ARRAY: // byte[][]: 2b
+                        return arrayUseDirectly(annotation, getterOrField);
+                    case SINGLE: // byte[]: 2d
+                        return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
+                    default:
+                        throw new IllegalStateException("Unhandled type-category: " + typeCategory);
+                }
+            default:
+                throw new IllegalStateException("Unhandled annotation: " + annotation);
         }
-        if (tryConvertFromArray(method, fieldName, propertyName, property)) {
-            return;
+    }
+
+    // 1a: CollectionForLoopAssign
+    //     Collection contains boxed Long, Integer, Double, Float, Boolean, byte[].
+    //     We have to pack it into a primitive array of type long[], double[], boolean[] or
+    //     byte[][] by reading each element one-by-one and assigning it. The compiler takes
+    //     care of unboxing and widening where necessary.
+    @NonNull
+    private CodeBlock collectionForLoopAssign(
+            @NonNull DataPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField,
+            @NonNull TypeMirror targetArrayComponentType) {
+        TypeMirror jvmType = getterOrField.getJvmType(); // e.g. List<Long>
+        TypeMirror componentType = getterOrField.getComponentType(); // e.g. Long
+        String jvmName = getterOrField.getJvmName(); // e.g. mProp|getProp
+        return CodeBlock.builder()
+                .addStatement("$T $NCopy = $L",
+                        jvmType, jvmName, createReadExpr(getterOrField))
+                .beginControlFlow("if ($NCopy != null)", jvmName)
+                .addStatement("$T[] $NConv = $L",
+                        targetArrayComponentType,
+                        jvmName,
+                        newArrayExpr(
+                                targetArrayComponentType,
+                                /* size= */CodeBlock.of("$NCopy.size()", jvmName)))
+                .addStatement("int i = 0")
+                .beginControlFlow("for ($T item : $NCopy)", componentType, jvmName)
+                .addStatement("$NConv[i++] = item", jvmName)
+                .endControlFlow() // for (...) {
+                .addStatement("builder.$N($S, $NConv)",
+                        getterOrField.getAnnotation().getGenericDocSetterName(),
+                        annotation.getName(),
+                        jvmName)
+                .endControlFlow() //  if ($NCopy != null) {
+                .build();
+    }
+
+    // 1b: CollectionCallToArray
+    //     Collection contains String or GenericDocument.
+    //     We have to convert this into an array of String[] or GenericDocument[], but no
+    //     conversion of the collection elements is needed. We can use Collection#toArray for
+    //     this.
+    @NonNull
+    private CodeBlock collectionCallToArray(
+            @NonNull DataPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) {
+        TypeMirror collectionType = getterOrField.getJvmType(); // e.g. List<String>
+        TypeMirror componentType = getterOrField.getComponentType(); // e.g. String
+        String jvmName = getterOrField.getJvmName(); // e.g. mProp|getProp
+        return CodeBlock.builder()
+                .addStatement("$T $NCopy = $L",
+                        collectionType, jvmName, createReadExpr(getterOrField))
+                .beginControlFlow("if ($NCopy != null)", jvmName)
+                .addStatement("$T[] $NConv = $NCopy.toArray(new $T[0])",
+                        componentType, jvmName, jvmName, componentType)
+                .addStatement("builder.$N($S, $NConv)",
+                        getterOrField.getAnnotation().getGenericDocSetterName(),
+                        annotation.getName(),
+                        jvmName)
+                .endControlFlow() //  if ($NCopy != null) {
+                .build();
+    }
+
+    // 1c: CollectionForLoopCallToGenericDocument
+    //     Collection contains a class which is annotated with @Document.
+    //     We have to convert this into an array of GenericDocument[], by reading each element
+    //     one-by-one and converting it through the standard conversion machinery.
+    @NonNull
+    private CodeBlock collectionForLoopCallToGenericDocument(
+            @NonNull DocumentPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) {
+        TypeMirror collectionType = getterOrField.getJvmType(); // e.g. List<Person>
+        TypeMirror documentClass = getterOrField.getComponentType(); // e.g. Person
+        String jvmName = getterOrField.getJvmName(); // e.g. mProp|getProp
+        return CodeBlock.builder()
+                .addStatement("$T $NCopy = $L",
+                        collectionType, jvmName, createReadExpr(getterOrField))
+                .beginControlFlow("if ($NCopy != null)", jvmName)
+                .addStatement("$T[] $NConv = new $T[$NCopy.size()]",
+                        GENERIC_DOCUMENT_CLASS, jvmName, GENERIC_DOCUMENT_CLASS, jvmName)
+                .addStatement("int i = 0")
+                .beginControlFlow("for ($T item : $NCopy)", documentClass, jvmName)
+                .addStatement("$NConv[i++] = $T.fromDocumentClass(item)",
+                        jvmName, GENERIC_DOCUMENT_CLASS)
+                .endControlFlow() // for (...) {
+                .addStatement("builder.setPropertyDocument($S, $NConv)",
+                        annotation.getName(), jvmName)
+                .endControlFlow() //  if ($NCopy != null) {
+                .build();
+    }
+
+    // 2a: ArrayForLoopAssign
+    //     Array is of type Long[], Integer[], int[], Double[], Float[], float[], Boolean[].
+    //     We have to pack it into a primitive array of type long[], double[], boolean[]
+    //     by reading each element one-by-one and assigning it. The compiler takes care of
+    //     unboxing and widening where necessary.
+    @NonNull
+    private CodeBlock arrayForLoopAssign(
+            @NonNull DataPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField,
+            @NonNull TypeMirror targetArrayComponentType) {
+        TypeMirror jvmType = getterOrField.getJvmType(); // e.g. Long[]
+        String jvmName = getterOrField.getJvmName(); // e.g. mProp|getProp
+        return CodeBlock.builder()
+                .addStatement("$T $NCopy = $L",
+                        jvmType, jvmName, createReadExpr(getterOrField))
+                .beginControlFlow("if ($NCopy != null)", jvmName)
+                .addStatement("$T[] $NConv = $L",
+                        targetArrayComponentType,
+                        jvmName,
+                        newArrayExpr(
+                                targetArrayComponentType,
+                                /* size= */CodeBlock.of("$NCopy.length", jvmName)))
+                .beginControlFlow("for (int i = 0; i < $NCopy.length; i++)", jvmName)
+                .addStatement("$NConv[i] = $NCopy[i]", jvmName, jvmName)
+                .endControlFlow() // for (...) {
+                .addStatement("builder.$N($S, $NConv)",
+                        getterOrField.getAnnotation().getGenericDocSetterName(),
+                        annotation.getName(),
+                        jvmName)
+                .endControlFlow() // if ($NCopy != null)
+                .build();
+    }
+
+    // 2b: ArrayUseDirectly
+    //     Array is of type String[], long[], double[], boolean[], byte[][] or
+    //     GenericDocument[].
+    //     We can directly use this field with no conversion.
+    @NonNull
+    private CodeBlock arrayUseDirectly(
+            @NonNull DataPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) {
+        TypeMirror jvmType = getterOrField.getJvmType(); // e.g. String[]
+        String jvmName = getterOrField.getJvmName(); // e.g. mProp|getProp
+        return CodeBlock.builder()
+                .addStatement("$T $NCopy = $L",
+                        jvmType, jvmName, createReadExpr(getterOrField))
+                .beginControlFlow("if ($NCopy != null)", jvmName)
+                .addStatement("builder.$N($S, $NCopy)",
+                        getterOrField.getAnnotation().getGenericDocSetterName(),
+                        annotation.getName(),
+                        jvmName)
+                .endControlFlow() // if ($NCopy != null)
+                .build();
+    }
+
+    // 2c: ArrayForLoopCallToGenericDocument
+    //     Array is of a class which is annotated with @Document.
+    //     We have to convert this into an array of GenericDocument[], by reading each element
+    //     one-by-one and converting it through the standard conversion machinery.
+    @NonNull
+    private CodeBlock arrayForLoopCallToGenericDocument(
+            @NonNull DocumentPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) {
+        TypeMirror jvmType = getterOrField.getJvmType(); // e.g. Person[]
+        String jvmName = getterOrField.getJvmName(); // e.g. mProp|getProp
+        return CodeBlock.builder()
+                .addStatement("$T $NCopy = $L",
+                        jvmType, jvmName, createReadExpr(getterOrField))
+                .beginControlFlow("if ($NCopy != null)", jvmName)
+                .addStatement("$T[] $NConv = new $T[$NCopy.length]",
+                        GENERIC_DOCUMENT_CLASS, jvmName, GENERIC_DOCUMENT_CLASS, jvmName)
+                .beginControlFlow("for (int i = 0; i < $NConv.length; i++)", jvmName)
+                .addStatement("$NConv[i] = $T.fromDocumentClass($NCopy[i])",
+                        jvmName, GENERIC_DOCUMENT_CLASS, jvmName)
+                .endControlFlow() // for (...) {
+                .addStatement("builder.setPropertyDocument($S, $NConv)",
+                        annotation.getName(), jvmName)
+                .endControlFlow() //  if ($NCopy != null) {
+                .build();
+    }
+
+    // 3a: FieldUseDirectlyWithNullCheck
+    //     Field is of type String, Long, Integer, Double, Float, Boolean.
+    //     We can use this field directly, after testing for null. The java compiler will box
+    //     or unbox as needed.
+    @NonNull
+    private CodeBlock fieldUseDirectlyWithNullCheck(
+            @NonNull PropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) {
+        TypeMirror jvmType = getterOrField.getJvmType();
+        String jvmName = getterOrField.getJvmName(); // e.g. mProp|getProp
+        CodeBlock.Builder codeBlock = CodeBlock.builder()
+                .addStatement("$T $NCopy = $L",
+                        jvmType, jvmName, createReadExpr(getterOrField))
+                .beginControlFlow("if ($NCopy != null)", jvmName);
+
+        switch (annotation.getPropertyKind()) {
+            case METADATA_PROPERTY:
+                codeBlock.addStatement("builder.$N($NCopy)",
+                        getterOrField.getAnnotation().getGenericDocSetterName(), jvmName);
+                break;
+            case DATA_PROPERTY:
+                codeBlock.addStatement("builder.$N($S, $NCopy)",
+                        getterOrField.getAnnotation().getGenericDocSetterName(),
+                        ((DataPropertyAnnotation) annotation).getName(),
+                        jvmName);
+                break;
+            default:
+                throw new IllegalStateException("Unhandled annotation: " + annotation);
         }
-        convertFromField(method, fieldName, propertyName, property);
+
+        return codeBlock.endControlFlow() // if ($NCopy != null)
+                .build();
+    }
+
+    // 3b: FieldUseDirectlyWithoutNullCheck
+    //     Field is of type long, int, double, float, or boolean.
+    //     We can use this field directly without testing for null.
+    @NonNull
+    private CodeBlock fieldUseDirectlyWithoutNullCheck(
+            @NonNull DataPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) {
+        return CodeBlock.builder()
+                .addStatement("builder.$N($S, $L)",
+                        getterOrField.getAnnotation().getGenericDocSetterName(),
+                        annotation.getName(),
+                        createReadExpr(getterOrField))
+                .build();
+    }
+
+    // 3c: FieldCallToGenericDocument
+    //     Field is of a class which is annotated with @Document.
+    //     We have to convert this into a GenericDocument through the standard conversion
+    //     machinery.
+    @NonNull
+    private CodeBlock fieldCallToGenericDocument(
+            @NonNull DocumentPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) {
+        TypeMirror documentClass = getterOrField.getJvmType(); // Person
+        String jvmName = getterOrField.getJvmName(); // e.g. mProp|getProp
+        return CodeBlock.builder()
+                .addStatement("$T $NCopy = $L",
+                        documentClass, jvmName, createReadExpr(getterOrField))
+                .beginControlFlow("if ($NCopy != null)", jvmName)
+                .addStatement("$T $NConv = $T.fromDocumentClass($NCopy)",
+                        GENERIC_DOCUMENT_CLASS, jvmName, GENERIC_DOCUMENT_CLASS, jvmName)
+                .addStatement("builder.setPropertyDocument($S, $NConv)",
+                        annotation.getName(), jvmName)
+                .endControlFlow() // if ($NCopy != null) {
+                .build();
+    }
+
+    private CodeBlock newArrayExpr(@NonNull TypeMirror componentType, @NonNull CodeBlock size) {
+        // Component type itself may be an array type e.g. byte[]
+        // Deduce the base component type e.g. byte
+        TypeMirror baseComponentType = componentType;
+        int dims = 1;
+        while (baseComponentType.getKind() == TypeKind.ARRAY) {
+            baseComponentType = ((ArrayType) baseComponentType).getComponentType();
+            dims++;
+        }
+        CodeBlock.Builder codeBlock = CodeBlock.builder()
+                .add("new $T[$L]", baseComponentType, size);
+        for (int i = 1; i < dims; i++) {
+            codeBlock.add("[]");
+        }
+        return codeBlock.build();
     }
 
     /**
-     * If the given field is a Collection, generates code to read it and convert it into a form
-     * suitable for GenericDocument and returns true. If the field is not a Collection, returns
-     * false.
+     * Returns an expr that reading the annotated getter/fields from a document class var.
+     *
+     * <p>Assumes there is a document class var in-scope called {@code document}.
      */
-    private boolean tryConvertFromCollection(
-            @NonNull MethodSpec.Builder method,
-            @NonNull String fieldName,
-            @NonNull String propertyName,
-            @NonNull Element property) throws ProcessingException {
-        Types typeUtil = mEnv.getTypeUtils();
-        TypeMirror propertyType = getPropertyType(property);
-        if (!typeUtil.isAssignable(typeUtil.erasure(propertyType), mHelper.mCollectionType)) {
-            return false;  // This is not a scenario 1 collection
+    private CodeBlock createReadExpr(@NonNull AnnotatedGetterOrField annotatedGetterOrField) {
+        PropertyAccessor accessor = mModel.getAccessor(annotatedGetterOrField);
+        if (accessor.isField()) {
+            return CodeBlock.of("document.$N", accessor.getElement().getSimpleName().toString());
+        } else { // getter
+            return CodeBlock.of("document.$N()", accessor.getElement().getSimpleName().toString());
         }
-
-        // Copy the field into a local variable to make it easier to refer to it repeatedly.
-        CodeBlock.Builder body = CodeBlock.builder()
-                .addStatement(
-                        "$T $NCopy = $L",
-                        propertyType,
-                        fieldName,
-                        createAppSearchFieldRead(fieldName));
-
-        List<? extends TypeMirror> genericTypes = ((DeclaredType) propertyType).getTypeArguments();
-        TypeMirror componentType = genericTypes.get(0);
-
-        if (!tryCollectionForLoopAssign(body, fieldName, propertyName, componentType)         // 1a
-                && !tryCollectionCallToArray(body, fieldName, propertyName, componentType)    // 1b
-                && !tryCollectionForLoopCallToGenericDocument(
-                body, fieldName, propertyName, componentType)) {                        // 1c
-            // Scenario 1x
-            throw new ProcessingException(
-                    "Unhandled out property type (1x): " + propertyType.toString(), property);
-        }
-
-        method.addCode(body.build());
-        return true;
-    }
-
-    //   1a: CollectionForLoopAssign
-    //       Collection contains boxed Long, Integer, Double, Float, Boolean or byte[].
-    //       We have to pack it into a primitive array of type long[], double[], boolean[],
-    //       or byte[][] by reading each element one-by-one and assigning it. The compiler takes
-    //       care of unboxing.
-    private boolean tryCollectionForLoopAssign(
-            @NonNull CodeBlock.Builder method,
-            @NonNull String fieldName,
-            @NonNull String propertyName,
-            @NonNull TypeMirror propertyType) {
-        Types typeUtil = mEnv.getTypeUtils();
-        CodeBlock.Builder body = CodeBlock.builder()
-                .add("if ($NCopy != null) {\n", fieldName).indent();
-
-        String setPropertyMethod;
-        if (typeUtil.isSameType(propertyType, mHelper.mLongBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)) {
-            setPropertyMethod = "setPropertyLong";
-            body.addStatement(
-                    "long[] $NConv = new long[$NCopy.size()]", fieldName, fieldName);
-
-        } else if (typeUtil.isSameType(propertyType, mHelper.mDoubleBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)) {
-            setPropertyMethod = "setPropertyDouble";
-            body.addStatement(
-                    "double[] $NConv = new double[$NCopy.size()]", fieldName, fieldName);
-
-        } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanBoxType)) {
-            setPropertyMethod = "setPropertyBoolean";
-            body.addStatement(
-                    "boolean[] $NConv = new boolean[$NCopy.size()]", fieldName, fieldName);
-
-        } else if (typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)) {
-            setPropertyMethod = "setPropertyBytes";
-            body.addStatement(
-                    "byte[][] $NConv = new byte[$NCopy.size()][]", fieldName, fieldName);
-
-        } else {
-            // This is not a type 1a collection.
-            return false;
-        }
-
-        // Iterate over each element of the collection, assigning it to the output array.
-        body.addStatement("int i = 0")
-                .add("for ($T item : $NCopy) {\n", propertyType, fieldName).indent()
-                .addStatement("$NConv[i++] = item", fieldName)
-                .unindent().add("}\n")
-                .addStatement("builder.$N($S, $NConv)", setPropertyMethod, propertyName, fieldName)
-                .unindent().add("}\n");
-
-        method.add(body.build());
-        return true;
-    }
-
-    //   1b: CollectionCallToArray
-    //       Collection contains String. We have to convert this into an array of String[] or but no
-    //       conversion of the collection elements is needed. We can use Collection#toArray for
-    //       this.
-    private boolean tryCollectionCallToArray(
-            @NonNull CodeBlock.Builder method,
-            @NonNull String fieldName,
-            @NonNull String propertyName,
-            @NonNull TypeMirror propertyType) {
-        Types typeUtil = mEnv.getTypeUtils();
-        CodeBlock.Builder body = CodeBlock.builder()
-                .add("if ($NCopy != null) {\n", fieldName).indent();
-
-        if (typeUtil.isSameType(propertyType, mHelper.mStringType)) {
-            body.addStatement(
-                    "String[] $NConv = $NCopy.toArray(new String[0])", fieldName, fieldName);
-
-        } else {
-            // This is not a type 1b collection.
-            return false;
-        }
-
-        body.addStatement(
-                "builder.setPropertyString($S, $NConv)", propertyName, fieldName)
-                .unindent().add("}\n");
-
-        method.add(body.build());
-        return true;
-    }
-
-    //   1c: CollectionForLoopCallToGenericDocument
-    //       Collection contains a class which is annotated with @Document.
-    //       We have to convert this into an array of GenericDocument[], by reading each element
-    //       one-by-one and converting it through the standard conversion machinery.
-    private boolean tryCollectionForLoopCallToGenericDocument(
-            @NonNull CodeBlock.Builder method,
-            @NonNull String fieldName,
-            @NonNull String propertyName,
-            @NonNull TypeMirror propertyType) {
-        Types typeUtil = mEnv.getTypeUtils();
-        CodeBlock.Builder body = CodeBlock.builder()
-                .add("if ($NCopy != null) {\n", fieldName).indent();
-
-        Element element = typeUtil.asElement(propertyType);
-        if (element == null) {
-            // The propertyType is not an element, this is not a type 1c list.
-            return false;
-        }
-        if (getDocumentAnnotation(element) == null) {
-            // The propertyType doesn't have @Document annotation, this is not a type 1c
-            // list.
-            return false;
-        }
-
-        body.addStatement(
-                "GenericDocument[] $NConv = new GenericDocument[$NCopy.size()]",
-                fieldName, fieldName);
-        body.addStatement("int i = 0");
-        body
-                .add("for ($T item : $NCopy) {\n", propertyType, fieldName).indent()
-                .addStatement(
-                        "$NConv[i++] = $T.fromDocumentClass(item)",
-                        fieldName, mHelper.getAppSearchClass("GenericDocument"))
-                .unindent().add("}\n");
-
-        body
-                .addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName)
-                .unindent()
-                .add("}\n");   //  if ($NCopy != null) {
-
-        method.add(body.build());
-        return true;
-    }
-
-    /**
-     * If the given field is an array, generates code to read it and convert it into a form suitable
-     * for GenericDocument and returns true. If the field is not an array, returns false.
-     */
-    private boolean tryConvertFromArray(
-            @NonNull MethodSpec.Builder method,
-            @NonNull String fieldName,
-            @NonNull String propertyName,
-            @NonNull Element property) throws ProcessingException {
-        Types typeUtil = mEnv.getTypeUtils();
-        TypeMirror propertyType = getPropertyType(property);
-        if (propertyType.getKind() != TypeKind.ARRAY
-                // Byte arrays have a native representation in Icing, so they are not considered a
-                // "repeated" type
-                || typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)) {
-            return false;  // This is not a scenario 2 array
-        }
-
-        // Copy the field into a local variable to make it easier to refer to it repeatedly.
-        CodeBlock.Builder body = CodeBlock.builder()
-                .addStatement(
-                        "$T $NCopy = $L",
-                        propertyType,
-                        fieldName,
-                        createAppSearchFieldRead(fieldName));
-
-        TypeMirror componentType = ((ArrayType) propertyType).getComponentType();
-
-        if (!tryArrayForLoopAssign(body, fieldName, propertyName, componentType)              // 2a
-                && !tryArrayUseDirectly(body, fieldName, propertyName, componentType)         // 2b
-                && !tryArrayForLoopCallToGenericDocument(
-                body, fieldName, propertyName, componentType)) {                        // 2c
-            // Scenario 2x
-            throw new ProcessingException(
-                    "Unhandled out property type (2x): " + propertyType.toString(), property);
-        }
-
-        method.addCode(body.build());
-        return true;
-    }
-
-    //   2a: ArrayForLoopAssign
-    //       Array is of type Long[], Integer[], int[], Double[], Float[], float[], Boolean[],
-    //       or Byte[].
-    //       We have to pack it into a primitive array of type long[], double[], boolean[] or
-    //       byte[] by reading each element one-by-one and assigning it. The compiler takes care
-    //       of unboxing.
-    private boolean tryArrayForLoopAssign(
-            @NonNull CodeBlock.Builder method,
-            @NonNull String fieldName,
-            @NonNull String propertyName,
-            @NonNull TypeMirror propertyType) {
-        Types typeUtil = mEnv.getTypeUtils();
-        CodeBlock.Builder body = CodeBlock.builder()
-                .add("if ($NCopy != null) {\n", fieldName).indent();
-
-        String setPropertyMethod;
-        if (typeUtil.isSameType(propertyType, mHelper.mLongBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mIntPrimitiveType)) {
-            setPropertyMethod = "setPropertyLong";
-            body.addStatement(
-                    "long[] $NConv = new long[$NCopy.length]", fieldName, fieldName);
-
-        } else if (typeUtil.isSameType(propertyType, mHelper.mDoubleBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mFloatPrimitiveType)) {
-            setPropertyMethod = "setPropertyDouble";
-            body.addStatement(
-                    "double[] $NConv = new double[$NCopy.length]", fieldName, fieldName);
-
-        } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanBoxType)) {
-            setPropertyMethod = "setPropertyBoolean";
-            body.addStatement(
-                    "boolean[] $NConv = new boolean[$NCopy.length]", fieldName, fieldName);
-
-        } else if (typeUtil.isSameType(propertyType, mHelper.mByteBoxType)) {
-            setPropertyMethod = "setPropertyBytes";
-            body.addStatement(
-                    "byte[] $NConv = new byte[$NCopy.length]", fieldName, fieldName);
-
-        } else {
-            // This is not a type 2a array.
-            return false;
-        }
-
-        // Iterate over each element of the array, assigning it to the output array.
-        body.add("for (int i = 0 ; i < $NCopy.length ; i++) {\n", fieldName)
-                .indent()
-                .addStatement("$NConv[i] = $NCopy[i]", fieldName, fieldName)
-                .unindent().add("}\n")
-                .addStatement("builder.$N($S, $NConv)", setPropertyMethod, propertyName, fieldName)
-                .unindent().add("}\n");
-
-        method.add(body.build());
-        return true;
-    }
-
-    //   2b: ArrayUseDirectly
-    //       Array is of type String[], long[], double[], boolean[], byte[][].
-    //       We can directly use this field with no conversion.
-    private boolean tryArrayUseDirectly(
-            @NonNull CodeBlock.Builder method,
-            @NonNull String fieldName,
-            @NonNull String propertyName,
-            @NonNull TypeMirror propertyType) {
-        Types typeUtil = mEnv.getTypeUtils();
-        CodeBlock.Builder body = CodeBlock.builder()
-                .add("if ($NCopy != null) {\n", fieldName).indent();
-
-        String setPropertyMethod;
-        if (typeUtil.isSameType(propertyType, mHelper.mStringType)) {
-            setPropertyMethod = "setPropertyString";
-        } else if (typeUtil.isSameType(propertyType, mHelper.mLongPrimitiveType)) {
-            setPropertyMethod = "setPropertyLong";
-        } else if (typeUtil.isSameType(propertyType, mHelper.mDoublePrimitiveType)) {
-            setPropertyMethod = "setPropertyDouble";
-        } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanPrimitiveType)) {
-            setPropertyMethod = "setPropertyBoolean";
-        } else if (typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)) {
-            setPropertyMethod = "setPropertyBytes";
-        } else {
-            // This is not a type 2b array.
-            return false;
-        }
-
-        body.addStatement(
-                        "builder.$N($S, $NCopy)", setPropertyMethod, propertyName, fieldName)
-                .unindent().add("}\n");
-
-        method.add(body.build());
-        return true;
-    }
-
-    //   2c: ArrayForLoopCallToGenericDocument
-    //       Array is of a class which is annotated with @Document.
-    //       We have to convert this into an array of GenericDocument[], by reading each element
-    //       one-by-one and converting it through the standard conversion machinery.
-    private boolean tryArrayForLoopCallToGenericDocument(
-            @NonNull CodeBlock.Builder method,
-            @NonNull String fieldName,
-            @NonNull String propertyName,
-            @NonNull TypeMirror propertyType) {
-        Types typeUtil = mEnv.getTypeUtils();
-        CodeBlock.Builder body = CodeBlock.builder()
-                .add("if ($NCopy != null) {\n", fieldName).indent();
-
-        Element element = typeUtil.asElement(propertyType);
-        if (element == null) {
-            // The propertyType is not an element, this is not a type 1c list.
-            return false;
-        }
-        if (getDocumentAnnotation(element) == null)  {
-            // The propertyType doesn't have @Document annotation, this is not a type 1c
-            // list.
-            return false;
-        }
-
-        body.addStatement(
-                "GenericDocument[] $NConv = new GenericDocument[$NCopy.length]",
-                fieldName, fieldName);
-        body
-                .add("for (int i = 0; i < $NConv.length; i++) {\n", fieldName).indent()
-                .addStatement(
-                        "$NConv[i] = $T.fromDocumentClass($NCopy[i])",
-                        fieldName, mHelper.getAppSearchClass("GenericDocument"), fieldName)
-                .unindent().add("}\n");
-
-        body.addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName)
-                .unindent().add("}\n");    //  if ($NCopy != null) {
-
-        method.add(body.build());
-        return true;
-    }
-
-    /**
-     * Given a field which is a single element (non-collection), generates code to read it and
-     * convert it into a form suitable for GenericDocument.
-     */
-    private void convertFromField(
-            @NonNull MethodSpec.Builder method,
-            @NonNull String fieldName,
-            @NonNull String propertyName,
-            @NonNull Element property) throws ProcessingException {
-        // TODO(b/156296904): Handle scenario 3c (FieldCallToGenericDocument)
-        TypeMirror propertyType = getPropertyType(property);
-        CodeBlock.Builder body = CodeBlock.builder();
-        if (!tryFieldUseDirectlyWithNullCheck(
-                body, fieldName, propertyName, propertyType)  // 3a
-                && !tryFieldUseDirectlyWithoutNullCheck(
-                body, fieldName, propertyName, propertyType)  // 3b
-                && !tryFieldCallToGenericDocument(
-                body, fieldName, propertyName, propertyType)) {  // 3c
-            throw new ProcessingException("Unhandled property type.", property);
-        }
-        method.addCode(body.build());
-    }
-
-    //   3a: FieldUseDirectlyWithNullCheck
-    //       Field is of type String, Long, Integer, Double, Float, Boolean, byte[].
-    //       We can use this field directly, after testing for null. The java compiler will box
-    //       or unbox as needed.
-    private boolean tryFieldUseDirectlyWithNullCheck(
-            @NonNull CodeBlock.Builder method,
-            @NonNull String fieldName,
-            @NonNull String propertyName,
-            @NonNull TypeMirror propertyType) {
-        Types typeUtil = mEnv.getTypeUtils();
-        // Copy the field into a local variable to make it easier to refer to it repeatedly.
-        CodeBlock.Builder body = CodeBlock.builder()
-                .addStatement(
-                        "$T $NCopy = $L",
-                        propertyType,
-                        fieldName,
-                        createAppSearchFieldRead(fieldName))
-                .add("if ($NCopy != null) {\n", fieldName).indent();
-
-        String setPropertyMethod;
-        if (typeUtil.isSameType(propertyType, mHelper.mStringType)) {
-            setPropertyMethod = "setPropertyString";
-        } else if (typeUtil.isSameType(propertyType, mHelper.mLongBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)) {
-            setPropertyMethod = "setPropertyLong";
-        } else if (typeUtil.isSameType(propertyType, mHelper.mDoubleBoxType)
-                || typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)) {
-            setPropertyMethod = "setPropertyDouble";
-        } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanBoxType)) {
-            setPropertyMethod = "setPropertyBoolean";
-        } else if (typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)) {
-            setPropertyMethod = "setPropertyBytes";
-        } else {
-            // This is not a type 3a field
-            return false;
-        }
-
-        body.addStatement(
-                        "builder.$N($S, $NCopy)", setPropertyMethod, propertyName, fieldName)
-                .unindent().add("}\n");
-
-        method.add(body.build());
-        return true;
-    }
-
-    //   3b: FieldUseDirectlyWithoutNullCheck
-    //       Field is of type long, int, double, float, or boolean.
-    //       We can use this field directly without testing for null.
-    private boolean tryFieldUseDirectlyWithoutNullCheck(
-            @NonNull CodeBlock.Builder method,
-            @NonNull String fieldName,
-            @NonNull String propertyName,
-            @NonNull TypeMirror propertyType) {
-        Types typeUtil = mEnv.getTypeUtils();
-        String setPropertyMethod;
-        if (typeUtil.isSameType(propertyType, mHelper.mLongPrimitiveType)
-                || typeUtil.isSameType(propertyType, mHelper.mIntPrimitiveType)) {
-            setPropertyMethod = "setPropertyLong";
-        } else if (typeUtil.isSameType(propertyType, mHelper.mDoublePrimitiveType)
-                || typeUtil.isSameType(propertyType, mHelper.mFloatPrimitiveType)) {
-            setPropertyMethod = "setPropertyDouble";
-        } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanPrimitiveType)) {
-            setPropertyMethod = "setPropertyBoolean";
-        } else {
-            // This is not a type 3b field
-            return false;
-        }
-
-        method.addStatement(
-                "builder.$N($S, $L)",
-                setPropertyMethod,
-                propertyName,
-                createAppSearchFieldRead(fieldName));
-        return true;
-    }
-
-    //   3c: FieldCallToGenericDocument
-    //       Field is of a class which is annotated with @Document.
-    //       We have to convert this into a GenericDocument through the standard conversion
-    //       machinery.
-    private boolean tryFieldCallToGenericDocument(
-            @NonNull CodeBlock.Builder method,
-            @NonNull String fieldName,
-            @NonNull String propertyName,
-            @NonNull TypeMirror propertyType) {
-        Types typeUtil = mEnv.getTypeUtils();
-
-        Element element = typeUtil.asElement(propertyType);
-        if (element == null) {
-            // The propertyType is not an element, this is not a type 3c field.
-            return false;
-        }
-        if (getDocumentAnnotation(element) == null) {
-            // The propertyType doesn't have @Document annotation, this is not a type 3c
-            // field.
-            return false;
-        }
-        method.addStatement(
-                "$T $NCopy = $L", propertyType, fieldName, createAppSearchFieldRead(fieldName));
-
-        method.add("if ($NCopy != null) {\n", fieldName).indent();
-
-        method
-                .addStatement(
-                        "GenericDocument $NConv = $T.fromDocumentClass($NCopy)",
-                        fieldName, mHelper.getAppSearchClass("GenericDocument"), fieldName)
-                .addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName);
-
-        method.unindent().add("}\n");
-        return true;
-    }
-
-    private void setSpecialFields(MethodSpec.Builder method) {
-        for (DocumentModel.SpecialField specialField :
-                DocumentModel.SpecialField.values()) {
-            String fieldName = mModel.getSpecialFieldName(specialField);
-            if (fieldName == null) {
-                continue;  // The document class doesn't have this field, so no need to set it.
-            }
-            switch (specialField) {
-                case ID:
-                    break;  // Always provided to builder constructor; cannot be set separately.
-                case NAMESPACE:
-                    break;  // Always provided to builder constructor; cannot be set separately.
-                case CREATION_TIMESTAMP_MILLIS:
-                    method.addStatement(
-                            "builder.setCreationTimestampMillis($L)",
-                            createAppSearchFieldReadNumeric(fieldName));
-                    break;
-                case TTL_MILLIS:
-                    method.addStatement(
-                            "builder.setTtlMillis($L)", createAppSearchFieldReadNumeric(fieldName));
-                    break;
-                case SCORE:
-                    method.addStatement(
-                            "builder.setScore($L)", createAppSearchFieldReadNumeric(fieldName));
-                    break;
-            }
-        }
-    }
-
-    private CodeBlock createAppSearchFieldRead(@NonNull String fieldName) {
-        switch (Objects.requireNonNull(mModel.getElementReadKind(fieldName))) {
-            case FIELD:
-                return CodeBlock.of("document.$N", fieldName);
-            case GETTER:
-                String getter = mModel.getGetterForElement(fieldName).getSimpleName().toString();
-                return CodeBlock.of("document.$N()", getter);
-        }
-        return null;
-    }
-
-    private CodeBlock createAppSearchFieldReadNumeric(@NonNull String fieldName) {
-        CodeBlock fieldRead = createAppSearchFieldRead(fieldName);
-
-        TypeMirror fieldType =
-                IntrospectionHelper.getPropertyType(mModel.getAllElements().get(fieldName));
-
-        String toPrimitiveMethod;
-        Object primitiveFallback;
-        if (fieldType.toString().equals("java.lang.Integer")) {
-            toPrimitiveMethod = "intValue";
-            primitiveFallback = 0;
-        } else if (fieldType.toString().equals("java.lang.Long")) {
-            toPrimitiveMethod = "longValue";
-            primitiveFallback = "0L";
-        } else {
-            return fieldRead;
-        }
-
-        return CodeBlock.of("($L != null) ? $L.$L() : $L",
-                fieldRead, fieldRead, toPrimitiveMethod, primitiveFallback);
     }
 }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/BooleanPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/BooleanPropertyAnnotation.java
index f4fd047..01d7706 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/BooleanPropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/BooleanPropertyAnnotation.java
@@ -16,11 +16,13 @@
 
 package androidx.appsearch.compiler.annotationwrapper;
 
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_SCHEMA_CLASS;
 import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
 
 import androidx.annotation.NonNull;
 
 import com.google.auto.value.AutoValue;
+import com.squareup.javapoet.ClassName;
 
 import java.util.Map;
 
@@ -29,11 +31,14 @@
  */
 @AutoValue
 public abstract class BooleanPropertyAnnotation extends DataPropertyAnnotation {
-    public static final String SIMPLE_CLASS_NAME = "BooleanProperty";
-    public static final String CLASS_NAME = DOCUMENT_ANNOTATION_CLASS + "." + SIMPLE_CLASS_NAME;
+    public static final ClassName CLASS_NAME =
+            DOCUMENT_ANNOTATION_CLASS.nestedClass("BooleanProperty");
+
+    public static final ClassName CONFIG_CLASS =
+            APPSEARCH_SCHEMA_CLASS.nestedClass("BooleanPropertyConfig");
 
     public BooleanPropertyAnnotation() {
-        super(SIMPLE_CLASS_NAME);
+        super(CLASS_NAME, CONFIG_CLASS, /* genericDocSetterName= */"setPropertyBoolean");
     }
 
     /**
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/BytesPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/BytesPropertyAnnotation.java
index b367e2f..53b7a37 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/BytesPropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/BytesPropertyAnnotation.java
@@ -16,11 +16,13 @@
 
 package androidx.appsearch.compiler.annotationwrapper;
 
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_SCHEMA_CLASS;
 import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
 
 import androidx.annotation.NonNull;
 
 import com.google.auto.value.AutoValue;
+import com.squareup.javapoet.ClassName;
 
 import java.util.Map;
 
@@ -29,11 +31,14 @@
  */
 @AutoValue
 public abstract class BytesPropertyAnnotation extends DataPropertyAnnotation {
-    public static final String SIMPLE_CLASS_NAME = "BytesProperty";
-    public static final String CLASS_NAME = DOCUMENT_ANNOTATION_CLASS + "." + SIMPLE_CLASS_NAME;
+    public static final ClassName CLASS_NAME =
+            DOCUMENT_ANNOTATION_CLASS.nestedClass("BytesProperty");
+
+    public static final ClassName CONFIG_CLASS =
+            APPSEARCH_SCHEMA_CLASS.nestedClass("BytesPropertyConfig");
 
     public BytesPropertyAnnotation() {
-        super(SIMPLE_CLASS_NAME);
+        super(CLASS_NAME, CONFIG_CLASS, /* genericDocSetterName= */"setPropertyBytes");
     }
 
     /**
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java
index 74ee9b9..25f7f28 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java
@@ -20,6 +20,8 @@
 import androidx.annotation.Nullable;
 import androidx.appsearch.compiler.IntrospectionHelper;
 
+import com.squareup.javapoet.ClassName;
+
 import java.util.Map;
 
 import javax.lang.model.element.AnnotationMirror;
@@ -44,10 +46,21 @@
     }
 
     @NonNull
-    private final String mSimpleClassName;
+    private final ClassName mClassName;
 
-    DataPropertyAnnotation(@NonNull String simpleClassName) {
-        mSimpleClassName = simpleClassName;
+    @NonNull
+    private final ClassName mConfigClassName;
+
+    @NonNull
+    private final String mGenericDocSetterName;
+
+    DataPropertyAnnotation(
+            @NonNull ClassName className,
+            @NonNull ClassName configClassName,
+            @NonNull String genericDocSetterName) {
+        mClassName = className;
+        mConfigClassName = configClassName;
+        mGenericDocSetterName = genericDocSetterName;
     }
 
     /**
@@ -63,22 +76,21 @@
             @NonNull IntrospectionHelper helper) {
         Map<String, Object> annotationParams = helper.getAnnotationParams(annotation);
         String qualifiedClassName = annotation.getAnnotationType().toString();
-        switch (qualifiedClassName) {
-            case BooleanPropertyAnnotation.CLASS_NAME:
-                return BooleanPropertyAnnotation.parse(annotationParams, defaultName);
-            case BytesPropertyAnnotation.CLASS_NAME:
-                return BytesPropertyAnnotation.parse(annotationParams, defaultName);
-            case DocumentPropertyAnnotation.CLASS_NAME:
-                return DocumentPropertyAnnotation.parse(annotationParams, defaultName);
-            case DoublePropertyAnnotation.CLASS_NAME:
-                return DoublePropertyAnnotation.parse(annotationParams, defaultName);
-            case LongPropertyAnnotation.CLASS_NAME:
-                return LongPropertyAnnotation.parse(annotationParams, defaultName);
-            case StringPropertyAnnotation.CLASS_NAME:
-                return StringPropertyAnnotation.parse(annotationParams, defaultName);
-            default:
-                return null;
+        if (qualifiedClassName.equals(BooleanPropertyAnnotation.CLASS_NAME.canonicalName())) {
+            return BooleanPropertyAnnotation.parse(annotationParams, defaultName);
+        } else if (qualifiedClassName.equals(BytesPropertyAnnotation.CLASS_NAME.canonicalName())) {
+            return BytesPropertyAnnotation.parse(annotationParams, defaultName);
+        } else if (qualifiedClassName.equals(
+                DocumentPropertyAnnotation.CLASS_NAME.canonicalName())) {
+            return DocumentPropertyAnnotation.parse(annotationParams, defaultName);
+        } else if (qualifiedClassName.equals(DoublePropertyAnnotation.CLASS_NAME.canonicalName())) {
+            return DoublePropertyAnnotation.parse(annotationParams, defaultName);
+        } else if (qualifiedClassName.equals(LongPropertyAnnotation.CLASS_NAME.canonicalName())) {
+            return LongPropertyAnnotation.parse(annotationParams, defaultName);
+        } else if (qualifiedClassName.equals(StringPropertyAnnotation.CLASS_NAME.canonicalName())) {
+            return StringPropertyAnnotation.parse(annotationParams, defaultName);
         }
+        return null;
     }
 
     /**
@@ -94,8 +106,25 @@
 
     @NonNull
     @Override
-    public final String getSimpleClassName() {
-        return mSimpleClassName;
+    public final ClassName getClassName() {
+        return mClassName;
+    }
+
+    /**
+     * The class used to configure data properties of this kind.
+     *
+     * <p>For example, {@link androidx.appsearch.app.AppSearchSchema.StringPropertyConfig} for
+     * {@link StringPropertyAnnotation}.
+     */
+    @NonNull
+    public final ClassName getConfigClassName() {
+        return mConfigClassName;
+    }
+
+    @NonNull
+    @Override
+    public final String getGenericDocSetterName() {
+        return mGenericDocSetterName;
     }
 
     @NonNull
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DocumentPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DocumentPropertyAnnotation.java
index 732d891..3922775 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DocumentPropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DocumentPropertyAnnotation.java
@@ -16,11 +16,13 @@
 
 package androidx.appsearch.compiler.annotationwrapper;
 
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_SCHEMA_CLASS;
 import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
 
 import androidx.annotation.NonNull;
 
 import com.google.auto.value.AutoValue;
+import com.squareup.javapoet.ClassName;
 
 import java.util.Map;
 
@@ -29,11 +31,14 @@
  */
 @AutoValue
 public abstract class DocumentPropertyAnnotation extends DataPropertyAnnotation {
-    public static final String SIMPLE_CLASS_NAME = "DocumentProperty";
-    public static final String CLASS_NAME = DOCUMENT_ANNOTATION_CLASS + "." + SIMPLE_CLASS_NAME;
+    public static final ClassName CLASS_NAME =
+            DOCUMENT_ANNOTATION_CLASS.nestedClass("DocumentProperty");
+
+    public static final ClassName CONFIG_CLASS =
+            APPSEARCH_SCHEMA_CLASS.nestedClass("DocumentPropertyConfig");
 
     public DocumentPropertyAnnotation() {
-        super(SIMPLE_CLASS_NAME);
+        super(CLASS_NAME, CONFIG_CLASS, /* genericDocSetterName= */"setPropertyDocument");
     }
 
     /**
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DoublePropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DoublePropertyAnnotation.java
index 2b14892..6313b7e 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DoublePropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DoublePropertyAnnotation.java
@@ -16,11 +16,13 @@
 
 package androidx.appsearch.compiler.annotationwrapper;
 
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_SCHEMA_CLASS;
 import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
 
 import androidx.annotation.NonNull;
 
 import com.google.auto.value.AutoValue;
+import com.squareup.javapoet.ClassName;
 
 import java.util.Map;
 
@@ -29,11 +31,14 @@
  */
 @AutoValue
 public abstract class DoublePropertyAnnotation extends DataPropertyAnnotation {
-    public static final String SIMPLE_CLASS_NAME = "DoubleProperty";
-    public static final String CLASS_NAME = DOCUMENT_ANNOTATION_CLASS + "." + SIMPLE_CLASS_NAME;
+    public static final ClassName CLASS_NAME =
+            DOCUMENT_ANNOTATION_CLASS.nestedClass("DoubleProperty");
+
+    public static final ClassName CONFIG_CLASS =
+            APPSEARCH_SCHEMA_CLASS.nestedClass("DoublePropertyConfig");
 
     public DoublePropertyAnnotation() {
-        super(SIMPLE_CLASS_NAME);
+        super(CLASS_NAME, CONFIG_CLASS, /* genericDocSetterName= */"setPropertyDouble");
     }
 
     /**
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/LongPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/LongPropertyAnnotation.java
index c519ee8..2767fe8 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/LongPropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/LongPropertyAnnotation.java
@@ -16,11 +16,13 @@
 
 package androidx.appsearch.compiler.annotationwrapper;
 
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_SCHEMA_CLASS;
 import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
 
 import androidx.annotation.NonNull;
 
 import com.google.auto.value.AutoValue;
+import com.squareup.javapoet.ClassName;
 
 import java.util.Map;
 
@@ -29,11 +31,14 @@
  */
 @AutoValue
 public abstract class LongPropertyAnnotation extends DataPropertyAnnotation {
-    public static final String SIMPLE_CLASS_NAME = "LongProperty";
-    public static final String CLASS_NAME = DOCUMENT_ANNOTATION_CLASS + "." + SIMPLE_CLASS_NAME;
+    public static final ClassName CLASS_NAME =
+            DOCUMENT_ANNOTATION_CLASS.nestedClass("LongProperty");
+
+    public static final ClassName CONFIG_CLASS =
+            APPSEARCH_SCHEMA_CLASS.nestedClass("LongPropertyConfig");
 
     public LongPropertyAnnotation() {
-        super(SIMPLE_CLASS_NAME);
+        super(CLASS_NAME, CONFIG_CLASS, /* genericDocSetterName= */"setPropertyLong");
     }
 
     /**
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/MetadataPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/MetadataPropertyAnnotation.java
index bb9f1ce..144d338 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/MetadataPropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/MetadataPropertyAnnotation.java
@@ -16,11 +16,13 @@
 
 package androidx.appsearch.compiler.annotationwrapper;
 
-import static java.util.Objects.requireNonNull;
+import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.squareup.javapoet.ClassName;
+
 import java.util.Arrays;
 
 import javax.lang.model.element.AnnotationMirror;
@@ -29,11 +31,13 @@
  * An annotation for a metadata property e.g. {@code @Document.Id}.
  */
 public enum MetadataPropertyAnnotation implements PropertyAnnotation {
-    ID(/* simpleClassName= */"Id"),
-    NAMESPACE(/* simpleClassName= */"Namespace"),
-    CREATION_TIMESTAMP_MILLIS(/* simpleClassName= */"CreationTimestampMillis"),
-    TTL_MILLIS(/* simpleClassName= */"TtlMillis"),
-    SCORE(/* simpleClassName= */"Score");
+    ID(/* simpleClassName= */"Id", /* genericDocSetterName= */"setId"),
+    NAMESPACE(/* simpleClassName= */"Namespace", /* genericDocSetterName= */"setNamespace"),
+    CREATION_TIMESTAMP_MILLIS(
+            /* simpleClassName= */"CreationTimestampMillis",
+            /* genericDocSetterName= */"setCreationTimestampMillis"),
+    TTL_MILLIS(/* simpleClassName= */"TtlMillis", /* genericDocSetterName= */"setTtlMillis"),
+    SCORE(/* simpleClassName= */"Score", /* genericDocSetterName= */"setScore");
 
     /**
      * Attempts to parse an {@link AnnotationMirror} into a {@link MetadataPropertyAnnotation},
@@ -43,28 +47,42 @@
     public static MetadataPropertyAnnotation tryParse(@NonNull AnnotationMirror annotation) {
         String qualifiedClassName = annotation.getAnnotationType().toString();
         return Arrays.stream(values())
-                .filter(val -> val.getQualifiedClassName().equals(qualifiedClassName))
+                .filter(val -> val.getClassName().canonicalName().equals(qualifiedClassName))
                 .findFirst()
                 .orElse(null);
     }
 
     @NonNull
-    private final String mSimpleClassName;
+    @SuppressWarnings("ImmutableEnumChecker") // ClassName is an immutable third-party type
+    private final ClassName mClassName;
 
-    MetadataPropertyAnnotation(@NonNull String simpleClassName) {
-        mSimpleClassName = requireNonNull(simpleClassName);
+    @NonNull
+    private final String mGenericDocSetterName;
+
+    MetadataPropertyAnnotation(
+            @NonNull String simpleClassName, @NonNull String genericDocSetterName) {
+        mClassName = DOCUMENT_ANNOTATION_CLASS.nestedClass(simpleClassName);
+        mGenericDocSetterName = genericDocSetterName;
     }
 
     @Override
     @NonNull
-    public String getSimpleClassName() {
-        return mSimpleClassName;
+    public ClassName getClassName() {
+        return mClassName;
     }
 
+
+
     @Override
     @NonNull
     public PropertyAnnotation.Kind getPropertyKind() {
         return Kind.METADATA_PROPERTY;
     }
+
+    @NonNull
+    @Override
+    public String getGenericDocSetterName() {
+        return mGenericDocSetterName;
+    }
 }
 
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/PropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/PropertyAnnotation.java
index b46847b..22a7cb9 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/PropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/PropertyAnnotation.java
@@ -16,10 +16,10 @@
 
 package androidx.appsearch.compiler.annotationwrapper;
 
-import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
-
 import androidx.annotation.NonNull;
 
+import com.squareup.javapoet.ClassName;
+
 /**
  * An instance of an AppSearch property annotation.
  *
@@ -35,27 +35,25 @@
     }
 
     /**
-     * The annotation class' simple name.
+     * The annotation class' name.
      *
-     * <p>For example, {@code StringProperty} for a {@link StringPropertyAnnotation}.
-     */
-    @NonNull
-    String getSimpleClassName();
-
-    /**
-     * The annotation class' qualified name
-     *
-     * <p>{@code androidx.appsearch.annotation.Document.StringProperty} for a
+     * <p>For example, {@code androidx.appsearch.annotation.Document.StringProperty} for a
      * {@link StringPropertyAnnotation}.
      */
     @NonNull
-    default String getQualifiedClassName() {
-        return DOCUMENT_ANNOTATION_CLASS + "." + getSimpleClassName();
-    }
+    ClassName getClassName();
 
     /**
      * The {@link Kind} of {@link PropertyAnnotation}.
      */
     @NonNull
     Kind getPropertyKind();
+
+    /**
+     * The corresponding setter within {@link androidx.appsearch.app.GenericDocument.Builder}.
+     *
+     * <p>For example, {@code setPropertyString} for a {@link StringPropertyAnnotation}.
+     */
+    @NonNull
+    String getGenericDocSetterName();
 }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/StringPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/StringPropertyAnnotation.java
index 7891344..b016dc5 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/StringPropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/StringPropertyAnnotation.java
@@ -16,11 +16,13 @@
 
 package androidx.appsearch.compiler.annotationwrapper;
 
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_SCHEMA_CLASS;
 import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
 
 import androidx.annotation.NonNull;
 
 import com.google.auto.value.AutoValue;
+import com.squareup.javapoet.ClassName;
 
 import java.util.Map;
 
@@ -29,11 +31,14 @@
  */
 @AutoValue
 public abstract class StringPropertyAnnotation extends DataPropertyAnnotation {
-    public static final String SIMPLE_CLASS_NAME = "StringProperty";
-    public static final String CLASS_NAME = DOCUMENT_ANNOTATION_CLASS + "." + SIMPLE_CLASS_NAME;
+    public static final ClassName CLASS_NAME =
+            DOCUMENT_ANNOTATION_CLASS.nestedClass("StringProperty");
+
+    public static final ClassName CONFIG_CLASS =
+            APPSEARCH_SCHEMA_CLASS.nestedClass("StringPropertyConfig");
 
     public StringPropertyAnnotation() {
-        super(SIMPLE_CLASS_NAME);
+        super(CLASS_NAME, CONFIG_CLASS, /* genericDocSetterName= */"setPropertyString");
     }
 
     /**
diff --git a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
index 503dbcf..74b7a3f 100644
--- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
+++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
@@ -541,15 +541,6 @@
                         + "    String mString;\n"
                         + "}\n");
 
-        checkResultContains(/*className=*/"Gift.java",
-                /*content=*/"builder.setCreationTimestampMillis((document.mCreationTimestampMillis "
-                        + "!= null) ? document.mCreationTimestampMillis.longValue() : 0L)");
-        checkResultContains(/*className=*/"Gift.java",
-                /*content=*/"builder.setTtlMillis((document.getTtlMillis() != null) ? document"
-                        + ".getTtlMillis().longValue() : 0L)");
-        checkResultContains(/*className=*/"Gift.java",
-                /*content=*/"builder.setScore((document.mScore != null) ? document.mScore.intValue"
-                        + "() : 0)");
         checkEqualsGolden("Gift.java");
     }
 
@@ -564,8 +555,8 @@
                         + "}\n");
 
         assertThat(compilation).hadErrorContaining(
-                "Field cannot be read: it is private and we failed to find a suitable getter "
-                        + "for field \"price\"");
+                "Field 'price' cannot be read: it is private and has no suitable getters "
+                        + "[public] int price() OR [public] int getPrice()");
     }
 
     @Test
@@ -580,8 +571,8 @@
                         + "}\n");
 
         assertThat(compilation).hadErrorContaining(
-                "Field cannot be read: it is private and we failed to find a suitable getter "
-                        + "for field \"price\"");
+                "Field 'price' cannot be read: it is private and has no suitable getters "
+                        + "[public] int price() OR [public] int getPrice()");
         assertThat(compilation).hadWarningContaining("Getter cannot be used: private visibility");
     }
 
@@ -597,8 +588,8 @@
                         + "}\n");
 
         assertThat(compilation).hadErrorContaining(
-                "Field cannot be read: it is private and we failed to find a suitable getter "
-                        + "for field \"price\"");
+                "Field 'price' cannot be read: it is private and has no suitable getters "
+                        + "[public] int price() OR [public] int getPrice()");
         assertThat(compilation).hadWarningContaining(
                 "Getter cannot be used: should take no parameters");
     }
@@ -616,8 +607,25 @@
                         + "}\n");
 
         assertThat(compilation).hadErrorContaining(
-                "Field cannot be read: it is private and we failed to find a suitable getter "
-                        + "for field \"price\"");
+                "Field 'price' cannot be read: it is private and has no suitable getters "
+                        + "[public] int price() OR [public] int getPrice()");
+    }
+
+    @Test
+    public void testCantRead_noSuitableBooleanGetter() {
+        Compilation compilation = compile(
+                "@Document\n"
+                        + "public class Gift {\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.BooleanProperty private boolean wrapped;\n"
+                        + "}\n");
+
+        assertThat(compilation).hadErrorContaining(
+                "Field 'wrapped' cannot be read: it is private and has no suitable getters "
+                        + "[public] boolean isWrapped() "
+                        + "OR [public] boolean getWrapped() "
+                        + "OR [public] boolean wrapped()");
     }
 
     @Test
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
index 8a935c1..ec6639e 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
@@ -39,9 +39,9 @@
   public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
         new GenericDocument.Builder<>(document.namespace, document.getId(), SCHEMA_NAME);
+    builder.setScore(document.getScore());
     builder.setCreationTimestampMillis(document.getCreationTs());
     builder.setTtlMillis(document.getTtlMs());
-    builder.setScore(document.getScore());
     builder.setPropertyLong("price", document.getPrice());
     return builder.build();
   }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testClassSpecialValues.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testClassSpecialValues.JAVA
index 292f17b..d8539d6 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testClassSpecialValues.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testClassSpecialValues.JAVA
@@ -5,6 +5,8 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
+import java.lang.Integer;
+import java.lang.Long;
 import java.lang.Override;
 import java.lang.String;
 import java.util.Collections;
@@ -41,9 +43,18 @@
   public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
         new GenericDocument.Builder<>(document.mNamespace, document.mId, SCHEMA_NAME);
-    builder.setCreationTimestampMillis((document.mCreationTimestampMillis != null) ? document.mCreationTimestampMillis.longValue() : 0L);
-    builder.setTtlMillis((document.getTtlMillis() != null) ? document.getTtlMillis().longValue() : 0L);
-    builder.setScore((document.mScore != null) ? document.mScore.intValue() : 0);
+    Long mCreationTimestampMillisCopy = document.mCreationTimestampMillis;
+    if (mCreationTimestampMillisCopy != null) {
+      builder.setCreationTimestampMillis(mCreationTimestampMillisCopy);
+    }
+    Integer mScoreCopy = document.mScore;
+    if (mScoreCopy != null) {
+      builder.setScore(mScoreCopy);
+    }
+    Long mTtlMillisCopy = document.getTtlMillis();
+    if (mTtlMillisCopy != null) {
+      builder.setTtlMillis(mTtlMillisCopy);
+    }
     String mStringCopy = document.mString;
     if (mStringCopy != null) {
       builder.setPropertyString("string", mStringCopy);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongPropertyIndexingType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongPropertyIndexingType.JAVA
index 64e240a..b924069 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongPropertyIndexingType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongPropertyIndexingType.JAVA
@@ -98,7 +98,7 @@
     Integer[] arrBoxIntCopy = document.arrBoxInt;
     if (arrBoxIntCopy != null) {
       long[] arrBoxIntConv = new long[arrBoxIntCopy.length];
-      for (int i = 0 ; i < arrBoxIntCopy.length ; i++) {
+      for (int i = 0; i < arrBoxIntCopy.length; i++) {
         arrBoxIntConv[i] = arrBoxIntCopy[i];
       }
       builder.setPropertyLong("arrBoxInt", arrBoxIntConv);
@@ -106,7 +106,7 @@
     int[] arrUnboxIntCopy = document.arrUnboxInt;
     if (arrUnboxIntCopy != null) {
       long[] arrUnboxIntConv = new long[arrUnboxIntCopy.length];
-      for (int i = 0 ; i < arrUnboxIntCopy.length ; i++) {
+      for (int i = 0; i < arrUnboxIntCopy.length; i++) {
         arrUnboxIntConv[i] = arrUnboxIntCopy[i];
       }
       builder.setPropertyLong("arrUnboxInt", arrUnboxIntConv);
@@ -114,7 +114,7 @@
     Long[] arrBoxLongCopy = document.arrBoxLong;
     if (arrBoxLongCopy != null) {
       long[] arrBoxLongConv = new long[arrBoxLongCopy.length];
-      for (int i = 0 ; i < arrBoxLongCopy.length ; i++) {
+      for (int i = 0; i < arrBoxLongCopy.length; i++) {
         arrBoxLongConv[i] = arrBoxLongCopy[i];
       }
       builder.setPropertyLong("arrBoxLong", arrBoxLongConv);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
index ec1e5ac..ba71c9a 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
@@ -239,7 +239,7 @@
     Long[] arrBoxLongCopy = document.arrBoxLong;
     if (arrBoxLongCopy != null) {
       long[] arrBoxLongConv = new long[arrBoxLongCopy.length];
-      for (int i = 0 ; i < arrBoxLongCopy.length ; i++) {
+      for (int i = 0; i < arrBoxLongCopy.length; i++) {
         arrBoxLongConv[i] = arrBoxLongCopy[i];
       }
       builder.setPropertyLong("arrBoxLong", arrBoxLongConv);
@@ -251,7 +251,7 @@
     Integer[] arrBoxIntegerCopy = document.arrBoxInteger;
     if (arrBoxIntegerCopy != null) {
       long[] arrBoxIntegerConv = new long[arrBoxIntegerCopy.length];
-      for (int i = 0 ; i < arrBoxIntegerCopy.length ; i++) {
+      for (int i = 0; i < arrBoxIntegerCopy.length; i++) {
         arrBoxIntegerConv[i] = arrBoxIntegerCopy[i];
       }
       builder.setPropertyLong("arrBoxInteger", arrBoxIntegerConv);
@@ -259,7 +259,7 @@
     int[] arrUnboxIntCopy = document.arrUnboxInt;
     if (arrUnboxIntCopy != null) {
       long[] arrUnboxIntConv = new long[arrUnboxIntCopy.length];
-      for (int i = 0 ; i < arrUnboxIntCopy.length ; i++) {
+      for (int i = 0; i < arrUnboxIntCopy.length; i++) {
         arrUnboxIntConv[i] = arrUnboxIntCopy[i];
       }
       builder.setPropertyLong("arrUnboxInt", arrUnboxIntConv);
@@ -267,7 +267,7 @@
     Double[] arrBoxDoubleCopy = document.arrBoxDouble;
     if (arrBoxDoubleCopy != null) {
       double[] arrBoxDoubleConv = new double[arrBoxDoubleCopy.length];
-      for (int i = 0 ; i < arrBoxDoubleCopy.length ; i++) {
+      for (int i = 0; i < arrBoxDoubleCopy.length; i++) {
         arrBoxDoubleConv[i] = arrBoxDoubleCopy[i];
       }
       builder.setPropertyDouble("arrBoxDouble", arrBoxDoubleConv);
@@ -279,7 +279,7 @@
     Float[] arrBoxFloatCopy = document.arrBoxFloat;
     if (arrBoxFloatCopy != null) {
       double[] arrBoxFloatConv = new double[arrBoxFloatCopy.length];
-      for (int i = 0 ; i < arrBoxFloatCopy.length ; i++) {
+      for (int i = 0; i < arrBoxFloatCopy.length; i++) {
         arrBoxFloatConv[i] = arrBoxFloatCopy[i];
       }
       builder.setPropertyDouble("arrBoxFloat", arrBoxFloatConv);
@@ -287,7 +287,7 @@
     float[] arrUnboxFloatCopy = document.arrUnboxFloat;
     if (arrUnboxFloatCopy != null) {
       double[] arrUnboxFloatConv = new double[arrUnboxFloatCopy.length];
-      for (int i = 0 ; i < arrUnboxFloatCopy.length ; i++) {
+      for (int i = 0; i < arrUnboxFloatCopy.length; i++) {
         arrUnboxFloatConv[i] = arrUnboxFloatCopy[i];
       }
       builder.setPropertyDouble("arrUnboxFloat", arrUnboxFloatConv);
@@ -295,7 +295,7 @@
     Boolean[] arrBoxBooleanCopy = document.arrBoxBoolean;
     if (arrBoxBooleanCopy != null) {
       boolean[] arrBoxBooleanConv = new boolean[arrBoxBooleanCopy.length];
-      for (int i = 0 ; i < arrBoxBooleanCopy.length ; i++) {
+      for (int i = 0; i < arrBoxBooleanCopy.length; i++) {
         arrBoxBooleanConv[i] = arrBoxBooleanCopy[i];
       }
       builder.setPropertyBoolean("arrBoxBoolean", arrBoxBooleanConv);
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index 615eda8..2625438 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -189,6 +189,12 @@
         return contents
 
     def _TransformTestCode(self, contents):
+        # Add imports used in tests
+        imports_to_add = []
+        if '@exportToFramework:SdkSuppress' in contents:
+            imports_to_add.append('androidx.test.filters.SdkSuppress')
+            imports_to_add.append('android.os.Build')
+
         contents = (contents
             .replace('androidx.appsearch.testutil.', 'android.app.appsearch.testutil.')
             .replace(
@@ -199,8 +205,18 @@
                     'android.app.appsearch.AppSearchManager')
             .replace('LocalStorage.', 'AppSearchManager.')
         )
+        contents = re.sub(
+            r'/\*@exportToFramework:SdkSuppress\(minSdkVersion = (.*?)\)\*/',
+            r'@SdkSuppress(minSdkVersion = \1)',
+            contents)
         for shim in ['AppSearchSession', 'GlobalSearchSession', 'SearchResults']:
             contents = re.sub(r"([^a-zA-Z])(%s)([^a-zA-Z0-9])" % shim, r'\1\2Shim\3', contents)
+
+        for import_to_add in imports_to_add:
+            contents = re.sub(
+                    r'^(\s*package [^;]+;\s*)$', r'\1\nimport %s;\n' % import_to_add, contents,
+                    flags=re.MULTILINE)
+
         return self._TransformCommonCode(contents)
 
     def _TransformAndCopyFolder(self, source_dir, dest_dir, transform_func=None):
diff --git a/benchmark/benchmark-common/lint-baseline.xml b/benchmark/benchmark-common/lint-baseline.xml
new file mode 100644
index 0000000..c56433f
--- /dev/null
+++ b/benchmark/benchmark-common/lint-baseline.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 23 (current min is 14): `stopAllPerfettoProcesses`"
+        errorLine1="        PerfettoHelper.stopAllPerfettoProcesses()"
+        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/benchmark/PerfettoHelperTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 21 (current min is 14): `getPidsForProcess`"
+        errorLine1="        fun getPerfettoPids() = Shell.getPidsForProcess(if (unbundled) &quot;tracebox&quot; else &quot;perfetto&quot;)"
+        errorLine2="                                      ~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/benchmark/PerfettoHelperTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 23 (current min is 14): `PerfettoCapture`"
+        errorLine1="        val capture = PerfettoCapture(unbundled)"
+        errorLine2="                      ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/benchmark/PerfettoHelperTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 23 (current min is 14): `start`"
+        errorLine1="        capture.start("
+        errorLine2="                ~~~~~">
+        <location
+            file="src/androidTest/java/androidx/benchmark/PerfettoHelperTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 23 (current min is 14): `isRunning`"
+        errorLine1="        assertTrue(capture.isRunning())"
+        errorLine2="                           ~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/benchmark/PerfettoHelperTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 23 (current min is 14): `stopAllPerfettoProcesses`"
+        errorLine1="        PerfettoHelper.stopAllPerfettoProcesses()"
+        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/benchmark/PerfettoHelperTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 23 (current min is 14): `isRunning`"
+        errorLine1="        assertFalse(capture.isRunning())"
+        errorLine2="                            ~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/benchmark/PerfettoHelperTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 23 (current min is 14): `isAbiSupported`"
+        errorLine1="        Assume.assumeTrue(PerfettoHelper.isAbiSupported())"
+        errorLine2="                                         ~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/benchmark/PerfettoHelperTest.kt"/>
+    </issue>
+
+</issues>
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt
index d778a9f..bae0b4c 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt
@@ -298,6 +298,9 @@
         absoluteFilePath: String,
         reportOnRunEndOnly: Boolean = false
     ) {
+        require(!key.contains('=')) {
+            "Key must not contain '=', which breaks instrumentation result string parsing"
+        }
         if (reportOnRunEndOnly) {
             InstrumentationResultScope(runEndResultBundle).fileRecord(key, absoluteFilePath)
         } else {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
index 53c59e4..44262c6 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
@@ -117,7 +117,6 @@
      */
     fun writeFile(
         fileName: String,
-        reportKey: String,
         reportOnRunEndOnly: Boolean = false,
         block: (file: File) -> Unit,
     ): String {
@@ -143,7 +142,7 @@
         }
 
         InstrumentationResults.reportAdditionalFileToCopy(
-            key = reportKey,
+            key = sanitizedName,
             absoluteFilePath = destination.absolutePath,
             reportOnRunEndOnly = reportOnRunEndOnly
         )
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
index 19d47fc..1a7b9308 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
@@ -293,8 +293,7 @@
     override fun stop() {
         session!!.stopRecording()
         Outputs.writeFile(
-            fileName = outputRelativePath!!,
-            reportKey = "simpleperf_trace"
+            fileName = outputRelativePath!!
         ) {
             session!!.convertSimpleperfOutputToProto("simpleperf.data", it.absolutePath)
         }
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/ResultWriter.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/ResultWriter.kt
index 873ead3..03c8c81 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/ResultWriter.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/ResultWriter.kt
@@ -43,7 +43,6 @@
 
             Outputs.writeFile(
                 fileName = "$packageName-benchmarkData.json",
-                reportKey = "results_json",
                 reportOnRunEndOnly = true
             ) {
                 Log.d(
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
index 2f5a387..77855c0 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
@@ -18,6 +18,7 @@
 
 import android.os.Build
 import android.util.JsonReader
+import androidx.annotation.CheckResult
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.benchmark.Outputs
@@ -86,9 +87,15 @@
         helper.stopCollecting(destinationPath)
     }
 
-    /** Enables Perfetto SDK tracing in an app if present */
+    /**
+     * Enables Perfetto SDK tracing in the [PerfettoSdkConfig.targetPackage]
+     *
+     * @return a pair of [androidx.tracing.perfetto.handshake.protocol.ResultCode] and
+     * a user-friendly message explaining the code
+     */
     @RequiresApi(30) // TODO(234351579): Support API < 30
-    fun enableAndroidxTracingPerfetto(config: PerfettoSdkConfig): String? =
+    @CheckResult
+    fun enableAndroidxTracingPerfetto(config: PerfettoSdkConfig): Pair<Int, String> =
         enableAndroidxTracingPerfetto(
             targetPackage = config.targetPackage,
             provideBinariesIfMissing = config.provideBinariesIfMissing,
@@ -100,11 +107,18 @@
         )
 
     @RequiresApi(30) // TODO(234351579): Support API < 30
+    @CheckResult
+    /**
+     * Enables Perfetto SDK tracing in the [PerfettoSdkConfig.targetPackage]
+     *
+     * @return a pair of [androidx.tracing.perfetto.handshake.protocol.ResultCode] and
+     * a user-friendly message explaining the code
+     */
     private fun enableAndroidxTracingPerfetto(
         targetPackage: String,
         provideBinariesIfMissing: Boolean,
         isColdStartupTracing: Boolean
-    ): String? {
+    ): Pair<Int, String> {
         if (!isAbiSupported()) {
             throw IllegalStateException("Unsupported ABI (${Build.SUPPORTED_ABIS.joinToString()})")
         }
@@ -153,11 +167,11 @@
         }
 
         // process the response
-        return when (response.resultCode) {
+        val message = when (response.resultCode) {
             0 -> "The broadcast to enable tracing was not received. This most likely means " +
                 "that the app does not contain the `androidx.tracing.tracing-perfetto` " +
                 "library as its dependency."
-            RESULT_CODE_SUCCESS -> null
+            RESULT_CODE_SUCCESS -> "Success"
             RESULT_CODE_ALREADY_ENABLED -> "Perfetto SDK already enabled."
             RESULT_CODE_ERROR_BINARY_MISSING ->
                 "Perfetto SDK binary dependencies missing. " +
@@ -180,6 +194,7 @@
             RESULT_CODE_ERROR_OTHER -> "Error: ${response.message}."
             else -> throw RuntimeException("Unrecognized result code: ${response.resultCode}.")
         }
+        return response.resultCode to message
     }
 
     private fun constructLibrarySource(): PerfettoSdkHandshake.LibrarySource {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
index 5eaa17c..9e958c3 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
@@ -27,7 +27,10 @@
 import androidx.benchmark.Shell
 import androidx.benchmark.perfetto.PerfettoHelper.Companion.LOG_TAG
 import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_ALREADY_ENABLED
+import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_SUCCESS
 import java.io.FileOutputStream
+import java.lang.RuntimeException
 
 /**
  * Wrapper for [PerfettoCapture] which does nothing below API 23.
@@ -64,8 +67,15 @@
             if (perfettoSdkConfig != null &&
                 Build.VERSION.SDK_INT >= 30
             ) {
-                val result = enableAndroidxTracingPerfetto(perfettoSdkConfig) ?: "Success"
-                Log.d(LOG_TAG, "Enable full tracing result=$result")
+                val (resultCode, message) = enableAndroidxTracingPerfetto(perfettoSdkConfig)
+                Log.d(LOG_TAG, "Enable full tracing result=$message")
+
+                if (resultCode !in arrayOf(RESULT_CODE_SUCCESS, RESULT_CODE_ALREADY_ENABLED)) {
+                    throw RuntimeException(
+                        "Issue while enabling Perfetto SDK tracing in" +
+                            " ${perfettoSdkConfig.targetPackage}: $message"
+                    )
+                }
             }
             start(config)
         }
@@ -76,8 +86,7 @@
     @RequiresApi(23)
     private fun stop(traceLabel: String): String {
         return Outputs.writeFile(
-            fileName = "${traceLabel}_${dateToFileName()}.perfetto-trace",
-            reportKey = "perfetto_trace_$traceLabel"
+            fileName = "${traceLabel}_${dateToFileName()}.perfetto-trace"
         ) {
             capture!!.stop(it.absolutePath)
             if (Outputs.forceFilesForShellAccessible) {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
index b4fdbc1..6c67eab 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
@@ -421,8 +421,6 @@
                     // supported by a device. That is why we need to search from most specific to
                     // least specific. For e.g. emulators claim to support aarch64, when in reality
                     // they can only support x86 or x86_64.
-                    // Note: Cuttlefish is x86 but claims support for x86_64
-                    Build.MODEL.contains("Cuttlefish") -> "x86" // TODO(204892353): handle properly
                     Build.SUPPORTED_64_BIT_ABIS.any { it.startsWith("x86_64") } -> "x86_64"
                     Build.SUPPORTED_32_BIT_ABIS.any { it.startsWith("x86") } -> "x86"
                     Build.SUPPORTED_64_BIT_ABIS.any { it.startsWith("arm64") } -> "aarch64"
diff --git a/benchmark/benchmark-junit4/build.gradle b/benchmark/benchmark-junit4/build.gradle
index ba2cecb..ed93e83 100644
--- a/benchmark/benchmark-junit4/build.gradle
+++ b/benchmark/benchmark-junit4/build.gradle
@@ -37,7 +37,7 @@
     api(libs.kotlinStdlib)
 
     implementation("androidx.test:rules:1.5.0")
-    implementation("androidx.test:runner:1.5.0")
+    implementation("androidx.test:runner:1.5.2")
     implementation("androidx.tracing:tracing-ktx:1.1.0")
     api("androidx.annotation:annotation:1.1.0")
 
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/InstrumentationResultsRunListener.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/InstrumentationResultsRunListener.kt
index 32837e2..49a7697 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/InstrumentationResultsRunListener.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/InstrumentationResultsRunListener.kt
@@ -29,7 +29,7 @@
  * See [InstrumentationResults.runEndResultBundle]
  *
  */
-@Suppress("unused") // referenced by inst arg at runtime
+@Suppress("unused", "RestrictedApiAndroidX") // referenced by inst arg at runtime
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 public class InstrumentationResultsRunListener : InstrumentationRunListener() {
     override fun instrumentationRunFinished(
diff --git a/benchmark/benchmark-macro-junit4/build.gradle b/benchmark/benchmark-macro-junit4/build.gradle
index 40cc011..1e1dcaf4 100644
--- a/benchmark/benchmark-macro-junit4/build.gradle
+++ b/benchmark/benchmark-macro-junit4/build.gradle
@@ -36,10 +36,10 @@
     api(libs.kotlinStdlib)
     api("androidx.annotation:annotation:1.1.0")
     api(project(":benchmark:benchmark-macro"))
-    api("androidx.test.uiautomator:uiautomator:2.2.0")
+    api("androidx.test.uiautomator:uiautomator:2.3.0-alpha04")
     implementation(project(":benchmark:benchmark-common"))
     implementation("androidx.test:rules:1.5.0")
-    implementation("androidx.test:runner:1.5.0")
+    implementation("androidx.test:runner:1.5.2")
 
     androidTestImplementation(project(":internal-testutils-ktx"))
     androidTestImplementation(libs.testExtJunit)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index 3e42bcb..3db43e4 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -221,16 +221,17 @@
         // Launch first activity, and validate it is displayed
         scope.startActivityAndWait(ConfigurableActivity.createIntent("InitialText"))
         assertTrue(device.hasObject(By.text("InitialText")))
-        scope.stopMethodTracing()
+        scope.stopMethodTracing("TEST_UNIQUE_NAME")
         val outputs = Outputs.outputDirectory.walk().filter {
             it.isFile
         }.toSet()
         val testOutputs = outputs - files
         val trace = testOutputs.singleOrNull { file ->
-            file.absolutePath.endsWith("method.trace")
+            file.name.endsWith(".trace") && file.name.contains("_method_")
         }
         // One method trace should have been created
         assertNotNull(trace)
+        assertTrue(trace.name.startsWith("TEST_UNIQUE_NAME_method_"))
     }
 
     private fun validateLaunchAndFrameStats(pressHome: Boolean) {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
index e66ce68..441f9ff 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
@@ -567,5 +567,9 @@
             if (isColdStartupTracing) InitialProcessState.NotAlive else InitialProcessState.Alive,
             provideBinariesIfMissing
         )
-    )
+    ).let { (resultCode, message) ->
+        // Maps the response into the old contract of [enableAndroidxTracingPerfetto], where for
+        // success we get a [null] response; otherwise an error message
+        if (resultCode == RESULT_CODE_SUCCESS) null else message
+    }
 }
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
index bb55014..54c3276 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
@@ -48,6 +48,8 @@
 ) {
     val scope = buildMacrobenchmarkScope(packageName)
     val startTime = System.nanoTime()
+    // Ensure the device is awake
+    scope.device.wakeUp()
     val killProcessBlock = scope.killProcessBlock()
     // always kill the process at beginning of a collection.
     killProcessBlock.invoke()
@@ -190,23 +192,21 @@
     // Write a file with a timestamp to be able to disambiguate between runs with the same
     // unique name.
 
-    val (fileName, reportKey, tsFileName) =
+    val (fileName, tsFileName) =
         if (includeInStartupProfile && Arguments.enableStartupProfiles) {
             arrayOf(
                 "$uniqueFilePrefix-startup-prof.txt",
-                "startup-profile",
                 "$uniqueFilePrefix-startup-prof-${Outputs.dateToFileName()}.txt"
             )
         } else {
             arrayOf(
                 "$uniqueFilePrefix-baseline-prof.txt",
-                "baseline-profile",
                 "$uniqueFilePrefix-baseline-prof-${Outputs.dateToFileName()}.txt"
             )
         }
 
-    val absolutePath = Outputs.writeFile(fileName, reportKey) { it.writeText(profile) }
-    val tsAbsolutePath = Outputs.writeFile(tsFileName, "baseline-profile-ts") {
+    val absolutePath = Outputs.writeFile(fileName) { it.writeText(profile) }
+    val tsAbsolutePath = Outputs.writeFile(tsFileName) {
         Log.d(TAG, "Pull Baseline Profile with: `adb pull \"${it.absolutePath}\" .`")
         it.writeText(profile)
     }
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 217cf5d..f3214ed 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -233,7 +233,7 @@
     // output, and give it different (test-wide) lifecycle
     val perfettoCollector = PerfettoCaptureWrapper()
     val tracePaths = mutableListOf<String>()
-    val resultFiles = mutableListOf<Profiler.ResultFile>()
+    val methodTracingResultFiles = mutableListOf<Profiler.ResultFile>()
     try {
         metrics.forEach {
             it.configure(packageName)
@@ -291,12 +291,12 @@
                                 it.stop()
                             }
                             if (launchWithMethodTracing && scope.isMethodTracing) {
-                                val (label, tracePath) = scope.stopMethodTracing()
+                                val (label, tracePath) = scope.stopMethodTracing(fileLabel)
                                 val resultFile = Profiler.ResultFile(
                                     label = label,
                                     absolutePath = tracePath
                                 )
-                                resultFiles += resultFile
+                                methodTracingResultFiles += resultFile
                                 scope.isMethodTracing = false
                             }
                         }
@@ -348,12 +348,18 @@
             """.trimIndent()
         }
         InstrumentationResults.instrumentationReport {
+            if (launchWithMethodTracing && methodTracingResultFiles.size < iterations) {
+                warningMessage += "\nNOTE: Method traces cannot be captured during iterations" +
+                    " that start while the target process is already running (including HOT/WARM" +
+                    " launches)."
+            }
+
             reportSummaryToIde(
                 warningMessage = warningMessage,
                 testName = uniqueName,
                 measurements = measurements,
                 iterationTracePaths = tracePaths,
-                profilerResults = resultFiles
+                profilerResults = methodTracingResultFiles
             )
 
             warningMessage = "" // warning only printed once
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
index 9b141c6..b8ed386 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
@@ -25,6 +25,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.benchmark.DeviceInfo
 import androidx.benchmark.Outputs
+import androidx.benchmark.Outputs.dateToFileName
 import androidx.benchmark.Shell
 import androidx.benchmark.macro.MacrobenchmarkScope.Companion.Api24ContextHelper.createDeviceProtectedStorageContextCompat
 import androidx.benchmark.macro.perfetto.forceTrace
@@ -154,7 +155,7 @@
                 ))
             ) {
             isMethodTracing = true
-            val tracePath = methodTracePath(packageName, iteration ?: 0)
+            val tracePath = methodTraceRecordPath(packageName)
             "--start-profiler \"$tracePath\" --streaming"
         } else {
             ""
@@ -310,14 +311,13 @@
      *
      * @return a [Pair] representing the label, and the absolute path of the method trace.
      */
-    @SuppressLint("BanThreadSleep") // Need to sleep to wait for the traces to be flushed.
-    internal fun stopMethodTracing(): Pair<String, String> {
+    internal fun stopMethodTracing(uniqueLabel: String): Pair<String, String> {
         Shell.executeScriptSilent("am profile stop $packageName")
         // Wait for the profiles to get dumped :(
         // ART Method tracing has a buffer size of 8M, so 1 second should be enough
         // to dump the contents of the buffer.
-        val currentIteration = iteration ?: 0
-        val tracePath = methodTracePath(packageName, currentIteration)
+
+        val tracePath = methodTraceRecordPath(packageName)
         // Using 50 ms as a poll duration for a max of 20 iterations. This is because
         // we don't want to wait for longer than 1s. Also, anecdotally when polling from the
         // shell I found a stable iteration count of 3 to be sufficient.
@@ -327,19 +327,23 @@
             stableIterations = 3,
             pollDurationMs = 50L
         )
-        val fileName = methodTraceName(packageName, currentIteration)
+        // unique label so source is clear, dateToFileName so each run of test is unique on host
+        val outputFileName = "$uniqueLabel-method-${dateToFileName()}.trace"
         val stagingFile = File.createTempFile("methodTrace", null, Outputs.dirUsableByAppAndShell)
         // Staging location before we write it again using Outputs.writeFile(...)
+        // NOTE: staging copy may be unnecessary if we just use a single `cp`
         Shell.executeScriptSilent("cp '$tracePath' '$stagingFile'")
-        // Report(
-        val outputPath = Outputs.writeFile(fileName, fileName) {
+
+        // Report file
+        val outputPath = Outputs.writeFile(outputFileName) {
             Log.d(TAG, "Writing method traces to ${it.absolutePath}")
             stagingFile.copyTo(it, overwrite = true)
+
             // Cleanup
             stagingFile.delete()
             Shell.executeScriptSilent("rm \"$tracePath\"")
         }
-        return fileName to outputPath
+        return "MethodTrace iteration ${this.iteration ?: 0}" to outputPath
     }
 
     /**
@@ -416,12 +420,11 @@
             return shaderDirectory.absolutePath.replace(context.packageName, packageName)
         }
 
-        fun methodTracePath(packageName: String, iteration: Int): String {
-            return "/data/local/tmp/${methodTraceName(packageName, iteration)}"
-        }
-
-        fun methodTraceName(packageName: String, iteration: Int): String {
-            return "$packageName-$iteration-method.trace"
+        /**
+         * Path for method trace during record, before fully flushed/stopped, move to outputs
+         */
+        fun methodTraceRecordPath(packageName: String): String {
+            return "/data/local/tmp/$packageName-method.trace"
         }
 
         @RequiresApi(Build.VERSION_CODES.N)
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MethodTracing.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MethodTracing.kt
deleted file mode 100644
index f77efc40e..0000000
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MethodTracing.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package androidx.benchmark.macro
-
-import android.content.Context
-import androidx.annotation.RestrictTo
-import androidx.benchmark.Outputs
-import androidx.benchmark.Shell
-import androidx.benchmark.getFirstMountedMediaDir
-import androidx.benchmark.inMemoryTrace
-import androidx.test.platform.app.InstrumentationRegistry
-import java.io.File
-
-private const val OPTION_SAMPLED = "Sampled"
-private const val RECEIVER_NAME = "androidx.benchmark.internal.MethodTracingReceiver"
-
-// Extras
-private const val ACTION = "ACTION"
-private const val UNIQUE_NAME = "UNIQUE_NAME"
-
-// Actions
-private const val METHOD_TRACE_START = "METHOD_TRACE_START"
-private const val METHOD_TRACE_START_SAMPLED = "METHOD_TRACE_START_SAMPLED"
-private const val METHOD_TRACE_END = "ACTION_METHOD_TRACE_END"
-
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-object MethodTracing {
-    private val context: Context = InstrumentationRegistry.getInstrumentation().context
-
-    /**
-     * Starts method tracing on the [packageName].
-     * <br/>
-     * The only [options] supported are `Full` and `Sampled`.
-     */
-    fun startTracing(packageName: String, uniqueName: String, options: String) {
-        val fileName = fileName(uniqueName)
-        // The only options possible are `Full` and `Sampled`
-        val extras = if (options == (OPTION_SAMPLED)) {
-            "-e $ACTION $METHOD_TRACE_START_SAMPLED -e $UNIQUE_NAME $fileName"
-        } else {
-            "-e $ACTION $METHOD_TRACE_START -e $UNIQUE_NAME $fileName"
-        }
-        broadcast(packageName, extras)
-    }
-
-    /**
-     * Stops method tracing and copies the output trace to the `additionalTestOutputDir`.
-     */
-    fun stopTracing(packageName: String, uniqueName: String) {
-        val fileName = fileName(uniqueName)
-        val extras = "-e $ACTION $METHOD_TRACE_END"
-        broadcast(packageName, extras)
-        // The reason we are doing this is to make trace collection possible.
-        // The target APK stores the method traces in the first media mounted directory.
-        // This is because, that happens to be the only directory accessible to the app and shell.
-        // We then subsequently copy it to the actual `Outputs.dirUsableByAppAndShell` from the
-        // perspective of the test APK.
-        val mediaDirParent = context.getFirstMountedMediaDir()?.parentFile
-        val sourcePath = "$mediaDirParent/$packageName/$fileName"
-        // Staging location before we write it again using Outputs.writeFile(...)
-        // :(
-        val stagingPath = "${Outputs.dirUsableByAppAndShell}/_$fileName"
-        Shell.executeScriptSilent("cp '$sourcePath' '$stagingPath'")
-        // Report
-        Outputs.writeFile(fileName, fileName) {
-            val staging = File(stagingPath)
-            // No need to clean up, here because the clean up happens automatically on subsequent
-            // test runs.
-            staging.copyTo(it, overwrite = true)
-        }
-    }
-
-    private fun fileName(uniqueName: String): String {
-        return "${uniqueName}_method.trace"
-    }
-
-    private fun broadcast(targetPackageName: String, extras: String) {
-        inMemoryTrace("methodTracingBroadcast") {
-            val action = "androidx.benchmark.experiments.ACTION_METHOD_TRACE"
-            val result =
-                Shell.amBroadcast("-a $action $extras $targetPackageName/$RECEIVER_NAME") ?: 0
-            require(result > 0) {
-                """
-                    Operation with $extras failed (result code $result).
-                    Make sure you add a dependency on:
-                    `project(":benchmark:benchmark-internal")`
-                """.trimIndent()
-            }
-        }
-    }
-}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
index 692175f..5a08436 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
@@ -22,7 +22,7 @@
 internal object AudioUnderrunQuery {
     @Language("sql")
     private fun getFullQuery() = """
-        SELECT track.name, counter.value, counter.ts
+        SELECT counter.value, counter.ts
         FROM track
         JOIN counter ON track.id = counter.track_id
         WHERE track.type = 'process_counter_track' AND track.name LIKE 'nRdy%'
@@ -38,7 +38,6 @@
     ): SubMetrics {
         val queryResult = session.query(getFullQuery())
 
-        var trackName: String? = null
         var lastTs: Long? = null
         var totalNs: Long = 0
         var zeroNs: Long = 0
@@ -46,16 +45,9 @@
         queryResult
             .asSequence()
             .forEach { lineVals ->
-
                 if (lineVals.size != EXPECTED_COLUMN_COUNT)
                     throw IllegalStateException("query failed")
 
-                if (trackName == null) {
-                    trackName = lineVals[VAL_NAME] as String?
-                } else if (trackName!! != lineVals[VAL_NAME]) {
-                    throw RuntimeException("There could be only one AudioTrack per measure")
-                }
-
                 if (lastTs == null) {
                     lastTs = lineVals[VAL_TS] as Long
                 } else {
@@ -74,8 +66,7 @@
         return SubMetrics((totalNs / 1_000_000).toInt(), (zeroNs / 1_000_000).toInt())
     }
 
-    private const val VAL_NAME = "name"
     private const val VAL_VALUE = "value"
     private const val VAL_TS = "ts"
-    private const val EXPECTED_COLUMN_COUNT = 3
+    private const val EXPECTED_COLUMN_COUNT = 2
 }
diff --git a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoSdkOverheadBenchmark.kt b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoSdkOverheadBenchmark.kt
index 8388e3d..6c46a2c 100644
--- a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoSdkOverheadBenchmark.kt
+++ b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoSdkOverheadBenchmark.kt
@@ -78,10 +78,10 @@
     fun traceBeginEnd_perfettoSdkTrace() {
         PerfettoCapture().enableAndroidxTracingPerfetto(
             PerfettoSdkConfig(targetPackage, InitialProcessState.Alive)
-        ).let { response ->
+        ).let { (resultCode, _) ->
             assertTrue(
                 "Ensuring Perfetto SDK is enabled",
-                response == null || response.contains("already enabled")
+                resultCode in arrayOf(1, 2) // 1 = success, 2 = already enabled
             )
         }
         var ix = 0
diff --git a/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh b/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
index cc07a21..36b2d3c 100755
--- a/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
+++ b/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
@@ -107,6 +107,69 @@
     done
 }
 
+# Given a target frequency and list of available frequencies, selects one closest to the target
+function_find_target_mhz() {
+    targetFreq=$1
+    availFreq=$2
+
+    # Choose a frequency to lock to that's >= $CPU_TARGET_FREQ_PERCENT% of max
+    # (below, 100M = 1K for KHz->MHz * 100 for %)
+    targetFreqMhz=$(( ($targetFreq * $CPU_TARGET_FREQ_PERCENT) / 100000 ))
+    outputFreq=0
+    outputFreqDiff=100000000
+    for freq in ${availFreq}; do
+        freqMhz=$(( ${freq} / 1000 ))
+        if [ ${freqMhz} -ge ${targetFreqMhz} ]; then
+            newOutputFreqDiff=$(( $freq - $targetFreqMhz ))
+            if [ $newOutputFreqDiff -lt $outputFreqDiff ]; then
+                outputFreq=${freq}
+                outputFreqDiff=$(( $outputFreq - $targetFreqMhz ))
+            fi
+        fi
+    done
+
+    echo $outputFreq
+}
+
+# enable a set of CPUs with a given set of frequencies
+function_enable_cpus() {
+    enableCpuIndices=$1
+    enableCpuFreq=$2
+
+    # enable 'big' CPUs
+    for cpu in ${enableCpuIndices}; do
+        freq=${CPU_BASE}/cpu$cpu/cpufreq
+
+        # Try to enable core, so we can find its frequencies.
+        # Note: In cases where the online file is inaccessible, it represents a
+        # core which cannot be turned off, so we simply assume it is enabled if
+        # this command fails.
+        if [ -f "$CPU_BASE/cpu$cpu/online" ]; then
+            echo 1 > ${CPU_BASE}/cpu${cpu}/online || true
+        fi
+
+        # scaling_max_freq must be set before scaling_min_freq
+        echo ${enableCpuFreq} > ${freq}/scaling_max_freq
+        echo ${enableCpuFreq} > ${freq}/scaling_min_freq
+        echo ${enableCpuFreq} > ${freq}/scaling_setspeed
+
+        # Give system a bit of time to propagate the change to scaling_setspeed.
+        sleep 0.1
+
+        # validate setting the freq worked
+        obsCur=`cat ${freq}/scaling_cur_freq`
+        obsMin=`cat ${freq}/scaling_min_freq`
+        obsMax=`cat ${freq}/scaling_max_freq`
+        if [ "$obsCur" -ne "$enableCpuFreq" ] || [ "$obsMin" -ne "$enableCpuFreq" ] || [ "$obsMax" -ne "$enableCpuFreq" ]; then
+            echo "Failed to set CPU$cpu to $enableCpuFreq Hz! Aborting..."
+            echo "${freq}/scaling_cur_freq = $obsCur"
+            echo "${freq}/scaling_min_freq = $obsMin"
+            echo "${freq}/scaling_max_freq = $obsMax"
+            exit -1
+        fi
+    done
+}
+
 # Find the min or max (little vs big) of CPU max frequency, and lock cores of the selected type to
 # an available frequency that's >= $CPU_TARGET_FREQ_PERCENT% of max. Disable other cores.
 function_lock_cpu() {
@@ -116,11 +179,18 @@
     # Options to make clock locking on go devices more sticky.
     function_setup_go
 
-    # Find max CPU freq, and associated list of available freqs
-    cpuIdealFreq=$CPU_IDEAL_START_FREQ_KHZ
-    cpuAvailFreqCmpr=0
-    cpuAvailFreq=0
+    # Find max (or min) CPU freq, and associated list of available freqs
     enableIndices=''
+    cpuIdealFreq=$CPU_IDEAL_START_FREQ_KHZ
+    cpuIdealAvailFreqCmpr=0
+    cpuIdealAvailFreq=0
+
+    # While iterating, also capture 2nd best group of cores, to be enabled if needed
+    maybeEnableIndices=''
+    cpuMaybeFreq=$CPU_IDEAL_START_FREQ_KHZ
+    cpuMaybeAvailFreqCmpr=0
+    cpuMaybeAvailFreq=0
+
     disableIndices=''
     cpu=0
 
@@ -131,7 +201,6 @@
     # "cpu#" instead of cpu#/online or cpu#/cpufreq directly, since they may
     # not be accessible yet.
     while [ -d ${CPU_BASE}/cpu${cpu} ]; do
-
         # Try to enable core, so we can find its frequencies.
         # Note: In cases where the online file is inaccessible, it represents a
         # core which cannot be turned off, so we simply assume it is enabled if
@@ -148,19 +217,43 @@
         availFreqCmpr=${availFreq// /-}
 
         if (function_core_check $maxFreq $cpuIdealFreq); then
-            # new min/max of max freq, look for cpus with same max freq and same avail freq list
-            cpuIdealFreq=${maxFreq}
-            cpuAvailFreq=${availFreq}
-            cpuAvailFreqCmpr=${availFreqCmpr}
-
+            # New ideal freq
+            ### demote maybeEnable to disabled
             if [ -z "$disableIndices" ]; then
-                disableIndices="$enableIndices"
+                disableIndices="$maybeEnableIndices"
             else
-                disableIndices="$disableIndices $enableIndices"
+                disableIndices="$maybeEnableIndices $disableIndices"
             fi
+
+            ### demote enabled to maybeEnabled
+            maybeEnableIndices="$enableIndices"
+            cpuMaybeFreq=${cpuIdealFreq}
+            cpuMaybeAvailFreq=${cpuIdealAvailFreq}
+            cpuMaybeAvailFreqCmpr=${cpuIdealAvailFreqCmpr}
+
+            ### new min/max of max freq, look for cpus with same max freq and same avail freq list
             enableIndices=${cpu}
-        elif [ ${maxFreq} == ${cpuIdealFreq} ] && [ ${availFreqCmpr} == ${cpuAvailFreqCmpr} ]; then
+            cpuIdealFreq=${maxFreq}
+            cpuIdealAvailFreq=${availFreq}
+            cpuIdealAvailFreqCmpr=${availFreqCmpr}
+        elif [ ${maxFreq} == ${cpuIdealFreq} ] && [ ${availFreqCmpr} == ${cpuIdealAvailFreqCmpr} ]; then
             enableIndices="$enableIndices $cpu"
+        elif (function_core_check $maxFreq $cpuMaybeFreq); then
+            # New secondary ideal freq
+            ### demote maybeEnable to disabled
+            if [ -z "$disableIndices" ]; then
+                disableIndices="$maybeEnableIndices"
+            else
+                disableIndices="$disableIndices $maybeEnableIndices"
+            fi
+
+            ### new second best set cpu
+            maybeEnableIndices=${cpu}
+            cpuMaybeFreq=${maxFreq}
+            cpuMaybeAvailFreq=${availFreq}
+            cpuMaybeAvailFreqCmpr=${availFreqCmpr}
+        elif [ ${maxFreq} == ${cpuMaybeFreq} ] && [ ${availFreqCmpr} == ${cpuMaybeAvailFreqCmpr} ]; then
+            maybeEnableIndices="$maybeEnableIndices $cpu"
         else
             if [ -z "$disableIndices" ]; then
                 disableIndices="$cpu"
@@ -172,27 +265,32 @@
         cpu=$(($cpu + 1))
     done
 
+    # Clear maybeEnableIndices if they're not needed (already have >1 core enabled)
+    enableCount=0
+    for cpu in ${enableIndices}; do
+        enableCount=$(($enableCount + 1))
+    done
+    if [ "$enableCount" -gt 1 ]; then
+        # more than one core would be enabled without maybe list, demote
+        # maybeEnable to disabled
+        if [ -z "$disableIndices" ]; then
+            disableIndices="$maybeEnableIndices"
+        else
+            disableIndices="$maybeEnableIndices $disableIndices"
+        fi
+        maybeEnableIndices=""
+    fi
+
     # check that some CPUs will be enabled
     if [ -z "$enableIndices" ]; then
         echo "Failed to find any $ARG_CORES cores to enable, aborting."
         exit -1
     fi
 
-    # Chose a frequency to lock to that's >= $CPU_TARGET_FREQ_PERCENT% of max
-    # (below, 100M = 1K for KHz->MHz * 100 for %)
-    TARGET_FREQ_MHZ=$(( ($cpuIdealFreq * $CPU_TARGET_FREQ_PERCENT) / 100000 ))
-    chosenFreq=0
-    chosenFreqDiff=100000000
-    for freq in ${cpuAvailFreq}; do
-        freqMhz=$(( ${freq} / 1000 ))
-        if [ ${freqMhz} -ge ${TARGET_FREQ_MHZ} ]; then
-            newChosenFreqDiff=$(( $freq - $TARGET_FREQ_MHZ ))
-            if [ $newChosenFreqDiff -lt $chosenFreqDiff ]; then
-                chosenFreq=${freq}
-                chosenFreqDiff=$(( $chosenFreq - $TARGET_FREQ_MHZ ))
-            fi
-        fi
-    done
+    chosenIdealFreq=`function_find_target_mhz "$cpuIdealFreq" "$cpuIdealAvailFreq"`
+    if [ -n "$maybeEnableIndices" ]; then
+        chosenMaybeFreq=`function_find_target_mhz "$cpuMaybeFreq" "$cpuMaybeAvailFreq"`
+    fi
 
     # Lock wembley clocks using high-priority op code method.
     # This block depends on the shell utility awk, which is only available on API 27+
@@ -205,7 +303,7 @@
                 echo "${line:1:${#line}-2}"
             done`
 
-        # Compute the closest available frequency to the desired frequency, $chosenFreq.
+        # Compute the closest available frequency to the desired frequency, $chosenIdealFreq.
         # This assumes the op codes listen in /proc/cpufreq/MT_CPU_DVFS_LL/cpufreq_oppidx are listed
         # in order and 0-indexed.
         opCode=-1
@@ -214,9 +312,9 @@
         for currOpFreq in $AVAIL_OP_FREQS; do
             currOpCode=$((currOpCode + 1))
 
-            prevDiff=$((chosenFreq-opFreq))
+            prevDiff=$((chosenIdealFreq-opFreq))
             prevDiff=`function_abs $prevDiff`
-            currDiff=$((chosenFreq-currOpFreq))
+            currDiff=$((chosenIdealFreq-currOpFreq))
             currDiff=`function_abs $currDiff`
             if [ $currDiff -lt $prevDiff ]; then
                 opCode="$currOpCode"
@@ -227,38 +325,11 @@
         echo "$opCode" > /proc/ppm/policy/ut_fix_freq_idx
     fi
 
-    # enable 'big' CPUs
-    for cpu in ${enableIndices}; do
-        freq=${CPU_BASE}/cpu$cpu/cpufreq
+    function_enable_cpus "$enableIndices" "$chosenIdealFreq"
 
-        # Try to enable core, so we can find its frequencies.
-        # Note: In cases where the online file is inaccessible, it represents a
-        # core which cannot be turned off, so we simply assume it is enabled if
-        # this command fails.
-        if [ -f "$CPU_BASE/cpu$cpu/online" ]; then
-            echo 1 > ${CPU_BASE}/cpu${cpu}/online || true
-        fi
-
-        # scaling_max_freq must be set before scaling_min_freq
-        echo ${chosenFreq} > ${freq}/scaling_max_freq
-        echo ${chosenFreq} > ${freq}/scaling_min_freq
-        echo ${chosenFreq} > ${freq}/scaling_setspeed
-
-        # Give system a bit of time to propagate the change to scaling_setspeed.
-        sleep 0.1
-
-        # validate setting the freq worked
-        obsCur=`cat ${freq}/scaling_cur_freq`
-        obsMin=`cat ${freq}/scaling_min_freq`
-        obsMax=`cat ${freq}/scaling_max_freq`
-        if [ "$obsCur" -ne "$chosenFreq" ] || [ "$obsMin" -ne "$chosenFreq" ] || [ "$obsMax" -ne "$chosenFreq" ]; then
-            echo "Failed to set CPU$cpu to $chosenFreq Hz! Aborting..."
-            echo "scaling_cur_freq = $obsCur"
-            echo "scaling_min_freq = $obsMin"
-            echo "scaling_max_freq = $obsMax"
-            exit -1
-        fi
-    done
+    if [ -n "$maybeEnableIndices" ]; then
+        function_enable_cpus "$maybeEnableIndices" "$chosenMaybeFreq"
+    fi
 
     # disable other CPUs (Note: important to enable big cores first!)
     for cpu in ${disableIndices}; do
@@ -266,7 +337,10 @@
     done
 
     echo "=================================="
-    echo "Locked CPUs ${enableIndices// /,} to $chosenFreq / $cpuIdealFreq KHz"
+    echo "Locked CPUs ${enableIndices// /,} to $chosenIdealFreq / $cpuIdealFreq KHz"
+    if [ -n "$maybeEnableIndices" ]; then
+        echo "Locked CPUs ${maybeEnableIndices// /,} to $chosenMaybeFreq / $cpuMaybeFreq KHz"
+    fi
     echo "Disabled CPUs ${disableIndices// /,}"
     echo "=================================="
 }
diff --git a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
index 8229714..9a9791e 100644
--- a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
@@ -27,7 +27,6 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.UiDevice
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -35,7 +34,7 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @OptIn(ExperimentalMetricApi::class)
-class AudioUnderrunBenchmark() {
+class AudioUnderrunBenchmark {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
@@ -48,7 +47,6 @@
     }
 
     @Test
-    @Ignore("b/297916125")
     fun start() {
         benchmarkRule.measureRepeated(
             packageName = PACKAGE_NAME,
diff --git a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt
index c252b60..cfaa555 100644
--- a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt
@@ -29,6 +29,7 @@
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.Assume.assumeTrue
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -42,6 +43,7 @@
     private val filterRegex = "^.*L${PACKAGE_NAME.replace(".", "/")}".toRegex()
 
     @Test
+    @Ignore("b/294123161")
     fun appNotInstalled() {
         val error = assertFailsWith<AssertionError> {
             baselineRule.collect(
@@ -56,6 +58,7 @@
     }
 
     @Test
+    @Ignore("b/294123161")
     fun filter() {
         // TODO: share this 'is supported' check with the one inside BaselineProfileRule, once this
         //  test class is moved out of integration-tests, into benchmark-macro-junit4
@@ -90,6 +93,7 @@
     }
 
     @Test
+    @Ignore("b/294123161")
     fun profileType() {
         assumeTrue(Build.VERSION.SDK_INT >= 33 || Shell.isSessionRooted())
 
diff --git a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricAdvertiseTest.kt b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricAdvertiseTest.kt
index 34276ac..9d96b85 100644
--- a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricAdvertiseTest.kt
+++ b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricAdvertiseTest.kt
@@ -18,10 +18,9 @@
 
 import android.content.Context
 import androidx.bluetooth.AdvertiseParams
-import androidx.bluetooth.AdvertiseResult
 import androidx.bluetooth.BluetoothLe
 import java.util.UUID
-import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.cancel
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert
@@ -40,8 +39,10 @@
     fun advertiseSuccess() = runTest {
         val params = AdvertiseParams()
         launch {
-            val result = bluetoothLe.advertise(params).first()
-            Assert.assertEquals(AdvertiseResult.ADVERTISE_STARTED, result)
+            bluetoothLe.advertise(params) { result ->
+                Assert.assertEquals(BluetoothLe.ADVERTISE_STARTED, result)
+                cancel()
+            }
         }
     }
 
@@ -59,8 +60,9 @@
         )
 
         launch {
-            val result = bluetoothLe.advertise(advertiseParams).first()
-            Assert.assertEquals(AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE, result)
+            bluetoothLe.advertise(advertiseParams) { result ->
+                Assert.assertEquals(BluetoothLe.ADVERTISE_FAILED_DATA_TOO_LARGE, result)
+            }
         }
     }
 }
diff --git a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattClientTest.kt b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattClientTest.kt
index 7bab0cd..09b12f3 100644
--- a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattClientTest.kt
+++ b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattClientTest.kt
@@ -28,18 +28,24 @@
 import android.bluetooth.BluetoothGattService as FwkService
 import android.bluetooth.BluetoothManager
 import android.content.Context
+import android.os.Build
 import androidx.bluetooth.BluetoothDevice
 import androidx.bluetooth.BluetoothLe
 import androidx.bluetooth.GattClient
 import java.util.UUID
 import java.util.concurrent.atomic.AtomicInteger
 import junit.framework.TestCase.fail
-import kotlinx.coroutines.CompletableDeferred
+import kotlin.coroutines.cancellation.CancellationException
+import kotlin.test.assertFailsWith
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -105,33 +111,49 @@
     @Test
     fun connectGatt() = runTest {
         val device = createDevice("00:11:22:33:44:55")
-        val closed = CompletableDeferred<Unit>()
 
         acceptConnect()
 
-        Assert.assertEquals(true, bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
-            sampleServices.forEachIndexed { index, service ->
-                Assert.assertEquals(service.uuid, getServices()[index].uuid)
-            }
-            awaitClose { closed.complete(Unit) }
-            true
-        }.getOrNull())
+        bluetoothLe.connectGatt(device) {
+            assertTrue(clientAdapter.shadowBluetoothGatt.isConnected)
 
-        Assert.assertTrue(closed.isCompleted)
+            Assert.assertEquals(sampleServices.size, services.size)
+            sampleServices.forEachIndexed { index, service ->
+                Assert.assertEquals(service.uuid, services[index].uuid)
+            }
+        }
+
+        assertTrue(clientAdapter.shadowBluetoothGatt.isClosed)
+        assertFalse(clientAdapter.shadowBluetoothGatt.isConnected)
+    }
+
+    @Test
+    fun connectGatt_throwException_closeGatt() = runTest {
+        val device = createDevice("00:11:22:33:44:55")
+
+        acceptConnect()
+
+        assertFailsWith<RuntimeException> {
+            bluetoothLe.connectGatt(device) {
+                assertTrue(clientAdapter.shadowBluetoothGatt.isConnected)
+                throw RuntimeException()
+            }
+        }
+
+        assertTrue(clientAdapter.shadowBluetoothGatt.isClosed)
+        assertFalse(clientAdapter.shadowBluetoothGatt.isConnected)
     }
 
     @Test
     fun connectFail() = runTest {
         val device = createDevice("00:11:22:33:44:55")
         rejectConnect()
-        Assert.assertEquals(true, bluetoothLe.connectGatt(device) { true }.isFailure)
+        assertFailsWith<CancellationException> { bluetoothLe.connectGatt(device) { } }
     }
 
     @Test
     fun readCharacteristic() = runTest {
         val testValue = 48
-        val closed = CompletableDeferred<Unit>()
         val device = createDevice("00:11:22:33:44:55")
         acceptConnect()
 
@@ -150,16 +172,14 @@
         }
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
+            Assert.assertEquals(sampleServices.size, services.size)
             Assert.assertEquals(testValue,
                 readCharacteristic(
-                    getServices()[0].getCharacteristic(readCharUuid)!!
+                    services[0].getCharacteristic(readCharUuid)!!
                 ).getOrNull()?.toInt())
-            awaitClose {
-                closed.complete(Unit)
-            }
         }
-        Assert.assertTrue(closed.isCompleted)
+        assertTrue(clientAdapter.shadowBluetoothGatt.isClosed)
+        assertFalse(clientAdapter.shadowBluetoothGatt.isConnected)
     }
 
     @Test
@@ -174,10 +194,10 @@
             }
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
-            Assert.assertTrue(
+            Assert.assertEquals(sampleServices.size, services.size)
+            assertTrue(
                 readCharacteristic(
-                    getServices()[0].getCharacteristic(noPropertyCharUuid)!!
+                    services[0].getCharacteristic(noPropertyCharUuid)!!
                 ).exceptionOrNull()
                 is IllegalArgumentException)
         }
@@ -187,7 +207,6 @@
     fun writeCharacteristic() = runTest {
         val initialValue = 48
         val valueToWrite = 96
-        val closed = CompletableDeferred<Unit>()
         val device = createDevice("00:11:22:33:44:55")
         val currentValue = AtomicInteger(initialValue)
 
@@ -219,8 +238,8 @@
         }
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
-            val characteristic = getServices()[0].getCharacteristic(writeCharUuid)!!
+            Assert.assertEquals(sampleServices.size, services.size)
+            val characteristic = services[0].getCharacteristic(writeCharUuid)!!
 
             Assert.assertEquals(initialValue,
                 readCharacteristic(characteristic).getOrNull()?.toInt())
@@ -228,11 +247,9 @@
                 valueToWrite.toByteArray())
             Assert.assertEquals(valueToWrite,
                 readCharacteristic(characteristic).getOrNull()?.toInt())
-            awaitClose {
-                closed.complete(Unit)
-            }
         }
-        Assert.assertTrue(closed.isCompleted)
+        assertTrue(clientAdapter.shadowBluetoothGatt.isClosed)
+        assertFalse(clientAdapter.shadowBluetoothGatt.isConnected)
     }
 
     @Test
@@ -247,10 +264,10 @@
             }
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
-            Assert.assertTrue(
+            Assert.assertEquals(sampleServices.size, services.size)
+            assertTrue(
                 writeCharacteristic(
-                    getServices()[0].getCharacteristic(readCharUuid)!!,
+                    services[0].getCharacteristic(readCharUuid)!!,
                     48.toByteArray()
                 ).exceptionOrNull()
                 is IllegalArgumentException)
@@ -261,7 +278,6 @@
     fun subscribeToCharacteristic() = runTest {
         val initialValue = 48
         val valueToNotify = 96
-        val closed = CompletableDeferred<Unit>()
         val device = createDevice("00:11:22:33:44:55")
         val currentValue = AtomicInteger(initialValue)
 
@@ -295,8 +311,8 @@
         }
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
-            val characteristic = getServices()[0].getCharacteristic(notifyCharUuid)!!
+            Assert.assertEquals(sampleServices.size, services.size)
+            val characteristic = services[0].getCharacteristic(notifyCharUuid)!!
 
             Assert.assertEquals(initialValue,
                 readCharacteristic(characteristic).getOrNull()?.toInt())
@@ -305,11 +321,9 @@
                 subscribeToCharacteristic(characteristic).first().toInt())
             Assert.assertEquals(valueToNotify,
                 readCharacteristic(characteristic).getOrNull()?.toInt())
-            awaitClose {
-                closed.complete(Unit)
-            }
         }
-        Assert.assertTrue(closed.isCompleted)
+        assertTrue(clientAdapter.shadowBluetoothGatt.isClosed)
+        assertFalse(clientAdapter.shadowBluetoothGatt.isConnected)
     }
 
     @Test
@@ -324,9 +338,9 @@
             }
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
+            Assert.assertEquals(sampleServices.size, services.size)
             subscribeToCharacteristic(
-                getServices()[0].getCharacteristic(readCharUuid)!!,
+                services[0].getCharacteristic(readCharUuid)!!,
             ).collect {
                 // Should not be notified
                 fail()
@@ -334,36 +348,69 @@
         }
     }
 
-    private fun acceptConnect() {
-        clientAdapter.onConnectListener =
-            StubClientFrameworkAdapter.OnConnectListener { device, _ ->
-            shadowOf(device).simulateGattConnectionChange(
-                BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED
-            )
-            true
-        }
+    @Test
+    fun servicesFlow_emittedWhenServicesChange() = runTest {
+        val device = createDevice("00:11:22:33:44:55")
 
-        clientAdapter.onRequestMtuListener =
-            StubClientFrameworkAdapter.OnRequestMtuListener { mtu ->
-            clientAdapter.callback?.onMtuChanged(clientAdapter.bluetoothGatt, mtu,
-                BluetoothGatt.GATT_SUCCESS)
-        }
+        val newServiceUuid = UUID.randomUUID()
+        val newService = FwkService(newServiceUuid, FwkService.SERVICE_TYPE_PRIMARY)
+        val newServices = sampleServices + newService
+
+        acceptConnect()
 
         clientAdapter.onDiscoverServicesListener =
             StubClientFrameworkAdapter.OnDiscoverServicesListener {
-            clientAdapter.gattServices = sampleServices
-            clientAdapter.callback?.onServicesDiscovered(clientAdapter.bluetoothGatt,
-                BluetoothGatt.GATT_SUCCESS)
+                if (clientAdapter.gattServices.isEmpty()) {
+                    clientAdapter.gattServices = sampleServices
+                }
+                clientAdapter.callback?.onServicesDiscovered(
+                    clientAdapter.bluetoothGatt,
+                    BluetoothGatt.GATT_SUCCESS
+                )
+            }
+
+        bluetoothLe.connectGatt(device) {
+            launch {
+                clientAdapter.gattServices = newServices
+                if (Build.VERSION.SDK_INT >= 31) {
+                    clientAdapter.callback?.onServiceChanged(clientAdapter.bluetoothGatt!!)
+                }
+            }
+            val servicesEmitted = servicesFlow.take(2).toList()
+            Assert.assertEquals(sampleServices.size, servicesEmitted[0].size)
+            Assert.assertEquals(sampleServices.size + 1, servicesEmitted[1].size)
+            Assert.assertEquals(newServiceUuid, servicesEmitted[1][sampleServices.size].uuid)
         }
     }
 
+    private fun acceptConnect() {
+        clientAdapter.onConnectListener =
+            StubClientFrameworkAdapter.OnConnectListener { device, _ ->
+                clientAdapter.shadowBluetoothGatt.notifyConnection(device.address)
+                true
+            }
+
+        clientAdapter.onRequestMtuListener =
+            StubClientFrameworkAdapter.OnRequestMtuListener { mtu ->
+                clientAdapter.callback?.onMtuChanged(clientAdapter.bluetoothGatt, mtu,
+                    BluetoothGatt.GATT_SUCCESS)
+            }
+
+        clientAdapter.onDiscoverServicesListener =
+            StubClientFrameworkAdapter.OnDiscoverServicesListener {
+                clientAdapter.gattServices = sampleServices
+                clientAdapter.callback?.onServicesDiscovered(clientAdapter.bluetoothGatt,
+                    BluetoothGatt.GATT_SUCCESS)
+            }
+    }
+
     private fun rejectConnect() {
         clientAdapter.onConnectListener =
             StubClientFrameworkAdapter.OnConnectListener { device, _ ->
-            shadowOf(device).simulateGattConnectionChange(
-                BluetoothGatt.GATT_FAILURE, BluetoothGatt.STATE_DISCONNECTED
-            )
-            false
+                shadowOf(device).simulateGattConnectionChange(
+                    BluetoothGatt.GATT_FAILURE, BluetoothGatt.STATE_DISCONNECTED
+                )
+                false
         }
     }
 
@@ -446,6 +493,10 @@
                 ?.onSetCharacteristicNotification(characteristic, enable)
         }
 
+        override fun closeGatt() {
+            baseAdapter.closeGatt()
+        }
+
         fun interface OnConnectListener {
             fun onConnect(device: FwkDevice, callback: BluetoothGattCallback): Boolean
         }
diff --git a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattServerTest.kt b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattServerTest.kt
index fea074b..14f8951 100644
--- a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattServerTest.kt
+++ b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattServerTest.kt
@@ -18,6 +18,7 @@
 
 import android.bluetooth.BluetoothAdapter
 import android.bluetooth.BluetoothDevice as FwkDevice
+import android.bluetooth.BluetoothGatt.GATT_SUCCESS
 import android.bluetooth.BluetoothGattCharacteristic as FwkCharacteristic
 import android.bluetooth.BluetoothGattServer
 import android.bluetooth.BluetoothGattServerCallback
@@ -41,6 +42,7 @@
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert
+import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -110,11 +112,11 @@
             }
 
         bluetoothLe.openGattServer(listOf()) {
-            connectRequest.first().accept {}
+            connectRequests.first().accept {}
         }
 
-        Assert.assertTrue(opened.isCompleted)
-        Assert.assertTrue(closed.isCompleted)
+        assertTrue(opened.isCompleted)
+        assertTrue(closed.isCompleted)
     }
 
     @Test
@@ -136,7 +138,7 @@
 
         launch {
             bluetoothLe.openGattServer(services) {
-                connectRequest.collect {
+                connectRequests.collect {
                     it.reject()
                     Assert.assertThrows(IllegalStateException::class.java) {
                         runBlocking {
@@ -148,7 +150,7 @@
             }
         }.join()
 
-        Assert.assertTrue(closed.isCompleted)
+        assertTrue(closed.isCompleted)
         Assert.assertEquals(0, serverAdapter.shadowGattServer.responses.size)
     }
 
@@ -171,7 +173,7 @@
 
         launch {
             bluetoothLe.openGattServer(services) {
-                connectRequest.collect {
+                connectRequests.collect {
                     it.accept {}
                     Assert.assertThrows(IllegalStateException::class.java) {
                         it.reject()
@@ -181,7 +183,7 @@
             }
         }.join()
 
-        Assert.assertTrue(closed.isCompleted)
+        assertTrue(closed.isCompleted)
         Assert.assertEquals(0, serverAdapter.shadowGattServer.responses.size)
     }
 
@@ -205,11 +207,11 @@
 
         launch {
             bluetoothLe.openGattServer(services) {
-                connectRequest.collect {
+                connectRequests.collect {
                     it.accept {
                         when (val request = requests.first()) {
-                            is GattServerRequest.ReadCharacteristicRequest -> {
-                                request.sendResponse(true, valueToRead.toByteArray())
+                            is GattServerRequest.ReadCharacteristic -> {
+                                request.sendResponse(valueToRead.toByteArray())
                             }
                             else -> fail("unexpected request")
                         }
@@ -221,12 +223,59 @@
         }.join()
 
         // Ensure if the server is closed
-        Assert.assertTrue(closed.isCompleted)
+        assertTrue(closed.isCompleted)
         Assert.assertEquals(1, serverAdapter.shadowGattServer.responses.size)
         Assert.assertEquals(valueToRead, serverAdapter.shadowGattServer.responses[0].toInt())
     }
 
     @Test
+    fun readCharacteristic_sendFailure() = runTest {
+        val services = listOf(service1, service2)
+        val device = createDevice("00:11:22:33:44:55")
+        val closed = CompletableDeferred<Unit>()
+        val responsed = CompletableDeferred<Unit>()
+
+        runAfterServicesAreAdded(services.size) {
+            connectDevice(device) {
+                serverAdapter.callback.onCharacteristicReadRequest(
+                    device, /*requestId=*/1, /*offset=*/0, readCharacteristic.fwkCharacteristic)
+            }
+        }
+        serverAdapter.onCloseGattServerListener =
+            StubServerFrameworkAdapter.OnCloseGattServerListener {
+                closed.complete(Unit)
+            }
+        serverAdapter.onSendResponseListener =
+            StubServerFrameworkAdapter.OnSendResponseListener { _, requestId, status, _, value ->
+                Assert.assertEquals(1, requestId)
+                Assert.assertNotEquals(GATT_SUCCESS, status)
+                Assert.assertNull(value)
+                responsed.complete(Unit)
+            }
+
+        launch {
+            bluetoothLe.openGattServer(services) {
+                connectRequests.collect {
+                    it.accept {
+                        when (val request = requests.first()) {
+                            is GattServerRequest.ReadCharacteristic -> {
+                                request.sendFailure()
+                            }
+                            else -> fail("unexpected request")
+                        }
+                        // Close the server
+                        this@launch.cancel()
+                    }
+                }
+            }
+        }.join()
+
+        // Ensure if the server is closed
+        assertTrue(closed.isCompleted)
+        assertTrue(responsed.isCompleted)
+    }
+
+    @Test
     fun readUnknownCharacteristic_failsWithoutNotified() = runTest {
         val services = listOf(service1, service2)
         val device = createDevice("00:11:22:33:44:55")
@@ -248,12 +297,12 @@
 
         launch {
             bluetoothLe.openGattServer(services) {
-                connectRequest.collect {
+                connectRequests.collect {
                     it.accept {
                         when (val request = requests.first()) {
-                            is GattServerRequest.ReadCharacteristicRequest -> {
+                            is GattServerRequest.ReadCharacteristic -> {
                                 Assert.assertEquals(readCharacteristic, request.characteristic)
-                                request.sendResponse(true, valueToRead.toByteArray())
+                                request.sendResponse(valueToRead.toByteArray())
                             }
 
                             else -> fail("unexpected request")
@@ -265,7 +314,7 @@
             }
         }.join()
 
-        Assert.assertTrue(closed.isCompleted)
+        assertTrue(closed.isCompleted)
     }
     @Test
     fun writeCharacteristic() = runTest {
@@ -289,12 +338,12 @@
 
         launch {
             bluetoothLe.openGattServer(services) {
-                connectRequest.collect {
+                connectRequests.collect {
                     it.accept {
                         when (val request = requests.first()) {
-                            is GattServerRequest.WriteCharacteristicRequest -> {
-                                Assert.assertEquals(valueToWrite, request.value?.toInt())
-                                request.sendResponse(true, request.value)
+                            is GattServerRequest.WriteCharacteristics -> {
+                                Assert.assertEquals(valueToWrite, request.parts[0].value.toInt())
+                                request.sendResponse()
                             }
 
                             else -> fail("unexpected request")
@@ -306,9 +355,58 @@
             }
         }.join()
 
-        Assert.assertTrue(closed.isCompleted)
-        Assert.assertEquals(1, serverAdapter.shadowGattServer.responses.size)
-        Assert.assertEquals(valueToWrite, serverAdapter.shadowGattServer.responses[0].toInt())
+        assertTrue(closed.isCompleted)
+    }
+
+    @Test
+    fun writeCharacteristic_sendFailure() = runTest {
+        val services = listOf(service1, service2)
+        val device = createDevice("00:11:22:33:44:55")
+        val closed = CompletableDeferred<Unit>()
+        val responded = CompletableDeferred<Unit>()
+        val valueToWrite = 42
+
+        runAfterServicesAreAdded(services.size) {
+            connectDevice(device) {
+                serverAdapter.callback.onCharacteristicWriteRequest(
+                    device, /*requestId=*/1, writeCharacteristic.fwkCharacteristic,
+                    /*preparedWrite=*/false, /*responseNeeded=*/false,
+                    /*offset=*/0, valueToWrite.toByteArray()
+                )
+            }
+        }
+        serverAdapter.onCloseGattServerListener =
+            StubServerFrameworkAdapter.OnCloseGattServerListener {
+                closed.complete(Unit)
+            }
+        serverAdapter.onSendResponseListener =
+            StubServerFrameworkAdapter.OnSendResponseListener { _, requestId, status, _, value ->
+                Assert.assertEquals(1, requestId)
+                Assert.assertNotEquals(GATT_SUCCESS, status)
+                Assert.assertNull(value)
+                responded.complete(Unit)
+            }
+
+        launch {
+            bluetoothLe.openGattServer(services) {
+                connectRequests.collect {
+                    it.accept {
+                        when (val request = requests.first()) {
+                            is GattServerRequest.WriteCharacteristics -> {
+                                Assert.assertEquals(valueToWrite, request.parts[0].value.toInt())
+                                request.sendFailure()
+                            }
+
+                            else -> fail("unexpected request")
+                        }
+                        // Close the server
+                        this@launch.cancel()
+                    }
+                }
+            }
+        }.join()
+
+        assertTrue(closed.isCompleted)
     }
 
     @Test
@@ -327,8 +425,9 @@
         }
         serverAdapter.onNotifyCharacteristicChangedListener =
             StubServerFrameworkAdapter.OnNotifyCharacteristicChangedListener {
-                    _, _, _, value ->
+                    fwkDevice, _, _, value ->
                 notified.complete(value.toInt())
+                serverAdapter.callback.onNotificationSent(fwkDevice, GATT_SUCCESS)
             }
         serverAdapter.onCloseGattServerListener =
             StubServerFrameworkAdapter.OnCloseGattServerListener {
@@ -337,9 +436,9 @@
 
         launch {
             bluetoothLe.openGattServer(services) {
-                connectRequest.collect {
+                connectRequests.collect {
                     it.accept {
-                        notify(notifyCharacteristic, valueToNotify.toByteArray())
+                        assertTrue(notify(notifyCharacteristic, valueToNotify.toByteArray()))
                         // Close the server
                         this@launch.cancel()
                     }
@@ -348,7 +447,7 @@
         }.join()
 
         // Ensure if the server is closed
-        Assert.assertTrue(closed.isCompleted)
+        assertTrue(closed.isCompleted)
         Assert.assertEquals(valueToNotify, notified.await())
     }
 
@@ -370,11 +469,62 @@
         launch {
             bluetoothLe.openGattServer(listOf(service1)) {
                 updateServices(listOf(service2))
-                connectRequest.first().accept {}
+                connectRequests.first().accept {}
             }
         }.join()
 
-        Assert.assertTrue(opened.isCompleted)
+        assertTrue(opened.isCompleted)
+        assertTrue(closed.isCompleted)
+    }
+
+    @Test
+    fun writeLongCharacteristic() = runTest {
+        val services = listOf(service1, service2)
+        val device = createDevice("00:11:22:33:44:55")
+        val closed = CompletableDeferred<Unit>()
+        val values = listOf(byteArrayOf(0, 1), byteArrayOf(2, 3))
+
+        runAfterServicesAreAdded(services.size) {
+            connectDevice(device) {
+                var offset = 0
+                values.forEachIndexed { index, value ->
+                    serverAdapter.callback.onCharacteristicWriteRequest(
+                        device, /*requestId=*/index + 1, writeCharacteristic.fwkCharacteristic,
+                        /*preparedWrite=*/true, /*responseNeeded=*/false,
+                        offset, value
+                    )
+                    offset += value.size
+                }
+                serverAdapter.callback.onExecuteWrite(device, /*requestId=*/values.size + 1, true)
+            }
+        }
+        serverAdapter.onCloseGattServerListener =
+            StubServerFrameworkAdapter.OnCloseGattServerListener {
+                closed.complete(Unit)
+            }
+
+        launch {
+            bluetoothLe.openGattServer(services) {
+                connectRequests.collect {
+                    it.accept {
+                        when (val request = requests.first()) {
+                            is GattServerRequest.WriteCharacteristics -> {
+                                Assert.assertEquals(values.size, request.parts.size)
+                                values.forEachIndexed { index, value ->
+                                    Assert.assertEquals(value, request.parts[index].value)
+                                }
+                                request.sendResponse()
+                            }
+
+                            else -> fail("unexpected request")
+                        }
+                        // Close the server
+                        this@launch.cancel()
+                    }
+                }
+            }
+        }.join()
+
         Assert.assertTrue(closed.isCompleted)
     }
 
@@ -413,6 +563,7 @@
         var onCloseGattServerListener: OnCloseGattServerListener? = null
         var onAddServiceListener: OnAddServiceListener? = null
         var onNotifyCharacteristicChangedListener: OnNotifyCharacteristicChangedListener? = null
+        var onSendResponseListener: OnSendResponseListener? = null
 
         override fun openGattServer(context: Context, callback: BluetoothGattServerCallback) {
             baseAdapter.openGattServer(context, callback)
@@ -452,6 +603,8 @@
             value: ByteArray?
         ) {
             baseAdapter.sendResponse(device, requestId, status, offset, value)
+            onSendResponseListener
+                ?.onSendResponse(device, requestId, status, offset, value)
         }
 
         fun interface OnOpenGattServerListener {
@@ -463,6 +616,15 @@
         fun interface OnCloseGattServerListener {
             fun onCloseGattServer()
         }
+        fun interface OnSendResponseListener {
+            fun onSendResponse(
+                device: FwkDevice,
+                requestId: Int,
+                status: Int,
+                offset: Int,
+                value: ByteArray?
+            )
+        }
         fun interface OnNotifyCharacteristicChangedListener {
             fun onNotifyCharacteristicChanged(
                 device: FwkDevice,
diff --git a/bluetooth/bluetooth/api/current.txt b/bluetooth/bluetooth/api/current.txt
index 2bfa8e7..6ad9af9 100644
--- a/bluetooth/bluetooth/api/current.txt
+++ b/bluetooth/bluetooth/api/current.txt
@@ -2,15 +2,16 @@
 package androidx.bluetooth {
 
   public final class AdvertiseParams {
-    ctor public AdvertiseParams(optional boolean shouldIncludeDeviceAddress, optional boolean shouldIncludeDeviceName, optional boolean isConnectable, optional boolean isDiscoverable, optional int timeoutMillis, optional java.util.Map<java.lang.Integer,byte[]> manufacturerData, optional java.util.Map<java.util.UUID,byte[]> serviceData, optional java.util.List<java.util.UUID> serviceUuids);
+    ctor public AdvertiseParams(optional boolean shouldIncludeDeviceAddress, optional boolean shouldIncludeDeviceName, optional boolean isConnectable, optional boolean isDiscoverable, optional @IntRange(from=0L, to=655350L) int durationMillis, optional java.util.Map<java.lang.Integer,byte[]> manufacturerData, optional java.util.Map<java.util.UUID,byte[]> serviceData, optional java.util.List<java.util.UUID> serviceUuids);
+    method public int getDurationMillis();
     method public java.util.Map<java.lang.Integer,byte[]> getManufacturerData();
     method public java.util.Map<java.util.UUID,byte[]> getServiceData();
     method public java.util.List<java.util.UUID> getServiceUuids();
     method public boolean getShouldIncludeDeviceAddress();
     method public boolean getShouldIncludeDeviceName();
-    method public int getTimeoutMillis();
     method public boolean isConnectable();
     method public boolean isDiscoverable();
+    property public final int durationMillis;
     property public final boolean isConnectable;
     property public final boolean isDiscoverable;
     property public final java.util.Map<java.lang.Integer,byte[]> manufacturerData;
@@ -18,27 +19,12 @@
     property public final java.util.List<java.util.UUID> serviceUuids;
     property public final boolean shouldIncludeDeviceAddress;
     property public final boolean shouldIncludeDeviceName;
-    property public final int timeoutMillis;
-  }
-
-  public final class AdvertiseResult {
-    ctor public AdvertiseResult();
-    field public static final int ADVERTISE_FAILED_DATA_TOO_LARGE = 102; // 0x66
-    field public static final int ADVERTISE_FAILED_FEATURE_UNSUPPORTED = 103; // 0x67
-    field public static final int ADVERTISE_FAILED_INTERNAL_ERROR = 104; // 0x68
-    field public static final int ADVERTISE_FAILED_TOO_MANY_ADVERTISERS = 105; // 0x69
-    field public static final int ADVERTISE_STARTED = 101; // 0x65
-    field public static final androidx.bluetooth.AdvertiseResult.Companion Companion;
-  }
-
-  public static final class AdvertiseResult.Companion {
   }
 
   public final class BluetoothAddress {
     ctor public BluetoothAddress(String address, int addressType);
     method public String getAddress();
     method public int getAddressType();
-    method public void setAddressType(int);
     property public final String address;
     property public final int addressType;
     field public static final int ADDRESS_TYPE_PUBLIC = 0; // 0x0
@@ -63,19 +49,30 @@
 
   public final class BluetoothLe {
     ctor public BluetoothLe(android.content.Context context);
-    method @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE") public kotlinx.coroutines.flow.Flow<java.lang.Integer> advertise(androidx.bluetooth.AdvertiseParams advertiseParams);
-    method @RequiresPermission("android.permission.BLUETOOTH_CONNECT") public suspend <R> Object? connectGatt(androidx.bluetooth.BluetoothDevice device, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattClientScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super kotlin.Result<? extends R>>);
-    method public suspend <R> Object? openGattServer(java.util.List<androidx.bluetooth.GattService> services, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattServerConnectScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super kotlin.Result<? extends R>>);
+    method @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE") public suspend Object? advertise(androidx.bluetooth.AdvertiseParams advertiseParams, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>? block, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission("android.permission.BLUETOOTH_CONNECT") public suspend <R> Object? connectGatt(androidx.bluetooth.BluetoothDevice device, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattClientScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
+    method public suspend <R> Object? openGattServer(java.util.List<androidx.bluetooth.GattService> services, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattServerConnectScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
     method @RequiresPermission("android.permission.BLUETOOTH_SCAN") public kotlinx.coroutines.flow.Flow<androidx.bluetooth.ScanResult> scan(optional java.util.List<androidx.bluetooth.ScanFilter> filters);
+    field public static final int ADVERTISE_FAILED_DATA_TOO_LARGE = 102; // 0x66
+    field public static final int ADVERTISE_FAILED_FEATURE_UNSUPPORTED = 103; // 0x67
+    field public static final int ADVERTISE_FAILED_INTERNAL_ERROR = 104; // 0x68
+    field public static final int ADVERTISE_FAILED_TOO_MANY_ADVERTISERS = 105; // 0x69
+    field public static final int ADVERTISE_STARTED = 101; // 0x65
+    field public static final androidx.bluetooth.BluetoothLe.Companion Companion;
+  }
+
+  public static final class BluetoothLe.Companion {
   }
 
   public static interface BluetoothLe.GattClientScope {
-    method public suspend Object? awaitClose(kotlin.jvm.functions.Function0<kotlin.Unit> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public androidx.bluetooth.GattService? getService(java.util.UUID uuid);
-    method public java.util.List<androidx.bluetooth.GattService> getServices();
+    method public default java.util.List<androidx.bluetooth.GattService> getServices();
+    method public kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.bluetooth.GattService>> getServicesFlow();
     method public suspend Object? readCharacteristic(androidx.bluetooth.GattCharacteristic characteristic, kotlin.coroutines.Continuation<? super kotlin.Result<? extends byte[]>>);
     method public kotlinx.coroutines.flow.Flow<byte[]> subscribeToCharacteristic(androidx.bluetooth.GattCharacteristic characteristic);
     method public suspend Object? writeCharacteristic(androidx.bluetooth.GattCharacteristic characteristic, byte[] value, kotlin.coroutines.Continuation<? super kotlin.Result<? extends kotlin.Unit>>);
+    property public default java.util.List<androidx.bluetooth.GattService> services;
+    property public abstract kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.bluetooth.GattService>> servicesFlow;
   }
 
   public static final class BluetoothLe.GattServerConnectRequest {
@@ -86,15 +83,15 @@
   }
 
   public static interface BluetoothLe.GattServerConnectScope {
-    method public kotlinx.coroutines.flow.Flow<androidx.bluetooth.BluetoothLe.GattServerConnectRequest> getConnectRequest();
+    method public kotlinx.coroutines.flow.Flow<androidx.bluetooth.BluetoothLe.GattServerConnectRequest> getConnectRequests();
     method public void updateServices(java.util.List<androidx.bluetooth.GattService> services);
-    property public abstract kotlinx.coroutines.flow.Flow<androidx.bluetooth.BluetoothLe.GattServerConnectRequest> connectRequest;
+    property public abstract kotlinx.coroutines.flow.Flow<androidx.bluetooth.BluetoothLe.GattServerConnectRequest> connectRequests;
   }
 
   public static interface BluetoothLe.GattServerSessionScope {
     method public androidx.bluetooth.BluetoothDevice getDevice();
     method public kotlinx.coroutines.flow.Flow<androidx.bluetooth.GattServerRequest> getRequests();
-    method public void notify(androidx.bluetooth.GattCharacteristic characteristic, byte[] value);
+    method public suspend Object? notify(androidx.bluetooth.GattCharacteristic characteristic, byte[] value, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
     property public abstract androidx.bluetooth.BluetoothDevice device;
     property public abstract kotlinx.coroutines.flow.Flow<androidx.bluetooth.GattServerRequest> requests;
   }
@@ -119,21 +116,30 @@
   public static final class GattCharacteristic.Companion {
   }
 
-  public interface GattServerRequest {
+  public class GattServerRequest {
   }
 
-  public static final class GattServerRequest.ReadCharacteristicRequest implements androidx.bluetooth.GattServerRequest {
+  public static final class GattServerRequest.ReadCharacteristic extends androidx.bluetooth.GattServerRequest {
     method public androidx.bluetooth.GattCharacteristic getCharacteristic();
-    method public void sendResponse(boolean success, byte[]? value);
+    method public void sendFailure();
+    method public void sendResponse(byte[] value);
     property public final androidx.bluetooth.GattCharacteristic characteristic;
   }
 
-  public static final class GattServerRequest.WriteCharacteristicRequest implements androidx.bluetooth.GattServerRequest {
+  public static final class GattServerRequest.WriteCharacteristics extends androidx.bluetooth.GattServerRequest {
+    method public java.util.List<androidx.bluetooth.GattServerRequest.WriteCharacteristics.Part> getParts();
+    method public void sendFailure();
+    method public void sendResponse();
+    property public final java.util.List<androidx.bluetooth.GattServerRequest.WriteCharacteristics.Part> parts;
+  }
+
+  public static final class GattServerRequest.WriteCharacteristics.Part {
     method public androidx.bluetooth.GattCharacteristic getCharacteristic();
-    method public byte[]? getValue();
-    method public void sendResponse(boolean success, byte[]? value);
+    method public int getOffset();
+    method public byte[] getValue();
     property public final androidx.bluetooth.GattCharacteristic characteristic;
-    property public final byte[]? value;
+    property public final int offset;
+    property public final byte[] value;
   }
 
   public final class GattService {
diff --git a/bluetooth/bluetooth/api/restricted_current.txt b/bluetooth/bluetooth/api/restricted_current.txt
index 2bfa8e7..6ad9af9 100644
--- a/bluetooth/bluetooth/api/restricted_current.txt
+++ b/bluetooth/bluetooth/api/restricted_current.txt
@@ -2,15 +2,16 @@
 package androidx.bluetooth {
 
   public final class AdvertiseParams {
-    ctor public AdvertiseParams(optional boolean shouldIncludeDeviceAddress, optional boolean shouldIncludeDeviceName, optional boolean isConnectable, optional boolean isDiscoverable, optional int timeoutMillis, optional java.util.Map<java.lang.Integer,byte[]> manufacturerData, optional java.util.Map<java.util.UUID,byte[]> serviceData, optional java.util.List<java.util.UUID> serviceUuids);
+    ctor public AdvertiseParams(optional boolean shouldIncludeDeviceAddress, optional boolean shouldIncludeDeviceName, optional boolean isConnectable, optional boolean isDiscoverable, optional @IntRange(from=0L, to=655350L) int durationMillis, optional java.util.Map<java.lang.Integer,byte[]> manufacturerData, optional java.util.Map<java.util.UUID,byte[]> serviceData, optional java.util.List<java.util.UUID> serviceUuids);
+    method public int getDurationMillis();
     method public java.util.Map<java.lang.Integer,byte[]> getManufacturerData();
     method public java.util.Map<java.util.UUID,byte[]> getServiceData();
     method public java.util.List<java.util.UUID> getServiceUuids();
     method public boolean getShouldIncludeDeviceAddress();
     method public boolean getShouldIncludeDeviceName();
-    method public int getTimeoutMillis();
     method public boolean isConnectable();
     method public boolean isDiscoverable();
+    property public final int durationMillis;
     property public final boolean isConnectable;
     property public final boolean isDiscoverable;
     property public final java.util.Map<java.lang.Integer,byte[]> manufacturerData;
@@ -18,27 +19,12 @@
     property public final java.util.List<java.util.UUID> serviceUuids;
     property public final boolean shouldIncludeDeviceAddress;
     property public final boolean shouldIncludeDeviceName;
-    property public final int timeoutMillis;
-  }
-
-  public final class AdvertiseResult {
-    ctor public AdvertiseResult();
-    field public static final int ADVERTISE_FAILED_DATA_TOO_LARGE = 102; // 0x66
-    field public static final int ADVERTISE_FAILED_FEATURE_UNSUPPORTED = 103; // 0x67
-    field public static final int ADVERTISE_FAILED_INTERNAL_ERROR = 104; // 0x68
-    field public static final int ADVERTISE_FAILED_TOO_MANY_ADVERTISERS = 105; // 0x69
-    field public static final int ADVERTISE_STARTED = 101; // 0x65
-    field public static final androidx.bluetooth.AdvertiseResult.Companion Companion;
-  }
-
-  public static final class AdvertiseResult.Companion {
   }
 
   public final class BluetoothAddress {
     ctor public BluetoothAddress(String address, int addressType);
     method public String getAddress();
     method public int getAddressType();
-    method public void setAddressType(int);
     property public final String address;
     property public final int addressType;
     field public static final int ADDRESS_TYPE_PUBLIC = 0; // 0x0
@@ -63,19 +49,30 @@
 
   public final class BluetoothLe {
     ctor public BluetoothLe(android.content.Context context);
-    method @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE") public kotlinx.coroutines.flow.Flow<java.lang.Integer> advertise(androidx.bluetooth.AdvertiseParams advertiseParams);
-    method @RequiresPermission("android.permission.BLUETOOTH_CONNECT") public suspend <R> Object? connectGatt(androidx.bluetooth.BluetoothDevice device, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattClientScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super kotlin.Result<? extends R>>);
-    method public suspend <R> Object? openGattServer(java.util.List<androidx.bluetooth.GattService> services, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattServerConnectScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super kotlin.Result<? extends R>>);
+    method @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE") public suspend Object? advertise(androidx.bluetooth.AdvertiseParams advertiseParams, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>? block, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission("android.permission.BLUETOOTH_CONNECT") public suspend <R> Object? connectGatt(androidx.bluetooth.BluetoothDevice device, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattClientScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
+    method public suspend <R> Object? openGattServer(java.util.List<androidx.bluetooth.GattService> services, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattServerConnectScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
     method @RequiresPermission("android.permission.BLUETOOTH_SCAN") public kotlinx.coroutines.flow.Flow<androidx.bluetooth.ScanResult> scan(optional java.util.List<androidx.bluetooth.ScanFilter> filters);
+    field public static final int ADVERTISE_FAILED_DATA_TOO_LARGE = 102; // 0x66
+    field public static final int ADVERTISE_FAILED_FEATURE_UNSUPPORTED = 103; // 0x67
+    field public static final int ADVERTISE_FAILED_INTERNAL_ERROR = 104; // 0x68
+    field public static final int ADVERTISE_FAILED_TOO_MANY_ADVERTISERS = 105; // 0x69
+    field public static final int ADVERTISE_STARTED = 101; // 0x65
+    field public static final androidx.bluetooth.BluetoothLe.Companion Companion;
+  }
+
+  public static final class BluetoothLe.Companion {
   }
 
   public static interface BluetoothLe.GattClientScope {
-    method public suspend Object? awaitClose(kotlin.jvm.functions.Function0<kotlin.Unit> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public androidx.bluetooth.GattService? getService(java.util.UUID uuid);
-    method public java.util.List<androidx.bluetooth.GattService> getServices();
+    method public default java.util.List<androidx.bluetooth.GattService> getServices();
+    method public kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.bluetooth.GattService>> getServicesFlow();
     method public suspend Object? readCharacteristic(androidx.bluetooth.GattCharacteristic characteristic, kotlin.coroutines.Continuation<? super kotlin.Result<? extends byte[]>>);
     method public kotlinx.coroutines.flow.Flow<byte[]> subscribeToCharacteristic(androidx.bluetooth.GattCharacteristic characteristic);
     method public suspend Object? writeCharacteristic(androidx.bluetooth.GattCharacteristic characteristic, byte[] value, kotlin.coroutines.Continuation<? super kotlin.Result<? extends kotlin.Unit>>);
+    property public default java.util.List<androidx.bluetooth.GattService> services;
+    property public abstract kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.bluetooth.GattService>> servicesFlow;
   }
 
   public static final class BluetoothLe.GattServerConnectRequest {
@@ -86,15 +83,15 @@
   }
 
   public static interface BluetoothLe.GattServerConnectScope {
-    method public kotlinx.coroutines.flow.Flow<androidx.bluetooth.BluetoothLe.GattServerConnectRequest> getConnectRequest();
+    method public kotlinx.coroutines.flow.Flow<androidx.bluetooth.BluetoothLe.GattServerConnectRequest> getConnectRequests();
     method public void updateServices(java.util.List<androidx.bluetooth.GattService> services);
-    property public abstract kotlinx.coroutines.flow.Flow<androidx.bluetooth.BluetoothLe.GattServerConnectRequest> connectRequest;
+    property public abstract kotlinx.coroutines.flow.Flow<androidx.bluetooth.BluetoothLe.GattServerConnectRequest> connectRequests;
   }
 
   public static interface BluetoothLe.GattServerSessionScope {
     method public androidx.bluetooth.BluetoothDevice getDevice();
     method public kotlinx.coroutines.flow.Flow<androidx.bluetooth.GattServerRequest> getRequests();
-    method public void notify(androidx.bluetooth.GattCharacteristic characteristic, byte[] value);
+    method public suspend Object? notify(androidx.bluetooth.GattCharacteristic characteristic, byte[] value, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
     property public abstract androidx.bluetooth.BluetoothDevice device;
     property public abstract kotlinx.coroutines.flow.Flow<androidx.bluetooth.GattServerRequest> requests;
   }
@@ -119,21 +116,30 @@
   public static final class GattCharacteristic.Companion {
   }
 
-  public interface GattServerRequest {
+  public class GattServerRequest {
   }
 
-  public static final class GattServerRequest.ReadCharacteristicRequest implements androidx.bluetooth.GattServerRequest {
+  public static final class GattServerRequest.ReadCharacteristic extends androidx.bluetooth.GattServerRequest {
     method public androidx.bluetooth.GattCharacteristic getCharacteristic();
-    method public void sendResponse(boolean success, byte[]? value);
+    method public void sendFailure();
+    method public void sendResponse(byte[] value);
     property public final androidx.bluetooth.GattCharacteristic characteristic;
   }
 
-  public static final class GattServerRequest.WriteCharacteristicRequest implements androidx.bluetooth.GattServerRequest {
+  public static final class GattServerRequest.WriteCharacteristics extends androidx.bluetooth.GattServerRequest {
+    method public java.util.List<androidx.bluetooth.GattServerRequest.WriteCharacteristics.Part> getParts();
+    method public void sendFailure();
+    method public void sendResponse();
+    property public final java.util.List<androidx.bluetooth.GattServerRequest.WriteCharacteristics.Part> parts;
+  }
+
+  public static final class GattServerRequest.WriteCharacteristics.Part {
     method public androidx.bluetooth.GattCharacteristic getCharacteristic();
-    method public byte[]? getValue();
-    method public void sendResponse(boolean success, byte[]? value);
+    method public int getOffset();
+    method public byte[] getValue();
     property public final androidx.bluetooth.GattCharacteristic characteristic;
-    property public final byte[]? value;
+    property public final int offset;
+    property public final byte[] value;
   }
 
   public final class GattService {
diff --git a/bluetooth/bluetooth/build.gradle b/bluetooth/bluetooth/build.gradle
index 8e7f695..9d626f8 100644
--- a/bluetooth/bluetooth/build.gradle
+++ b/bluetooth/bluetooth/build.gradle
@@ -26,8 +26,6 @@
     implementation(libs.kotlinStdlib)
     implementation(libs.kotlinCoroutinesCore)
 
-    implementation(project(":annotation:annotation-experimental"))
-
     implementation("androidx.annotation:annotation:1.6.0")
 
     androidTestImplementation(libs.testExtJunit)
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/AdvertiseParamsTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/AdvertiseParamsTest.kt
index 6b59576..dc7d197 100644
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/AdvertiseParamsTest.kt
+++ b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/AdvertiseParamsTest.kt
@@ -33,7 +33,7 @@
         assertEquals(false, advertiseParams.shouldIncludeDeviceName)
         assertEquals(false, advertiseParams.isConnectable)
         assertEquals(false, advertiseParams.isDiscoverable)
-        assertEquals(0, advertiseParams.timeoutMillis)
+        assertEquals(0, advertiseParams.durationMillis)
         assertEquals(0, advertiseParams.manufacturerData.size)
         assertEquals(0, advertiseParams.serviceData.size)
         assertEquals(0, advertiseParams.serviceUuids.size)
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothAddressTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothAddressTest.kt
index beaa6f1..3c0e6e4 100644
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothAddressTest.kt
+++ b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothAddressTest.kt
@@ -18,6 +18,7 @@
 
 import junit.framework.TestCase.assertEquals
 import kotlin.test.assertFailsWith
+import org.junit.Assert.assertTrue
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -66,10 +67,9 @@
     fun constructorWithInvalidAddressType() {
         val invalidAddressType = -1
 
-        val bluetoothAddress = BluetoothAddress(TEST_ADDRESS_UNKNOWN, invalidAddressType)
+        val result = runCatching { BluetoothAddress(TEST_ADDRESS_UNKNOWN, invalidAddressType) }
 
-        assertEquals(TEST_ADDRESS_UNKNOWN, bluetoothAddress.address)
-        assertEquals(BluetoothAddress.ADDRESS_TYPE_UNKNOWN, bluetoothAddress.addressType)
+        assertTrue(result.exceptionOrNull() is IllegalArgumentException)
     }
 
     @Test
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothLeTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothLeTest.kt
index 2b6b036..d0116af 100644
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothLeTest.kt
+++ b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothLeTest.kt
@@ -24,7 +24,6 @@
 import java.util.UUID
 import junit.framework.TestCase.assertEquals
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.runTest
 import org.junit.Assume
 import org.junit.Before
@@ -71,10 +70,9 @@
     fun advertise() = runTest {
         val advertiseParams = AdvertiseParams()
 
-        val advertiseResultStarted = bluetoothLe.advertise(advertiseParams)
-            .first()
-
-        assertEquals(AdvertiseResult.ADVERTISE_STARTED, advertiseResultStarted)
+        bluetoothLe.advertise(advertiseParams) {
+            assertEquals(BluetoothLe.ADVERTISE_STARTED, it)
+        }
     }
 
     @Test
@@ -86,9 +84,8 @@
             serviceData = mapOf(parcelUuid to serviceData)
         )
 
-        val advertiseResultStarted = bluetoothLe.advertise(advertiseParams)
-            .first()
-
-        assertEquals(AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE, advertiseResultStarted)
+        bluetoothLe.advertise(advertiseParams) {
+            assertEquals(BluetoothLe.ADVERTISE_FAILED_DATA_TOO_LARGE, it)
+        }
     }
 }
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseParams.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseParams.kt
index f45a905..a8a258b 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseParams.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseParams.kt
@@ -16,11 +16,11 @@
 
 package androidx.bluetooth
 
+import androidx.annotation.IntRange
 import java.util.UUID
 
 /**
- * A single class to provide a way to adjust advertising preferences and advertise data packet.
- *
+ * A class to provide a way to adjust advertising preferences and advertise data packet.
  */
 class AdvertiseParams(
     /* Whether the device address will be included in the advertisement packet. */
@@ -33,32 +33,35 @@
     val shouldIncludeDeviceName: Boolean = false,
     /* Whether the advertisement will indicate connectable. */
     val isConnectable: Boolean = false,
-    /* Whether the advertisement will be discoverable. */
-    val isDiscoverable: Boolean = false,
-    /* Advertising time limit in milliseconds. */
-    val timeoutMillis: Int = 0,
     /**
-     * A map of manufacturer specific data.
+     * Whether the advertisement will be discoverable.
+     *
+     * Please note that it would be ignored under API level 34 and [isConnectable] would be
+     * used instead.
+     */
+    val isDiscoverable: Boolean = false,
+    /**
+     * Advertising duration in milliseconds
+     *
+     * It must not exceed 655350 milliseconds. A value of 0 means advertising continues
+     * until it is stopped explicitly.
+     */
+    @IntRange(from = 0, to = 655350) val durationMillis: Int = 0,
+
+    /**
+     * A map of company identifiers to manufacturer specific data.
      * <p>
      * Please refer to the Bluetooth Assigned Numbers document provided by the <a
-     * href="https://www.bluetooth.org">Bluetooth SIG</a> for a list of existing company
+     * href="https://www.bluetooth.org">Bluetooth SIG</a> for the list of existing company
      * identifiers.
-     *
-     * Map<Int> Manufacturer ID assigned by Bluetooth SIG.
-     * Map<ByteArray> Manufacturer specific data
      */
     val manufacturerData: Map<Int, ByteArray> = emptyMap(),
     /**
-     * A map of service data to advertise data.
-     *
-     * UUID 16-bit UUID of the service the data is associated with
-     * ByteArray serviceData Service data
+     * A map of 16-bit UUIDs of the services to corresponding additional service data.
      */
     val serviceData: Map<UUID, ByteArray> = emptyMap(),
     /**
-     * A list of service UUID to advertise data.
-     *
-     * UUID A service UUID to be advertised.
+     * A list of service UUIDs to advertise.
      */
     val serviceUuids: List<UUID> = emptyList()
 )
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseResult.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseResult.kt
deleted file mode 100644
index 69f8687..0000000
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseResult.kt
+++ /dev/null
@@ -1,56 +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.bluetooth
-
-import androidx.annotation.IntDef
-import androidx.annotation.RestrictTo
-import kotlin.annotation.Retention
-
-/**
- * An advertise result indicates the result of a request to start advertising, whether success
- * or failure.
- */
-class AdvertiseResult {
-    @Target(AnnotationTarget.TYPE)
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    @Retention(AnnotationRetention.SOURCE)
-    @IntDef(
-        ADVERTISE_STARTED,
-        ADVERTISE_FAILED_DATA_TOO_LARGE,
-        ADVERTISE_FAILED_FEATURE_UNSUPPORTED,
-        ADVERTISE_FAILED_INTERNAL_ERROR,
-        ADVERTISE_FAILED_TOO_MANY_ADVERTISERS
-    )
-    annotation class ResultType
-
-    companion object {
-        /** Advertise started successfully. */
-        const val ADVERTISE_STARTED: Int = 101
-
-        /** Advertise failed to start because the data is too large. */
-        const val ADVERTISE_FAILED_DATA_TOO_LARGE: Int = 102
-
-        /** Advertise failed to start because the advertise feature is not supported. */
-        const val ADVERTISE_FAILED_FEATURE_UNSUPPORTED: Int = 103
-
-        /** Advertise failed to start because of an internal error. */
-        const val ADVERTISE_FAILED_INTERNAL_ERROR: Int = 104
-
-        /** Advertise failed to start because of too many advertisers. */
-        const val ADVERTISE_FAILED_TOO_MANY_ADVERTISERS: Int = 105
-    }
-}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt
index 3c272a0..ecbca45 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt
@@ -24,11 +24,10 @@
 /**
  * Represents a Bluetooth address for a remote device.
  *
- * @property address valid Bluetooth MAC address
- * @property addressType valid address type
- *
+ * @property address a valid Bluetooth MAC address
+ * @property addressType a valid address type
  */
-class BluetoothAddress(val address: String, @AddressType var addressType: Int) {
+class BluetoothAddress(val address: String, @AddressType val addressType: Int) {
     companion object {
         /** Address type is public and registered with the IEEE. */
         const val ADDRESS_TYPE_PUBLIC: Int = 0
@@ -61,12 +60,13 @@
             throw IllegalArgumentException("$address is not a valid Bluetooth address")
         }
 
-        addressType = when (addressType) {
+        when (addressType) {
             ADDRESS_TYPE_PUBLIC,
             ADDRESS_TYPE_RANDOM_STATIC,
             ADDRESS_TYPE_RANDOM_RESOLVABLE,
-            ADDRESS_TYPE_RANDOM_NON_RESOLVABLE -> addressType
-            else -> ADDRESS_TYPE_UNKNOWN
+            ADDRESS_TYPE_RANDOM_NON_RESOLVABLE,
+            ADDRESS_TYPE_UNKNOWN -> Unit
+            else -> throw IllegalArgumentException("$addressType is not a valid address type")
         }
     }
 
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
index e59357f..ecb8606 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
@@ -25,16 +25,27 @@
 import android.bluetooth.le.ScanResult as FwkScanResult
 import android.bluetooth.le.ScanSettings
 import android.content.Context
+import android.os.Build
 import android.os.ParcelUuid
 import android.util.Log
+import androidx.annotation.DoNotInline
+import androidx.annotation.IntDef
+import androidx.annotation.RequiresApi
 import androidx.annotation.RequiresPermission
 import androidx.annotation.RestrictTo
 import androidx.annotation.VisibleForTesting
 import java.util.UUID
+import kotlin.coroutines.coroutineContext
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.job
 
 /**
  * Entry point for BLE related operations. This class provides a way to perform Bluetooth LE
@@ -42,8 +53,48 @@
  */
 class BluetoothLe constructor(private val context: Context) {
 
-    private companion object {
+    companion object {
         private const val TAG = "BluetoothLe"
+
+        /** Advertise started successfully. */
+        const val ADVERTISE_STARTED: Int = 101
+
+        /** Advertise failed to start because the data is too large. */
+        const val ADVERTISE_FAILED_DATA_TOO_LARGE: Int = 102
+
+        /** Advertise failed to start because the advertise feature is not supported. */
+        const val ADVERTISE_FAILED_FEATURE_UNSUPPORTED: Int = 103
+
+        /** Advertise failed to start because of an internal error. */
+        const val ADVERTISE_FAILED_INTERNAL_ERROR: Int = 104
+
+        /** Advertise failed to start because of too many advertisers. */
+        const val ADVERTISE_FAILED_TOO_MANY_ADVERTISERS: Int = 105
+    }
+
+    @Target(AnnotationTarget.TYPE)
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(AnnotationRetention.SOURCE)
+    @IntDef(
+        ADVERTISE_STARTED,
+        ADVERTISE_FAILED_DATA_TOO_LARGE,
+        ADVERTISE_FAILED_FEATURE_UNSUPPORTED,
+        ADVERTISE_FAILED_INTERNAL_ERROR,
+        ADVERTISE_FAILED_TOO_MANY_ADVERTISERS
+    )
+    annotation class AdvertiseResult
+
+    @RequiresApi(34)
+    private object BluetoothLeApi34Impl {
+        @JvmStatic
+        @DoNotInline
+        fun setDiscoverable(
+            builder: AdvertiseSettings.Builder,
+            isDiscoverable: Boolean
+        ): AdvertiseSettings.Builder {
+            builder.setDiscoverable(isDiscoverable)
+            return builder
+        }
     }
 
     private val bluetoothManager =
@@ -67,39 +118,44 @@
     var onStartScanListener: OnStartScanListener? = null
 
     /**
-     * Returns a _cold_ [Flow] to start Bluetooth LE Advertising.
-     * When the flow is successfully collected, the operation status [AdvertiseResult] will be
-     * delivered via the flow [kotlinx.coroutines.channels.Channel].
+     * Starts Bluetooth LE advertising
      *
-     * @param advertiseParams [AdvertiseParams] for Bluetooth LE advertising
-     * @return a _cold_ [Flow] with [AdvertiseResult] status in the data stream
+     * Note that this method may not complete if the duration is set to 0.
+     * To stop advertising, in that case, you should cancel the coroutine.
+     *
+     * @param advertiseParams [AdvertiseParams] for Bluetooth LE advertising.
+     * @param block an optional block of code that is invoked when advertising is started or failed.
+     *
+     * @throws IllegalArgumentException if the advertise parameters are not valid.
      */
     @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE")
-    fun advertise(advertiseParams: AdvertiseParams): Flow<@AdvertiseResult.ResultType Int> =
-        callbackFlow {
+    suspend fun advertise(
+        advertiseParams: AdvertiseParams,
+        block: (suspend (@AdvertiseResult Int) -> Unit)? = null
+    ) {
+        val result = CompletableDeferred<Int>()
+
         val callback = object : AdvertiseCallback() {
             override fun onStartFailure(errorCode: Int) {
                 Log.d(TAG, "onStartFailure() called with: errorCode = $errorCode")
 
                 when (errorCode) {
                     ADVERTISE_FAILED_DATA_TOO_LARGE ->
-                        trySend(AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE)
+                        result.complete(BluetoothLe.ADVERTISE_FAILED_DATA_TOO_LARGE)
 
                     ADVERTISE_FAILED_FEATURE_UNSUPPORTED ->
-                        trySend(AdvertiseResult.ADVERTISE_FAILED_FEATURE_UNSUPPORTED)
+                        result.complete(BluetoothLe.ADVERTISE_FAILED_FEATURE_UNSUPPORTED)
 
                     ADVERTISE_FAILED_INTERNAL_ERROR ->
-                        trySend(AdvertiseResult.ADVERTISE_FAILED_INTERNAL_ERROR)
+                        result.complete(BluetoothLe.ADVERTISE_FAILED_INTERNAL_ERROR)
 
                     ADVERTISE_FAILED_TOO_MANY_ADVERTISERS ->
-                        trySend(AdvertiseResult.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS)
+                        result.complete(BluetoothLe.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS)
                 }
             }
 
             override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
-                Log.d(TAG, "onStartSuccess() called with: settingsInEffect = $settingsInEffect")
-
-                trySend(AdvertiseResult.ADVERTISE_STARTED)
+                result.complete(ADVERTISE_STARTED)
             }
         }
 
@@ -107,9 +163,14 @@
 
         val advertiseSettings = with(AdvertiseSettings.Builder()) {
             setConnectable(advertiseParams.isConnectable)
-            setTimeout(advertiseParams.timeoutMillis)
-            // TODO(b/290697177) Add when AndroidX is targeting Android U
-//            setDiscoverable(advertiseParams.isDiscoverable)
+            advertiseParams.durationMillis.let {
+                if (it !in 0..655350)
+                    throw IllegalArgumentException("advertise duration must be in [0, 655350]")
+                setTimeout(it)
+            }
+            if (Build.VERSION.SDK_INT >= 34) {
+                BluetoothLeApi34Impl.setDiscoverable(this, advertiseParams.isDiscoverable)
+            }
             build()
         }
 
@@ -127,13 +188,21 @@
             build()
         }
 
-        Log.d(TAG, "bleAdvertiser.startAdvertising($advertiseSettings, $advertiseData) called")
         bleAdvertiser?.startAdvertising(advertiseSettings, advertiseData, callback)
 
-        awaitClose {
-            Log.d(TAG, "bleAdvertiser.stopAdvertising() called")
+        coroutineContext.job.invokeOnCompletion {
             bleAdvertiser?.stopAdvertising(callback)
         }
+        result.await().let {
+            block?.invoke(it)
+            if (it == ADVERTISE_STARTED) {
+                if (advertiseParams.durationMillis > 0) {
+                    delay(advertiseParams.durationMillis.toLong())
+                } else {
+                    awaitCancellation()
+                }
+            }
+        }
     }
 
     /**
@@ -176,9 +245,20 @@
     interface GattClientScope {
 
         /**
-         * Gets the services discovered from the remote device.
+         * A flow of GATT services discovered from the remote device.
+         *
+         * If the services of the remote device has changed, the new services will be
+         * discovered and emitted automatically.
          */
-        fun getServices(): List<GattService>
+        val servicesFlow: StateFlow<List<GattService>>
+
+        /**
+         * GATT services recently discovered from the remote device.
+         *
+         * Note that this can be changed, subscribe to [servicesFlow] to get notified
+         * of services changes.
+         */
+        val services: List<GattService> get() = servicesFlow.value
 
         /**
          * Gets the service of the remote device by UUID.
@@ -213,12 +293,6 @@
          * Returns a _cold_ [Flow] that contains the indicated value of the given characteristic.
          */
         fun subscribeToCharacteristic(characteristic: GattCharacteristic): Flow<ByteArray>
-
-        /**
-         * Suspends the current coroutine until the pending operations are handled and the
-         * connection is closed, then it invokes the given [block] before resuming the coroutine.
-         */
-        suspend fun awaitClose(block: () -> Unit)
     }
 
     /**
@@ -230,6 +304,7 @@
      * @param device a [BluetoothDevice] to connect to
      * @param block a block of code that is invoked after the connection is made
      *
+     * @throws CancellationException if connect failed or it's canceled
      * @return a result returned by the given block if the connection was successfully finished
      *         or a failure with the corresponding reason
      *
@@ -238,14 +313,14 @@
     suspend fun <R> connectGatt(
         device: BluetoothDevice,
         block: suspend GattClientScope.() -> R
-    ): Result<R> {
+    ): R {
         return client.connect(device, block)
     }
 
     /**
      * A scope for handling connect requests from remote devices.
      *
-     * @property connectRequest connect requests from remote devices.
+     * @property connectRequests connect requests from remote devices.
      *
      * @see BluetoothLe#openGattServer
      */
@@ -253,7 +328,7 @@
         /**
          * A _hot_ flow of [GattServerConnectRequest].
          */
-        val connectRequest: Flow<GattServerConnectRequest>
+        val connectRequests: Flow<GattServerConnectRequest>
 
         /**
          * Updates the services of the opened GATT server.
@@ -281,8 +356,8 @@
         /**
          * A _hot_ [Flow] of incoming requests from the client.
          *
-         * A request is either [GattServerRequest.ReadCharacteristicRequest] or
-         * [GattServerRequest.WriteCharacteristicRequest]
+         * A request is either [GattServerRequest.ReadCharacteristic] or
+         * [GattServerRequest.WriteCharacteristics]
          */
         val requests: Flow<GattServerRequest>
 
@@ -291,8 +366,10 @@
          *
          * @param characteristic the updated characteristic
          * @param value the new value of the characteristic
+         *
+         * @return `true` if the notification sent successfully
          */
-        fun notify(characteristic: GattCharacteristic, value: ByteArray)
+        suspend fun notify(characteristic: GattCharacteristic, value: ByteArray): Boolean
     }
 
     /**
@@ -342,7 +419,7 @@
     suspend fun <R> openGattServer(
         services: List<GattService>,
         block: suspend GattServerConnectScope.() -> R
-    ): Result<R> {
+    ): R {
         return server.open(services, block)
     }
 
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClient.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClient.kt
index 290e1a9..bbd339a 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClient.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClient.kt
@@ -39,7 +39,10 @@
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filter
@@ -48,6 +51,7 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withTimeout
 
 /**
  * A class for handling operations as a GATT client role.
@@ -77,6 +81,8 @@
         fun writeDescriptor(descriptor: FwkDescriptor, value: ByteArray)
 
         fun setCharacteristicNotification(characteristic: FwkCharacteristic, enable: Boolean)
+
+        fun closeGatt()
     }
 
     @VisibleForTesting
@@ -88,6 +94,8 @@
          * The maximum ATT size(512) + header(3)
          */
         private const val GATT_MAX_MTU = 515
+
+        private const val CONNECT_TIMEOUT_MS = 30_000L
         private val CCCD_UID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
     }
 
@@ -131,20 +139,21 @@
     suspend fun <R> connect(
         device: BluetoothDevice,
         block: suspend BluetoothLe.GattClientScope.() -> R
-    ): Result<R> = coroutineScope {
+    ): R = coroutineScope {
         val connectResult = CompletableDeferred<Unit>(parent = coroutineContext.job)
         val callbackResultsFlow =
             MutableSharedFlow<CallbackResult>(extraBufferCapacity = Int.MAX_VALUE)
         val subscribeMap: MutableMap<FwkCharacteristic, SubscribeListener> = mutableMapOf()
         val subscribeMutex = Mutex()
         val attributeMap = AttributeMap()
+        val servicesFlow = MutableStateFlow<List<GattService>>(listOf())
 
         val callback = object : BluetoothGattCallback() {
             override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
                 if (newState == BluetoothGatt.STATE_CONNECTED) {
                     fwkAdapter.requestMtu(GATT_MAX_MTU)
                 } else {
-                    connectResult.cancel("connect failed")
+                    cancel("connect failed")
                 }
             }
 
@@ -152,14 +161,24 @@
                 if (status == BluetoothGatt.GATT_SUCCESS) {
                     fwkAdapter.discoverServices()
                 } else {
-                    connectResult.cancel("mtu request failed")
+                    cancel("mtu request failed")
                 }
             }
 
             override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
                 attributeMap.updateWithFrameworkServices(fwkAdapter.getServices())
                 if (status == BluetoothGatt.GATT_SUCCESS) connectResult.complete(Unit)
-                else connectResult.cancel("service discover failed")
+                else cancel("service discover failed")
+                servicesFlow.tryEmit(attributeMap.getServices())
+                if (connectResult.isActive) {
+                    if (status == BluetoothGatt.GATT_SUCCESS) connectResult.complete(Unit)
+                    else connectResult.cancel("service discover failed")
+                }
+            }
+
+            override fun onServiceChanged(gatt: BluetoothGatt) {
+                // TODO: under API 31, we have to subscribe to the service changed characteristic.
+                fwkAdapter.discoverServices()
             }
 
             override fun onCharacteristicRead(
@@ -219,13 +238,11 @@
             }
         }
         if (!fwkAdapter.connectGatt(context, device.fwkDevice, callback)) {
-            return@coroutineScope Result.failure(CancellationException("failed to connect"))
+            throw CancellationException("failed to connect")
         }
 
-        try {
+        withTimeout(CONNECT_TIMEOUT_MS) {
             connectResult.await()
-        } catch (e: Throwable) {
-            return@coroutineScope Result.failure(e)
         }
         val gattScope = object : BluetoothLe.GattClientScope {
             val taskMutex = Mutex()
@@ -235,9 +252,7 @@
                 }
             }
 
-            override fun getServices(): List<GattService> {
-                return attributeMap.getServices()
-            }
+            override val servicesFlow: StateFlow<List<GattService>> = servicesFlow.asStateFlow()
 
             override fun getService(uuid: UUID): GattService? {
                 return fwkAdapter.getService(uuid)?.let { attributeMap.fromFwkService(it) }
@@ -339,19 +354,6 @@
                 }
             }
 
-            override suspend fun awaitClose(block: () -> Unit) {
-                try {
-                    // Wait for queued tasks done
-                    taskMutex.withLock {
-                        subscribeMutex.withLock {
-                            subscribeMap.values.forEach { it.finish() }
-                        }
-                    }
-                } finally {
-                    block()
-                }
-            }
-
             private suspend fun registerSubscribeListener(
                 characteristic: FwkCharacteristic,
                 callback: SubscribeListener
@@ -373,11 +375,10 @@
                 }
             }
         }
-        try {
-            Result.success(gattScope.block())
-        } catch (e: CancellationException) {
-            Result.failure(e)
+        coroutineContext.job.invokeOnCompletion {
+            fwkAdapter.closeGatt()
         }
+        gattScope.block()
     }
 
     private suspend inline fun <reified R : CallbackResult> takeMatchingResult(
@@ -448,6 +449,12 @@
         ) {
             bluetoothGatt?.setCharacteristicNotification(characteristic, enable)
         }
+
+        @RequiresPermission(BLUETOOTH_CONNECT)
+        override fun closeGatt() {
+            bluetoothGatt?.close()
+            bluetoothGatt?.disconnect()
+        }
     }
 
     private open class FrameworkAdapterApi33 : FrameworkAdapterBase() {
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattServer.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattServer.kt
index 6d2edf4..95a35a7 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattServer.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattServer.kt
@@ -20,6 +20,7 @@
 import android.annotation.SuppressLint
 import android.bluetooth.BluetoothDevice as FwkDevice
 import android.bluetooth.BluetoothGatt
+import android.bluetooth.BluetoothGatt.GATT_SUCCESS
 import android.bluetooth.BluetoothGattCharacteristic as FwkCharacteristic
 import android.bluetooth.BluetoothGattServer
 import android.bluetooth.BluetoothGattServerCallback
@@ -33,10 +34,13 @@
 import androidx.annotation.VisibleForTesting
 import java.util.concurrent.atomic.AtomicBoolean
 import java.util.concurrent.atomic.AtomicInteger
+import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
 
 /**
  * Class for handling operations as a GATT server role
@@ -72,7 +76,7 @@
         }
 
         val device: BluetoothDevice
-
+        var pendingWriteParts: MutableList<GattServerRequest.WriteCharacteristics.Part>
         suspend fun acceptConnection(block: suspend BluetoothLe.GattServerSessionScope.() -> Unit)
         fun rejectConnection()
 
@@ -83,6 +87,10 @@
         private const val TAG = "GattServer"
     }
 
+    // Should be accessed only from the callback thread
+    private val sessions: MutableMap<FwkDevice, Session> = mutableMapOf()
+    private val attributeMap = AttributeMap()
+
     @SuppressLint("ObsoleteSdkInt")
     @VisibleForTesting
     @RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -93,8 +101,8 @@
     suspend fun <R> open(
         services: List<GattService>,
         block: suspend BluetoothLe.GattServerConnectScope.() -> R
-    ): Result<R> {
-        return Result.success(createServerScope(services).block())
+    ): R {
+        return createServerScope(services).block()
     }
 
     private fun createServerScope(services: List<GattService>): BluetoothLe.GattServerConnectScope {
@@ -102,8 +110,10 @@
             private val attributeMap = AttributeMap()
             // Should be accessed only from the callback thread
             private val sessions: MutableMap<FwkDevice, Session> = mutableMapOf()
+            private val notifyMutex = Mutex()
+            private var notifyJob: CompletableDeferred<Boolean>? = null
 
-            override val connectRequest = callbackFlow {
+            override val connectRequests = callbackFlow {
                     attributeMap.updateWithServices(services)
                     val callback = object : BluetoothGattServerCallback() {
                         override fun onConnectionStateChange(
@@ -133,7 +143,7 @@
                             attributeMap.fromFwkCharacteristic(characteristic)?.let { char ->
                                 findActiveSessionWithDevice(device)?.run {
                                     requestChannel.trySend(
-                                        GattServerRequest.ReadCharacteristicRequest(
+                                        GattServerRequest.ReadCharacteristic(
                                             this, requestId, offset, char
                                         )
                                     )
@@ -149,31 +159,77 @@
                         override fun onCharacteristicWriteRequest(
                             device: FwkDevice,
                             requestId: Int,
-                            characteristic: FwkCharacteristic,
+                            fwkCharacteristic: FwkCharacteristic,
                             preparedWrite: Boolean,
                             responseNeeded: Boolean,
                             offset: Int,
-                            value: ByteArray?
+                            value: ByteArray
                         ) {
-                            // TODO(b/296505524): handle preparedWrite == true
-                            attributeMap.fromFwkCharacteristic(characteristic)?.let {
-                                findActiveSessionWithDevice(device)?.run {
-                                    requestChannel.trySend(
-                                        GattServerRequest.WriteCharacteristicRequest(
-                                            this,
-                                            requestId,
-                                            it,
-                                            value
-                                        )
-                                    )
+                            attributeMap.fromFwkCharacteristic(fwkCharacteristic)?.let { char ->
+                                findActiveSessionWithDevice(device)?.let { session ->
+                                    if (preparedWrite) {
+                                        session.pendingWriteParts.add(
+                                            GattServerRequest.WriteCharacteristics.Part(
+                                                char,
+                                                offset,
+                                                value
+                                            ))
+                                        fwkAdapter.sendResponse(device, requestId,
+                                            BluetoothGatt.GATT_SUCCESS, offset, value)
+                                    } else {
+                                        session.requestChannel.trySend(
+                                            GattServerRequest.WriteCharacteristics(
+                                                session,
+                                                requestId,
+                                                listOf(GattServerRequest.WriteCharacteristics.Part(
+                                                    char,
+                                                    0,
+                                                    value
+                                                ))
+                                            ))
+                                    }
                                 }
                             } ?: run {
-                                fwkAdapter.sendResponse(
-                                    device, requestId, BluetoothGatt.GATT_WRITE_NOT_PERMITTED,
-                                    offset, /*value=*/null
-                                )
+                                fwkAdapter.sendResponse(device, requestId,
+                                    BluetoothGatt.GATT_WRITE_NOT_PERMITTED, offset, /*value=*/null)
                             }
                         }
+
+                        override fun onExecuteWrite(
+                            device: FwkDevice,
+                            requestId: Int,
+                            execute: Boolean
+                        ) {
+                            findActiveSessionWithDevice(device)?.let { session ->
+                                if (execute) {
+                                    session.requestChannel.trySend(
+                                        GattServerRequest.WriteCharacteristics(
+                                            session,
+                                            requestId,
+                                            session.pendingWriteParts
+                                        )
+                                    )
+                                } else {
+                                    fwkAdapter.sendResponse(
+                                        device, requestId,
+                                        BluetoothGatt.GATT_SUCCESS, /*offset=*/0, /*value=*/null
+                                    )
+                                }
+                                session.pendingWriteParts = mutableListOf()
+                            } ?: run {
+                                fwkAdapter.sendResponse(device, requestId,
+                                    BluetoothGatt.GATT_WRITE_NOT_PERMITTED,
+                                    /*offset=*/0, /*value=*/null)
+                            }
+                        }
+
+                        override fun onNotificationSent(
+                            device: android.bluetooth.BluetoothDevice?,
+                            status: Int
+                        ) {
+                            notifyJob?.complete(status == GATT_SUCCESS)
+                            notifyJob = null
+                        }
                     }
                     fwkAdapter.openGattServer(context, callback)
                     services.forEach { fwkAdapter.addService(it.fwkService) }
@@ -208,6 +264,8 @@
 
                 val state: AtomicInteger = AtomicInteger(GattServer.Session.STATE_CONNECTING)
                 val requestChannel = Channel<GattServerRequest>(Channel.UNLIMITED)
+                override var pendingWriteParts =
+                    mutableListOf<GattServerRequest.WriteCharacteristics.Part>()
 
                 override suspend fun acceptConnection(
                     block: suspend BluetoothLe.GattServerSessionScope.() -> Unit
@@ -225,16 +283,22 @@
                             get() = this@Session.device
                         override val requests = requestChannel.receiveAsFlow()
 
-                        override fun notify(
+                        override suspend fun notify(
                             characteristic: GattCharacteristic,
                             value: ByteArray
-                        ) {
-                            fwkAdapter.notifyCharacteristicChanged(
-                                device.fwkDevice,
-                                characteristic.fwkCharacteristic,
-                                false,
-                                value
-                            )
+                        ): Boolean {
+                            notifyMutex.withLock {
+                                CompletableDeferred<Boolean>().also {
+                                    notifyJob = it
+                                    fwkAdapter.notifyCharacteristicChanged(
+                                        device.fwkDevice,
+                                        characteristic.fwkCharacteristic,
+                                        false,
+                                        value
+                                    )
+                                    return it.await()
+                                }
+                            }
                         }
                     }
                     scope.block()
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattServerRequest.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattServerRequest.kt
index 8dd6554..5419aaa 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattServerRequest.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattServerRequest.kt
@@ -19,68 +19,99 @@
 import android.bluetooth.BluetoothGatt.GATT_READ_NOT_PERMITTED
 import android.bluetooth.BluetoothGatt.GATT_SUCCESS
 import android.bluetooth.BluetoothGatt.GATT_WRITE_NOT_PERMITTED
+import java.util.concurrent.atomic.AtomicBoolean
 
 /**
  * Represents a request to be handled as a GATT server role.
  *
  * @see BluetoothLe.GattServerConnectRequest.accept
  */
-interface GattServerRequest {
+open class GattServerRequest private constructor() {
+    private val handled = AtomicBoolean(false)
+
+    internal inline fun handleRequest(block: () -> Unit) {
+        if (handled.compareAndSet(false, true)) {
+            block()
+        } else {
+            throw IllegalStateException("Request is already handled")
+        }
+    }
+
     /**
      * Represents a read characteristic request.
      *
      * @property characteristic a characteristic to read
      */
-    class ReadCharacteristicRequest internal constructor(
+    class ReadCharacteristic internal constructor(
         private val session: GattServer.Session,
         private val requestId: Int,
         private val offset: Int,
         val characteristic: GattCharacteristic
-    ) : GattServerRequest {
+    ) : GattServerRequest() {
         /**
          * Sends the result for the read request.
          *
-         * @param success true if the request was successful
-         * @param value a value of the characteristic or `null` if it failed.
+         * @param value a value of the characteristic
          */
-        fun sendResponse(success: Boolean, value: ByteArray?) {
-            val resValue: ByteArray? = if (offset == 0 || value == null) value
-            else if (value.size > offset) value.copyOfRange(offset, value.size - 1)
-            else ByteArray(0)
-            session.sendResponse(
-                requestId,
-                if (success) GATT_SUCCESS else GATT_READ_NOT_PERMITTED,
-                offset,
-                resValue
-            )
+        fun sendResponse(value: ByteArray) {
+            handleRequest {
+                val resValue: ByteArray = if (offset == 0) value
+                else if (value.size > offset) value.copyOfRange(offset, value.size - 1)
+                else if (value.size == offset) byteArrayOf()
+                else byteArrayOf()
+                session.sendResponse(requestId, GATT_SUCCESS, offset, resValue)
+            }
+        }
+
+        /**
+         * Notifies the failure for the read request.
+         */
+        fun sendFailure() {
+            handleRequest {
+                session.sendResponse(requestId, GATT_READ_NOT_PERMITTED, offset, null)
+            }
         }
     }
 
     /**
-     * Represents a write characteristic request.
+     * Represents a request to write characteristics.
      *
-     * @property characteristic a characteristic to write
-     * @property value a value to write
+     * @property parts a list of write request parts
      */
-    class WriteCharacteristicRequest internal constructor(
+    class WriteCharacteristics internal constructor(
         private val session: GattServer.Session,
         private val requestId: Int,
-        val characteristic: GattCharacteristic,
-        val value: ByteArray?
-    ) : GattServerRequest {
+        val parts: List<Part>
+    ) : GattServerRequest() {
         /**
-         * Sends the result for the write request.
-         *
-         * @param success true if the request was successful
-         * @param value an optional value that is written
+         * Notifies the success of the write request.
          */
-        fun sendResponse(success: Boolean, value: ByteArray?) {
-            session.sendResponse(
-                requestId,
-                if (success) GATT_SUCCESS else GATT_WRITE_NOT_PERMITTED,
-                0,
-                value
-            )
+        fun sendResponse() {
+            handleRequest {
+                session.sendResponse(requestId, GATT_SUCCESS, 0, null)
+            }
         }
+
+        /**
+         * Notifies the failure of the write request.
+         */
+        fun sendFailure() {
+            handleRequest {
+                session.sendResponse(requestId, GATT_WRITE_NOT_PERMITTED, 0, null)
+            }
+        }
+
+        /**
+         * A part of write requests.
+         *
+         * @property characteristic a characteristic to write
+         * @property offset an offset of the first octet to be written
+         * @property value a value to be written
+         */
+        class Part internal constructor(
+            val characteristic: GattCharacteristic,
+            val offset: Int,
+            val value: ByteArray
+        )
     }
 }
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
index 89a87ca..66cddcb 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
@@ -30,7 +30,6 @@
 import android.widget.EditText
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.widget.PopupMenu
-import androidx.bluetooth.AdvertiseResult
 import androidx.bluetooth.BluetoothLe
 import androidx.bluetooth.GattCharacteristic
 import androidx.bluetooth.GattServerRequest
@@ -325,32 +324,33 @@
         advertiseJob = advertiseScope.launch {
             isAdvertising = true
 
-            bluetoothLe.advertise(viewModel.advertiseParams)
-                .collect {
-                    Log.d(TAG, "AdvertiseResult collected: $it")
+            bluetoothLe.advertise(viewModel.advertiseParams) {
+                when (it) {
+                    BluetoothLe.ADVERTISE_STARTED -> {
+                        toast("ADVERTISE_STARTED").show()
+                    }
 
-                    when (it) {
-                        AdvertiseResult.ADVERTISE_STARTED -> {
-                            toast("ADVERTISE_STARTED").show()
-                        }
-                        AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE -> {
-                            isAdvertising = false
-                            toast("ADVERTISE_FAILED_DATA_TOO_LARGE").show()
-                        }
-                        AdvertiseResult.ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> {
-                            isAdvertising = false
-                            toast("ADVERTISE_FAILED_FEATURE_UNSUPPORTED").show()
-                        }
-                        AdvertiseResult.ADVERTISE_FAILED_INTERNAL_ERROR -> {
-                            isAdvertising = false
-                            toast("ADVERTISE_FAILED_INTERNAL_ERROR").show()
-                        }
-                        AdvertiseResult.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> {
-                            isAdvertising = false
-                            toast("ADVERTISE_FAILED_TOO_MANY_ADVERTISERS").show()
-                        }
+                    BluetoothLe.ADVERTISE_FAILED_DATA_TOO_LARGE -> {
+                        isAdvertising = false
+                        toast("ADVERTISE_FAILED_DATA_TOO_LARGE").show()
+                    }
+
+                    BluetoothLe.ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> {
+                        isAdvertising = false
+                        toast("ADVERTISE_FAILED_FEATURE_UNSUPPORTED").show()
+                    }
+
+                    BluetoothLe.ADVERTISE_FAILED_INTERNAL_ERROR -> {
+                        isAdvertising = false
+                        toast("ADVERTISE_FAILED_INTERNAL_ERROR").show()
+                    }
+
+                    BluetoothLe.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> {
+                        isAdvertising = false
+                        toast("ADVERTISE_FAILED_TOO_MANY_ADVERTISERS").show()
                     }
                 }
+            }
         }
     }
 
@@ -460,19 +460,19 @@
             isGattServerOpen = true
 
             bluetoothLe.openGattServer(viewModel.gattServerServices) {
-                connectRequest.collect {
+                connectRequests.collect {
                     launch {
                         it.accept {
                             requests.collect {
+                                // TODO(b/269390098): handle request correctly
                                 when (it) {
-                                    is GattServerRequest.ReadCharacteristicRequest ->
-                                        it.sendResponse(/*success=*/true,
-                                            ByteBuffer.allocate(Int.SIZE_BYTES).putInt(1)
-                                                .array()
+                                    is GattServerRequest.ReadCharacteristic ->
+                                        it.sendResponse(
+                                            ByteBuffer.allocate(Int.SIZE_BYTES).putInt(1).array()
                                         )
 
-                                    is GattServerRequest.WriteCharacteristicRequest ->
-                                        it.sendResponse(/*success=*/true, null)
+                                    is GattServerRequest.WriteCharacteristics ->
+                                        it.sendResponse()
 
                                     else -> throw NotImplementedError("unknown request")
                                 }
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt
index ba55df3..99cb7f8 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt
@@ -32,7 +32,7 @@
     var includeDeviceName = false
     var connectable = false
     var discoverable = false
-    var timeoutMillis = 0
+    var durationMillis = 0
     var manufacturerDatas = mutableListOf<Pair<Int, ByteArray>>()
     var serviceDatas = mutableListOf<Pair<UUID, ByteArray>>()
     var serviceUuids = mutableListOf<UUID>()
@@ -56,7 +56,7 @@
             includeDeviceName,
             connectable,
             discoverable,
-            timeoutMillis,
+            durationMillis,
             manufacturerDatas.toMap(),
             serviceDatas.toMap(),
             serviceUuids
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
index acdb648..186431f 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
@@ -258,10 +258,10 @@
 
             try {
                 bluetoothLe.connectGatt(deviceConnection.bluetoothDevice) {
-                    Log.d(TAG, "connectGatt result: getServices() = ${getServices()}")
+                    Log.d(TAG, "connectGatt result: services() = $services")
 
                     deviceConnection.status = Status.CONNECTED
-                    deviceConnection.services = getServices()
+                    deviceConnection.services = services
                     launch(Dispatchers.Main) {
                         updateDeviceUI(deviceConnection)
                     }
diff --git a/browser/browser/api/current.txt b/browser/browser/api/current.txt
index 13659a7..02dd082 100644
--- a/browser/browser/api/current.txt
+++ b/browser/browser/api/current.txt
@@ -131,9 +131,9 @@
     field public static final String EXTRA_COLOR_SCHEME = "androidx.browser.customtabs.extra.COLOR_SCHEME";
     field public static final String EXTRA_COLOR_SCHEME_PARAMS = "androidx.browser.customtabs.extra.COLOR_SCHEME_PARAMS";
     field @Deprecated public static final String EXTRA_DEFAULT_SHARE_MENU_ITEM = "android.support.customtabs.extra.SHARE_MENU_ITEM";
+    field public static final String EXTRA_DISABLE_BACKGROUND_INTERACTION = "androidx.browser.customtabs.extra.DISABLE_BACKGROUND_INTERACTION";
     field public static final String EXTRA_DISABLE_BOOKMARKS_BUTTON = "org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_STAR_BUTTON";
     field public static final String EXTRA_DISABLE_DOWNLOAD_BUTTON = "org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON";
-    field public static final String EXTRA_ENABLE_BACKGROUND_INTERACTION = "androidx.browser.customtabs.extra.ENABLE_BACKGROUND_INTERACTION";
     field public static final String EXTRA_ENABLE_INSTANT_APPS = "android.support.customtabs.extra.EXTRA_ENABLE_INSTANT_APPS";
     field public static final String EXTRA_ENABLE_URLBAR_HIDING = "android.support.customtabs.extra.ENABLE_URLBAR_HIDING";
     field public static final String EXTRA_EXIT_ANIMATION_BUNDLE = "android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
diff --git a/browser/browser/api/restricted_current.txt b/browser/browser/api/restricted_current.txt
index ef43681..1e7b718 100644
--- a/browser/browser/api/restricted_current.txt
+++ b/browser/browser/api/restricted_current.txt
@@ -142,9 +142,9 @@
     field public static final String EXTRA_COLOR_SCHEME = "androidx.browser.customtabs.extra.COLOR_SCHEME";
     field public static final String EXTRA_COLOR_SCHEME_PARAMS = "androidx.browser.customtabs.extra.COLOR_SCHEME_PARAMS";
     field @Deprecated public static final String EXTRA_DEFAULT_SHARE_MENU_ITEM = "android.support.customtabs.extra.SHARE_MENU_ITEM";
+    field public static final String EXTRA_DISABLE_BACKGROUND_INTERACTION = "androidx.browser.customtabs.extra.DISABLE_BACKGROUND_INTERACTION";
     field public static final String EXTRA_DISABLE_BOOKMARKS_BUTTON = "org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_STAR_BUTTON";
     field public static final String EXTRA_DISABLE_DOWNLOAD_BUTTON = "org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON";
-    field public static final String EXTRA_ENABLE_BACKGROUND_INTERACTION = "androidx.browser.customtabs.extra.ENABLE_BACKGROUND_INTERACTION";
     field public static final String EXTRA_ENABLE_INSTANT_APPS = "android.support.customtabs.extra.EXTRA_ENABLE_INSTANT_APPS";
     field public static final String EXTRA_ENABLE_URLBAR_HIDING = "android.support.customtabs.extra.ENABLE_URLBAR_HIDING";
     field public static final String EXTRA_EXIT_ANIMATION_BUNDLE = "android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
index df73c25..877a2f5 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
@@ -185,11 +185,11 @@
             "androidx.browser.customtabs.extra.TRANSLATE_LANGUAGE_TAG";
 
     /**
-     * Extra that, when set to false, disables interactions with the background app
-     * when a Partial Custom Tab is launched.
+     * Extra tha disables interactions with the background app when a Partial Custom Tab
+     * is launched.
      */
-    public static final String EXTRA_ENABLE_BACKGROUND_INTERACTION =
-            "androidx.browser.customtabs.extra.ENABLE_BACKGROUND_INTERACTION";
+    public static final String EXTRA_DISABLE_BACKGROUND_INTERACTION =
+            "androidx.browser.customtabs.extra.DISABLE_BACKGROUND_INTERACTION";
 
     /**
      * Extra that enables the client to add an additional action button to the toolbar.
@@ -1173,11 +1173,11 @@
          * Enables the interactions with the background app when a Partial Custom Tab is launched.
          *
          * @param enabled Whether the background interaction is enabled.
-         * @see CustomTabsIntent#EXTRA_ENABLE_BACKGROUND_INTERACTION
+         * @see CustomTabsIntent#EXTRA_DISABLE_BACKGROUND_INTERACTION
          */
         @NonNull
         public Builder setBackgroundInteractionEnabled(boolean enabled) {
-            mIntent.putExtra(EXTRA_ENABLE_BACKGROUND_INTERACTION, enabled);
+            mIntent.putExtra(EXTRA_DISABLE_BACKGROUND_INTERACTION, !enabled);
             return this;
         }
 
@@ -1456,10 +1456,10 @@
 
     /**
      * @return Whether the background interaction is enabled.
-     * @see CustomTabsIntent#EXTRA_ENABLE_BACKGROUND_INTERACTION
+     * @see CustomTabsIntent#EXTRA_DISABLE_BACKGROUND_INTERACTION
      */
     public static boolean isBackgroundInteractionEnabled(@NonNull Intent intent) {
-        return intent.getBooleanExtra(EXTRA_ENABLE_BACKGROUND_INTERACTION, false);
+        return !intent.getBooleanExtra(EXTRA_DISABLE_BACKGROUND_INTERACTION, false);
     }
 
     /**
diff --git a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
index 08dea64..aee0f19 100644
--- a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
+++ b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
@@ -586,16 +586,17 @@
     @Test
     public void testBackgroundInteraction() {
         Intent intent = new CustomTabsIntent.Builder().build().intent;
-        assertFalse(CustomTabsIntent.isBackgroundInteractionEnabled(intent));
+        assertTrue(CustomTabsIntent.isBackgroundInteractionEnabled(intent));
 
         intent = new CustomTabsIntent.Builder()
-                .setBackgroundInteractionEnabled(false).build().intent;
-        assertFalse(CustomTabsIntent.isBackgroundInteractionEnabled(intent));
-
-        // The extra is set to true only when explicitly called to enable it.
-        intent = new CustomTabsIntent.Builder()
                 .setBackgroundInteractionEnabled(true).build().intent;
         assertTrue(CustomTabsIntent.isBackgroundInteractionEnabled(intent));
+
+        // The extra (EXTRA_DISABLE_BACKGROUND_INTERACTION) is set to true
+        // only when explicitly called to disable it.
+        intent = new CustomTabsIntent.Builder()
+                .setBackgroundInteractionEnabled(false).build().intent;
+        assertFalse(CustomTabsIntent.isBackgroundInteractionEnabled(intent));
     }
 
     @Test
diff --git a/buildSrc-tests/src/test/java/androidx/build/SourceJarTaskHelperTest.kt b/buildSrc-tests/src/test/java/androidx/build/SourceJarTaskHelperTest.kt
new file mode 100644
index 0000000..3e82e00
--- /dev/null
+++ b/buildSrc-tests/src/test/java/androidx/build/SourceJarTaskHelperTest.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.build
+
+import com.google.common.truth.Truth.assertThat
+import org.gradle.testfixtures.ProjectBuilder
+import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
+import org.junit.Test
+
+class SourceJarTaskHelperTest {
+    @Test
+    fun generateMetadata() {
+        val project = ProjectBuilder.builder().build()
+        project.plugins.apply(KotlinMultiplatformPluginWrapper::class.java)
+        val extension = project.multiplatformExtension!!
+        extension.jvm()
+        val commonMain = extension.sourceSets.getByName("commonMain")
+        val jvmMain = extension.sourceSets.getByName("jvmMain")
+        val extraMain = extension.sourceSets.create("extraMain")
+        extraMain.dependsOn(commonMain)
+        jvmMain.dependsOn(commonMain)
+        jvmMain.dependsOn(extraMain)
+
+        val result = createSourceSetMetadata(extension)
+        assertThat(result).isEqualTo("""
+        {
+          "sourceSets": [
+            {
+              "name": "commonMain",
+              "dependencies": [],
+              "analysisPlatform": "common"
+            },
+            {
+              "name": "extraMain",
+              "dependencies": [
+                "commonMain"
+              ],
+              "analysisPlatform": "jvm"
+            },
+            {
+              "name": "jvmMain",
+              "dependencies": [
+                "commonMain",
+                "extraMain"
+              ],
+              "analysisPlatform": "jvm"
+            }
+          ]
+        }
+        """.trimIndent())
+    }
+}
diff --git a/buildSrc/lint.xml b/buildSrc/lint.xml
index 749708c..1eaf525 100644
--- a/buildSrc/lint.xml
+++ b/buildSrc/lint.xml
@@ -28,7 +28,8 @@
         <ignore path="**/src/test/**" />
         <ignore path="**/src/androidTest/**" />
         <!-- Required for Kotlin multi-platform tests. -->
-        <ignore path="**/src/androidAndroidTest/**" />
+        <ignore path="**/src/androidInstrumentedTest/**" />
+        <ignore path="**/src/androidUnitTest/**" />
         <ignore path="**/src/jvmTest/**" />
         <ignore path="**/src/commonTest/**" />
         <!-- Required for AppSearch icing tests. -->
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 73b8580..1051dfc 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -460,6 +460,9 @@
                 it.artRewritingWorkaround()
             }
         }
+
+        project.buildOnServerDependsOnAssembleRelease()
+        project.buildOnServerDependsOnLint()
     }
 
     private fun configureWithTestPlugin(project: Project, androidXExtension: AndroidXExtension) {
@@ -473,6 +476,30 @@
         project.addToProjectMap(androidXExtension)
     }
 
+    private fun Project.buildOnServerDependsOnAssembleRelease() {
+        project.addToBuildOnServer("assembleRelease")
+    }
+
+    private fun Project.buildOnServerDependsOnLint() {
+        if (!project.usingMaxDepVersions()) {
+            project.agpVariants.all { variant ->
+                // in AndroidX, release and debug variants are essentially the same,
+                // so we don't run the lintRelease task on the build server
+                if (!variant.name.lowercase(Locale.getDefault()).contains("release")) {
+                    val taskName =
+                        "lint${variant.name.replaceFirstChar {
+                        if (it.isLowerCase()) {
+                            it.titlecase(Locale.getDefault())
+                        } else {
+                            it.toString()
+                        }
+                    }}"
+                    project.addToBuildOnServer(taskName)
+                }
+            }
+        }
+    }
+
     private fun HasAndroidTest.configureTests() {
         configureLicensePackaging()
         excludeVersionFilesFromTestApks()
@@ -595,6 +622,9 @@
         )
 
         project.addToProjectMap(androidXExtension)
+
+        project.buildOnServerDependsOnAssembleRelease()
+        project.buildOnServerDependsOnLint()
     }
 
     private fun configureWithJavaPlugin(project: Project, extension: AndroidXExtension) {
@@ -657,6 +687,8 @@
             configuration.resolutionStrategy.preferProjectModules()
         }
 
+        project.addToBuildOnServer("jar")
+
         project.addToProjectMap(extension)
     }
 
@@ -792,14 +824,20 @@
             File(project.buildDir, "../nativeBuildStaging")
     }
 
+    @Suppress("UnstableApiUsage") // finalizeDsl, minCompileSdkExtension
     private fun LibraryExtension.configureAndroidLibraryOptions(
         project: Project,
         androidXExtension: AndroidXExtension
     ) {
-        // Note, this should really match COMPILE_SDK_VERSION, however
-        // this API takes an integer and we are unable to set it to a
-        // pre-release SDK.
-        defaultConfig.aarMetadata.minCompileSdk = project.defaultAndroidConfig.targetSdk
+        // Propagate the compileSdk value into minCompileSdk. Don't propagate compileSdkExtension,
+        // since only one library actually depends on the extension APIs and they can explicitly
+        // declare that in their build.gradle. Note that when we're using a preview SDK, the value
+        // for compileSdk will be null and the resulting AAR metadata won't have a minCompileSdk --
+        // this is okay because AGP automatically embeds forceCompileSdkPreview in the AAR metadata
+        // and uses it instead of minCompileSdk.
+        project.extensions.findByType<LibraryAndroidComponentsExtension>()!!.finalizeDsl {
+            it.defaultConfig.aarMetadata.minCompileSdk = it.compileSdk
+        }
 
         // The full Guava artifact is very large, so they split off a special artifact containing a
         // standalone version of the commonly-used ListenableFuture interface. However, they also
@@ -864,7 +902,7 @@
         sourceSets
             .findByName("androidTest")!!
             .manifest
-            .srcFile("src/androidAndroidTest/AndroidManifest.xml")
+            .srcFile("src/androidInstrumentedTest/AndroidManifest.xml")
     }
 
     /** Sets the konan distribution url to the prebuilts directory. */
@@ -1222,9 +1260,9 @@
         ?.findByName("androidTest")
         ?.let { if (it.kotlin.files.isNotEmpty()) return true }
 
-    // check kotlin-multiplatform androidAndroidTest source set
+    // check kotlin-multiplatform androidInstrumentedTest source set
     multiplatformExtension?.apply {
-        sourceSets.findByName("androidAndroidTest")?.let {
+        sourceSets.findByName("androidInstrumentedTest")?.let {
             if (it.kotlin.files.isNotEmpty()) return true
         }
     }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index fc33853..9eb83d2 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -29,16 +29,12 @@
 import androidx.build.uptodatedness.TaskUpToDateValidator
 import androidx.build.uptodatedness.cacheEvenIfNoOutputs
 import com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
-import com.android.build.gradle.AppPlugin
-import com.android.build.gradle.LibraryPlugin
 import java.io.File
-import java.util.Locale
 import java.util.concurrent.ConcurrentHashMap
 import org.gradle.api.GradleException
 import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.api.artifacts.component.ModuleComponentSelector
-import org.gradle.api.plugins.JavaPlugin
 import org.gradle.api.plugins.JvmEcosystemPlugin
 import org.gradle.api.tasks.bundling.Zip
 import org.gradle.api.tasks.bundling.ZipEntryCompression
@@ -101,37 +97,6 @@
         }
 
         extra.set("projects", ConcurrentHashMap<String, String>())
-        subprojects { project ->
-            project.afterEvaluate {
-                if (
-                    project.plugins.hasPlugin(LibraryPlugin::class.java) ||
-                        project.plugins.hasPlugin(AppPlugin::class.java)
-                ) {
-
-                    buildOnServerTask.dependsOn("${project.path}:assembleRelease")
-                    if (!project.usingMaxDepVersions()) {
-                        project.agpVariants.all { variant ->
-                            // in AndroidX, release and debug variants are essentially the same,
-                            // so we don't run the lintRelease task on the build server
-                            if (!variant.name.lowercase(Locale.getDefault()).contains("release")) {
-                                val taskName =
-                                    "lint${variant.name.replaceFirstChar {
-                                    if (it.isLowerCase()) {
-                                        it.titlecase(Locale.getDefault())
-                                    } else {
-                                        it.toString()
-                                    }
-                                }}"
-                                buildOnServerTask.dependsOn("${project.path}:$taskName")
-                            }
-                        }
-                    }
-                }
-            }
-            project.plugins.withType(JavaPlugin::class.java) {
-                buildOnServerTask.dependsOn("${project.path}:jar")
-            }
-        }
 
         // NOTE: this task is used by the Github CI as well. If you make any changes here,
         // please update the .github/workflows files as well, if necessary.
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index a56b608..a307c9d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -345,6 +345,9 @@
             disable.add("LintError")
         }
 
+        // Disable a check that's only relevant for apps that ship to Play Store. (b/299278101)
+        disable.add("ExpiredTargetSdkVersion")
+
         // Reenable after b/238892319 is resolved
         disable.add("NotificationPermission")
 
@@ -367,9 +370,6 @@
         disable.add("RestrictedApi")
         fatal.add("RestrictedApiAndroidX")
 
-        // Disable until ag/19949626 goes in (b/261918265)
-        disable.add("MissingQuantity")
-
         // Provide stricter enforcement for project types intended to run on a device.
         if (extension.type.compilationTarget == CompilationTarget.DEVICE) {
             fatal.add("Assert")
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
index cd60cc3..9380bb9 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
@@ -443,6 +443,9 @@
         scm.url.set("https://cs.android.com/androidx/platform/frameworks/support")
         scm.connection.set(ANDROID_GIT_URL)
     }
+    pom.organization { org ->
+        org.name.set("The Android Open Source Project")
+    }
     pom.developers { devs ->
         devs.developer { dev -> dev.name.set("The Android Open Source Project") }
     }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
index 1e6ed89..04c4da3 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
@@ -258,7 +258,7 @@
             "commonMain" to
                 mapOf(
                     "name" to commonMain.name,
-                    "dependencies" to commonMain.dependsOn.map { it.name },
+                    "dependencies" to commonMain.dependsOn.map { it.name }.sorted(),
                     "analysisPlatform" to DokkaAnalysisPlatform.COMMON.jsonName
                 )
         )
@@ -267,13 +267,15 @@
             sourceSetsByName.getOrPut(it.name) {
                 mapOf(
                     "name" to it.name,
-                    "dependencies" to it.dependsOn.map { it.name },
+                    "dependencies" to it.dependsOn.map { it.name }.sorted(),
                     "analysisPlatform" to target.docsPlatform().jsonName
                 )
             }
         }
     }
-    val sourceSetMetadata = mutableMapOf("sourceSets" to sourceSetsByName.values)
+    val sourceSetMetadata = mapOf(
+        "sourceSets" to sourceSetsByName.keys.sorted().map { sourceSetsByName[it] }
+    )
     val gson = GsonBuilder().setPrettyPrinting().create()
     return gson.toJson(sourceSetMetadata)
 }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/VerifyDependencyVersionsTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/VerifyDependencyVersionsTask.kt
index c961f39..96da0c0 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/VerifyDependencyVersionsTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/VerifyDependencyVersionsTask.kt
@@ -178,6 +178,8 @@
     if (name.startsWith("androidTest")) return false
     if (name.startsWith("androidAndroidTest")) return false
     if (name.startsWith("androidCommonTest")) return false
+    if (name.startsWith("androidInstrumentedTest")) return false
+    if (name.startsWith("androidUnitTest")) return false
     if (name.startsWith("debug")) return false
     if (name.startsWith("androidDebug")) return false
     if (name.startsWith("release")) return false
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
index c7006f8..144ed14 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
@@ -34,6 +34,7 @@
 import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.InputFile
 import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
 import org.gradle.api.tasks.OutputDirectory
 import org.gradle.api.tasks.PathSensitive
 import org.gradle.api.tasks.PathSensitivity
@@ -49,6 +50,8 @@
 constructor(private val workerExecutor: WorkerExecutor, private val objects: ObjectFactory) :
     DefaultTask() {
 
+    @Internal lateinit var argsJsonFile: File
+
     @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
     abstract val projectStructureMetadataFile: RegularFileProperty
 
@@ -220,10 +223,8 @@
             )
 
         val json = gson.toJson(jsonMap)
-        val outputFile = File.createTempFile("dackkaArgs", ".json")
-        outputFile.deleteOnExit()
-        outputFile.writeText(json)
-        return outputFile
+        argsJsonFile.writeText(json)
+        return argsJsonFile
     }
 
     /**
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 e39904c2..e6ccb06 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -487,6 +487,10 @@
         val dackkaTask =
             project.tasks.register("docs", DackkaTask::class.java) { task ->
                 var taskStartTime: LocalDateTime? = null
+                task.argsJsonFile = File(
+                    project.rootProject.getDistributionDirectory(),
+                    "dackkaArgs-${project.name}.json"
+                )
                 task.apply {
                     dependsOn(unzipJvmSourcesTask)
                     dependsOn(unzipSamplesTask)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt b/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
index 669aa02..7b470d9 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
@@ -239,7 +239,7 @@
 
         task.taskExtension.set(
             object : DefaultSpdxSbomTaskExtension() {
-                override fun mapRepoUri(repoUri: URI, artifact: ModuleVersionIdentifier): URI {
+                override fun mapRepoUri(repoUri: URI?, artifact: ModuleVersionIdentifier): URI {
                     val uriString = repoUri.toString()
                     for (repo in repos) {
                         val ourRepoUrl = repo.key
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt b/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt
index c29f465..99ed273 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt
@@ -22,12 +22,12 @@
 
 const val BUILD_ON_SERVER_TASK = "buildOnServer"
 
-/** Configures the root project's buildOnServer task to run the specified task. */
+/** Configures the project's buildOnServer task to run the specified task. */
 fun <T : Task> Project.addToBuildOnServer(taskProvider: TaskProvider<T>) {
     tasks.named(BUILD_ON_SERVER_TASK).configure { it.dependsOn(taskProvider) }
 }
 
-/** Configures the root project's buildOnServer task to run the specified task. */
-fun <T : Task> Project.addToBuildOnServer(taskPath: String) {
+/** Configures the project's buildOnServer task to run the specified task. */
+fun Project.addToBuildOnServer(taskPath: String) {
     tasks.named(BUILD_ON_SERVER_TASK).configure { it.dependsOn(taskPath) }
 }
diff --git a/buildSrc/repos.gradle b/buildSrc/repos.gradle
index 9b5077d..297a520 100644
--- a/buildSrc/repos.gradle
+++ b/buildSrc/repos.gradle
@@ -56,6 +56,12 @@
             mavenPom()
             artifact()
         }
+        if (metalavaRepoOverride != null) {
+            // When using custom metalava repo, do not resolve metalava artifacts from this repo
+            content {
+                excludeGroup "com.android.tools.metalava"
+            }
+        }
     }
     if (System.getenv("ALLOW_PUBLIC_REPOS") != null || System.getProperty("ALLOW_PUBLIC_REPOS") != null) {
         handler.mavenCentral()
diff --git a/busytown/impl/build-metalava-and-androidx.sh b/busytown/impl/build-metalava-and-androidx.sh
index b609c1a..160f418 100755
--- a/busytown/impl/build-metalava-and-androidx.sh
+++ b/busytown/impl/build-metalava-and-androidx.sh
@@ -32,7 +32,7 @@
 
 function buildMetalava() {
   METALAVA_BUILD_LOG="$OUT_DIR/metalava.log"
-  if $gw -p $METALAVA_DIR createArchive --stacktrace --no-daemon > "$METALAVA_BUILD_LOG" 2>&1; then
+  if $gw -p $METALAVA_DIR publish --stacktrace --no-daemon > "$METALAVA_BUILD_LOG" 2>&1; then
     echo built metalava successfully
   else
     cat "$METALAVA_BUILD_LOG" >&2
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index f8d6362..6fd951c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -210,6 +210,14 @@
         return setOf(SDR)
     }
 
+    override fun isPreviewStabilizationSupported(): Boolean {
+        return false
+    }
+
+    override fun isVideoStabilizationSupported(): Boolean {
+        return false
+    }
+
     private fun profileSetToDynamicRangeSet(profileSet: Set<Long>): Set<DynamicRange> {
         return profileSet.map { profileToDynamicRange(it) }.toSet()
     }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
index e8b59257..72d3930 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
@@ -122,6 +122,7 @@
      * @param newUseCaseConfigsSupportedSizeMap map of configurations of the use cases to the
      *                                          supported sizes list that will be given a
      *                                          suggested stream specification
+     * @param isPreviewStabilizationOn          whether the preview stabilization is enabled.
      * @return map of suggested stream specifications for given use cases
      * @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
      *                                  there isn't a supported combination of surfaces
@@ -132,7 +133,8 @@
         cameraMode: Int,
         cameraId: String,
         existingSurfaces: List<AttachedSurfaceInfo>,
-        newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>
+        newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>,
+        isPreviewStabilizationOn: Boolean
     ): Pair<Map<UseCaseConfig<*>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>> {
 
         if (!checkIfSupportedCombinationExist(cameraId)) {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
index 8361fb3..7d1c04f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -81,13 +81,17 @@
         when (captureType) {
             CaptureType.IMAGE_CAPTURE,
             CaptureType.PREVIEW,
+                // Uses TEMPLATE_PREVIEW instead of TEMPLATE_RECORD for StreamSharing. Since there
+                // is a issue that captured results being stretched when requested for recording on
+                // some models, it would be safer to request for preview, which is also better
+                // tested. More detail please see b/297167569.
+            CaptureType.STREAM_SHARING,
             CaptureType.METERING_REPEATING,
             CaptureType.IMAGE_ANALYSIS -> sessionBuilder.setTemplateType(
                 CameraDevice.TEMPLATE_PREVIEW
             )
 
-            CaptureType.VIDEO_CAPTURE,
-            CaptureType.STREAM_SHARING -> sessionBuilder.setTemplateType(
+            CaptureType.VIDEO_CAPTURE -> sessionBuilder.setTemplateType(
                 CameraDevice.TEMPLATE_RECORD
             )
         }
@@ -102,9 +106,13 @@
 
             CaptureType.PREVIEW,
             CaptureType.IMAGE_ANALYSIS,
-            CaptureType.VIDEO_CAPTURE,
+                // Uses TEMPLATE_PREVIEW instead of TEMPLATE_RECORD for StreamSharing to align with
+                // SessionConfig's setup. More detail please see b/297167569.
             CaptureType.STREAM_SHARING,
             CaptureType.METERING_REPEATING ->
+                captureBuilder.templateType = CameraDevice.TEMPLATE_PREVIEW
+
+            CaptureType.VIDEO_CAPTURE ->
                 captureBuilder.templateType = CameraDevice.TEMPLATE_RECORD
         }
         mutableConfig.insertOption(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/GuaranteedConfigurationsUtil.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/GuaranteedConfigurationsUtil.kt
index ce74b04..33fac2e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/GuaranteedConfigurationsUtil.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/GuaranteedConfigurationsUtil.kt
@@ -718,6 +718,56 @@
         return surfaceCombinations
     }
 
+    /**
+     * Returns the minimally guaranteed stream combinations when one or more
+     * streams are configured as a 10-bit input.
+     */
+    @JvmStatic
+    fun get10BitSupportedCombinationList(): List<SurfaceCombination> {
+        return listOf(
+            // (PRIV, MAXIMUM)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.MAXIMUM))
+            },
+            // (YUV, MAXIMUM)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.YUV, ConfigSize.MAXIMUM))
+            },
+            // (PRIV, PREVIEW) + (JPEG, MAXIMUM)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.JPEG, ConfigSize.MAXIMUM))
+            },
+            // (PRIV, PREVIEW) + (YUV, MAXIMUM)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.YUV, ConfigSize.MAXIMUM))
+            },
+            // (YUV, PREVIEW) + (YUV, MAXIMUM)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.YUV, ConfigSize.PREVIEW))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.YUV, ConfigSize.MAXIMUM))
+            },
+            // (PRIV, PREVIEW) + (PRIV, RECORD)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.RECORD))
+            },
+            // (PRIV, PREVIEW) + (PRIV, RECORD) + (YUV, RECORD)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.RECORD))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.YUV, ConfigSize.RECORD))
+            },
+            // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.RECORD))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.JPEG, ConfigSize.RECORD))
+            },
+        )
+    }
+
     @JvmStatic
     fun generateConcurrentSupportedCombinationList(): List<SurfaceCombination> {
         val surfaceCombinations: MutableList<SurfaceCombination> = arrayListOf()
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index daa54a8..2122ec2 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -18,20 +18,19 @@
 
 import android.annotation.SuppressLint
 import android.content.Context
+import android.content.pm.PackageManager
 import android.content.pm.PackageManager.FEATURE_CAMERA_CONCURRENT
 import android.graphics.ImageFormat
-import android.graphics.Point
 import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.params.StreamConfigurationMap
-import android.hardware.display.DisplayManager
 import android.media.CamcorderProfile
 import android.media.MediaRecorder
 import android.os.Build
 import android.util.Pair
+import android.util.Range
 import android.util.Rational
 import android.util.Size
-import android.view.Display
 import androidx.annotation.RequiresApi
 import androidx.annotation.VisibleForTesting
 import androidx.camera.camera2.pipe.CameraMetadata
@@ -41,6 +40,8 @@
 import androidx.camera.camera2.pipe.integration.compat.workaround.ResolutionCorrector
 import androidx.camera.camera2.pipe.integration.compat.workaround.TargetAspectRatio
 import androidx.camera.camera2.pipe.integration.impl.DisplayInfoManager
+import androidx.camera.camera2.pipe.integration.internal.DynamicRangeResolver
+import androidx.camera.core.DynamicRange
 import androidx.camera.core.impl.AttachedSurfaceInfo
 import androidx.camera.core.impl.CameraMode
 import androidx.camera.core.impl.EncoderProfilesProxy
@@ -60,6 +61,8 @@
 import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA
 import java.util.Arrays
 import java.util.Collections
+import kotlin.math.floor
+import kotlin.math.min
 
 /**
  * Camera device supported surface configuration combinations
@@ -85,22 +88,22 @@
     private val concurrentSurfaceCombinations: MutableList<SurfaceCombination> = mutableListOf()
     private val surfaceCombinations: MutableList<SurfaceCombination> = mutableListOf()
     private val ultraHighSurfaceCombinations: MutableList<SurfaceCombination> = mutableListOf()
-    private val cameraModeToSupportedCombinationsMap: MutableMap<Int, List<SurfaceCombination>> =
-        mutableMapOf()
+    private val featureSettingsToSupportedCombinationsMap:
+        MutableMap<FeatureSettings, List<SurfaceCombination>> = mutableMapOf()
+    private val surfaceCombinations10Bit: MutableList<SurfaceCombination> = mutableListOf()
     private var isRawSupported = false
     private var isBurstCaptureSupported = false
     private var isConcurrentCameraModeSupported = false
     private var isUltraHighResolutionSensorSupported = false
     internal lateinit var surfaceSizeDefinition: SurfaceSizeDefinition
     private val surfaceSizeDefinitionFormats = mutableListOf<Int>()
-    private val displayManager: DisplayManager =
-        (context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager)
     private val streamConfigurationMapCompat = getStreamConfigurationMapCompat()
     private val extraSupportedSurfaceCombinationsContainer =
         ExtraSupportedSurfaceCombinationsContainer()
     private val displayInfoManager = DisplayInfoManager(context)
     private val resolutionCorrector = ResolutionCorrector()
     private val targetAspectRatio: TargetAspectRatio = TargetAspectRatio()
+    private val dynamicRangeResolver: DynamicRangeResolver = DynamicRangeResolver(cameraMetadata)
 
     init {
         checkCapabilities()
@@ -113,6 +116,10 @@
         if (isConcurrentCameraModeSupported) {
             generateConcurrentSupportedCombinationList()
         }
+
+        if (dynamicRangeResolver.is10BitDynamicRangeSupported()) {
+            generate10BitSupportedCombinationList()
+        }
         generateSurfaceSizeDefinition()
     }
 
@@ -120,48 +127,50 @@
      * Check whether the input surface configuration list is under the capability of any combination
      * of this object.
      *
-     * @param cameraMode        the working camera mode.
+     * @param featureSettings  the settings for the camera's features/capabilities.
      * @param surfaceConfigList the surface configuration list to be compared
+     *
      * @return the check result that whether it could be supported
      */
     fun checkSupported(
-        cameraMode: Int,
+        featureSettings: FeatureSettings,
         surfaceConfigList: List<SurfaceConfig>
     ): Boolean {
-        // TODO(b/262772650): camera-pipe support for concurrent camera
-        val targetSurfaceCombinations = getSurfaceCombinationsByCameraMode(cameraMode)
-        for (surfaceCombination in targetSurfaceCombinations) {
-            if (surfaceCombination
-                    .getOrderedSupportedSurfaceConfigList(surfaceConfigList) != null
-            ) {
-                return true
-            }
+        return getSurfaceCombinationsByFeatureSettings(featureSettings).any {
+            it.getOrderedSupportedSurfaceConfigList(surfaceConfigList) != null
         }
-        return false
     }
 
     /**
-     * Returns the supported surface combinations according to the specified camera mode.
+     * Returns the supported surface combinations according to the specified feature
+     * settings.
      */
-    private fun getSurfaceCombinationsByCameraMode(
-        @CameraMode.Mode cameraMode: Int
+    private fun getSurfaceCombinationsByFeatureSettings(
+        featureSettings: FeatureSettings
     ): List<SurfaceCombination> {
-        if (cameraModeToSupportedCombinationsMap.containsKey(cameraMode)) {
-            return cameraModeToSupportedCombinationsMap[cameraMode]!!
+        if (featureSettingsToSupportedCombinationsMap.containsKey(featureSettings)) {
+            return featureSettingsToSupportedCombinationsMap[featureSettings]!!
         }
         var supportedSurfaceCombinations: MutableList<SurfaceCombination> = mutableListOf()
-        when (cameraMode) {
-            CameraMode.CONCURRENT_CAMERA -> supportedSurfaceCombinations =
-                concurrentSurfaceCombinations
+        if (featureSettings.requiredMaxBitDepth == DynamicRange.BIT_DEPTH_8_BIT) {
+            when (featureSettings.cameraMode) {
+                CameraMode.CONCURRENT_CAMERA -> supportedSurfaceCombinations =
+                    concurrentSurfaceCombinations
 
-            CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA -> {
-                supportedSurfaceCombinations.addAll(ultraHighSurfaceCombinations)
-                supportedSurfaceCombinations.addAll(surfaceCombinations)
+                CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA -> {
+                    supportedSurfaceCombinations.addAll(ultraHighSurfaceCombinations)
+                    supportedSurfaceCombinations.addAll(surfaceCombinations)
+                }
+
+                else -> supportedSurfaceCombinations.addAll(surfaceCombinations)
             }
-
-            else -> supportedSurfaceCombinations.addAll(surfaceCombinations)
+        } else if (featureSettings.requiredMaxBitDepth == DynamicRange.BIT_DEPTH_10_BIT) {
+            // For 10-bit outputs, only the default camera mode is currently supported.
+            if (featureSettings.cameraMode == CameraMode.DEFAULT) {
+                supportedSurfaceCombinations.addAll(surfaceCombinations10Bit)
+            }
         }
-        cameraModeToSupportedCombinationsMap[cameraMode] = supportedSurfaceCombinations
+        featureSettingsToSupportedCombinationsMap[featureSettings] = supportedSurfaceCombinations
         return supportedSurfaceCombinations
     }
 
@@ -188,7 +197,7 @@
      * Finds the suggested stream specification of the newly added UseCaseConfig.
      *
      * @param cameraMode        the working camera mode.
-     * @param existingSurfaces  the existing surfaces.
+     * @param attachedSurfaces  the existing surfaces.
      * @param newUseCaseConfigsSupportedSizeMap newly added UseCaseConfig to supported output sizes
      * map.
      * @return the suggested stream specs, which is a mapping from UseCaseConfig to the suggested
@@ -198,15 +207,34 @@
      */
     fun getSuggestedStreamSpecifications(
         cameraMode: Int,
-        existingSurfaces: List<AttachedSurfaceInfo>,
+        attachedSurfaces: List<AttachedSurfaceInfo>,
         newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>
     ): Pair<Map<UseCaseConfig<*>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>> {
+        // Refresh Preview Size based on current display configurations.
         refreshPreviewSize()
-        val surfaceConfigs: MutableList<SurfaceConfig> = ArrayList()
-        for (scc in existingSurfaces) {
+        val surfaceConfigs: MutableList<SurfaceConfig> = mutableListOf()
+        for (scc in attachedSurfaces) {
             surfaceConfigs.add(scc.surfaceConfig)
         }
         val newUseCaseConfigs = newUseCaseConfigsSupportedSizeMap.keys.toList()
+
+        // Get the index order list by the use case priority for finding stream configuration
+        val useCasesPriorityOrder = getUseCasesPriorityOrder(newUseCaseConfigs)
+        val resolvedDynamicRanges = dynamicRangeResolver.resolveAndValidateDynamicRanges(
+            attachedSurfaces,
+            newUseCaseConfigs, useCasesPriorityOrder
+        )
+        val requiredMaxBitDepth: Int = getRequiredMaxBitDepth(resolvedDynamicRanges)
+        val featureSettings = FeatureSettings(cameraMode, requiredMaxBitDepth)
+        require(
+            !(cameraMode != CameraMode.DEFAULT &&
+                requiredMaxBitDepth == DynamicRange.BIT_DEPTH_10_BIT)
+        ) {
+            "No supported surface combination is " +
+                "found for camera device - Id : $cameraId. 10 bit dynamic range is not " +
+                "currently supported in ${CameraMode.toLabelString(cameraMode)} camera mode."
+        }
+
         // Use the small size (640x480) for new use cases to check whether there is any possible
         // supported combination first
         for (useCaseConfig in newUseCaseConfigs) {
@@ -220,83 +248,476 @@
             )
         }
 
-        if (!checkSupported(cameraMode, surfaceConfigs)) {
+        if (!checkSupported(featureSettings, surfaceConfigs)) {
             throw java.lang.IllegalArgumentException(
                 "No supported surface combination is found for camera device - Id : " + cameraId +
                     ".  May be attempting to bind too many use cases. " + "Existing surfaces: " +
-                    existingSurfaces + " New configs: " + newUseCaseConfigs
+                    attachedSurfaces + " New configs: " + newUseCaseConfigs
             )
         }
-        // Get the index order list by the use case priority for finding stream configuration
-        val useCasesPriorityOrder: List<Int> = getUseCasesPriorityOrder(
-            newUseCaseConfigs
+
+        val targetFpsRange =
+            getTargetFpsRange(attachedSurfaces, newUseCaseConfigs, useCasesPriorityOrder)
+        val maxSupportedFps = getMaxSupportedFps(attachedSurfaces)
+
+        val bestSizesAndFps = findBestSizesAndFps(
+            newUseCaseConfigsSupportedSizeMap,
+            attachedSurfaces,
+            newUseCaseConfigs,
+            maxSupportedFps,
+            useCasesPriorityOrder,
+            targetFpsRange,
+            featureSettings
         )
-        val supportedOutputSizesList: MutableList<List<Size>> = ArrayList()
+
+        val suggestedStreamSpecMap = generateSuggestedStreamSpecMap(
+            bestSizesAndFps.first,
+            targetFpsRange,
+            bestSizesAndFps.second,
+            newUseCaseConfigs,
+            useCasesPriorityOrder,
+            resolvedDynamicRanges,
+        )
+
+        return Pair.create(suggestedStreamSpecMap, mapOf<AttachedSurfaceInfo, StreamSpec>())
+    }
+
+    private fun getSupportedOutputSizesList(
+        newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        useCasesPriorityOrder: List<Int>,
+    ): List<List<Size>> {
+        val supportedOutputSizesList: MutableList<List<Size>> = mutableListOf()
 
         // Collect supported output sizes for all use cases
         for (index in useCasesPriorityOrder) {
-            var supportedOutputSizes: List<Size> =
-                newUseCaseConfigsSupportedSizeMap[newUseCaseConfigs[index]]!!
+            var supportedOutputSizes = newUseCaseConfigsSupportedSizeMap[newUseCaseConfigs[index]]!!
             supportedOutputSizes = applyResolutionSelectionOrderRelatedWorkarounds(
                 supportedOutputSizes,
                 newUseCaseConfigs[index].inputFormat
             )
             supportedOutputSizesList.add(supportedOutputSizes)
         }
-        // Get all possible size arrangements
-        val allPossibleSizeArrangements: List<List<Size>> = getAllPossibleSizeArrangements(
-            supportedOutputSizesList
-        )
+        return supportedOutputSizesList
+    }
 
-        var suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec>? = null
+    private fun getTargetFpsRange(
+        attachedSurfaces: List<AttachedSurfaceInfo>,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        useCasesPriorityOrder: List<Int>
+    ): Range<Int>? {
+        var targetFrameRateForConfig: Range<Int>? = null
+        for (attachedSurfaceInfo in attachedSurfaces) {
+            // init target fps range for new configs from existing surfaces
+            targetFrameRateForConfig = getUpdatedTargetFrameRate(
+                attachedSurfaceInfo.targetFrameRate,
+                targetFrameRateForConfig
+            )
+        }
+        // update target fps for new configs using new use cases' priority order
+        for (index in useCasesPriorityOrder) {
+            targetFrameRateForConfig = getUpdatedTargetFrameRate(
+                newUseCaseConfigs[index].getTargetFrameRate(null),
+                targetFrameRateForConfig
+            )
+        }
+        return targetFrameRateForConfig
+    }
+
+    private fun getMaxSupportedFps(
+        attachedSurfaces: List<AttachedSurfaceInfo>,
+    ): Int {
+        var existingSurfaceFrameRateCeiling = Int.MAX_VALUE
+        for (attachedSurfaceInfo in attachedSurfaces) {
+            // get the fps ceiling for existing surfaces
+            existingSurfaceFrameRateCeiling = getUpdatedMaximumFps(
+                existingSurfaceFrameRateCeiling,
+                attachedSurfaceInfo.imageFormat, attachedSurfaceInfo.size
+            )
+        }
+        return existingSurfaceFrameRateCeiling
+    }
+
+    private fun findBestSizesAndFps(
+        newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>,
+        attachedSurfaces: List<AttachedSurfaceInfo>,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        existingSurfaceFrameRateCeiling: Int,
+        useCasesPriorityOrder: List<Int>,
+        targetFrameRateForConfig: Range<Int>?,
+        featureSettings: FeatureSettings
+    ): Pair<List<Size>, Int> {
+        var bestSizes: List<Size>? = null
+        var bestConfigMaxFps = Int.MAX_VALUE
+        val allPossibleSizeArrangements = getAllPossibleSizeArrangements(
+            getSupportedOutputSizesList(
+                newUseCaseConfigsSupportedSizeMap,
+                newUseCaseConfigs,
+                useCasesPriorityOrder
+            )
+        )
         // Transform use cases to SurfaceConfig list and find the first (best) workable combination
         for (possibleSizeList in allPossibleSizeArrangements) {
             // Attach SurfaceConfig of original use cases since it will impact the new use cases
-            val surfaceConfigList: MutableList<SurfaceConfig> = ArrayList()
-            for (sc in existingSurfaces) {
-                surfaceConfigList.add(sc.surfaceConfig)
-            }
-
-            // Attach SurfaceConfig of new use cases
-            for (i in possibleSizeList.indices) {
-                val size = possibleSizeList[i]
-                val newUseCase = newUseCaseConfigs[useCasesPriorityOrder[i]]
-                surfaceConfigList.add(
-                    SurfaceConfig.transformSurfaceConfig(
-                        cameraMode,
-                        newUseCase.inputFormat,
-                        size,
-                        getUpdatedSurfaceSizeDefinitionByFormat(newUseCase.inputFormat)
-                    )
-                )
-            }
-
-            // Check whether the SurfaceConfig combination can be supported
-            if (checkSupported(cameraMode, surfaceConfigList)) {
-                suggestedStreamSpecMap = HashMap()
-                for (useCaseConfig in newUseCaseConfigs) {
-                    suggestedStreamSpecMap.put(
-                        useCaseConfig,
-                        StreamSpec.builder(
-                            possibleSizeList[useCasesPriorityOrder.indexOf(
-                                newUseCaseConfigs.indexOf(useCaseConfig)
-                            )]
-                        ).build()
-                    )
+            val surfaceConfigList = getSurfaceConfigList(
+                featureSettings.cameraMode,
+                attachedSurfaces, possibleSizeList, newUseCaseConfigs,
+                useCasesPriorityOrder
+            )
+            val currentConfigFrameRateCeiling = getCurrentConfigFrameRateCeiling(
+                possibleSizeList, newUseCaseConfigs,
+                useCasesPriorityOrder, existingSurfaceFrameRateCeiling
+            )
+            var isConfigFrameRateAcceptable = true
+            if (targetFrameRateForConfig != null) {
+                if (existingSurfaceFrameRateCeiling > currentConfigFrameRateCeiling &&
+                    currentConfigFrameRateCeiling < targetFrameRateForConfig.lower
+                ) {
+                    // if the max fps before adding new use cases supports our target fps range
+                    // BUT the max fps of the new configuration is below
+                    // our target fps range, we'll want to check the next configuration until we
+                    // get one that supports our target FPS
+                    isConfigFrameRateAcceptable = false
                 }
-                break
+            }
+
+            // only change the saved config if you get another that has a better max fps
+            if (checkSupported(featureSettings, surfaceConfigList)) {
+                // if we have a configuration where the max fps is acceptable for our target, break
+                if (isConfigFrameRateAcceptable) {
+                    bestConfigMaxFps = currentConfigFrameRateCeiling
+                    bestSizes = possibleSizeList
+                    break
+                }
+                // if the config is supported by the device but doesn't meet the target frame rate,
+                // save the config
+                if (bestConfigMaxFps == Int.MAX_VALUE) {
+                    bestConfigMaxFps = currentConfigFrameRateCeiling
+                    bestSizes = possibleSizeList
+                } else if (bestConfigMaxFps < currentConfigFrameRateCeiling) {
+                    // only change the saved config if the max fps is better
+                    bestConfigMaxFps = currentConfigFrameRateCeiling
+                    bestSizes = possibleSizeList
+                }
             }
         }
-        if (suggestedStreamSpecMap == null) {
-            throw java.lang.IllegalArgumentException(
-                "No supported surface combination is found for camera device - Id : " +
-                    cameraId + " and Hardware level: " + hardwareLevel +
-                    ". May be the specified resolution is too large and not supported." +
-                    " Existing surfaces: " + existingSurfaces +
-                    " New configs: " + newUseCaseConfigs
+        require(bestSizes != null) {
+            "No supported surface combination is found for camera device - Id : $cameraId " +
+                "and Hardware level: $hardwareLevel. " +
+                "May be the specified resolution is too large and not supported. " +
+                "Existing surfaces: $attachedSurfaces. New configs: $newUseCaseConfigs."
+        }
+        return Pair(bestSizes, bestConfigMaxFps)
+    }
+
+    private fun generateSuggestedStreamSpecMap(
+        bestSizes: List<Size>,
+        targetFpsRange: Range<Int>?,
+        bestConfigMaxFps: Int,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        useCasesPriorityOrder: List<Int>,
+        resolvedDynamicRanges: Map<UseCaseConfig<*>, DynamicRange>,
+    ): Map<UseCaseConfig<*>, StreamSpec> {
+        val suggestedStreamSpecMap = mutableMapOf<UseCaseConfig<*>, StreamSpec>()
+        var targetFrameRateForDevice: Range<Int>? = null
+        if (targetFpsRange != null) {
+            targetFrameRateForDevice = getClosestSupportedDeviceFrameRate(
+                targetFpsRange,
+                bestConfigMaxFps
             )
         }
-        return Pair.create(suggestedStreamSpecMap, mapOf<AttachedSurfaceInfo, StreamSpec>())
+        for ((index, useCaseConfig) in newUseCaseConfigs.withIndex()) {
+            val resolutionForUseCase =
+                bestSizes[
+                    useCasesPriorityOrder.indexOf(index)]
+            val streamSpecBuilder = StreamSpec.builder(resolutionForUseCase)
+                .setDynamicRange(
+                    checkNotNull(resolvedDynamicRanges[useCaseConfig])
+                )
+            if (targetFrameRateForDevice != null) {
+                streamSpecBuilder.setExpectedFrameRateRange(targetFrameRateForDevice)
+            }
+            suggestedStreamSpecMap[useCaseConfig] = streamSpecBuilder.build()
+        }
+        return suggestedStreamSpecMap
+    }
+
+    private fun getRequiredMaxBitDepth(
+        resolvedDynamicRanges: Map<UseCaseConfig<*>, DynamicRange>
+    ): Int {
+        for (dynamicRange in resolvedDynamicRanges.values) {
+            if (dynamicRange.bitDepth == DynamicRange.BIT_DEPTH_10_BIT) {
+                return DynamicRange.BIT_DEPTH_10_BIT
+            }
+        }
+        return DynamicRange.BIT_DEPTH_8_BIT
+    }
+
+    private fun getSurfaceConfigList(
+        @CameraMode.Mode cameraMode: Int,
+        attachedSurfaces: List<AttachedSurfaceInfo>,
+        possibleSizeList: List<Size>,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        useCasesPriorityOrder: List<Int>,
+    ): List<SurfaceConfig> {
+        val surfaceConfigList: MutableList<SurfaceConfig> = mutableListOf()
+        for (attachedSurfaceInfo in attachedSurfaces) {
+            surfaceConfigList.add(attachedSurfaceInfo.surfaceConfig)
+        }
+
+        // Attach SurfaceConfig of new use cases
+        for ((i, size) in possibleSizeList.withIndex()) {
+            val newUseCase = newUseCaseConfigs[useCasesPriorityOrder[i]]
+            val imageFormat = newUseCase.inputFormat
+            // add new use case/size config to list of surfaces
+            val surfaceConfig = SurfaceConfig.transformSurfaceConfig(
+                cameraMode,
+                imageFormat,
+                size,
+                getUpdatedSurfaceSizeDefinitionByFormat(imageFormat)
+            )
+            surfaceConfigList.add(surfaceConfig)
+        }
+        return surfaceConfigList
+    }
+
+    private fun getCurrentConfigFrameRateCeiling(
+        possibleSizeList: List<Size>,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        useCasesPriorityOrder: List<Int>,
+        currentConfigFrameRateCeiling: Int,
+    ): Int {
+        var newConfigFrameRateCeiling: Int = currentConfigFrameRateCeiling
+        // Attach SurfaceConfig of new use cases
+        for ((i, size) in possibleSizeList.withIndex()) {
+            val newUseCase = newUseCaseConfigs[useCasesPriorityOrder[i]]
+            // get the maximum fps of the new surface and update the maximum fps of the
+            // proposed configuration
+            newConfigFrameRateCeiling = getUpdatedMaximumFps(
+                newConfigFrameRateCeiling,
+                newUseCase.inputFormat,
+                size
+            )
+        }
+        return newConfigFrameRateCeiling
+    }
+
+    private fun getMaxFrameRate(
+        imageFormat: Int,
+        size: Size?
+    ): Int {
+        var maxFrameRate = 0
+        try {
+            val minFrameDuration = getStreamConfigurationMapCompat().getOutputMinFrameDuration(
+                imageFormat,
+                size
+            ) ?: return 0
+            maxFrameRate = floor(1_000_000_000.0 / minFrameDuration + 0.05).toInt()
+        } catch (e1: IllegalArgumentException) {
+            // TODO: this try catch is in place for the rare that a surface config has a size
+            //  incompatible for getOutputMinFrameDuration...  put into a Quirk
+        }
+        return maxFrameRate
+    }
+
+    /**
+     *
+     * @param range
+     * @return the length of the range
+     */
+    private fun getRangeLength(range: Range<Int>): Int {
+        return range.upper - range.lower + 1
+    }
+
+    /**
+     * @return the distance between the nearest limits of two non-intersecting ranges
+     */
+    private fun getRangeDistance(firstRange: Range<Int>, secondRange: Range<Int>): Int {
+        require(
+            !firstRange.contains(secondRange.upper) &&
+                !firstRange.contains(secondRange.lower)
+        ) { "Ranges must not intersect" }
+        return if (firstRange.lower > secondRange.upper) {
+            firstRange.lower - secondRange.upper
+        } else {
+            secondRange.lower - firstRange.upper
+        }
+    }
+
+    /**
+     * @param targetFps the target frame rate range used while comparing to device-supported ranges
+     * @param storedRange the device-supported range that is currently saved and intersects with
+     * targetFps
+     * @param newRange a new potential device-supported range that intersects with targetFps
+     * @return the device-supported range that better matches the target fps
+     */
+    private fun compareIntersectingRanges(
+        targetFps: Range<Int>,
+        storedRange: Range<Int>,
+        newRange: Range<Int>
+    ): Range<Int> {
+        // TODO(b/272075984): some ranges may may have a larger intersection but may also have an
+        //  excessively large portion that is non-intersecting. Will want to do further
+        //  investigation to find a more optimized way to decide when a potential range has too
+        //  much non-intersecting value and discard it
+        val storedIntersectionsize =
+            getRangeLength(storedRange.intersect(targetFps)).toDouble()
+        val newIntersectionSize = getRangeLength(newRange.intersect(targetFps)).toDouble()
+        val newRangeRatio = newIntersectionSize / getRangeLength(newRange)
+        val storedRangeRatio = storedIntersectionsize / getRangeLength(storedRange)
+        if (newIntersectionSize > storedIntersectionsize) {
+            // if new, the new range must have at least 50% of its range intersecting, OR has a
+            // larger percentage of intersection than the previous stored range
+            if (newRangeRatio >= .5 || newRangeRatio >= storedRangeRatio) {
+                return newRange
+            }
+        } else if (newIntersectionSize == storedIntersectionsize) {
+            // if intersecting ranges have same length... pick the one that has the higher
+            // intersection ratio
+            if (newRangeRatio > storedRangeRatio) {
+                return newRange
+            } else if (newRangeRatio == storedRangeRatio && newRange.lower > storedRange.lower
+            ) {
+                // if equal intersection size AND ratios pick the higher range
+                return newRange
+            }
+        } else if (storedRangeRatio < .5 && newRangeRatio > storedRangeRatio
+        ) {
+            // if the new one has a smaller range... only change if existing has an intersection
+            // ratio < 50% and the new one has an intersection ratio > than the existing one
+            return newRange
+        }
+        return storedRange
+    }
+
+    /**
+     * Finds a frame rate range supported by the device that is closest to the target frame rate
+     *
+     * @param targetFrameRate the Target Frame Rate resolved from all current existing surfaces
+     * and incoming new use cases
+     * @return a frame rate range supported by the device that is closest to targetFrameRate
+     */
+    private fun getClosestSupportedDeviceFrameRate(
+        targetFrameRate: Range<Int>,
+        maxFps: Int
+    ): Range<Int> {
+        var newTargetFrameRate = targetFrameRate
+        // get all fps ranges supported by device
+        val availableFpsRanges =
+            cameraMetadata[CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES]
+                ?: return StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
+        // if  whole target frame rate range > maxFps of configuration, the target for this
+        // calculation will be [max,max].
+
+        // if the range is partially larger than  maxFps, the target for this calculation will be
+        // [target.lower, max] for the sake of this calculation
+        newTargetFrameRate = Range(
+            min(newTargetFrameRate.lower, maxFps),
+            min(newTargetFrameRate.upper, maxFps)
+        )
+        var bestRange = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
+        var currentIntersectSize = 0
+        for (potentialRange in availableFpsRanges) {
+            // ignore ranges completely larger than configuration's maximum fps
+            if (maxFps < potentialRange.lower) {
+                continue
+            }
+            if (bestRange == StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED) {
+                bestRange = potentialRange
+            }
+            // take if range is a perfect match
+            if (potentialRange == newTargetFrameRate) {
+                bestRange = potentialRange
+                break
+            }
+            try {
+                // bias towards a range that intersects on the upper end
+                val newIntersection = potentialRange.intersect(newTargetFrameRate)
+                val newIntersectSize: Int = getRangeLength(
+                    newIntersection
+                )
+                // if this range intersects our target + no other range was already
+                if (currentIntersectSize == 0) {
+                    bestRange = potentialRange
+                    currentIntersectSize = newIntersectSize
+                } else if (newIntersectSize >= currentIntersectSize) {
+                    // if the currently stored range + new range both intersect, check to see
+                    // which one should be picked over the other
+                    bestRange = compareIntersectingRanges(
+                        newTargetFrameRate, bestRange,
+                        potentialRange
+                    )
+                    currentIntersectSize = getRangeLength(
+                        newTargetFrameRate.intersect(bestRange)
+                    )
+                }
+            } catch (e: IllegalArgumentException) {
+                if (currentIntersectSize != 0) {
+                    continue
+                }
+
+                // if no intersection is present, pick the range that is closer to our target
+                if (getRangeDistance(potentialRange, newTargetFrameRate)
+                    < getRangeDistance(
+                        bestRange, newTargetFrameRate
+                    )
+                ) {
+                    bestRange = potentialRange
+                } else if (getRangeDistance(potentialRange, newTargetFrameRate) ==
+                    getRangeDistance(bestRange, newTargetFrameRate)
+                ) {
+                    if (potentialRange.lower > bestRange.upper) {
+                        // if they both have the same distance, pick the higher range
+                        bestRange = potentialRange
+                    } else if (getRangeLength(potentialRange)
+                        < getRangeLength(bestRange)
+                    ) {
+                        // if one isn't higher than the other, pick the range with the
+                        // shorter length
+                        bestRange = potentialRange
+                    }
+                }
+            }
+        }
+        return bestRange
+    }
+
+    /**
+     * @param newTargetFrameRate    an incoming frame rate range
+     * @param storedTargetFrameRate a stored frame rate range to be modified
+     * @return adjusted target frame rate
+     *
+     * If the two ranges are both nonnull and disjoint of each other, then the range that was
+     * already stored will be used
+     */
+    private fun getUpdatedTargetFrameRate(
+        newTargetFrameRate: Range<Int>?,
+        storedTargetFrameRate: Range<Int>?
+    ): Range<Int>? {
+        var updatedTarget = storedTargetFrameRate
+        if (storedTargetFrameRate == null) {
+            // if stored value was null before, set it to the new value
+            updatedTarget = newTargetFrameRate
+        } else if (newTargetFrameRate != null) {
+            updatedTarget = try {
+                // get intersection of existing target fps
+                storedTargetFrameRate
+                    .intersect(newTargetFrameRate)
+            } catch (e: java.lang.IllegalArgumentException) {
+                // no intersection, keep the previously stored value
+                storedTargetFrameRate
+            }
+        }
+        return updatedTarget
+    }
+
+    /**
+     * @param currentMaxFps the previously stored Max FPS
+     * @param imageFormat   the image format of the incoming surface
+     * @param size          the size of the incoming surface
+     */
+    private fun getUpdatedMaximumFps(currentMaxFps: Int, imageFormat: Int, size: Size): Int {
+        return min(currentMaxFps, getMaxFrameRate(imageFormat, size))
     }
 
     /**
@@ -317,7 +738,7 @@
         imageFormat: Int
     ): List<Size> {
         // Applies TargetAspectRatio workaround
-        var ratio: Rational? =
+        val ratio: Rational? =
             when (targetAspectRatio[cameraMetadata, streamConfigurationMapCompat]) {
                 TargetAspectRatio.RATIO_4_3 ->
                     AspectRatioUtil.ASPECT_RATIO_4_3
@@ -426,6 +847,12 @@
         )
     }
 
+    private fun generate10BitSupportedCombinationList() {
+        surfaceCombinations10Bit.addAll(
+            GuaranteedConfigurationsUtil.get10BitSupportedCombinationList()
+        )
+    }
+
     /**
      * Generation the size definition for VGA, s720p, PREVIEW, s1440p, RECORD, MAXIMUM and
      * ULTRA_MAXIMUM.
@@ -621,42 +1048,14 @@
     }
 
     /**
-     * Retrieves the display which has the max size among all displays.
-     */
-    private fun getMaxSizeDisplay(): Display {
-        val displays: Array<Display> = displayManager.displays
-        if (displays.size == 1) {
-            return displays[0]
-        }
-        var maxDisplay: Display? = null
-        var maxDisplaySize = -1
-        for (display: Display in displays) {
-            if (display.state != Display.STATE_OFF) {
-                val displaySize = Point()
-                display.getRealSize(displaySize)
-                if (displaySize.x * displaySize.y > maxDisplaySize) {
-                    maxDisplaySize = displaySize.x * displaySize.y
-                    maxDisplay = display
-                }
-            }
-        }
-        if (maxDisplay == null) {
-            throw IllegalArgumentException(
-                "No display can be found from the input display manager!"
-            )
-        }
-        return maxDisplay
-    }
-
-    /**
      * Once the stream resource is occupied by one use case, it will impact the other use cases.
      * Therefore, we need to define the priority for stream resource usage. For the use cases
      * with the higher priority, we will try to find the best one for them in priority as
      * possible.
      */
     private fun getUseCasesPriorityOrder(newUseCaseConfigs: List<UseCaseConfig<*>>): List<Int> {
-        val priorityOrder: MutableList<Int> = ArrayList()
-        val priorityValueList: MutableList<Int> = ArrayList()
+        val priorityOrder: MutableList<Int> = mutableListOf()
+        val priorityValueList: MutableList<Int> = mutableListOf()
         for (config in newUseCaseConfigs) {
             val priority = config.getSurfaceOccupancyPriority(0)
             if (!priorityValueList.contains(priority)) {
@@ -711,13 +1110,13 @@
 
         if (Build.VERSION.SDK_INT >= 23 && highResolutionIncluded) {
             val highResolutionOutputSizes = map?.getHighResolutionOutputSizes(imageFormat)
-            if (highResolutionOutputSizes != null && highResolutionOutputSizes.isNotEmpty()) {
+            if (!highResolutionOutputSizes.isNullOrEmpty()) {
                 maxHighResolutionSize =
                     Collections.max(highResolutionOutputSizes.asList(), compareSizesByArea)
             }
         }
 
-        return Collections.max(Arrays.asList(maxSize, maxHighResolutionSize), compareSizesByArea)
+        return Collections.max(listOf(maxSize, maxHighResolutionSize), compareSizesByArea)
     }
 
     /**
@@ -735,11 +1134,11 @@
         // supportedOutputSizes
         // for some use case
         require(totalArrangementsCount != 0) { "Failed to find supported resolutions." }
-        val allPossibleSizeArrangements: MutableList<MutableList<Size>> = ArrayList()
+        val allPossibleSizeArrangements: MutableList<MutableList<Size>> = mutableListOf()
 
         // Initialize allPossibleSizeArrangements for the following operations
         for (i in 0 until totalArrangementsCount) {
-            val sizeList: MutableList<Size> = ArrayList()
+            val sizeList: MutableList<Size> = mutableListOf()
             allPossibleSizeArrangements.add(sizeList)
         }
 
@@ -769,7 +1168,23 @@
         return allPossibleSizeArrangements
     }
 
-    companion object {
-        private const val TAG = "SupportedSurfaceCombination"
-    }
+    /**
+     * A collection of feature settings related to the Camera2 capabilities exposed by
+     * [CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES] and device features exposed
+     * by [PackageManager.hasSystemFeature].
+     *
+     * @param cameraMode The camera mode. This involves the following mapping of mode to features:
+     *           [CameraMode.CONCURRENT_CAMERA] -> [PackageManager.FEATURE_CAMERA_CONCURRENT]
+     *           [CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA] ->
+     *           [CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR]
+     * @param requiredMaxBitDepth The required maximum bit depth for any non-RAW stream attached to
+     *           the camera. A value of [DynamicRange.BIT_DEPTH_10_BIT] corresponds to the camera
+     *           capability
+     *           [CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT].
+     *
+     */
+    data class FeatureSettings(
+        @CameraMode.Mode val cameraMode: Int,
+        val requiredMaxBitDepth: Int
+    )
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompat.kt
new file mode 100644
index 0000000..4d1fddd
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompat.kt
@@ -0,0 +1,161 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.params.DynamicRangeProfiles
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.core.checkApi
+import androidx.camera.core.DynamicRange
+
+/**
+ * Helper for accessing features in DynamicRangeProfiles in a backwards compatible fashion.
+ */
+@RequiresApi(21)
+class DynamicRangeProfilesCompat internal constructor(
+    private val mImpl: DynamicRangeProfilesCompatImpl
+) {
+    /**
+     * Returns a set of supported [DynamicRange] that can be referenced in a single
+     * capture request.
+     *
+     * For example if a particular 10-bit output capable device returns (STANDARD,
+     * HLG10, HDR10) as result from calling [getSupportedDynamicRanges] and
+     * [DynamicRangeProfiles.getProfileCaptureRequestConstraints]
+     * returns (STANDARD, HLG10) when given an argument
+     * of STANDARD. This means that the corresponding camera device will only accept and process
+     * capture requests that reference outputs configured using HDR10 dynamic range or
+     * alternatively some combination of STANDARD and HLG10. However trying to queue capture
+     * requests to outputs that reference both HDR10 and STANDARD/HLG10 will result in
+     * IllegalArgumentException.
+     *
+     * The list will be empty in case there are no constraints for the given dynamic range.
+     *
+     * @param dynamicRange The dynamic range that will be checked for constraints
+     * @return non-modifiable set of dynamic ranges
+     * @throws IllegalArgumentException If the dynamic range argument is not within the set
+     * returned by [getSupportedDynamicRanges].
+     */
+    fun getDynamicRangeCaptureRequestConstraints(
+        dynamicRange: DynamicRange
+    ): Set<DynamicRange> {
+        return mImpl.getDynamicRangeCaptureRequestConstraints(dynamicRange)
+    }
+
+    /**
+     * Returns a set of supported dynamic ranges.
+     *
+     * @return a non-modifiable set of dynamic ranges.
+     */
+    fun getSupportedDynamicRanges(): Set<DynamicRange> {
+        return mImpl.getSupportedDynamicRanges()
+    }
+
+    /**
+     * Checks whether a given dynamic range is suitable for latency sensitive use cases.
+     *
+     * Due to internal lookahead logic, camera outputs configured with some dynamic range
+     * profiles may experience additional latency greater than 3 buffers. Using camera outputs
+     * with such dynamic ranges for latency sensitive use cases such as camera preview is not
+     * recommended. Dynamic ranges that have such extra streaming delay are typically utilized for
+     * scenarios such as offscreen video recording.
+     *
+     * @param dynamicRange The dynamic range to check for extra latency
+     * @return `true` if the given profile is not suitable for latency sensitive use cases,
+     * `false` otherwise.
+     * @throws IllegalArgumentException If the dynamic range argument is not within the set
+     * returned by [getSupportedDynamicRanges].
+     */
+    fun isExtraLatencyPresent(dynamicRange: DynamicRange): Boolean {
+        return mImpl.isExtraLatencyPresent(dynamicRange)
+    }
+
+    /**
+     * Returns the underlying framework
+     * [DynamicRangeProfiles].
+     *
+     * @return the underlying [DynamicRangeProfiles] or
+     * `null` if the device doesn't support 10 bit dynamic range.
+     */
+    @RequiresApi(33)
+    fun toDynamicRangeProfiles(): DynamicRangeProfiles? {
+        checkApi(
+            33, "DynamicRangesCompat can only be " +
+                "converted to DynamicRangeProfiles on API 33 or higher."
+        )
+        return mImpl.unwrap()
+    }
+
+    internal interface DynamicRangeProfilesCompatImpl {
+        fun getDynamicRangeCaptureRequestConstraints(
+            dynamicRange: DynamicRange
+        ): Set<DynamicRange>
+
+        fun getSupportedDynamicRanges(): Set<DynamicRange>
+
+        fun isExtraLatencyPresent(dynamicRange: DynamicRange): Boolean
+        fun unwrap(): DynamicRangeProfiles?
+    }
+
+    companion object {
+        /**
+         * Returns a [DynamicRangeProfilesCompat] using the capabilities derived from the provided
+         * characteristics.
+         *
+         * @param cameraMetadata the metaData used to derive dynamic range information.
+         * @return a [DynamicRangeProfilesCompat] object.
+         */
+        fun fromCameraMetaData(
+            cameraMetadata: CameraMetadata
+        ): DynamicRangeProfilesCompat {
+            var rangesCompat: DynamicRangeProfilesCompat? = null
+            if (Build.VERSION.SDK_INT >= 33) {
+                rangesCompat = toDynamicRangesCompat(
+                    cameraMetadata[CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES]
+                )
+            }
+            return rangesCompat ?: DynamicRangeProfilesCompatBaseImpl.COMPAT_INSTANCE
+        }
+
+        /**
+         * Creates an instance from a framework [DynamicRangeProfiles]
+         * object.
+         *
+         * @param dynamicRangeProfiles a [DynamicRangeProfiles].
+         * @return an equivalent [DynamicRangeProfilesCompat] object.
+         */
+        @RequiresApi(33)
+        fun toDynamicRangesCompat(
+            dynamicRangeProfiles: DynamicRangeProfiles?
+        ): DynamicRangeProfilesCompat? {
+            if (dynamicRangeProfiles == null) {
+                return null
+            }
+            checkApi(
+                33, "DynamicRangeProfiles can only " +
+                    "be converted to DynamicRangesCompat on API 33 or higher."
+            )
+            return DynamicRangeProfilesCompat(
+                DynamicRangeProfilesCompatApi33Impl(
+                    dynamicRangeProfiles
+                )
+            )
+        }
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatApi33Impl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatApi33Impl.kt
new file mode 100644
index 0000000..12a5687
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatApi33Impl.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat
+
+import android.hardware.camera2.params.DynamicRangeProfiles
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.internal.DynamicRangeConversions
+import androidx.camera.core.DynamicRange
+import java.util.Collections
+
+@RequiresApi(33)
+internal class DynamicRangeProfilesCompatApi33Impl(
+    private val dynamicRangeProfiles: DynamicRangeProfiles
+) : DynamicRangeProfilesCompat.DynamicRangeProfilesCompatImpl {
+
+    override fun getDynamicRangeCaptureRequestConstraints(
+        dynamicRange: DynamicRange
+    ): Set<DynamicRange> {
+        val dynamicRangeProfile = dynamicRangeToFirstSupportedProfile(dynamicRange)
+        require(dynamicRangeProfile != null) {
+            "DynamicRange is not supported: $dynamicRange"
+        }
+        return profileSetToDynamicRangeSet(
+            dynamicRangeProfiles.getProfileCaptureRequestConstraints(dynamicRangeProfile)
+        )
+    }
+
+    override fun getSupportedDynamicRanges() = profileSetToDynamicRangeSet(
+        dynamicRangeProfiles.supportedProfiles
+    )
+
+    override fun isExtraLatencyPresent(dynamicRange: DynamicRange): Boolean {
+        val dynamicRangeProfile = dynamicRangeToFirstSupportedProfile(dynamicRange)
+        require(
+            dynamicRangeProfile != null
+        ) {
+            "DynamicRange is not supported: $dynamicRange"
+        }
+        return dynamicRangeProfiles.isExtraLatencyPresent(dynamicRangeProfile)
+    }
+
+    override fun unwrap() = dynamicRangeProfiles
+
+    private fun dynamicRangeToFirstSupportedProfile(dynamicRange: DynamicRange) =
+        DynamicRangeConversions.dynamicRangeToFirstSupportedProfile(
+            dynamicRange,
+            dynamicRangeProfiles
+        )
+
+    private fun profileToDynamicRange(profile: Long): DynamicRange {
+        val result = DynamicRangeConversions.profileToDynamicRange(
+            profile
+        )
+        require(result != null) {
+            "Dynamic range profile cannot be converted to a DynamicRange object: $profile"
+        }
+        return result
+    }
+
+    private fun profileSetToDynamicRangeSet(profileSet: Set<Long>): Set<DynamicRange> {
+        if (profileSet.isEmpty()) {
+            return emptySet()
+        }
+        val dynamicRangeSet: MutableSet<DynamicRange> = mutableSetOf()
+        for (profile in profileSet) {
+            dynamicRangeSet.add(profileToDynamicRange(profile))
+        }
+        return Collections.unmodifiableSet(dynamicRangeSet)
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatBaseImpl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatBaseImpl.kt
new file mode 100644
index 0000000..b9a0cad
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatBaseImpl.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat
+
+import android.hardware.camera2.params.DynamicRangeProfiles
+import androidx.annotation.RequiresApi
+import androidx.camera.core.DynamicRange
+import androidx.core.util.Preconditions
+
+@RequiresApi(21)
+internal class DynamicRangeProfilesCompatBaseImpl :
+    DynamicRangeProfilesCompat.DynamicRangeProfilesCompatImpl {
+    override fun getDynamicRangeCaptureRequestConstraints(
+        dynamicRange: DynamicRange
+    ): Set<DynamicRange> {
+        Preconditions.checkArgument(
+            DynamicRange.SDR == dynamicRange,
+            "DynamicRange is not supported: $dynamicRange"
+        )
+        return SDR_ONLY
+    }
+
+    override fun getSupportedDynamicRanges(): Set<DynamicRange> {
+        return SDR_ONLY
+    }
+
+    override fun isExtraLatencyPresent(dynamicRange: DynamicRange): Boolean {
+        Preconditions.checkArgument(
+            DynamicRange.SDR == dynamicRange,
+            "DynamicRange is not supported: $dynamicRange"
+        )
+        return false
+    }
+
+    override fun unwrap(): DynamicRangeProfiles? {
+        return null
+    }
+
+    companion object {
+        val COMPAT_INSTANCE: DynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat(DynamicRangeProfilesCompatBaseImpl())
+        private val SDR_ONLY = setOf(DynamicRange.SDR)
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompat.kt
index 5b963d5..00ebe44 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompat.kt
@@ -136,6 +136,10 @@
         return outputSizes?.clone()
     }
 
+    fun getOutputMinFrameDuration(format: Int, size: Size?): Long? {
+        return impl.getOutputMinFrameDuration(format, size)
+    }
+
     /**
      * Returns the [StreamConfigurationMap] represented by this object.
      */
@@ -147,6 +151,7 @@
         fun getOutputSizes(format: Int): Array<Size>?
         fun <T> getOutputSizes(klass: Class<T>): Array<Size>?
         fun getHighResolutionOutputSizes(format: Int): Array<Size>?
+        fun getOutputMinFrameDuration(format: Int, size: Size?): Long?
 
         /**
          * Returns the underlying [StreamConfigurationMap] instance.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
index b5bfa26..fef2df7 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
@@ -50,6 +50,10 @@
         return null
     }
 
+    override fun getOutputMinFrameDuration(format: Int, size: Size?): Long? {
+        return streamConfigurationMap?.getOutputMinFrameDuration(format, size)
+    }
+
     override fun unwrap(): StreamConfigurationMap? {
         return streamConfigurationMap
     }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirk.kt
index 9dfecbd..46ab296 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirk.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirk.kt
@@ -26,11 +26,12 @@
  * Quirks that denotes the device has a slow flash sequence that could result in blurred pictures.
  *
  * QuirkSummary
- * - Bug Id: 211474332, 286190938, 280221967
+ * - Bug Id: 211474332, 286190938, 280221967, 296814664, 296816175
  * - Description: When capturing still photos in auto flash mode, it needs more than 1 second to
  *   flash or capture actual photo after flash, and therefore it easily results in blurred or dark
  *   or overexposed pictures.
- * - Device(s): Pixel 3a / Pixel 3a XL, all models of Pixel 4 and 5, SM-A320
+ * - Device(s): Pixel 3a / Pixel 3a XL, all models of Pixel 4 and 5, SM-A320, Moto G20, Itel A48,
+ *   Realme C11 2021
  *
  * TODO(b/270421716): enable CameraXQuirksClassDetector lint check when kotlin is supported.
  */
@@ -44,7 +45,10 @@
             "PIXEL 3A XL",
             "PIXEL 4", // includes Pixel 4 XL, 4A, and 4A (5g) too
             "PIXEL 5", // includes Pixel 5A too
-            "SM-A320"
+            "SM-A320",
+            "MOTO G(20)",
+            "ITEL L6006", // Itel A48
+            "RMX3231" // Realme C11 2021
         )
 
         fun isEnabled(cameraMetadata: CameraMetadata): Boolean {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/InvalidVideoProfilesQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/InvalidVideoProfilesQuirk.kt
index 4c2c27a..7df4b6c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/InvalidVideoProfilesQuirk.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/InvalidVideoProfilesQuirk.kt
@@ -28,13 +28,14 @@
  * Quirk denoting the video profile list returns by [EncoderProfiles] is invalid.
  *
  * QuirkSummary
- * - Bug Id: 267727595, 278860860
- * - Description: When using [EncoderProfiles] on TP1A or TD1A builds of Android API 33,
+ * - Bug Id: 267727595, 278860860, 298951126, 298952500
+ * - Description: When using [EncoderProfiles] on some builds of Android API 33,
  *   [EncoderProfiles.getVideoProfiles] returns a list with size one, but the single value in the
  *   list is null. This is not the expected behavior, and makes [EncoderProfiles] lack of video
  *   information.
  * - Device(s): Pixel 4 and above pixel devices with TP1A or TD1A builds (API 33), Samsung devices
- *              with TP1A build (API 33).
+ *              with TP1A build (API 33), Xiaomi devices with TKQ1 build (API 33), OnePlus and Oppo
+ *              devices with API 33 build.
  *
  * TODO: enable CameraXQuirksClassDetector lint check when kotlin is supported.
  */
@@ -56,8 +57,19 @@
             "pixel 7 pro"
         )
 
+        private val AFFECTED_ONE_PLUS_MODELS: List<String> = listOf(
+            "cph2417",
+            "cph2451"
+        )
+
+        private val AFFECTED_OPPO_MODELS: List<String> = listOf(
+            "cph2437",
+            "cph2525"
+        )
+
         fun isEnabled(): Boolean {
-            return isAffectedSamsungDevices() || isAffectedPixelDevices()
+            return isAffectedSamsungDevices() || isAffectedPixelDevices() ||
+                isAffectedXiaomiDevices() || isAffectedOppoDevices() || isAffectedOnePlusDevices()
         }
 
         private fun isAffectedSamsungDevices(): Boolean {
@@ -68,12 +80,37 @@
             return isAffectedPixelModel() && isAffectedPixelBuild()
         }
 
+        private fun isAffectedXiaomiDevices(): Boolean {
+            return ("redmi".equals(Build.BRAND, true) || "xiaomi".equals(Build.BRAND, true)) &&
+                isTkq1Build()
+        }
+
+        private fun isAffectedOnePlusDevices(): Boolean {
+            return isAffectedOnePlusModel() && isAPI33()
+        }
+
+        private fun isAffectedOppoDevices(): Boolean {
+            return isAffectedOppoModel() && isAPI33()
+        }
+
         private fun isAffectedPixelModel(): Boolean {
             return AFFECTED_PIXEL_MODELS.contains(
                 Build.MODEL.lowercase()
             )
         }
 
+        private fun isAffectedOnePlusModel(): Boolean {
+            return AFFECTED_ONE_PLUS_MODELS.contains(
+                Build.MODEL.lowercase()
+            )
+        }
+
+        private fun isAffectedOppoModel(): Boolean {
+            return AFFECTED_OPPO_MODELS.contains(
+                Build.MODEL.lowercase()
+            )
+        }
+
         private fun isAffectedPixelBuild(): Boolean {
             return isTp1aBuild() || isTd1aBuild()
         }
@@ -85,5 +122,13 @@
         private fun isTd1aBuild(): Boolean {
             return Build.ID.startsWith("TD1A", true)
         }
+
+        private fun isTkq1Build(): Boolean {
+            return Build.ID.startsWith("TKQ1", true)
+        }
+
+        private fun isAPI33(): Boolean {
+            return Build.VERSION.SDK_INT == 33
+        }
     }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.kt
index 553c96f..9f17c35 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.kt
@@ -25,7 +25,7 @@
  * QuirkSummary
  * - Bug Id: 228272227
  * - Description: The Torch is unexpectedly turned off after taking a picture.
- * - Device(s): Redmi 4X, Redmi 5A, Redmi Note 5, Mi A1, Mi A2, Mi A2 lite and Redmi 6 Pro.
+ * - Device(s): Redmi 4X, Redmi 5A, Redmi Note 5 (Pro), Mi A1, Mi A2, Mi A2 lite and Redmi 6 Pro.
  */
 @SuppressLint("CameraXQuirksClassDetector") // TODO(b/270421716): enable when kotlin is supported.
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@@ -40,6 +40,7 @@
             "redmi 4x", // Xiaomi Redmi 4X
             "redmi 5a", // Xiaomi Redmi 5A
             "redmi note 5", // Xiaomi Redmi Note 5
+            "redmi note 5 pro", // Xiaomi Redmi Note 5 Pro
             "redmi 6 pro", // Xiaomi Redmi 6 Pro
         )
 
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 84bd53e..88ff62b 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -43,6 +43,7 @@
 import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
 import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
 import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
+import androidx.camera.core.DynamicRange
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.CameraInternal
 import androidx.camera.core.impl.CameraMode
@@ -457,7 +458,12 @@
             )
         }
 
-        return supportedSurfaceCombination.checkSupported(CameraMode.DEFAULT, surfaceConfigs)
+        return supportedSurfaceCombination.checkSupported(
+            SupportedSurfaceCombination.FeatureSettings(
+                CameraMode.DEFAULT,
+                DynamicRange.BIT_DEPTH_8_BIT
+            ), surfaceConfigs
+        )
     }
 
     private fun Collection<UseCase>.surfaceCount(): Int =
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeConversions.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeConversions.kt
new file mode 100644
index 0000000..090e587
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeConversions.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.camera.camera2.pipe.integration.internal
+
+import android.hardware.camera2.params.DynamicRangeProfiles
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.camera.core.DynamicRange
+
+/**
+ * Utilities for converting between [DynamicRange] and profiles from
+ * [DynamicRangeProfiles].
+ */
+@RequiresApi(33)
+internal object DynamicRangeConversions {
+    private val PROFILE_TO_DR_MAP: MutableMap<Long, DynamicRange> = mutableMapOf()
+    private val DR_TO_PROFILE_MAP: MutableMap<DynamicRange?, List<Long>> = mutableMapOf()
+
+    init {
+        // SDR
+        PROFILE_TO_DR_MAP[DynamicRangeProfiles.STANDARD] = DynamicRange.SDR
+        DR_TO_PROFILE_MAP[DynamicRange.SDR] = listOf(DynamicRangeProfiles.STANDARD)
+
+        // HLG
+        PROFILE_TO_DR_MAP[DynamicRangeProfiles.HLG10] = DynamicRange.HLG_10_BIT
+        DR_TO_PROFILE_MAP[PROFILE_TO_DR_MAP[DynamicRangeProfiles.HLG10]] =
+            listOf(DynamicRangeProfiles.HLG10)
+
+        // HDR10
+        PROFILE_TO_DR_MAP[DynamicRangeProfiles.HDR10] = DynamicRange.HDR10_10_BIT
+        DR_TO_PROFILE_MAP[DynamicRange.HDR10_10_BIT] = listOf(DynamicRangeProfiles.HDR10)
+
+        // HDR10+
+        PROFILE_TO_DR_MAP[DynamicRangeProfiles.HDR10_PLUS] = DynamicRange.HDR10_PLUS_10_BIT
+        DR_TO_PROFILE_MAP[DynamicRange.HDR10_PLUS_10_BIT] = listOf(DynamicRangeProfiles.HDR10_PLUS)
+
+        // Dolby Vision 10-bit
+        // A list of the Camera2 10-bit dolby vision profiles ordered by priority. Any API that
+        // takes a DynamicRange with dolby vision encoding will attempt to convert to these
+        // profiles in order, using the first one that is supported. We will need to add a
+        // mechanism for choosing between these
+        val dolbyVision10BitProfilesOrdered = listOf(
+            DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM,
+            DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM_PO,
+            DynamicRangeProfiles.DOLBY_VISION_10B_HDR_REF,
+            DynamicRangeProfiles.DOLBY_VISION_10B_HDR_REF_PO
+        )
+        for (profile in dolbyVision10BitProfilesOrdered) {
+            PROFILE_TO_DR_MAP[profile] = DynamicRange.DOLBY_VISION_10_BIT
+        }
+        DR_TO_PROFILE_MAP[DynamicRange.DOLBY_VISION_10_BIT] =
+            dolbyVision10BitProfilesOrdered
+
+        // Dolby vision 8-bit
+        val dolbyVision8BitProfilesOrdered = listOf(
+            DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM,
+            DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM_PO,
+            DynamicRangeProfiles.DOLBY_VISION_8B_HDR_REF,
+            DynamicRangeProfiles.DOLBY_VISION_8B_HDR_REF_PO
+        )
+        for (profile in dolbyVision8BitProfilesOrdered) {
+            PROFILE_TO_DR_MAP[profile] = DynamicRange.DOLBY_VISION_8_BIT
+        }
+        DR_TO_PROFILE_MAP[DynamicRange.DOLBY_VISION_8_BIT] =
+            dolbyVision8BitProfilesOrdered
+    }
+
+    /**
+     * Converts Camera2 dynamic range profile constants to [DynamicRange].
+     */
+    @DoNotInline
+    fun profileToDynamicRange(profile: Long): DynamicRange? {
+        return PROFILE_TO_DR_MAP[profile]
+    }
+
+    /**
+     * Converts a [DynamicRange] to a Camera2 dynamic range profile.
+     *
+     *
+     * For dynamic ranges which can resolve to multiple profiles, the first supported profile
+     * from the passed [android.hardware.camera2.params.DynamicRangeProfiles] will be
+     * returned. The order in which profiles are checked for support is internally defined.
+     *
+     *
+     * This will only return profiles for fully defined dynamic ranges. For instance, if the
+     * format returned by [DynamicRange.getEncoding] is
+     * [DynamicRange.ENCODING_HDR_UNSPECIFIED], this will return `null`.
+     */
+    @DoNotInline
+    fun dynamicRangeToFirstSupportedProfile(
+        dynamicRange: DynamicRange,
+        dynamicRangeProfiles: DynamicRangeProfiles
+    ): Long? {
+        val orderedProfiles = DR_TO_PROFILE_MAP[dynamicRange]
+        if (orderedProfiles != null) {
+            val supportedList = dynamicRangeProfiles.supportedProfiles
+            for (profile in orderedProfiles) {
+                if (supportedList.contains(profile)) {
+                    return profile
+                }
+            }
+        }
+
+        // No profile supported
+        return null
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeResolver.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeResolver.kt
new file mode 100644
index 0000000..947d31c
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeResolver.kt
@@ -0,0 +1,479 @@
+package androidx.camera.camera2.pipe.integration.internal
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.integration.compat.DynamicRangeProfilesCompat
+import androidx.camera.core.DynamicRange
+import androidx.camera.core.impl.AttachedSurfaceInfo
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.core.util.Preconditions
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+class DynamicRangeResolver(val cameraMetadata: CameraMetadata) {
+    private val is10BitSupported: Boolean
+    private val dynamicRangesInfo: DynamicRangeProfilesCompat
+
+    init {
+        val availableCapabilities: IntArray? =
+            cameraMetadata[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES]
+        is10BitSupported =
+            availableCapabilities?.contains(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT
+            )
+                ?: false
+        dynamicRangesInfo = DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+    }
+
+    /**
+     * Returns whether 10-bit dynamic ranges are supported on this device.
+     */
+    fun is10BitDynamicRangeSupported(): Boolean = is10BitSupported
+
+    /**
+     * Returns a set of supported dynamic ranges for the dynamic ranges requested by the list of
+     * attached and new use cases.
+     *
+     *
+     * If a new use case requests a dynamic range that isn't supported, an
+     * IllegalArgumentException will be thrown.
+     */
+    fun resolveAndValidateDynamicRanges(
+        existingSurfaces: List<AttachedSurfaceInfo>,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        useCasePriorityOrder: List<Int>
+    ): Map<UseCaseConfig<*>, DynamicRange> {
+        // Create an ordered set of already-attached surface's dynamic ranges. These are assumed
+        // to be valid since they are already attached.
+        val orderedExistingDynamicRanges = mutableSetOf<DynamicRange>()
+        for (asi in existingSurfaces) {
+            orderedExistingDynamicRanges.add(asi.dynamicRange)
+        }
+
+        // Get the supported dynamic ranges from the device
+        val supportedDynamicRanges = dynamicRangesInfo.getSupportedDynamicRanges()
+
+        // Collect initial dynamic range constraints. This set will potentially shrink as we add
+        // more dynamic ranges. We start with the initial set of supported dynamic ranges to
+        // denote no constraints.
+        val combinedConstraints = supportedDynamicRanges.toMutableSet()
+        for (dynamicRange in orderedExistingDynamicRanges) {
+            updateConstraints(combinedConstraints, dynamicRange, dynamicRangesInfo)
+        }
+
+        // We want to resolve and validate dynamic ranges in the following order:
+        // 1. First validate fully defined dynamic ranges. No resolving is required here.
+        // 2. Resolve and validate partially defined dynamic ranges, such as HDR_UNSPECIFIED or
+        // dynamic ranges with concrete encodings but BIT_DEPTH_UNSPECIFIED. We can now potentially
+        // infer a dynamic range based on constraints of the fully defined dynamic ranges or
+        // the list of supported HDR dynamic ranges.
+        // 3. Finally, resolve and validate UNSPECIFIED dynamic ranges. These will resolve
+        // to dynamic ranges from the first 2 groups, or fall back to SDR if no other dynamic
+        // ranges are defined.
+        //
+        // To accomplish this, we need to partition the use cases into 3 categories.
+        val orderedFullyDefinedUseCaseConfigs: MutableList<UseCaseConfig<*>> = mutableListOf()
+        val orderedPartiallyDefinedUseCaseConfigs: MutableList<UseCaseConfig<*>> = mutableListOf()
+        val orderedUndefinedUseCaseConfigs: MutableList<UseCaseConfig<*>> = mutableListOf()
+        for (priorityIdx in useCasePriorityOrder) {
+            val config = newUseCaseConfigs[priorityIdx]
+            val requestedDynamicRange = config.dynamicRange
+            if (isFullyUnspecified(requestedDynamicRange)
+            ) {
+                orderedUndefinedUseCaseConfigs.add(config)
+            } else if (isPartiallySpecified(requestedDynamicRange)
+            ) {
+                orderedPartiallyDefinedUseCaseConfigs.add(config)
+            } else {
+                orderedFullyDefinedUseCaseConfigs.add(config)
+            }
+        }
+        val resolvedDynamicRanges: MutableMap<UseCaseConfig<*>, DynamicRange> = mutableMapOf()
+        // Keep track of new dynamic ranges for more fine-grained error messages in exceptions.
+        // This allows us to differentiate between dynamic ranges from already-attached use cases
+        // and requested dynamic ranges from newly added use cases.
+        val orderedNewDynamicRanges: MutableSet<DynamicRange> = mutableSetOf()
+        // Now resolve and validate all of the dynamic ranges in order of the 3 partitions form
+        // above.
+        val orderedUseCaseConfigs: MutableList<UseCaseConfig<*>> = mutableListOf()
+        orderedUseCaseConfigs.addAll(orderedFullyDefinedUseCaseConfigs)
+        orderedUseCaseConfigs.addAll(orderedPartiallyDefinedUseCaseConfigs)
+        orderedUseCaseConfigs.addAll(orderedUndefinedUseCaseConfigs)
+        for (config in orderedUseCaseConfigs) {
+            val resolvedDynamicRange: DynamicRange = resolveDynamicRangeAndUpdateConstraints(
+                supportedDynamicRanges, orderedExistingDynamicRanges,
+                orderedNewDynamicRanges, config, combinedConstraints
+            )
+            resolvedDynamicRanges[config] = resolvedDynamicRange
+            if (!orderedExistingDynamicRanges.contains(resolvedDynamicRange)) {
+                orderedNewDynamicRanges.add(resolvedDynamicRange)
+            }
+        }
+        return resolvedDynamicRanges
+    }
+
+    private fun resolveDynamicRangeAndUpdateConstraints(
+        supportedDynamicRanges: Set<DynamicRange?>,
+        orderedExistingDynamicRanges: Set<DynamicRange>,
+        orderedNewDynamicRanges: Set<DynamicRange>,
+        config: UseCaseConfig<*>,
+        outCombinedConstraints: MutableSet<DynamicRange>
+    ): DynamicRange {
+        val requestedDynamicRange = config.dynamicRange
+        val resolvedDynamicRange: DynamicRange? = resolveDynamicRange(
+            requestedDynamicRange,
+            outCombinedConstraints, orderedExistingDynamicRanges, orderedNewDynamicRanges,
+            config.targetName
+        )
+        if (resolvedDynamicRange != null) {
+            updateConstraints(outCombinedConstraints, resolvedDynamicRange, dynamicRangesInfo)
+        } else {
+            throw IllegalArgumentException(
+                "Unable to resolve supported " +
+                    "dynamic range. The dynamic range may not be supported on the device " +
+                    "or may not be allowed concurrently with other attached use cases.\n" +
+                    "Use case:\n" +
+                    "  ${config.targetName}\n" +
+                    "Requested dynamic range:\n" +
+                    "  $requestedDynamicRange\n" +
+                    "Supported dynamic ranges:\n" +
+                    "  $supportedDynamicRanges\n" +
+                    "Constrained set of concurrent dynamic ranges:\n" +
+                    "  $outCombinedConstraints",
+            )
+        }
+        return resolvedDynamicRange
+    }
+
+    /**
+     * Resolves the requested dynamic range into a fully specified dynamic range.
+     *
+     *
+     * This uses existing fully-specified dynamic ranges, new fully-specified dynamic ranges,
+     * dynamic range constraints and the list of supported dynamic ranges to exhaustively search
+     * for a dynamic range if the requested dynamic range is not fully specified, i.e., it has an
+     * UNSPECIFIED encoding or UNSPECIFIED bitrate.
+     *
+     *
+     * Any dynamic range returned will be validated to work according to the constraints and
+     * supported dynamic ranges provided.
+     *
+     *
+     * If no suitable dynamic range can be found, returns `null`.
+     */
+    private fun resolveDynamicRange(
+        requestedDynamicRange: DynamicRange,
+        combinedConstraints: Set<DynamicRange>,
+        orderedExistingDynamicRanges: Set<DynamicRange>,
+        orderedNewDynamicRanges: Set<DynamicRange>,
+        rangeOwnerLabel: String
+    ): DynamicRange? {
+
+        // Dynamic range is already resolved if it is fully specified.
+        if (requestedDynamicRange.isFullySpecified) {
+            return if (combinedConstraints.contains(requestedDynamicRange)) {
+                requestedDynamicRange
+            } else null
+            // Requested dynamic range is full specified but unsupported. No need to continue
+            // trying to resolve.
+        }
+
+        // Explicitly handle the case of SDR with unspecified bit depth.
+        // SDR is only supported as 8-bit.
+        val requestedEncoding = requestedDynamicRange.encoding
+        val requestedBitDepth = requestedDynamicRange.bitDepth
+        if (requestedEncoding == DynamicRange.ENCODING_SDR &&
+            requestedBitDepth == DynamicRange.BIT_DEPTH_UNSPECIFIED
+        ) {
+            return if (combinedConstraints.contains(DynamicRange.SDR)) {
+                DynamicRange.SDR
+            } else null
+            // If SDR isn't supported, we can't resolve to any other dynamic range.
+        }
+
+        // First attempt to find another fully specified HDR dynamic range to resolve to from
+        // existing dynamic ranges
+        var resolvedDynamicRange = findSupportedHdrMatch(
+            requestedDynamicRange,
+            orderedExistingDynamicRanges, combinedConstraints
+        )
+        if (resolvedDynamicRange != null) {
+            Log.debug {
+                "DynamicRangeResolver: Resolved dynamic range for use case $rangeOwnerLabel " +
+                    "from existing attached surface.\n" +
+                    "$requestedDynamicRange\n->\n$resolvedDynamicRange"
+            }
+
+            return resolvedDynamicRange
+        }
+
+        // Attempt to find another fully specified HDR dynamic range to resolve to from
+        // new dynamic ranges
+        resolvedDynamicRange =
+            findSupportedHdrMatch(
+                requestedDynamicRange,
+                orderedNewDynamicRanges, combinedConstraints
+            )
+        if (resolvedDynamicRange != null) {
+            Log.debug {
+                "DynamicRangeResolver: Resolved dynamic range for use case $rangeOwnerLabel from " +
+                    "concurrently bound use case." +
+                    "\n$requestedDynamicRange\n->\n$resolvedDynamicRange"
+            }
+
+            return resolvedDynamicRange
+        }
+
+        // Now that we have checked existing HDR dynamic ranges, we must resolve fully unspecified
+        // and unspecified 8-bit dynamic ranges to SDR if it is supported. This ensures the
+        // default behavior for most use cases is to choose SDR when an HDR dynamic range isn't
+        // already present or explicitly requested.
+        if (canResolveWithinConstraints(
+                requestedDynamicRange, DynamicRange.SDR,
+                combinedConstraints
+            )
+        ) {
+            Log.debug {
+                "DynamicRangeResolver: Resolved dynamic range for use case $rangeOwnerLabel to " +
+                    "no compatible HDR dynamic ranges.\n$requestedDynamicRange\n" +
+                    "->\n${DynamicRange.SDR}"
+            }
+            return DynamicRange.SDR
+        }
+
+        // For unspecified HDR encodings (10-bit or unspecified bit depth), we have a
+        // couple options: the device recommended 10-bit encoding or the mandated HLG encoding.
+        if (requestedEncoding == DynamicRange.ENCODING_HDR_UNSPECIFIED &&
+            ((requestedBitDepth == DynamicRange.BIT_DEPTH_10_BIT ||
+                requestedBitDepth == DynamicRange.BIT_DEPTH_UNSPECIFIED))
+        ) {
+            val hdrDefaultRanges: MutableSet<DynamicRange> = mutableSetOf()
+
+            // Attempt to use the recommended 10-bit dynamic range
+            var recommendedRange: DynamicRange? = null
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                recommendedRange =
+                    Api33Impl.getRecommended10BitDynamicRange(
+                        cameraMetadata
+                    )
+                if (recommendedRange != null) {
+                    hdrDefaultRanges.add(recommendedRange)
+                }
+            }
+            // Attempt to fall back to HLG since it is a mandated required 10-bit
+            // dynamic range.
+            hdrDefaultRanges.add(DynamicRange.HLG_10_BIT)
+            resolvedDynamicRange =
+                findSupportedHdrMatch(
+                    requestedDynamicRange, hdrDefaultRanges, combinedConstraints
+                )
+            if (resolvedDynamicRange != null) {
+                Log.debug {
+                    "DynamicRangeResolver: Resolved dynamic range for use case $rangeOwnerLabel" +
+                        "from ${
+                            if ((resolvedDynamicRange == recommendedRange)) "recommended"
+                            else "required"
+                        } 10-bit supported dynamic range.\n" +
+                        "${requestedDynamicRange}\n" +
+                        "->\n" +
+                        "$resolvedDynamicRange"
+                }
+                return resolvedDynamicRange
+            }
+        }
+
+        // Finally, attempt to find an HDR dynamic range for HDR or 10-bit dynamic ranges from
+        // the constraints of the other validated dynamic ranges. If there are no other dynamic
+        // ranges, this should be the full list of supported dynamic ranges.
+        // The constraints are unordered, so it may not produce an "optimal" dynamic range. This
+        // works for 8-bit, 10-bit or partially specified HDR dynamic ranges.
+        for (candidateRange: DynamicRange in combinedConstraints) {
+            check(candidateRange.isFullySpecified) {
+                "Candidate dynamic range must be fully specified."
+            }
+
+            // Only consider HDR constraints
+            if ((candidateRange == DynamicRange.SDR)) {
+                continue
+            }
+            if (canResolveDynamicRange(
+                    requestedDynamicRange,
+                    candidateRange
+                )
+            ) {
+                Log.debug {
+                    "DynamicRangeResolver: Resolved dynamic range for use case $rangeOwnerLabel " +
+                        "from validated dynamic range constraints or supported HDR dynamic " +
+                        "ranges.\n$requestedDynamicRange\n->\n$candidateRange"
+                }
+                return candidateRange
+            }
+        }
+
+        // Unable to resolve dynamic range
+        return null
+    }
+
+    /**
+     * Updates the provided dynamic range constraints by combining them with the new constraints
+     * from the new dynamic range.
+     *
+     * @param combinedConstraints The constraints that will be updated. This set must not be empty.
+     * @param newDynamicRange     The new dynamic range for which we'll apply new constraints
+     * @param dynamicRangesInfo   Information about dynamic ranges to retrieve new constraints.
+     */
+    private fun updateConstraints(
+        combinedConstraints: MutableSet<DynamicRange>,
+        newDynamicRange: DynamicRange,
+        dynamicRangesInfo: DynamicRangeProfilesCompat
+    ) {
+        Preconditions.checkState(
+            combinedConstraints.isNotEmpty(), "Cannot update already-empty constraints."
+        )
+        val newConstraints =
+            dynamicRangesInfo.getDynamicRangeCaptureRequestConstraints(newDynamicRange)
+        if (newConstraints.isNotEmpty()) {
+            // Retain for potential exception message
+            val previousConstraints = combinedConstraints.toSet()
+            // Take the intersection of constraints
+            combinedConstraints.retainAll(newConstraints)
+            // This shouldn't happen if we're diligent about checking that dynamic range
+            // is within the existing constraints before attempting to call
+            // updateConstraints. If it happens, then the dynamic ranges are not mutually
+            // compatible.
+            require(combinedConstraints.isNotEmpty()) {
+                "Constraints of dynamic " +
+                    "range cannot be combined with existing constraints.\n" +
+                    "Dynamic range:\n" +
+                    "  $newDynamicRange\n" +
+                    "Constraints:\n" +
+                    "  $newConstraints\n" +
+                    "Existing constraints:\n" +
+                    "  $previousConstraints"
+            }
+        }
+    }
+
+    private fun findSupportedHdrMatch(
+        rangeToMatch: DynamicRange,
+        fullySpecifiedCandidateRanges: Collection<DynamicRange>,
+        constraints: Set<DynamicRange>
+    ): DynamicRange? {
+        // SDR can never match with HDR
+        if (rangeToMatch.encoding == DynamicRange.ENCODING_SDR) {
+            return null
+        }
+        for (candidateRange in fullySpecifiedCandidateRanges) {
+            val candidateEncoding = candidateRange.encoding
+            check(candidateRange.isFullySpecified) {
+                "Fully specified DynamicRange must have fully defined encoding."
+            }
+            if (candidateEncoding == DynamicRange.ENCODING_SDR) {
+                // Only consider HDR encodings
+                continue
+            }
+            if (canResolveWithinConstraints(
+                    rangeToMatch,
+                    candidateRange,
+                    constraints
+                )
+            ) {
+                return candidateRange
+            }
+        }
+        return null
+    }
+
+    /**
+     * Returns `true` if the dynamic range is ENCODING_UNSPECIFIED and BIT_DEPTH_UNSPECIFIED.
+     */
+    private fun isFullyUnspecified(dynamicRange: DynamicRange): Boolean {
+        return (dynamicRange == DynamicRange.UNSPECIFIED)
+    }
+
+    /**
+     * Returns `true` if the dynamic range has an unspecified HDR encoding, a concrete
+     * encoding with unspecified bit depth, or a concrete bit depth.
+     */
+    private fun isPartiallySpecified(dynamicRange: DynamicRange): Boolean {
+        return dynamicRange.encoding == DynamicRange.ENCODING_HDR_UNSPECIFIED ||
+            (dynamicRange.encoding != DynamicRange.ENCODING_UNSPECIFIED &&
+                dynamicRange.bitDepth == DynamicRange.BIT_DEPTH_UNSPECIFIED) ||
+            (dynamicRange.encoding == DynamicRange.ENCODING_UNSPECIFIED &&
+                dynamicRange.bitDepth != DynamicRange.BIT_DEPTH_UNSPECIFIED)
+    }
+
+    /**
+     * Returns `true` if the test dynamic range can resolve to the candidate, fully specified
+     * dynamic range, taking into account constraints.
+     *
+     *
+     * A range can resolve if test fields are unspecified and appropriately match the fields
+     * of the fully specified dynamic range, or the test fields exactly match the fields of
+     * the fully specified dynamic range.
+     */
+    private fun canResolveWithinConstraints(
+        rangeToResolve: DynamicRange,
+        candidateRange: DynamicRange,
+        constraints: Set<DynamicRange>
+    ): Boolean {
+        if (!constraints.contains(candidateRange)) {
+            Log.debug {
+                "DynamicRangeResolver: Candidate Dynamic range is not within constraints.\n" +
+                    "Dynamic range to resolve:\n" +
+                    "  $rangeToResolve\n" +
+                    "Candidate dynamic range:\n" +
+                    "  $candidateRange"
+            }
+            return false
+        }
+        return canResolveDynamicRange(rangeToResolve, candidateRange)
+    }
+
+    /**
+     * Returns `true` if the test dynamic range can resolve to the fully specified dynamic
+     * range.
+     *
+     *
+     * A range can resolve if test fields are unspecified and appropriately match the fields
+     * of the fully specified dynamic range, or the test fields exactly match the fields of
+     * the fully specified dynamic range.
+     */
+    private fun canResolveDynamicRange(
+        testRange: DynamicRange,
+        fullySpecifiedRange: DynamicRange
+    ): Boolean {
+        check(fullySpecifiedRange.isFullySpecified) {
+            "Fully specified range $fullySpecifiedRange not actually fully specified."
+        }
+        if ((testRange.encoding == DynamicRange.ENCODING_HDR_UNSPECIFIED &&
+                fullySpecifiedRange.encoding == DynamicRange.ENCODING_SDR)
+        ) {
+            return false
+        }
+        return if ((testRange.encoding != DynamicRange.ENCODING_HDR_UNSPECIFIED
+                ) && (testRange.encoding != DynamicRange.ENCODING_UNSPECIFIED
+                ) && (testRange.encoding != fullySpecifiedRange.encoding)
+        ) {
+            false
+        } else (testRange.bitDepth == DynamicRange.BIT_DEPTH_UNSPECIFIED ||
+            testRange.bitDepth == fullySpecifiedRange.bitDepth)
+    }
+
+    @RequiresApi(33)
+    internal object Api33Impl {
+        @DoNotInline
+        fun getRecommended10BitDynamicRange(
+            cameraMetadata: CameraMetadata
+        ): DynamicRange? {
+            val recommendedProfile = cameraMetadata[
+                CameraCharacteristics.REQUEST_RECOMMENDED_TEN_BIT_DYNAMIC_RANGE_PROFILE]
+            return if (recommendedProfile != null) {
+                DynamicRangeConversions.profileToDynamicRange(recommendedProfile)
+            } else null
+        }
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 4c76b49..6b4e41a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -20,8 +20,11 @@
 import android.graphics.ImageFormat
 import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES
+import android.hardware.camera2.CameraCharacteristics.REQUEST_RECOMMENDED_TEN_BIT_DYNAMIC_RANGE_PROFILE
 import android.hardware.camera2.CameraManager
 import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.params.DynamicRangeProfiles
 import android.hardware.camera2.params.StreamConfigurationMap
 import android.media.CamcorderProfile.QUALITY_1080P
 import android.media.CamcorderProfile.QUALITY_2160P
@@ -42,6 +45,17 @@
 import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getLimitedSupportedCombinationList
 import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getRAWSupportedCombinationList
 import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_10B_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_8B_SDR_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_8B_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_8B_UNCONSTRAINED_HLG10_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_CONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HDR10_HDR10_PLUS_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HDR10_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_CONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_SDR_CONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.LATENCY_NONE
 import androidx.camera.camera2.pipe.testing.FakeCameraBackend
 import androidx.camera.camera2.pipe.testing.FakeCameraDevices
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
@@ -49,6 +63,7 @@
 import androidx.camera.core.CameraSelector.LensFacing
 import androidx.camera.core.CameraX
 import androidx.camera.core.CameraXConfig
+import androidx.camera.core.DynamicRange
 import androidx.camera.core.UseCase
 import androidx.camera.core.concurrent.CameraCoordinator
 import androidx.camera.core.impl.AttachedSurfaceInfo
@@ -57,6 +72,8 @@
 import androidx.camera.core.impl.EncoderProfilesProxy
 import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy
 import androidx.camera.core.impl.ImageFormatConstants
+import androidx.camera.core.impl.ImageInputConfig
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.SurfaceConfig
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.UseCaseConfigFactory
@@ -85,6 +102,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 import org.robolectric.RobolectricTestRunner
@@ -197,7 +215,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -214,7 +235,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isFalse()
         }
@@ -231,7 +255,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isFalse()
         }
@@ -248,7 +275,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isFalse()
         }
@@ -265,7 +295,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -282,7 +315,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isFalse()
         }
@@ -299,7 +335,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isFalse()
         }
@@ -316,7 +355,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -333,7 +375,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isFalse()
         }
@@ -353,7 +398,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -373,7 +421,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -393,7 +444,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -413,7 +467,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -430,7 +487,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -450,7 +510,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.CONCURRENT_CAMERA, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.CONCURRENT_CAMERA,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -474,7 +537,10 @@
         GuaranteedConfigurationsUtil.getUltraHighResolutionSupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA, it.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), it.surfaceConfigList
                 )
             ).isTrue()
         }
@@ -1460,9 +1526,19 @@
         attachedSurfaceInfoList: List<AttachedSurfaceInfo> = emptyList(),
         hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
         capabilities: IntArray? = null,
-        compareWithAtMost: Boolean = false
+        compareWithAtMost: Boolean = false,
+        compareExpectedFps: Range<Int>? = null,
+        cameraMode: Int = CameraMode.DEFAULT,
+        useCasesExpectedDynamicRangeMap: Map<UseCase, DynamicRange> = emptyMap(),
+        dynamicRangeProfiles: DynamicRangeProfiles? = null,
+        default10BitProfile: Long? = null,
     ) {
-        setupCamera(hardwareLevel = hardwareLevel, capabilities = capabilities)
+        setupCamera(
+            hardwareLevel = hardwareLevel,
+            capabilities = capabilities,
+            dynamicRangeProfiles = dynamicRangeProfiles,
+            default10BitProfile = default10BitProfile
+        )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, fakeCameraMetadata,
             mockEncoderProfilesAdapter
@@ -1472,7 +1548,7 @@
         val useCaseConfigToOutputSizesMap =
             getUseCaseConfigToOutputSizesMap(useCaseConfigMap.values.toList())
         val suggestedStreamSpecs = supportedSurfaceCombination.getSuggestedStreamSpecifications(
-            CameraMode.DEFAULT,
+            cameraMode,
             attachedSurfaceInfoList,
             useCaseConfigToOutputSizesMap
         ).first
@@ -1485,6 +1561,19 @@
             } else {
                 assertThat(sizeIsAtMost(resultSize, expectedSize)).isTrue()
             }
+
+            compareExpectedFps?.let { _ ->
+                assertThat(
+                    suggestedStreamSpecs[useCaseConfigMap[it]]!!.expectedFrameRateRange
+                ).isEqualTo(compareExpectedFps)
+            }
+        }
+
+        useCasesExpectedDynamicRangeMap.keys.forEach {
+            val resultDynamicRange = suggestedStreamSpecs[useCaseConfigMap[it]]!!.dynamicRange
+            val expectedDynamicRange = useCasesExpectedDynamicRangeMap[it]
+
+            assertThat(resultDynamicRange).isEqualTo(expectedDynamicRange)
         }
     }
 
@@ -1519,6 +1608,1165 @@
 
     // //////////////////////////////////////////////////////////////////////////////////////////
     //
+    // StreamSpec selection tests for DynamicRange
+    //
+    // //////////////////////////////////////////////////////////////////////////////////////////
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun check10BitDynamicRangeCombinationsSupported() {
+        setupCamera(
+            capabilities = intArrayOf(
+                CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, fakeCameraMetadata,
+            mockEncoderProfilesAdapter
+        )
+
+        GuaranteedConfigurationsUtil.get10BitSupportedCombinationList().forEach {
+            assertThat(
+                supportedSurfaceCombination.checkSupported(
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_10_BIT
+                    ),
+                    it.surfaceConfigList
+                )
+            ).isTrue()
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun getSupportedStreamSpecThrows_whenUsingUnsupportedDynamicRange() {
+        val useCase =
+            createUseCase(
+                UseCaseConfigFactory.CaptureType.PREVIEW,
+                dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+            )
+        val useCaseExpectedResultMap = mapOf(
+            useCase to Size(0, 0) // Should throw before verifying size
+        )
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSuggestedSpecsAndVerify(
+                useCaseExpectedResultMap,
+                capabilities =
+                intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)
+            )
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun getSupportedStreamSpecThrows_whenUsingConcurrentCameraAndSupported10BitRange() {
+        Shadows.shadowOf(context.packageManager).setSystemFeature(
+            PackageManager.FEATURE_CAMERA_CONCURRENT, true
+        )
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to Size(0, 0) // Should throw before verifying size
+        )
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSuggestedSpecsAndVerify(
+                useCaseExpectedSizeMap,
+                cameraMode = CameraMode.CONCURRENT_CAMERA,
+                dynamicRangeProfiles = HLG10_CONSTRAINED
+            )
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun getSupportedStreamSpecThrows_whenUsingUltraHighResolutionAndSupported10BitRange() {
+        Shadows.shadowOf(context.packageManager).setSystemFeature(
+            PackageManager.FEATURE_CAMERA_CONCURRENT, true
+        )
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to Size(0, 0) // Should throw before verifying size
+        )
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSuggestedSpecsAndVerify(
+                useCaseExpectedSizeMap,
+                cameraMode = CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA,
+                dynamicRangeProfiles = HLG10_CONSTRAINED
+            )
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsHlg_dueToMandatory10Bit() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.HLG_10_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = HLG10_CONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsHdr10_dueToRecommended10BitDynamicRange() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.HDR10_10_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = HDR10_UNCONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap,
+            default10BitProfile = DynamicRangeProfiles.HDR10
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision8_dueToSupportedDynamicRanges() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_HDR_UNSPECIFIED,
+                DynamicRange.BIT_DEPTH_8_BIT
+            )
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_8B_UNCONSTRAINED,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision8_fromUnspecifiedBitDepth() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_DOLBY_VISION,
+                DynamicRange.BIT_DEPTH_UNSPECIFIED
+            )
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_8B_UNCONSTRAINED,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision10_fromUnspecifiedBitDepth() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_DOLBY_VISION,
+                DynamicRange.BIT_DEPTH_UNSPECIFIED
+            )
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.DOLBY_VISION_10_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_10B_UNCONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision8_fromUnspecifiedHdrWithUnspecifiedBitDepth() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_HDR_UNSPECIFIED,
+                DynamicRange.BIT_DEPTH_UNSPECIFIED
+            )
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_8B_UNCONSTRAINED,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision10_fromUnspecifiedHdrWithUnspecifiedBitDepth() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_HDR_UNSPECIFIED,
+                DynamicRange.BIT_DEPTH_UNSPECIFIED
+            )
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.DOLBY_VISION_10_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_CONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap,
+            default10BitProfile = DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision8_withUndefinedBitDepth_andFullyDefinedHlg10() {
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.HLG_10_BIT
+        )
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_DOLBY_VISION,
+                DynamicRange.BIT_DEPTH_UNSPECIFIED
+            )
+        )
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            videoUseCase to DynamicRange.HLG_10_BIT,
+            previewUseCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_8B_UNCONSTRAINED_HLG10_UNCONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision10_dueToDynamicRangeConstraints() {
+        // VideoCapture partially defined dynamic range
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+        )
+        // Preview fully defined dynamic range
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.DOLBY_VISION_8_BIT,
+
+            )
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            videoUseCase to DynamicRange.DOLBY_VISION_10_BIT,
+            previewUseCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_CONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_resolvesUnspecifiedDynamicRange_afterPartiallySpecifiedDynamicRange() {
+        // VideoCapture partially defined dynamic range
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+        )
+        // Preview unspecified dynamic range
+        val previewUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            previewUseCase to DynamicRange.HLG_10_BIT,
+            videoUseCase to DynamicRange.HLG_10_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = HLG10_UNCONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_resolvesUnspecifiedDynamicRangeToSdr() {
+        // Preview unspecified dynamic range
+        val useCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.SDR
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = HLG10_CONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Test
+    fun dynamicRangeResolver_resolvesToSdr_when10BitNotSupported() {
+        // Preview unspecified dynamic range
+        val useCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.SDR
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Test
+    fun dynamicRangeResolver_resolvesToSdr8Bit_whenSdrWithUnspecifiedBitDepthProvided() {
+        // Preview unspecified dynamic range
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_SDR,
+                DynamicRange.BIT_DEPTH_UNSPECIFIED
+            )
+        )
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.SDR
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_resolvesUnspecified8Bit_usingConstraintsFrom10BitDynamicRange() {
+        // VideoCapture has 10-bit HDR range with constraint for 8-bit non-SDR range
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.DOLBY_VISION_10_BIT
+        )
+        // Preview unspecified encoding but 8-bit bit depth
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_UNSPECIFIED,
+                DynamicRange.BIT_DEPTH_8_BIT
+            )
+        )
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            videoUseCase to DynamicRange.DOLBY_VISION_10_BIT,
+            previewUseCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            dynamicRangeProfiles = DOLBY_VISION_CONSTRAINED
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_resolvesToSdr_forUnspecified8Bit_whenNoOtherDynamicRangesPresent() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_UNSPECIFIED,
+                DynamicRange.BIT_DEPTH_8_BIT
+            )
+        )
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            useCase to maximumSize
+        )
+
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.SDR
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap,
+            dynamicRangeProfiles = DOLBY_VISION_8B_SDR_UNCONSTRAINED
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_resolvesUnspecified8BitToDolbyVision8Bit_whenAlreadyPresent() {
+        // VideoCapture fully resolved Dolby Vision 8-bit
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.DOLBY_VISION_8_BIT
+        )
+        // Preview unspecified encoding / 8-bit
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.UNSPECIFIED
+        )
+
+        // Since there are no 10-bit dynamic ranges, the 10-bit resolution table isn't used.
+        // Instead, this will use the camera default LIMITED table which is limited to preview
+        // size for 2 PRIV use cases.
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to previewSize,
+            previewUseCase to previewSize
+        )
+
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            videoUseCase to DynamicRange.DOLBY_VISION_8_BIT,
+            previewUseCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap,
+            dynamicRangeProfiles = DOLBY_VISION_8B_SDR_UNCONSTRAINED
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun tenBitTable_isUsed_whenAttaching10BitUseCaseToAlreadyAttachedSdrUseCases() {
+        // JPEG use case can't be attached with an existing PRIV + YUV in the 10-bit tables
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE,
+            dynamicRange = DynamicRange.HLG_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            // Size would be valid for LIMITED table
+            useCase to recordSize
+        )
+        // existing surfaces (Preview + ImageAnalysis)
+        val attachedPreview = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.PRIV,
+                SurfaceConfig.ConfigSize.PREVIEW
+            ),
+            ImageFormat.PRIVATE,
+            previewSize,
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.PREVIEW),
+            useCase.currentConfig,
+            /*targetFrameRate=*/null
+        )
+        val attachedAnalysis = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.YUV,
+                SurfaceConfig.ConfigSize.RECORD
+            ),
+            ImageFormat.YUV_420_888,
+            recordSize,
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS),
+            useCase.currentConfig,
+            /*targetFrameRate=*/null
+        )
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSuggestedSpecsAndVerify(
+                useCaseExpectedSizeMap,
+                attachedSurfaceInfoList = listOf(attachedPreview, attachedAnalysis),
+                // LIMITED allows this combination, but 10-bit table does not
+                hardwareLevel = CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+                dynamicRangeProfiles = HLG10_SDR_CONSTRAINED,
+                capabilities =
+                intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)
+            )
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeConstraints_causeAutoResolutionToThrow() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE,
+            dynamicRange = DynamicRange.HLG_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            // Size would be valid for 10-bit table within constraints
+            useCase to recordSize
+        )
+        // existing surfaces (PRIV + PRIV)
+        val attachedPriv1 = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.PRIV,
+                SurfaceConfig.ConfigSize.PREVIEW
+            ),
+            ImageFormat.PRIVATE,
+            previewSize,
+            DynamicRange.HDR10_10_BIT,
+            listOf(UseCaseConfigFactory.CaptureType.PREVIEW),
+            useCase.currentConfig,
+            /*targetFrameRate=*/null
+        )
+        val attachedPriv2 = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.PRIV,
+                SurfaceConfig.ConfigSize.RECORD
+            ),
+            ImageFormat.YUV_420_888,
+            recordSize,
+            DynamicRange.HDR10_PLUS_10_BIT,
+            listOf(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE),
+            useCase.currentConfig,
+            /*targetFrameRate=*/null
+        )
+
+        // These constraints say HDR10 and HDR10_PLUS can be combined, but not HLG
+        val constraintsTable =
+            DynamicRangeProfiles(
+                longArrayOf(
+                    DynamicRangeProfiles.HLG10,
+                    DynamicRangeProfiles.HLG10,
+                    LATENCY_NONE,
+                    DynamicRangeProfiles.HDR10,
+                    DynamicRangeProfiles.HDR10 or DynamicRangeProfiles.HDR10_PLUS,
+                    LATENCY_NONE,
+                    DynamicRangeProfiles.HDR10_PLUS,
+                    DynamicRangeProfiles.HDR10_PLUS or DynamicRangeProfiles.HDR10,
+                    LATENCY_NONE
+                )
+            )
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSuggestedSpecsAndVerify(
+                useCaseExpectedSizeMap,
+                attachedSurfaceInfoList = listOf(attachedPriv1, attachedPriv2),
+                dynamicRangeProfiles = constraintsTable,
+                capabilities =
+                intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)
+            )
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canAttachHlgDynamicRange_toExistingSdrStreams() {
+        // JPEG use case can be attached with an existing PRIV + PRIV in the 10-bit tables
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE,
+            dynamicRange = DynamicRange.HLG_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            // Size is valid for 10-bit table within constraints
+            useCase to recordSize
+        )
+        // existing surfaces (PRIV + PRIV)
+        val attachedPriv1 = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.PRIV,
+                SurfaceConfig.ConfigSize.PREVIEW
+            ),
+            ImageFormat.PRIVATE,
+            previewSize,
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.PREVIEW),
+            useCase.currentConfig,
+            /*targetFrameRate=*/null
+        )
+        val attachedPriv2 = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.PRIV,
+                SurfaceConfig.ConfigSize.RECORD
+            ),
+            ImageFormat.YUV_420_888,
+            recordSize,
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS),
+            useCase.currentConfig,
+            /*targetFrameRate=*/null
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            attachedSurfaceInfoList = listOf(attachedPriv1, attachedPriv2),
+            dynamicRangeProfiles = HLG10_SDR_CONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun requiredSdrDynamicRangeThrows_whenCombinedWithConstrainedHlg() {
+        // VideoCapture HLG dynamic range
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.HLG_10_BIT
+        )
+        // Preview SDR dynamic range
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.SDR
+        )
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+
+        // Fails because HLG10 is constrained to only HLG10
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSuggestedSpecsAndVerify(
+                useCaseExpectedSizeMap,
+                dynamicRangeProfiles = HLG10_CONSTRAINED,
+                capabilities =
+                intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            )
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun requiredSdrDynamicRange_canBeCombinedWithUnconstrainedHlg() {
+        // VideoCapture HLG dynamic range
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.HLG_10_BIT
+        )
+        // Preview SDR dynamic range
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.SDR
+        )
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+
+        // Should succeed due to HLG10 being unconstrained
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = HLG10_UNCONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun multiple10BitUnconstrainedDynamicRanges_canBeCombined() {
+        // VideoCapture HDR10 dynamic range
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.HDR10_10_BIT
+        )
+        // Preview HDR10_PLUS dynamic range
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.HDR10_PLUS_10_BIT
+        )
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+
+        // Succeeds because both HDR10 and HDR10_PLUS are unconstrained
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = HDR10_HDR10_PLUS_UNCONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+        )
+    }
+
+    // //////////////////////////////////////////////////////////////////////////////////////////
+    //
+    // Resolution selection tests for FPS settings
+    //
+    // //////////////////////////////////////////////////////////////////////////////////////////
+
+    @Test
+    fun getSupportedOutputSizes_single_valid_targetFPS() {
+        // a valid target means the device is capable of that fps
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(25, 30)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase, Size(3840, 2160))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_single_invalid_targetFPS() {
+        // an invalid target means the device would neve be able to reach that fps
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(65, 70)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase, Size(800, 450))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_multiple_targetFPS_first_is_larger() {
+        // a valid target means the device is capable of that fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 35)
+        )
+        val useCase2 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(15, 25)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // both selected size should be no larger than 1920 x 1445
+            put(useCase1, Size(1920, 1445))
+            put(useCase2, Size(1920, 1445))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_multiple_targetFPS_first_is_smaller() {
+        // a valid target means the device is capable of that fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 35)
+        )
+        val useCase2 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(45, 50)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // both selected size should be no larger than 1920 x 1440
+            put(useCase1, Size(1920, 1440))
+            put(useCase2, Size(1920, 1440))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_multiple_targetFPS_intersect() {
+        // first and second new use cases have target fps that intersect each other
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 40)
+        )
+        val useCase2 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(35, 45)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // effective target fps becomes 35-40
+            // both selected size should be no larger than 1920 x 1080
+            put(useCase1, Size(1920, 1080))
+            put(useCase2, Size(1920, 1080))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_multiple_cases_first_has_targetFPS() {
+        // first new use case has a target fps, second new use case does not
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 35)
+        )
+        val useCase2 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // both selected size should be no larger than 1920 x 1440
+            put(useCase1, Size(1920, 1440))
+            put(useCase2, Size(1920, 1440))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_multiple_cases_second_has_targetFPS() {
+        // second new use case does not have a target fps, first new use case does not
+        val useCase1 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+        val useCase2 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 35)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // both selected size should be no larger than 1920 x 1440
+            put(useCase1, Size(1920, 1440))
+            put(useCase2, Size(1920, 1440))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_attached_with_targetFPS_no_new_targetFPS() {
+        // existing surface with target fps + new use case without a target fps
+        val useCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // size should be no larger than 1280 x 960
+            put(useCase, Size(1280, 960))
+        }
+        // existing surface w/ target fps
+        val attachedSurfaceInfo = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.JPEG,
+                SurfaceConfig.ConfigSize.PREVIEW
+            ),
+            ImageFormat.JPEG,
+            Size(1280, 720),
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.PREVIEW),
+            useCase.currentConfig,
+            Range(40, 50)
+        )
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            attachedSurfaceInfoList = listOf(attachedSurfaceInfo),
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_attached_with_targetFPS_and_new_targetFPS_no_intersect() {
+        // existing surface with target fps + new use case with target fps that does not intersect
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 35)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // size of new surface should be no larger than 1280 x 960
+            put(useCase, Size(1280, 960))
+        }
+        // existing surface w/ target fps
+        val attachedSurfaceInfo = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.JPEG,
+                SurfaceConfig.ConfigSize.PREVIEW
+            ),
+            ImageFormat.JPEG,
+            Size(1280, 720),
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.PREVIEW),
+            useCase.currentConfig,
+            Range(40, 50)
+        )
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            attachedSurfaceInfoList = listOf(attachedSurfaceInfo),
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_attached_with_targetFPS_and_new_targetFPS_with_intersect() {
+        // existing surface with target fps + new use case with target fps that intersect each other
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(45, 50)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // size of new surface should be no larger than 1280 x 720
+            put(useCase, Size(1280, 720))
+        }
+        // existing surface w/ target fps
+        val attachedSurfaceInfo = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.JPEG,
+                SurfaceConfig.ConfigSize.PREVIEW
+            ),
+            ImageFormat.JPEG,
+            Size(1280, 720),
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.PREVIEW),
+            useCase.currentConfig,
+            Range(40, 50)
+        )
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            attachedSurfaceInfoList = listOf(attachedSurfaceInfo),
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_device_supported_expectedFrameRateRange() {
+        // use case with target fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(15, 25)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(4032, 3024))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true,
+            compareExpectedFps = Range(10, 22)
+        )
+        // expected fps 10,22 because it has the largest intersection
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_exact_device_supported_expectedFrameRateRange() {
+        // use case with target fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 40)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(1920, 1440))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true,
+            compareExpectedFps = Range(30, 30)
+        )
+        // expected fps 30,30 because the fps ceiling is 30
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_no_device_supported_expectedFrameRateRange() {
+        // use case with target fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(65, 65)
+        )
+
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(800, 450))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true,
+            compareExpectedFps = Range(60, 60)
+        )
+        // expected fps 60,60 because it is the closest range available
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_multiple_device_supported_expectedFrameRateRange() {
+
+        // use case with target fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(36, 45)
+        )
+
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(1280, 960))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true,
+            compareExpectedFps = Range(30, 40)
+        )
+        // expected size will give a maximum of 40 fps
+        // expected range 30,40. another range with the same intersection size was 30,50, but 30,40
+        // was selected instead because its range has a larger ratio of intersecting value vs
+        // non-intersecting
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_no_device_intersection_expectedFrameRateRange() {
+        // target fps is between ranges, but within device capability (for some reason lol)
+
+        // use case with target fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(26, 27)
+        )
+
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(1920, 1440))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true,
+            compareExpectedFps = Range(30, 30)
+        )
+        // 30,30 was expected because it is the closest and shortest range to our target fps
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_no_device_intersection_equidistant_expectedFrameRateRange() {
+
+        // use case with target fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(26, 26)
+        )
+
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(1920, 1440))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true,
+            compareExpectedFps = Range(30, 30)
+        )
+        // 30,30 selected because although there are other ranges that  have the same distance to
+        // the target, 30,30 is the shortest range that also happens to be on the upper side of the
+        // target range
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_no_expectedFrameRateRange() {
+        // a valid target means the device is capable of that fps
+
+        // use case with no target fps
+        val useCase1 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(4032, 3024))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareExpectedFps = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
+        )
+        // since no target fps present, no specific device fps will be selected, and is set to
+        // unspecified: (0,0)
+    }
+
+    // //////////////////////////////////////////////////////////////////////////////////////////
+    //
     // Other tests
     //
     // //////////////////////////////////////////////////////////////////////////////////////////
@@ -1726,6 +2974,8 @@
         highResolutionSupportedSizes: Array<Size>? = null,
         maximumResolutionSupportedSizes: Array<Size>? = null,
         maximumResolutionHighResolutionSupportedSizes: Array<Size>? = null,
+        dynamicRangeProfiles: DynamicRangeProfiles? = null,
+        default10BitProfile: Long? = null,
         capabilities: IntArray? = null,
         cameraId: CameraId = CameraId.fromCamera1Id(0)
     ) {
@@ -1759,6 +3009,17 @@
                 null
             }
 
+        val deviceFPSRanges: Array<Range<Int>?> = arrayOf(
+            Range(10, 22),
+            Range(22, 22),
+            Range(30, 30),
+            Range(30, 50),
+            Range(30, 40),
+            Range(30, 60),
+            Range(50, 60),
+            Range(60, 60)
+        )
+
         val characteristicsMap = mutableMapOf(
             CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to hardwareLevel,
             CameraCharacteristics.SENSOR_ORIENTATION to sensorOrientation,
@@ -1766,17 +3027,27 @@
             CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK,
             CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES to capabilities,
             CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP to mockMap,
+            CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES to deviceFPSRanges
         ).also { characteristicsMap ->
             mockMaximumResolutionMap?.let {
                 if (Build.VERSION.SDK_INT >= 31) {
                     characteristicsMap[
                         CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION
                     ] =
-                    mockMaximumResolutionMap
+                        mockMaximumResolutionMap
                 }
             }
         }
 
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            dynamicRangeProfiles?.let {
+                characteristicsMap[REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES] = it
+            }
+            default10BitProfile?.let {
+                characteristicsMap[REQUEST_RECOMMENDED_TEN_BIT_DYNAMIC_RANGE_PROFILE] = it
+            }
+        }
+
         // set up FakeCafakeCameraMetadatameraMetadata
         fakeCameraMetadata = FakeCameraMetadata(
             cameraId = cameraId,
@@ -1809,6 +3080,81 @@
                     .thenReturn(it)
             }
         }
+
+        // setup to return different minimum frame durations depending on resolution
+        // minimum frame durations were designated only for the purpose of testing
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(4032, 3024))
+            )
+        )
+            .thenReturn(50000000L) // 20 fps, size maximum
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(3840, 2160))
+            )
+        )
+            .thenReturn(40000000L) // 25, size record
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(1920, 1440))
+            )
+        )
+            .thenReturn(33333333L) // 30
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(1920, 1080))
+            )
+        )
+            .thenReturn(28571428L) // 35
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(1280, 960))
+            )
+        )
+            .thenReturn(25000000L) // 40
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(1280, 720))
+            )
+        )
+            .thenReturn(22222222L) // 45, size preview/display
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(960, 544))
+            )
+        )
+            .thenReturn(20000000L) // 50
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(800, 450))
+            )
+        )
+            .thenReturn(16666666L) // 60fps
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(640, 480))
+            )
+        )
+            .thenReturn(16666666L) // 60fps
+
         shadowCharacteristics.set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP, mockMap)
         mockMaximumResolutionMap?.let {
             whenever(mockMaximumResolutionMap.getOutputSizes(ArgumentMatchers.anyInt()))
@@ -1891,7 +3237,8 @@
 
     private fun createUseCase(
         captureType: UseCaseConfigFactory.CaptureType,
-        targetFrameRate: Range<Int>? = null
+        targetFrameRate: Range<Int>? = null,
+        dynamicRange: DynamicRange? = DynamicRange.UNSPECIFIED
     ): UseCase {
         val builder = FakeUseCaseConfig.Builder(
             captureType, when (captureType) {
@@ -1903,6 +3250,10 @@
         targetFrameRate?.let {
             builder.mutableConfig.insertOption(UseCaseConfig.OPTION_TARGET_FRAME_RATE, it)
         }
+        builder.mutableConfig.insertOption(
+            ImageInputConfig.OPTION_INPUT_DYNAMIC_RANGE,
+            dynamicRange
+        )
         return builder.build()
     }
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatTest.kt
new file mode 100644
index 0000000..b06ab92
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatTest.kt
@@ -0,0 +1,292 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.params.DynamicRangeProfiles
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_10B_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_10B_UNCONSTRAINED_SLOW
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_8B_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HDR10_PLUS_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HDR10_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_CONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_HDR10_CONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_SDR_CONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_UNCONSTRAINED
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.DynamicRange
+import com.google.common.truth.Truth
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.ShadowCameraCharacteristics
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class DynamicRangeProfilesCompatTest {
+
+    private val cameraId = CameraId.fromCamera1Id(0)
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canWrapAndUnwrapDynamicRangeProfiles() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_UNCONSTRAINED)
+
+        Truth.assertThat(dynamicRangeProfilesCompat).isNotNull()
+        Truth.assertThat(dynamicRangeProfilesCompat?.toDynamicRangeProfiles())
+            .isEqualTo(HLG10_UNCONSTRAINED)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromHlg10Profile() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_UNCONSTRAINED)
+        Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+            .contains(DynamicRange.HLG_10_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromHdr10Profile() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HDR10_UNCONSTRAINED)
+        Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+            .contains(DynamicRange.HDR10_10_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromHdr10PlusProfile() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HDR10_PLUS_UNCONSTRAINED)
+        Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+            .contains(DynamicRange.HDR10_PLUS_10_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromDolbyVision10bProfile() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(DOLBY_VISION_10B_UNCONSTRAINED)
+        Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+            .contains(DynamicRange.DOLBY_VISION_10_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromDolbyVision8bProfile() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(DOLBY_VISION_8B_UNCONSTRAINED)
+        Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+            .contains(DynamicRange.DOLBY_VISION_8_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canProduceConcurrentDynamicRangeConstraints() {
+        val hlg10ConstrainedWrapped =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_CONSTRAINED)
+        Truth.assertThat(
+            hlg10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(DynamicRange.SDR)
+        ).containsExactly(DynamicRange.SDR)
+        Truth.assertThat(
+            hlg10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(
+                DynamicRange.HLG_10_BIT
+            )
+        ).containsExactly(DynamicRange.HLG_10_BIT)
+
+        val hlg10SdrConstrainedWrapped =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_SDR_CONSTRAINED)
+        Truth.assertThat(
+            hlg10SdrConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(DynamicRange.SDR)
+        ).containsExactly(DynamicRange.SDR, DynamicRange.HLG_10_BIT)
+        Truth.assertThat(
+            hlg10SdrConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(
+                DynamicRange.HLG_10_BIT
+            )
+        ).containsExactly(DynamicRange.HLG_10_BIT, DynamicRange.SDR)
+
+        val hlg10Hdr10ConstrainedWrapped =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_HDR10_CONSTRAINED)
+        Truth.assertThat(
+            hlg10Hdr10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(DynamicRange.SDR)
+        ).containsExactly(DynamicRange.SDR)
+        Truth.assertThat(
+            hlg10Hdr10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(
+                DynamicRange.HLG_10_BIT
+            )
+        ).containsExactly(DynamicRange.HLG_10_BIT, DynamicRange.HDR10_10_BIT)
+        Truth.assertThat(
+            hlg10Hdr10ConstrainedWrapped
+                ?.getDynamicRangeCaptureRequestConstraints(DynamicRange.HDR10_10_BIT)
+        ).containsExactly(DynamicRange.HDR10_10_BIT, DynamicRange.HLG_10_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun producesDynamicRangeWithCorrectLatency() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(DOLBY_VISION_10B_UNCONSTRAINED_SLOW)
+        Truth.assertThat(dynamicRangeProfilesCompat?.isExtraLatencyPresent(DynamicRange.SDR))
+            .isFalse()
+        Truth.assertThat(
+            dynamicRangeProfilesCompat?.isExtraLatencyPresent(DynamicRange.DOLBY_VISION_10_BIT)
+        ).isTrue()
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canProduceDynamicRangeWithoutConstraints() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_UNCONSTRAINED)
+        Truth.assertThat(
+            dynamicRangeProfilesCompat?.getDynamicRangeCaptureRequestConstraints(
+                DynamicRange.HLG_10_BIT
+            )
+        ).isEmpty()
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun producesNullDynamicRangeProfilesFromNullCharacteristics() {
+        val cameraMetadata = FakeCameraMetadata(cameraId = cameraId)
+
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+
+        Truth.assertThat(dynamicRangeProfilesCompat.toDynamicRangeProfiles()).isNull()
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canProduceDynamicRangesCompatFromCharacteristics() {
+        val cameraMetadata = FakeCameraMetadata(
+            cameraId = cameraId, characteristics = mutableMapOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES to HLG10_CONSTRAINED
+            )
+        )
+
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+
+        Truth.assertThat(dynamicRangeProfilesCompat.toDynamicRangeProfiles())
+            .isEqualTo(HLG10_CONSTRAINED)
+    }
+
+    @Test
+    fun alwaysSupportsOnlySdrWithoutDynamicRangeProfilesInCharacteristics() {
+        val cameraMetadata = FakeCameraMetadata(cameraId = cameraId)
+
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+
+        Truth.assertThat(dynamicRangeProfilesCompat.getSupportedDynamicRanges())
+            .containsExactly(DynamicRange.SDR)
+        Truth.assertThat(
+            dynamicRangeProfilesCompat.getDynamicRangeCaptureRequestConstraints(DynamicRange.SDR)
+        ).containsExactly(DynamicRange.SDR)
+    }
+
+    @Test
+    fun unsupportedDynamicRangeAlwaysThrowsException() {
+        val characteristics = mutableMapOf<CameraCharacteristics.Key<*>, Any?>()
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            characteristics[CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES] =
+                DOLBY_VISION_8B_UNCONSTRAINED
+        }
+        val cameraMetadata = FakeCameraMetadata(
+            cameraId = cameraId, characteristics = characteristics
+        )
+
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            Truth.assertThat(dynamicRangeProfilesCompat.getSupportedDynamicRanges())
+                .containsExactly(DynamicRange.SDR)
+        } else {
+            Truth.assertThat(dynamicRangeProfilesCompat.getSupportedDynamicRanges())
+                .containsExactly(
+                    DynamicRange.SDR, DynamicRange.DOLBY_VISION_8_BIT
+                )
+        }
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            dynamicRangeProfilesCompat
+                .getDynamicRangeCaptureRequestConstraints(DynamicRange.DOLBY_VISION_10_BIT)
+        }
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            dynamicRangeProfilesCompat.isExtraLatencyPresent(DynamicRange.DOLBY_VISION_10_BIT)
+        }
+    }
+
+    @Test
+    fun sdrHasNoExtraLatency() {
+        val characteristics = mutableMapOf<CameraCharacteristics.Key<*>, Any?>()
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            characteristics[CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES] =
+                HLG10_CONSTRAINED
+        }
+        val cameraMetadata = FakeCameraMetadata(
+            cameraId = cameraId, characteristics = characteristics
+        )
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+
+        Truth.assertThat(dynamicRangeProfilesCompat.isExtraLatencyPresent(DynamicRange.SDR))
+            .isFalse()
+    }
+
+    @Test
+    fun sdrHasSdrConstraint_whenConcurrentDynamicRangesNotSupported() {
+        val characteristics = mutableMapOf<CameraCharacteristics.Key<*>, Any?>()
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            characteristics[CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES] =
+                HLG10_CONSTRAINED
+        }
+        val cameraMetadata = FakeCameraMetadata(
+            cameraId = cameraId, characteristics = characteristics
+        )
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+
+        Truth.assertThat(
+            dynamicRangeProfilesCompat.getDynamicRangeCaptureRequestConstraints(DynamicRange.SDR)
+        )
+            .containsExactly(DynamicRange.SDR)
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+fun ShadowCameraCharacteristics.addDynamicRangeProfiles(
+    dynamicRangeProfiles: DynamicRangeProfiles
+) {
+    set(
+        CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES,
+        dynamicRangeProfiles
+    )
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirkTest.kt
index fd8fa84..d8f0400 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirkTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirkTest.kt
@@ -58,6 +58,9 @@
             arrayOf("sm-a320f", CameraCharacteristics.LENS_FACING_BACK, true),
             arrayOf("SM-A320FL", CameraCharacteristics.LENS_FACING_BACK, true),
             arrayOf("Samsung S7", CameraCharacteristics.LENS_FACING_BACK, false),
+            arrayOf("moto g(20)", CameraCharacteristics.LENS_FACING_BACK, true),
+            arrayOf("itel l6006", CameraCharacteristics.LENS_FACING_BACK, true),
+            arrayOf("rmx3231", CameraCharacteristics.LENS_FACING_BACK, true),
         )
     }
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeTestCases.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeTestCases.kt
new file mode 100644
index 0000000..bcaae5a
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeTestCases.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+@file:RequiresApi(33)
+
+package androidx.camera.camera2.pipe.integration.internal
+
+import android.hardware.camera2.params.DynamicRangeProfiles
+import android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM
+import android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM
+import android.hardware.camera2.params.DynamicRangeProfiles.HDR10
+import android.hardware.camera2.params.DynamicRangeProfiles.HDR10_PLUS
+import android.hardware.camera2.params.DynamicRangeProfiles.HLG10
+import android.hardware.camera2.params.DynamicRangeProfiles.STANDARD
+import androidx.annotation.RequiresApi
+
+val HLG10_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(longArrayOf(HLG10, 0, 0))
+}
+
+val HLG10_CONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, HLG10, LATENCY_NONE
+        )
+    )
+}
+
+val HLG10_SDR_CONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, HLG10 or STANDARD, LATENCY_NONE
+        )
+    )
+}
+
+val HLG10_HDR10_CONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, HLG10 or HDR10, LATENCY_NONE,
+            HDR10, HDR10 or HLG10, LATENCY_NONE
+        )
+    )
+}
+
+val HDR10_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+            HDR10, CONSTRAINTS_NONE, LATENCY_NONE
+        )
+    )
+}
+
+val HDR10_PLUS_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+            HDR10_PLUS, CONSTRAINTS_NONE, LATENCY_NONE
+        )
+    )
+}
+
+val HDR10_HDR10_PLUS_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+            HDR10, CONSTRAINTS_NONE, LATENCY_NONE,
+            HDR10_PLUS, CONSTRAINTS_NONE, LATENCY_NONE
+        )
+    )
+}
+
+val DOLBY_VISION_10B_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+            DOLBY_VISION_10B_HDR_OEM, CONSTRAINTS_NONE, LATENCY_NONE
+        )
+    )
+}
+
+val DOLBY_VISION_10B_UNCONSTRAINED_SLOW by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+            DOLBY_VISION_10B_HDR_OEM, CONSTRAINTS_NONE, LATENCY_NON_ZERO
+        )
+    )
+}
+
+val DOLBY_VISION_8B_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            DOLBY_VISION_8B_HDR_OEM, CONSTRAINTS_NONE, LATENCY_NONE
+        )
+    )
+}
+
+val DOLBY_VISION_8B_SDR_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            DOLBY_VISION_8B_HDR_OEM, DOLBY_VISION_8B_HDR_OEM or STANDARD, LATENCY_NONE
+        )
+    )
+}
+
+val DOLBY_VISION_8B_UNCONSTRAINED_HLG10_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, CONSTRAINTS_NONE, LATENCY_NONE,
+            DOLBY_VISION_8B_HDR_OEM, CONSTRAINTS_NONE, LATENCY_NONE,
+        )
+    )
+}
+
+val DOLBY_VISION_CONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, HLG10, LATENCY_NONE, // HLG is mandated
+            DOLBY_VISION_10B_HDR_OEM, DOLBY_VISION_10B_HDR_OEM or DOLBY_VISION_8B_HDR_OEM,
+            LATENCY_NONE,
+            DOLBY_VISION_8B_HDR_OEM, DOLBY_VISION_8B_HDR_OEM or DOLBY_VISION_10B_HDR_OEM,
+            LATENCY_NONE
+        )
+    )
+}
+
+const val LATENCY_NONE = 0L
+private const val LATENCY_NON_ZERO = 3L
+private const val CONSTRAINTS_NONE = 0L
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
index 401c134..420f747 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
@@ -178,6 +178,14 @@
             override fun getSupportedDynamicRanges(): MutableSet<DynamicRange> {
                 throw NotImplementedError("Not used in testing")
             }
+
+            override fun isPreviewStabilizationSupported(): Boolean {
+                throw NotImplementedError("Not used in testing")
+            }
+
+            override fun isVideoStabilizationSupported(): Boolean {
+                throw NotImplementedError("Not used in testing")
+            }
         }
         Camera2CameraInfo.from(wrongCameraInfo)
     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
index ec0be0e..7274b3b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
@@ -36,8 +36,7 @@
  * This allows all fields to be accessed and return reasonable values on all OS versions.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-internal class Camera2CameraMetadata
-constructor(
+internal class Camera2CameraMetadata(
     override val camera: CameraId,
     override val isRedacted: Boolean,
     private val characteristics: CameraCharacteristics,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
index e7187fe..ba3953d7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
@@ -17,7 +17,9 @@
 package androidx.camera.camera2.pipe.compat
 
 import android.content.Context
+import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraManager
+import android.os.Build
 import android.util.ArrayMap
 import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
@@ -105,7 +107,13 @@
 
                 // Merge the camera specific and global cache blocklists together.
                 // this will prevent these values from being cached after first access.
-                val cameraBlocklist = cameraMetadataConfig.cameraCacheBlocklist[cameraId]
+                val cameraBlocklist =
+                    if (shouldBlockSensorOrientationCache(characteristics)) {
+                        (cameraMetadataConfig.cameraCacheBlocklist[cameraId] ?: emptySet()) +
+                            CameraCharacteristics.SENSOR_ORIENTATION
+                    } else {
+                        cameraMetadataConfig.cameraCacheBlocklist[cameraId]
+                    }
                 val cacheBlocklist =
                     if (cameraBlocklist == null) {
                         cameraMetadataConfig.cacheBlocklist
@@ -146,4 +154,9 @@
     }
 
     private fun isMetadataRedacted(): Boolean = !permissions.hasCameraPermission
+
+    private fun shouldBlockSensorOrientationCache(characteristics: CameraCharacteristics): Boolean {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2 &&
+            characteristics[CameraCharacteristics.INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP] != null
+    }
 }
diff --git a/camera/camera-camera2/lint-baseline.xml b/camera/camera-camera2/lint-baseline.xml
index 82170fb..eb3bdd8 100644
--- a/camera/camera-camera2/lint-baseline.xml
+++ b/camera/camera-camera2/lint-baseline.xml
@@ -1,5 +1,14 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-beta02" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta02)" variant="all" version="8.1.0-beta02">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
+
+    <issue
+        id="NewApi"
+        message="Field requires API level 30 (current min is 21): `android.hardware.camera2.CameraCharacteristics#CONTROL_ZOOM_RATIO_RANGE`"
+        errorLine1="                    CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE).getUpper();"
+        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/camera/camera2/internal/ZoomControlDeviceTest.java"/>
+    </issue>
 
     <issue
         id="BanThreadSleep"
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 78ff31d..5422996 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -1122,7 +1122,7 @@
 
         try {
             mSupportedSurfaceCombination.getSuggestedStreamSpecifications(cameraMode,
-                    attachedSurfaces, useCaseConfigToSizeMap);
+                    attachedSurfaces, useCaseConfigToSizeMap, false);
         } catch (IllegalArgumentException e) {
             debugLog("Surface combination with metering repeating  not supported!", e);
             return false;
@@ -1480,7 +1480,11 @@
                 }
 
                 Logger.e(TAG, "Unable to configure camera " + Camera2CameraImpl.this, t);
-                resetCaptureSession(/*abortInFlightCaptures=*/false);
+
+                // Reset capture session if the latest capture session fails to open.
+                if (mCaptureSession == captureSession) {
+                    resetCaptureSession(/*abortInFlightCaptures=*/false);
+                }
             }
         }, mExecutor);
     }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index 3729cf8..edcf484 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -16,10 +16,12 @@
 
 package androidx.camera.camera2.internal;
 
+import static android.hardware.camera2.CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES;
+import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON;
+import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION;
 import static android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING;
 import static android.hardware.camera2.CameraMetadata.SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME;
 import static android.hardware.camera2.CameraMetadata.SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN;
-
 import static androidx.camera.camera2.internal.ZslUtil.isCapabilitySupported;
 
 import android.hardware.camera2.CameraCharacteristics;
@@ -53,6 +55,7 @@
 import androidx.camera.core.ExposureState;
 import androidx.camera.core.FocusMeteringAction;
 import androidx.camera.core.Logger;
+import androidx.camera.core.PreviewCapabilities;
 import androidx.camera.core.ZoomState;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraInfoInternal;
@@ -505,6 +508,42 @@
         }
     }
 
+    @NonNull
+    @Override
+    public PreviewCapabilities getPreviewCapabilities() {
+        return Camera2PreviewCapabilities.from(this);
+    }
+
+    @Override
+    public boolean isVideoStabilizationSupported() {
+        int[] availableVideoStabilizationModes =
+                mCameraCharacteristicsCompat.get(
+                        CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES);
+        if (availableVideoStabilizationModes != null) {
+            for (int mode : availableVideoStabilizationModes) {
+                if (mode == CONTROL_VIDEO_STABILIZATION_MODE_ON) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isPreviewStabilizationSupported() {
+        int[] availableVideoStabilizationModes =
+                mCameraCharacteristicsCompat.get(
+                        CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES);
+        if (availableVideoStabilizationModes != null) {
+            for (int mode : availableVideoStabilizationModes) {
+                if (mode == CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     /**
      * Gets the implementation of {@link Camera2CameraInfo}.
      */
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java
index 60c74f0..2d66b8c 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java
@@ -28,6 +28,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
 import androidx.camera.camera2.impl.Camera2ImplConfig;
 import androidx.camera.camera2.interop.CaptureRequestOptions;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
@@ -37,6 +38,7 @@
 import androidx.camera.core.impl.Config;
 import androidx.camera.core.impl.DeferrableSurface;
 import androidx.camera.core.impl.StreamSpec;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -111,6 +113,21 @@
 
     }
 
+    @VisibleForTesting
+    static void applyVideoStabilization(@NonNull CaptureConfig captureConfig,
+            @NonNull CaptureRequest.Builder builder) {
+        if (captureConfig.getPreviewStabilizationMode() == StabilizationMode.OFF
+                || captureConfig.getVideoStabilizationMode() == StabilizationMode.OFF) {
+            builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
+                    CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF);
+        } else if (captureConfig.getPreviewStabilizationMode() == StabilizationMode.ON) {
+            builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
+                    CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION);
+        } else if (captureConfig.getVideoStabilizationMode() == StabilizationMode.ON) {
+            builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
+                    CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON);
+        }
+    }
 
     /**
      * Builds a {@link CaptureRequest} from a {@link CaptureConfig} and a {@link CameraDevice}.
@@ -155,6 +172,8 @@
 
         applyAeFpsRange(captureConfig, builder);
 
+        applyVideoStabilization(captureConfig, builder);
+
         if (captureConfig.getImplementationOptions().containsOption(
                 CaptureConfig.OPTION_ROTATION)) {
             builder.set(CaptureRequest.JPEG_ORIENTATION,
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
index 7c1d95e..1322af7 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
@@ -171,7 +171,8 @@
             @CameraMode.Mode int cameraMode,
             @NonNull String cameraId,
             @NonNull List<AttachedSurfaceInfo> existingSurfaces,
-            @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap) {
+            @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap,
+            boolean isPreviewStabilizationOn) {
         Preconditions.checkArgument(!newUseCaseConfigsSupportedSizeMap.isEmpty(),
                 "No new use cases to be bound.");
 
@@ -186,6 +187,7 @@
         return supportedSurfaceCombination.getSuggestedStreamSpecifications(
                 cameraMode,
                 existingSurfaces,
-                newUseCaseConfigsSupportedSizeMap);
+                newUseCaseConfigsSupportedSizeMap,
+                isPreviewStabilizationOn);
     }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PreviewCapabilities.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PreviewCapabilities.java
new file mode 100644
index 0000000..aa25edd
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PreviewCapabilities.java
@@ -0,0 +1,48 @@
+/*
+ * 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.camera.camera2.internal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.PreviewCapabilities;
+import androidx.camera.core.impl.CameraInfoInternal;
+
+/**
+ * Camera2 implementation of {@link PreviewCapabilities}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class Camera2PreviewCapabilities implements PreviewCapabilities {
+
+    private boolean mIsStabilizationSupported = false;
+
+    Camera2PreviewCapabilities(@NonNull CameraInfoInternal cameraInfoInternal) {
+        mIsStabilizationSupported = cameraInfoInternal.isPreviewStabilizationSupported();
+    }
+
+
+    @NonNull
+    static Camera2PreviewCapabilities from(@NonNull CameraInfo cameraInfo) {
+        return new Camera2PreviewCapabilities((CameraInfoInternal) cameraInfo);
+    }
+
+
+    @Override
+    public boolean isStabilizationSupported() {
+        return mIsStabilizationSupported;
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
index e64c90c..dc1207c 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
@@ -89,6 +89,10 @@
                         camera2Config.getSessionCaptureCallback(
                                 Camera2CaptureCallbacks.createNoOpCallback())));
 
+        // Set video stabilization mode
+        builder.setVideoStabilization(config.getVideoStabilizationMode());
+        builder.setPreviewStabilization(config.getPreviewStabilizationMode());
+
         // Copy extended Camera2 configurations
         MutableOptionsBundle extendedConfig = MutableOptionsBundle.create();
         extendedConfig.insertOption(Camera2ImplConfig.SESSION_PHYSICAL_CAMERA_ID_OPTION,
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/GuaranteedConfigurationsUtil.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/GuaranteedConfigurationsUtil.java
index d18bd1b..5a4413f 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/GuaranteedConfigurationsUtil.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/GuaranteedConfigurationsUtil.java
@@ -833,6 +833,93 @@
     }
 
     /**
+     * Returns the supported stream combinations for preview stabilization.
+     */
+    @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
+    @NonNull
+    public static List<SurfaceCombination> getPreviewStabilizationSupportedCombinationList() {
+        List<SurfaceCombination> combinationList = new ArrayList<>();
+
+        // (PRIV, s1440p)
+        SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+        surfaceCombination1.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.PRIV, ConfigSize.s1440p));
+        combinationList.add(surfaceCombination1);
+
+        // (YUV, s1440p)
+        SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+        surfaceCombination2.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.YUV, ConfigSize.s1440p));
+        combinationList.add(surfaceCombination2);
+
+        // (PRIV, s1440p) + (JPEG, MAXIMUM)
+        SurfaceCombination surfaceCombination3 = new SurfaceCombination();
+        surfaceCombination3.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.PRIV, ConfigSize.s1440p));
+        surfaceCombination3.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.JPEG, ConfigSize.MAXIMUM));
+        combinationList.add(surfaceCombination3);
+
+        // (YUV, s1440p) + (JPEG, MAXIMUM)
+        SurfaceCombination surfaceCombination4 = new SurfaceCombination();
+        surfaceCombination4.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.YUV, ConfigSize.s1440p));
+        surfaceCombination4.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.JPEG, ConfigSize.MAXIMUM));
+        combinationList.add(surfaceCombination4);
+
+        // (PRIV, s1440p) + (YUV, MAXIMUM)
+        SurfaceCombination surfaceCombination5 = new SurfaceCombination();
+        surfaceCombination5.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.PRIV, ConfigSize.s1440p));
+        surfaceCombination5.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.YUV, ConfigSize.MAXIMUM));
+        combinationList.add(surfaceCombination5);
+
+        // (YUV, s1440p) + (YUV, MAXIMUM)
+        SurfaceCombination surfaceCombination6 = new SurfaceCombination();
+        surfaceCombination6.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.YUV, ConfigSize.s1440p));
+        surfaceCombination6.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.YUV, ConfigSize.MAXIMUM));
+        combinationList.add(surfaceCombination6);
+
+        // (PRIV, PREVIEW) + (PRIV, s1440)
+        SurfaceCombination surfaceCombination7 = new SurfaceCombination();
+        surfaceCombination7.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW));
+        surfaceCombination7.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.PRIV, ConfigSize.s1440p));
+        combinationList.add(surfaceCombination7);
+
+        // (YUV, PREVIEW) + (PRIV, s1440)
+        SurfaceCombination surfaceCombination8 = new SurfaceCombination();
+        surfaceCombination8.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.YUV, ConfigSize.PREVIEW));
+        surfaceCombination8.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.PRIV, ConfigSize.s1440p));
+        combinationList.add(surfaceCombination8);
+
+        // (PRIV, PREVIEW) + (YUV, s1440)
+        SurfaceCombination surfaceCombination9 = new SurfaceCombination();
+        surfaceCombination9.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW));
+        surfaceCombination9.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.YUV, ConfigSize.s1440p));
+        combinationList.add(surfaceCombination9);
+
+        // (YUV, PREVIEW) + (YUV, s1440)
+        SurfaceCombination surfaceCombination10 = new SurfaceCombination();
+        surfaceCombination10.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.YUV, ConfigSize.PREVIEW));
+        surfaceCombination10.addSurfaceConfig(
+                SurfaceConfig.create(ConfigType.YUV, ConfigSize.s1440p));
+        combinationList.add(surfaceCombination10);
+
+        return combinationList;
+    }
+
+    /**
      * Returns the supported stream combinations based on the hardware level and capabilities of
      * the device.
      */
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
index 01357dc..e89c415 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
@@ -452,7 +452,12 @@
         switch (mProcessorState) {
             case ON_CAPTURE_SESSION_ENDED:
             case SESSION_INITIALIZED:
-                future.addListener(() -> mSessionProcessor.deInitSession(), mExecutor);
+                future.addListener(() -> {
+                    Logger.d(TAG, "== deInitSession (id=" + mInstanceId + ")");
+                    mSessionProcessor.deInitSession();
+                    // Use direct executor to ensure deInitSession is invoked as soon as session is
+                    // closed.
+                }, CameraXExecutors.directExecutor());
                 break;
             default:
                 break;
@@ -473,11 +478,12 @@
     }
 
     void onConfigured(@NonNull CaptureSession captureSession) {
-        Preconditions.checkArgument(mProcessorState == ProcessorState.SESSION_INITIALIZED,
-                "Invalid state state:" + mProcessorState);
-
+        if (mProcessorState != ProcessorState.SESSION_INITIALIZED) {
+            return;
+        }
         mRequestProcessor = new Camera2RequestProcessor(captureSession,
                 getSessionProcessorSurfaceList(mProcessorSessionConfig.getSurfaces()));
+        Logger.d(TAG, "== onCaptureSessinStarted (id = " + mInstanceId + ")");
         mSessionProcessor.onCaptureSessionStart(mRequestProcessor);
         mProcessorState = ProcessorState.ON_CAPTURE_SESSION_STARTED;
 
@@ -534,6 +540,7 @@
         Logger.d(TAG, "close (id=" + mInstanceId + ") state=" + mProcessorState);
 
         if (mProcessorState == ProcessorState.ON_CAPTURE_SESSION_STARTED) {
+            Logger.d(TAG, "== onCaptureSessionEnd (id = " + mInstanceId + ")");
             mSessionProcessor.onCaptureSessionEnd();
             if (mRequestProcessor != null) {
                 mRequestProcessor.close();
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index cd453ff..a6d9b50 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -18,7 +18,6 @@
 
 import static android.content.pm.PackageManager.FEATURE_CAMERA_CONCURRENT;
 import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES;
-
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_480P;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA;
@@ -95,6 +94,8 @@
     private final List<SurfaceCombination> mSurfaceCombinations = new ArrayList<>();
     private final List<SurfaceCombination> mUltraHighSurfaceCombinations = new ArrayList<>();
     private final List<SurfaceCombination> mConcurrentSurfaceCombinations = new ArrayList<>();
+    private final List<SurfaceCombination> mPreviewStabilizationSurfaceCombinations =
+            new ArrayList<>();
     private final Map<FeatureSettings, List<SurfaceCombination>>
             mFeatureSettingsToSupportedCombinationsMap = new HashMap<>();
     private final List<SurfaceCombination> mSurfaceCombinations10Bit = new ArrayList<>();
@@ -110,6 +111,7 @@
     private boolean mIsConcurrentCameraModeSupported = false;
     private boolean mIsStreamUseCaseSupported = false;
     private boolean mIsUltraHighResolutionSensorSupported = false;
+    private boolean mIsPreviewStabilizationSupported = false;
     @VisibleForTesting
     SurfaceSizeDefinition mSurfaceSizeDefinition;
     List<Integer> mSurfaceSizeDefinitionFormats = new ArrayList<>();
@@ -184,6 +186,12 @@
             generateStreamUseCaseSupportedCombinationList();
         }
 
+        mIsPreviewStabilizationSupported =
+                VideoStabilizationUtil.isPreviewStabilizationSupported(mCharacteristics);
+        if (mIsPreviewStabilizationSupported) {
+            generatePreviewStabilizationSupportedCombinationList();
+        }
+
         generateSurfaceSizeDefinition();
         checkCustomization();
     }
@@ -267,7 +275,8 @@
                     supportedSurfaceCombinations.addAll(mSurfaceCombinations);
                     break;
                 default:
-                    supportedSurfaceCombinations.addAll(mSurfaceCombinations);
+                    supportedSurfaceCombinations.addAll(featureSettings.isPreviewStabilizationOn()
+                            ? mPreviewStabilizationSurfaceCombinations : mSurfaceCombinations);
                     break;
             }
         } else if (featureSettings.getRequiredMaxBitDepth() == DynamicRange.BIT_DEPTH_10_BIT) {
@@ -521,6 +530,7 @@
      * @param attachedSurfaces                  the existing surfaces.
      * @param newUseCaseConfigsSupportedSizeMap newly added UseCaseConfig to supported output
      *                                          sizes map.
+     * @param isPreviewStabilizationOn          whether the preview stabilization is enabled.
      * @return the suggested stream specifications, which is a pair of mappings. The first
      * mapping is from UseCaseConfig to the suggested stream specification representing new
      * UseCases. The second mapping is from attachedSurfaceInfo to the suggested stream
@@ -536,7 +546,8 @@
             getSuggestedStreamSpecifications(
             @CameraMode.Mode int cameraMode,
             @NonNull List<AttachedSurfaceInfo> attachedSurfaces,
-            @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap) {
+            @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap,
+            boolean isPreviewStabilizationOn) {
         // Refresh Preview Size based on current display configurations.
         refreshPreviewSize();
         List<SurfaceConfig> surfaceConfigs = new ArrayList<>();
@@ -553,7 +564,8 @@
                 mDynamicRangeResolver.resolveAndValidateDynamicRanges(attachedSurfaces,
                         newUseCaseConfigs, useCasesPriorityOrder);
         int requiredMaxBitDepth = getRequiredMaxBitDepth(resolvedDynamicRanges);
-        FeatureSettings featureSettings = FeatureSettings.of(cameraMode, requiredMaxBitDepth);
+        FeatureSettings featureSettings = FeatureSettings.of(cameraMode, requiredMaxBitDepth,
+                isPreviewStabilizationOn);
         if (cameraMode != CameraMode.DEFAULT
                 && requiredMaxBitDepth == DynamicRange.BIT_DEPTH_10_BIT) {
             throw new IllegalArgumentException(String.format("No supported surface combination is "
@@ -1104,6 +1116,13 @@
         }
     }
 
+    private void generatePreviewStabilizationSupportedCombinationList() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            mPreviewStabilizationSurfaceCombinations.addAll(
+                    GuaranteedConfigurationsUtil.getPreviewStabilizationSupportedCombinationList());
+        }
+    }
+
     private void checkCustomization() {
         // TODO(b/119466260): Integrate found feasible stream combinations into supported list
     }
@@ -1334,9 +1353,10 @@
     abstract static class FeatureSettings {
         @NonNull
         static FeatureSettings of(@CameraMode.Mode int cameraMode,
-                @RequiredMaxBitDepth int requiredMaxBitDepth) {
+                @RequiredMaxBitDepth int requiredMaxBitDepth,
+                boolean isPreviewStabilizationOn) {
             return new AutoValue_SupportedSurfaceCombination_FeatureSettings(
-                    cameraMode, requiredMaxBitDepth);
+                    cameraMode, requiredMaxBitDepth, isPreviewStabilizationOn);
         }
 
         /**
@@ -1364,5 +1384,10 @@
          * {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT}.
          */
         abstract @RequiredMaxBitDepth int getRequiredMaxBitDepth();
+
+        /**
+         * Whether the preview stabilization is enabled.
+         */
+        abstract boolean isPreviewStabilizationOn();
     }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TemplateTypeUtil.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TemplateTypeUtil.java
index 905e01f..31f8b81 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TemplateTypeUtil.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TemplateTypeUtil.java
@@ -49,8 +49,12 @@
                         ? CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG :
                         CameraDevice.TEMPLATE_PREVIEW;
             case VIDEO_CAPTURE:
-            case STREAM_SHARING:
                 return CameraDevice.TEMPLATE_RECORD;
+            case STREAM_SHARING:
+                // Uses TEMPLATE_PREVIEW instead of TEMPLATE_RECORD. Since there is a issue that
+                // captured results being stretched when requested for recording on some models,
+                // it would be safer to request for preview, which is also better tested. More
+                // detail please see b/297167569.
             case PREVIEW:
             case IMAGE_ANALYSIS:
             default:
@@ -72,8 +76,10 @@
                         ? CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG :
                         CameraDevice.TEMPLATE_STILL_CAPTURE;
             case VIDEO_CAPTURE:
-            case STREAM_SHARING:
                 return CameraDevice.TEMPLATE_RECORD;
+            case STREAM_SHARING:
+                // Uses TEMPLATE_PREVIEW instead of TEMPLATE_RECORD to align with
+                // getSessionConfigTemplateType method. More detail please see b/297167569.
             case PREVIEW:
             case IMAGE_ANALYSIS:
             default:
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/VideoStabilizationUtil.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/VideoStabilizationUtil.java
new file mode 100644
index 0000000..b9f5eeb
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/VideoStabilizationUtil.java
@@ -0,0 +1,57 @@
+/*
+ * 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.camera.camera2.internal;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+
+/**
+ * A class that contains utility methods for video stabilization.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public final class VideoStabilizationUtil {
+
+    private VideoStabilizationUtil() {
+    }
+
+    /**
+     * Return true if the given camera characteristics support preview stabilization.
+     */
+    public static boolean isPreviewStabilizationSupported(
+            @NonNull CameraCharacteristicsCompat characteristicsCompat) {
+        if (Build.VERSION.SDK_INT < 33) {
+            return false;
+        }
+        int[] availableVideoStabilizationModes = characteristicsCompat.get(
+                CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES);
+        if (availableVideoStabilizationModes == null
+                || availableVideoStabilizationModes.length == 0) {
+            return false;
+        }
+        for (int mode : availableVideoStabilizationModes) {
+            if (mode == CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirk.java
index 8da23cc..2398ddf 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirk.java
@@ -33,11 +33,12 @@
  * Quirks that denotes the device has a slow flash sequence that could result in blurred pictures.
  *
  * <p>QuirkSummary
- *     Bug Id: 211474332, 286190938, 280221967
+ *     Bug Id: 211474332, 286190938, 280221967, 296814664, 296816175
  *     Description: When capturing still photos in auto flash mode, it needs more than 1 second to
  *     flash or capture actual photo after flash, and therefore it easily results in blurred or dark
  *     or overexposed pictures.
- *     Device(s): Pixel 3a / Pixel 3a XL, all models of Pixel 4 and 5, SM-A320
+ *     Device(s): Pixel 3a / Pixel 3a XL, all models of Pixel 4 and 5, SM-A320, Moto G20, Itel A48,
+ *     Realme C11 2021
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class FlashTooSlowQuirk implements UseTorchAsFlashQuirk {
@@ -46,7 +47,10 @@
             "PIXEL 3A XL",
             "PIXEL 4", // includes Pixel 4 XL, 4A, and 4A (5g) too
             "PIXEL 5", // includes Pixel 5A too
-            "SM-A320"
+            "SM-A320",
+            "MOTO G(20)",
+            "ITEL L6006", // Itel A48
+            "RMX3231" // Realme C11 2021
     );
 
     static boolean load(@NonNull CameraCharacteristicsCompat cameraCharacteristics) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/InvalidVideoProfilesQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/InvalidVideoProfilesQuirk.java
index ba916eb..842e16c 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/InvalidVideoProfilesQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/InvalidVideoProfilesQuirk.java
@@ -30,18 +30,19 @@
  * Quirk denoting the video profile list returns by {@link EncoderProfiles} is invalid.
  *
  * <p>QuirkSummary
- *     Bug Id: 267727595, 278860860
- *     Description: When using {@link EncoderProfiles} on TP1A or TD1A builds of Android API 33,
+ *     Bug Id: 267727595, 278860860, 298951126, 298952500
+ *     Description: When using {@link EncoderProfiles} on some builds of Android API 33,
  *                  {@link EncoderProfiles#getVideoProfiles()} returns a list with size one, but
  *                  the single value in the list is null. This is not the expected behavior, and
  *                  makes {@link EncoderProfiles} lack of video information.
  *     Device(s): Pixel 4 and above pixel devices with TP1A or TD1A builds (API 33), Samsung devices
- *                with TP1A build (API 33).
+ *                 with TP1A build (API 33), Xiaomi devices with TKQ1 build (API 33), OnePlus and
+ *                 Oppo devices with API 33 build.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class InvalidVideoProfilesQuirk implements Quirk {
 
-    static final List<String> AFFECTED_PIXEL_MODELS = Arrays.asList(
+    private static final List<String> AFFECTED_PIXEL_MODELS = Arrays.asList(
             "pixel 4",
             "pixel 4a",
             "pixel 4a (5g)",
@@ -55,8 +56,19 @@
             "pixel 7 pro"
     );
 
+    private static final List<String> AFFECTED_ONE_PLUS_MODELS = Arrays.asList(
+            "cph2417",
+            "cph2451"
+    );
+
+    private static final List<String> AFFECTED_OPPO_MODELS = Arrays.asList(
+            "cph2437",
+            "cph2525"
+    );
+
     static boolean load() {
-        return isAffectedSamsungDevices() || isAffectedPixelDevices();
+        return isAffectedSamsungDevices() || isAffectedPixelDevices() || isAffectedXiaomiDevices()
+                || isAffectedOnePlusDevices() || isAffectedOppoDevices();
     }
 
     private static boolean isAffectedSamsungDevices() {
@@ -67,10 +79,31 @@
         return isAffectedPixelModel() && isAffectedPixelBuild();
     }
 
+    private static boolean isAffectedOnePlusDevices() {
+        return isAffectedOnePlusModel() && isAPI33();
+    }
+
+    private static boolean isAffectedOppoDevices() {
+        return isAffectedOppoModel() && isAPI33();
+    }
+
+    private static boolean isAffectedXiaomiDevices() {
+        return ("redmi".equalsIgnoreCase(Build.BRAND) || "xiaomi".equalsIgnoreCase(Build.BRAND))
+                && isTkq1Build();
+    }
+
     private static boolean isAffectedPixelModel() {
         return AFFECTED_PIXEL_MODELS.contains(Build.MODEL.toLowerCase(Locale.ROOT));
     }
 
+    private static boolean isAffectedOnePlusModel() {
+        return AFFECTED_ONE_PLUS_MODELS.contains(Build.MODEL.toLowerCase(Locale.ROOT));
+    }
+
+    private static boolean isAffectedOppoModel() {
+        return AFFECTED_OPPO_MODELS.contains(Build.MODEL.toLowerCase(Locale.ROOT));
+    }
+
     private static boolean isAffectedPixelBuild() {
         return isTp1aBuild() || isTd1aBuild();
     }
@@ -82,4 +115,12 @@
     private static boolean isTd1aBuild() {
         return Build.ID.toLowerCase(Locale.ROOT).startsWith("td1a");
     }
+
+    private static boolean isTkq1Build() {
+        return Build.ID.toLowerCase(Locale.ROOT).startsWith("tkq1");
+    }
+
+    private static boolean isAPI33() {
+        return Build.VERSION.SDK_INT == 33;
+    }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.java
index cbff938..4b991ff 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.java
@@ -29,20 +29,21 @@
  * <p>QuirkSummary
  *     Bug Id: 228272227
  *     Description: The Torch is unexpectedly turned off after taking a picture.
- *     Device(s): Redmi 4X, Redmi 5A, Redmi Note 5, Mi A1, Mi A2, Mi A2 lite and Redmi 6 Pro.
+ *     Device(s): Redmi 4X, Redmi 5A, Redmi Note 5 (Pro), Mi A1, Mi A2, Mi A2 lite and Redmi 6 Pro.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class TorchIsClosedAfterImageCapturingQuirk implements Quirk {
 
     // List of devices with the issue. See b/228272227.
     public static final List<String> BUILD_MODELS = Arrays.asList(
-            "mi a1",        // Xiaomi Mi A1
-            "mi a2",        // Xiaomi Mi A2
-            "mi a2 lite",   // Xiaomi Mi A2 Lite
-            "redmi 4x",     // Xiaomi Redmi 4X
-            "redmi 5a",     // Xiaomi Redmi 5A
-            "redmi note 5", // Xiaomi Redmi Note 5
-            "redmi 6 pro"   // Xiaomi Redmi 6 Pro
+            "mi a1",            // Xiaomi Mi A1
+            "mi a2",            // Xiaomi Mi A2
+            "mi a2 lite",       // Xiaomi Mi A2 Lite
+            "redmi 4x",         // Xiaomi Redmi 4X
+            "redmi 5a",         // Xiaomi Redmi 5A
+            "redmi note 5",     // Xiaomi Redmi Note 5
+            "redmi note 5 pro", // Xiaomi Redmi Note 5 Pro
+            "redmi 6 pro"       // Xiaomi Redmi 6 Pro
     );
 
     static boolean load() {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index a0447b4..8a876ca 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -17,12 +17,12 @@
 package androidx.camera.camera2.internal;
 
 import static android.hardware.camera2.CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES;
-
+import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF;
+import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON;
+import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION;
 import static androidx.camera.core.DynamicRange.HLG_10_BIT;
 import static androidx.camera.core.DynamicRange.SDR;
-
 import static com.google.common.truth.Truth.assertThat;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -678,6 +678,40 @@
         assertThat(resultFpsRanges1).isEmpty();
     }
 
+    /**
+     * Test for preview stabilization.
+     */
+    @Test
+    public void cameraInfo_isPreviewStabilizationSupported()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ false);
+
+        // Camera0
+        Camera2CameraInfoImpl cameraInfo0 = new Camera2CameraInfoImpl(CAMERA0_ID,
+                mCameraManagerCompat);
+
+
+        if (Build.VERSION.SDK_INT >= 33) {
+            assertThat(cameraInfo0.isPreviewStabilizationSupported()).isTrue();
+        } else {
+            assertThat(cameraInfo0.isPreviewStabilizationSupported()).isFalse();
+        }
+        assertThat(cameraInfo0.isVideoStabilizationSupported()).isTrue();
+
+        // Camera1
+        Camera2CameraInfoImpl cameraInfo1 = new Camera2CameraInfoImpl(CAMERA1_ID,
+                mCameraManagerCompat);
+
+        assertThat(cameraInfo1.isPreviewStabilizationSupported()).isFalse();
+        assertThat(cameraInfo0.isVideoStabilizationSupported()).isTrue();
+
+        // Camera2
+        Camera2CameraInfoImpl cameraInfo2 = new Camera2CameraInfoImpl(CAMERA2_ID,
+                mCameraManagerCompat);
+        assertThat(cameraInfo2.isPreviewStabilizationSupported()).isFalse();
+        assertThat(cameraInfo2.isVideoStabilizationSupported()).isFalse();
+    }
+
     @Test
     public void cameraInfo_checkDefaultCameraIntrinsicZoomRatio()
             throws CameraAccessExceptionCompat {
@@ -804,6 +838,24 @@
                     CAMERA0_DYNAMIC_RANGE_PROFILES);
         }
 
+        // Add video stabilization modes
+        if (Build.VERSION.SDK_INT >= 33) {
+            shadowCharacteristics0.set(
+                    CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES,
+                    new int[] {
+                            CONTROL_VIDEO_STABILIZATION_MODE_OFF,
+                            CONTROL_VIDEO_STABILIZATION_MODE_ON,
+                            CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
+                    });
+        } else {
+            shadowCharacteristics0.set(
+                    CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES,
+                    new int[] {
+                            CONTROL_VIDEO_STABILIZATION_MODE_OFF,
+                            CONTROL_VIDEO_STABILIZATION_MODE_ON
+                    });
+        }
+
         // Mock the request capability
         if (hasAvailableCapabilities) {
             shadowCharacteristics0.set(REQUEST_AVAILABLE_CAPABILITIES,
@@ -837,6 +889,14 @@
         shadowCharacteristics1.set(
                 CameraCharacteristics.FLASH_INFO_AVAILABLE, CAMERA1_FLASH_INFO_BOOLEAN);
 
+        // Add video stabilization modes
+        shadowCharacteristics1.set(
+                CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES,
+                new int[] {
+                        CONTROL_VIDEO_STABILIZATION_MODE_OFF,
+                        CONTROL_VIDEO_STABILIZATION_MODE_ON
+                });
+
         // Mock the supported resolutions
         {
             int formatPrivate = ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
@@ -887,6 +947,13 @@
                 CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES,
                 CAMERA2_AE_FPS_RANGES);
 
+        // Add video stabilization modes
+        shadowCharacteristics2.set(
+                CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES,
+                new int[] {
+                        CONTROL_VIDEO_STABILIZATION_MODE_OFF
+                });
+
         // Add the camera to the camera service
         ((ShadowCameraManager)
                 Shadow.extract(
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java
index 76be504..def80e4 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java
@@ -34,11 +34,16 @@
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.Preview;
 import androidx.camera.core.impl.CameraCaptureCallback;
+import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.Config;
 import androidx.camera.core.impl.Config.OptionPriority;
 import androidx.camera.core.impl.ImageCaptureConfig;
 import androidx.camera.core.impl.PreviewConfig;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
+import androidx.camera.video.Recorder;
+import androidx.camera.video.VideoCapture;
+import androidx.camera.video.impl.VideoCaptureConfig;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -169,6 +174,83 @@
                 .isEqualTo(CaptureRequest.TONEMAP_MODE_HIGH_QUALITY);
     }
 
+    @Test
+    public void unpackerExtractsPreviewStabilizationMode() {
+        ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", "Google");
+        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "sunfish");
+
+        Preview.Builder previewConfigBuilder =
+                new Preview.Builder().setPreviewStabilizationEnabled(true);
+
+        PreviewConfig useCaseConfig = previewConfigBuilder.getUseCaseConfig();
+
+        SessionConfig.Builder sessionBuilder = new SessionConfig.Builder();
+        mUnpacker.unpack(RESOLUTION_VGA, useCaseConfig, sessionBuilder);
+        SessionConfig sessionConfig = sessionBuilder.build();
+
+        CaptureConfig captureConfig = sessionConfig.getRepeatingCaptureConfig();
+
+        assertThat(captureConfig.getVideoStabilizationMode())
+                .isEqualTo(StabilizationMode.UNSPECIFIED);
+        assertThat(captureConfig.getPreviewStabilizationMode())
+                .isEqualTo(StabilizationMode.ON);
+    }
+
+    @Test
+    public void unpackerExtractsVideoStabilizationMode() {
+        ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", "Google");
+        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "sunfish");
+
+        VideoCapture.Builder<Recorder> videoCaptureConfigBuilder =
+                new VideoCapture.Builder<>(new Recorder.Builder().build())
+                        .setVideoStabilizationEnabled(true);
+
+        VideoCaptureConfig<Recorder> useCaseConfig = videoCaptureConfigBuilder.getUseCaseConfig();
+
+        SessionConfig.Builder sessionBuilder = new SessionConfig.Builder();
+        mUnpacker.unpack(RESOLUTION_VGA, useCaseConfig, sessionBuilder);
+        SessionConfig sessionConfig = sessionBuilder.build();
+
+        CaptureConfig captureConfig = sessionConfig.getRepeatingCaptureConfig();
+
+        assertThat(captureConfig.getVideoStabilizationMode())
+                .isEqualTo(StabilizationMode.ON);
+        assertThat(captureConfig.getPreviewStabilizationMode())
+                .isEqualTo(StabilizationMode.UNSPECIFIED);
+    }
+
+    @Test
+    public void unpackerExtractsBothPreviewAndVideoStabilizationMode() {
+        ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", "Google");
+        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "sunfish");
+
+        // unpack for preview
+        Preview.Builder previewConfigBuilder =
+                new Preview.Builder().setPreviewStabilizationEnabled(true);
+
+        PreviewConfig previewConfig = previewConfigBuilder.getUseCaseConfig();
+
+        SessionConfig.Builder sessionBuilder = new SessionConfig.Builder();
+        mUnpacker.unpack(RESOLUTION_VGA, previewConfig, sessionBuilder);
+
+        // unpack for preview
+        VideoCapture.Builder<Recorder> videoCaptureConfigBuilder =
+                new VideoCapture.Builder<>(new Recorder.Builder().build())
+                        .setVideoStabilizationEnabled(true);
+
+        VideoCaptureConfig<Recorder> videoCaptureConfig =
+                videoCaptureConfigBuilder.getUseCaseConfig();
+
+        mUnpacker.unpack(RESOLUTION_VGA, videoCaptureConfig, sessionBuilder);
+        SessionConfig sessionConfig = sessionBuilder.build();
+        CaptureConfig captureConfig = sessionConfig.getRepeatingCaptureConfig();
+
+        assertThat(captureConfig.getVideoStabilizationMode())
+                .isEqualTo(StabilizationMode.ON);
+        assertThat(captureConfig.getPreviewStabilizationMode())
+                .isEqualTo(StabilizationMode.ON);
+    }
+
     private OptionPriority getCaptureRequestOptionPriority(Config config,
             CaptureRequest.Key<?> key) {
         Config.Option<?> option = Camera2ImplConfig.createCaptureRequestOption(key);
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
index 1c83442..496663c 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
@@ -17,12 +17,10 @@
 package androidx.camera.camera2.internal;
 
 import static android.os.Build.VERSION.SDK_INT;
-
 import static androidx.camera.camera2.internal.StreamUseCaseUtil.STREAM_USE_CASE_STREAM_SPEC_OPTION;
 import static androidx.camera.core.DynamicRange.BIT_DEPTH_10_BIT;
 import static androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY;
 import static androidx.camera.core.ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG;
-
 import static junit.framework.TestCase.assertFalse;
 import static junit.framework.TestCase.assertTrue;
 
@@ -198,14 +196,16 @@
     public void shouldUseStreamUseCase_cameraModeNotSupported() {
         assertFalse(StreamUseCaseUtil.shouldUseStreamUseCase(
                 SupportedSurfaceCombination.FeatureSettings.of(CameraMode.CONCURRENT_CAMERA,
-                        DynamicRange.BIT_DEPTH_8_BIT)));
+                        DynamicRange.BIT_DEPTH_8_BIT,
+                        false)));
     }
 
     @Test
     public void shouldUseStreamUseCase_bitDepthNotSupported() {
         assertFalse(StreamUseCaseUtil.shouldUseStreamUseCase(
                 SupportedSurfaceCombination.FeatureSettings.of(CameraMode.DEFAULT,
-                        BIT_DEPTH_10_BIT)));
+                        BIT_DEPTH_10_BIT,
+                        false)));
     }
 
     @Test
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index c5d0844..2db0d98 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -212,7 +212,7 @@
         GuaranteedConfigurationsUtil.getLegacySupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isTrue()
@@ -228,7 +228,7 @@
         GuaranteedConfigurationsUtil.getLimitedSupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isFalse()
@@ -244,7 +244,7 @@
         GuaranteedConfigurationsUtil.getFullSupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isFalse()
@@ -260,7 +260,7 @@
         GuaranteedConfigurationsUtil.getLevel3SupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isFalse()
@@ -278,7 +278,7 @@
         GuaranteedConfigurationsUtil.getLimitedSupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isTrue()
@@ -296,7 +296,7 @@
         GuaranteedConfigurationsUtil.getFullSupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isFalse()
@@ -314,7 +314,7 @@
         GuaranteedConfigurationsUtil.getLevel3SupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isFalse()
@@ -332,7 +332,7 @@
         GuaranteedConfigurationsUtil.getFullSupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isTrue()
@@ -350,7 +350,7 @@
         GuaranteedConfigurationsUtil.getLevel3SupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isFalse()
@@ -369,7 +369,7 @@
         GuaranteedConfigurationsUtil.getLimitedSupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isTrue()
@@ -388,7 +388,7 @@
         GuaranteedConfigurationsUtil.getLegacySupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isTrue()
@@ -407,7 +407,7 @@
         GuaranteedConfigurationsUtil.getFullSupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isTrue()
@@ -426,7 +426,7 @@
         GuaranteedConfigurationsUtil.getRAWSupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isTrue()
@@ -444,7 +444,7 @@
         GuaranteedConfigurationsUtil.getLevel3SupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isTrue()
@@ -465,7 +465,7 @@
         GuaranteedConfigurationsUtil.getConcurrentSupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.CONCURRENT_CAMERA, BIT_DEPTH_8_BIT),
+                    FeatureSettings.of(CameraMode.CONCURRENT_CAMERA, BIT_DEPTH_8_BIT, false),
                     it.surfaceConfigList
                 )
             ).isTrue()
@@ -490,7 +490,7 @@
             assertThat(
                 supportedSurfaceCombination.checkSupported(
                     FeatureSettings.of(
-                        CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA, BIT_DEPTH_8_BIT
+                        CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA, BIT_DEPTH_8_BIT, false
                     ),
                     it.surfaceConfigList
                 )
@@ -498,6 +498,25 @@
         }
     }
 
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    fun checkPreviewStabilizationSurfaceCombinationSupportedWhenEnabled() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+        GuaranteedConfigurationsUtil.getPreviewStabilizationSupportedCombinationList().forEach {
+            assertThat(
+                supportedSurfaceCombination.checkSupported(
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_8_BIT, true),
+                    it.surfaceConfigList
+                )
+            ).isTrue()
+        }
+    }
+
     // //////////////////////////////////////////////////////////////////////////////////////////
     //
     // Surface config transformation tests
@@ -1451,7 +1470,8 @@
         val resultPair = supportedSurfaceCombination.getSuggestedStreamSpecifications(
             cameraMode,
             attachedSurfaceInfoList,
-            useCaseConfigToOutputSizesMap
+            useCaseConfigToOutputSizesMap,
+            false
         )
         val suggestedStreamSpecsForNewUseCases = resultPair.first
         val suggestedStreamSpecsForOldSurfaces = resultPair.second
@@ -1564,7 +1584,7 @@
         GuaranteedConfigurationsUtil.get10BitSupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_10_BIT),
+                    FeatureSettings.of(CameraMode.DEFAULT, BIT_DEPTH_10_BIT, false),
                     it.surfaceConfigList
                 )
             ).isTrue()
@@ -3327,6 +3347,19 @@
                 set(CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES, uc)
             }
 
+            val vs: IntArray
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                vs = intArrayOf(
+                    CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_OFF,
+                    CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON,
+                    CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION)
+            } else {
+                vs = intArrayOf(
+                    CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_OFF,
+                    CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON)
+            }
+            set(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES, vs)
+
             capabilities?.let {
                 set(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, it)
             }
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirkTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirkTest.kt
index 4f156fc..c2f4239 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirkTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirkTest.kt
@@ -57,6 +57,9 @@
             arrayOf("sm-a320f", CameraCharacteristics.LENS_FACING_BACK, true),
             arrayOf("SM-A320FL", CameraCharacteristics.LENS_FACING_BACK, true),
             arrayOf("Samsung S7", CameraCharacteristics.LENS_FACING_BACK, false),
+            arrayOf("moto g(20)", CameraCharacteristics.LENS_FACING_BACK, true),
+            arrayOf("itel l6006", CameraCharacteristics.LENS_FACING_BACK, true),
+            arrayOf("rmx3231", CameraCharacteristics.LENS_FACING_BACK, true),
         )
     }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 105bde1..4d7519d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -298,6 +298,17 @@
     }
 
     /**
+     * Returns {@link PreviewCapabilities} to query preview stream related device capability.
+     *
+     * @return {@link PreviewCapabilities}
+     */
+    @NonNull
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    default PreviewCapabilities getPreviewCapabilities() {
+        return PreviewCapabilities.EMPTY;
+    }
+
+    /**
      * Returns if {@link ImageFormat#PRIVATE} reprocessing is supported on the device.
      *
      * @return true if supported, otherwise false.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index a7686c7..c39b5cb 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -41,6 +41,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_TYPE;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_PREVIEW_STABILIZATION_MODE;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_TARGET_FRAME_RATE;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
 import static androidx.camera.core.impl.utils.Threads.checkMainThread;
@@ -86,6 +87,7 @@
 import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.internal.TargetConfig;
 import androidx.camera.core.internal.ThreadConfig;
@@ -268,6 +270,7 @@
         SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config,
                 streamSpec.getResolution());
         sessionConfigBuilder.setExpectedFrameRateRange(streamSpec.getExpectedFrameRateRange());
+        sessionConfigBuilder.setPreviewStabilization(config.getPreviewStabilizationMode());
         if (streamSpec.getImplementationOptions() != null) {
             sessionConfigBuilder.addImplementationOptions(streamSpec.getImplementationOptions());
         }
@@ -670,6 +673,14 @@
     }
 
     /**
+     * Returns whether video stabilization is enabled for preview stream.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public boolean isPreviewStabilizationEnabled() {
+        return getCurrentConfig().getPreviewStabilizationMode() == StabilizationMode.ON;
+    }
+
+    /**
      * A interface implemented by the application to provide a {@link Surface} for {@link Preview}.
      *
      * <p> This interface is implemented by the application to provide a {@link Surface}. This
@@ -1148,6 +1159,20 @@
             return this;
         }
 
+        /**
+         * Enable preview stabilization.
+         *
+         * @param enabled True if enable, otherwise false.
+         * @return the current Builder.
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setPreviewStabilizationEnabled(boolean enabled) {
+            getMutableConfig().insertOption(OPTION_PREVIEW_STABILIZATION_MODE,
+                    enabled ? StabilizationMode.ON : StabilizationMode.OFF);
+            return this;
+        }
+
         // Implementations of UseCaseConfig.Builder default methods
 
         @RestrictTo(Scope.LIBRARY_GROUP)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/PreviewCapabilities.java b/camera/camera-core/src/main/java/androidx/camera/core/PreviewCapabilities.java
new file mode 100644
index 0000000..283b013
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/PreviewCapabilities.java
@@ -0,0 +1,52 @@
+/*
+ * 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.camera.core;
+
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * PreviewCapabilities is used to query preview stream capabilities on the device.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface PreviewCapabilities {
+
+    /**
+     * Returns if preview stabilization is supported on the device.
+     *
+     * @return true if
+     * {@link CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION} is supported,
+     * otherwise false.
+     *
+     * @see CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE
+     */
+    boolean isStabilizationSupported();
+
+
+    /** An empty implementation. */
+    @NonNull
+    PreviewCapabilities EMPTY = new PreviewCapabilities() {
+        @Override
+        public boolean isStabilizationSupported() {
+            return false;
+        }
+    };
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
index 9abb314..540f454 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
@@ -83,6 +83,7 @@
      * @param newUseCaseConfigsSupportedSizeMap map of configurations of the use cases to the
      *                                          supported output sizes list that will be given a
      *                                          suggested stream specification
+     * @param isPreviewStabilizationOn          whether the preview stabilization is enabled.
      * @return map of suggested stream specifications for given use cases
      * @throws IllegalStateException    if not initialized
      * @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
@@ -96,5 +97,6 @@
             @CameraMode.Mode int cameraMode,
             @NonNull String cameraId,
             @NonNull List<AttachedSurfaceInfo> existingSurfaces,
-            @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap);
+            @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap,
+            boolean isPreviewStabilizationOn);
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
index e6a1e9b..d78c73a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
@@ -18,6 +18,7 @@
 
 import android.graphics.ImageFormat;
 import android.graphics.PixelFormat;
+import android.hardware.camera2.CaptureRequest;
 import android.util.Size;
 
 import androidx.annotation.NonNull;
@@ -106,6 +107,27 @@
     Set<DynamicRange> getSupportedDynamicRanges();
 
     /**
+     * Returns if preview stabilization is supported on the device.
+     *
+     * @return true if
+     * {@link CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION} is supported,
+     * otherwise false.
+     *
+     * @see CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE
+     */
+    boolean isPreviewStabilizationSupported();
+
+    /**
+     * Returns if video stabilization is supported on the device.
+     *
+     * @return true if {@link CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE_ON} is supported,
+     * otherwise false.
+     *
+     * @see CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE
+     */
+    boolean isVideoStabilizationSupported();
+
+    /**
      * Gets the underlying implementation instance which could be cast into an implementation
      * specific class for further use in implementation module. Returns <code>this</code> if this
      * instance is the implementation instance.
@@ -115,7 +137,6 @@
         return this;
     }
 
-
     /** {@inheritDoc} */
     @NonNull
     @Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CaptureConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CaptureConfig.java
index 6cf83bf..600629c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CaptureConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CaptureConfig.java
@@ -25,6 +25,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -78,6 +79,12 @@
 
     final Range<Integer> mExpectedFrameRateRange;
 
+    @StabilizationMode.Mode
+    final int mPreviewStabilizationMode;
+
+    @StabilizationMode.Mode
+    final int mVideoStabilizationMode;
+
     /** The camera capture callback for a {@link CameraCaptureSession}. */
     final List<CameraCaptureCallback> mCameraCaptureCallbacks;
 
@@ -105,6 +112,9 @@
      * @param templateType           The template for parameters of the CaptureRequest. This
      *                               must match the
      *                               constants defined by {@link CameraDevice}.
+     * @param expectedFrameRateRange The expected frame rate range.
+     * @param previewStabilizationMode The preview stabilization mode.
+     * @param videoStabilizationMode The video stabilization mode.
      * @param cameraCaptureCallbacks All camera capture callbacks.
      * @param cameraCaptureResult     The {@link CameraCaptureResult} for reprocessing capture
      *                               request.
@@ -114,6 +124,8 @@
             Config implementationOptions,
             int templateType,
             @NonNull Range<Integer> expectedFrameRateRange,
+            int previewStabilizationMode,
+            int videoStabilizationMode,
             List<CameraCaptureCallback> cameraCaptureCallbacks,
             boolean useRepeatingSurface,
             @NonNull TagBundle tagBundle,
@@ -122,6 +134,8 @@
         mImplementationOptions = implementationOptions;
         mTemplateType = templateType;
         mExpectedFrameRateRange = expectedFrameRateRange;
+        mPreviewStabilizationMode = previewStabilizationMode;
+        mVideoStabilizationMode = videoStabilizationMode;
         mCameraCaptureCallbacks = Collections.unmodifiableList(cameraCaptureCallbacks);
         mUseRepeatingSurface = useRepeatingSurface;
         mTagBundle = tagBundle;
@@ -169,6 +183,16 @@
         return mExpectedFrameRateRange;
     }
 
+    @StabilizationMode.Mode
+    public int getPreviewStabilizationMode() {
+        return mPreviewStabilizationMode;
+    }
+
+    @StabilizationMode.Mode
+    public int getVideoStabilizationMode() {
+        return mVideoStabilizationMode;
+    }
+
     public boolean isUseRepeatingSurface() {
         return mUseRepeatingSurface;
     }
@@ -206,6 +230,10 @@
         private MutableConfig mImplementationOptions = MutableOptionsBundle.create();
         private int mTemplateType = TEMPLATE_TYPE_NONE;
         private Range<Integer> mExpectedFrameRateRange = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED;
+        @StabilizationMode.Mode
+        private int mPreviewStabilizationMode = StabilizationMode.UNSPECIFIED;
+        @StabilizationMode.Mode
+        private int mVideoStabilizationMode = StabilizationMode.UNSPECIFIED;
         private List<CameraCaptureCallback> mCameraCaptureCallbacks = new ArrayList<>();
         private boolean mUseRepeatingSurface = false;
         private MutableTagBundle mMutableTagBundle = MutableTagBundle.create();
@@ -220,6 +248,8 @@
             mImplementationOptions = MutableOptionsBundle.from(base.mImplementationOptions);
             mTemplateType = base.mTemplateType;
             mExpectedFrameRateRange = base.mExpectedFrameRateRange;
+            mVideoStabilizationMode = base.mVideoStabilizationMode;
+            mPreviewStabilizationMode = base.mPreviewStabilizationMode;
             mCameraCaptureCallbacks.addAll(base.getCameraCaptureCallbacks());
             mUseRepeatingSurface = base.isUseRepeatingSurface();
             mMutableTagBundle = MutableTagBundle.from(base.getTagBundle());
@@ -290,6 +320,26 @@
         }
 
         /**
+         * Set the preview stabilization mode of the CaptureConfig.
+         * @param mode {@link StabilizationMode}
+         */
+        public void setPreviewStabilization(@StabilizationMode.Mode int mode) {
+            if (mode != StabilizationMode.UNSPECIFIED) {
+                mPreviewStabilizationMode = mode;
+            }
+        }
+
+        /**
+         * Set the video stabilization mode of the CaptureConfig.
+         * @param mode {@link StabilizationMode}
+         */
+        public void setVideoStabilization(@StabilizationMode.Mode int mode) {
+            if (mode != StabilizationMode.UNSPECIFIED) {
+                mVideoStabilizationMode = mode;
+            }
+        }
+
+        /**
          * Adds a {@link CameraCaptureCallback} callback.
          */
         public void addCameraCaptureCallback(@NonNull CameraCaptureCallback cameraCaptureCallback) {
@@ -416,6 +466,8 @@
                     OptionsBundle.from(mImplementationOptions),
                     mTemplateType,
                     mExpectedFrameRateRange,
+                    mPreviewStabilizationMode,
+                    mVideoStabilizationMode,
                     new ArrayList<>(mCameraCaptureCallbacks),
                     mUseRepeatingSurface,
                     TagBundle.from(mMutableTagBundle),
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
index ab1314e..be3b3b8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
@@ -46,8 +46,8 @@
      * Returns whether this configuration contains the supplied option.
      *
      * @param id The {@link Option} to search for in this configuration.
-     * @return <code>true</code> if this configuration contains the supplied option; <code>false
-     * </code> otherwise.
+     * @return {@code true} if this configuration contains the supplied option; {@code false}
+     * otherwise.
      */
     boolean containsOption(@NonNull Option<?> id);
 
@@ -77,7 +77,7 @@
      * @param valueIfMissing The value to return if the specified {@link Option} does not exist in
      *                       this configuration.
      * @param <ValueT>       The type for the value associated with the supplied {@link Option}.
-     * @return The value stored in this configuration, or <code>valueIfMissing</code> if it does
+     * @return The value stored in this configuration, or {@code valueIfMissing} if it does
      * not exist.
      */
     @Nullable
@@ -116,12 +116,10 @@
      *                       option such as \"<code>
      *                       camerax.core.example</code>\".
      * @param matcher        A callback used to receive results of the search. Results will be
-     *                       sent to
-     *                       {@link OptionMatcher#onOptionMatched(Option)} in the order in which
-     *                       they are found inside
-     *                       this configuration. Subsequent results will continue to be sent as
-     *                       long as {@link
-     *                       OptionMatcher#onOptionMatched(Option)} returns <code>true</code>.
+     *                       sent to {@link OptionMatcher#onOptionMatched(Option)} in the order
+     *                       in which they are found inside this configuration. Subsequent
+     *                       results will continue to be sent as long as {@link
+     *                       OptionMatcher#onOptionMatched(Option)} returns {@code true}.
      */
     void findOptions(@NonNull String idSearchString, @NonNull OptionMatcher matcher);
 
@@ -199,8 +197,7 @@
          * @param valueClass The class of the value stored by this option.
          * @param <T>        The type of the value stored by this option.
          * @param token      An optional, type-erased object for storing more context for this
-         *                   specific
-         *                   option. Generally this object should have static scope and be
+         *                   specific option. Generally this object should have static scope and be
          *                   immutable.
          * @return An {@link Option} object which can be used to store/retrieve values from a {@link
          * Config}.
@@ -250,11 +247,14 @@
      */
     enum OptionPriority {
         /**
-         * Should only be used externally by apps. It takes precedence over any other option
-         * values at the risk of causing unexpected behavior.
+         * It takes precedence over any other option values at the risk of causing unexpected
+         * behavior.
          *
-         * <p>This should not used internally in CameraX. It conflicts when merging different
-         * values set to ALWAY_OVERRIDE.
+         * <p>If the same option is already set, the option with this priority will overwrite the
+         * value.
+         *
+         * <p>This priority should only be used to explicitly specify an option, such as used by
+         * {@code Camera2Interop} or {@code Camera2CameraControl}, and should be used with caution.
          */
         ALWAYS_OVERRIDE,
 
@@ -262,15 +262,17 @@
          * It's a required option value in order to achieve expected CameraX behavior. It takes
          * precedence over {@link #OPTIONAL} option values.
          *
-         * <p>If apps set ALWAYS_OVERRIDE options, it'll override REQUIRED option values and can
-         * potentially cause unexpected behaviors. It conflicts when merging different values set
-         * to REQUIRED.
+         * <p>If two values are set to the same option, the value with {@link #ALWAYS_OVERRIDE}
+         * priority will overwrite this priority and can potentially cause unexpected behaviors.
+         *
+         * <p>If two values are set to the same option with this priority, it might indicate a
+         * programming error internally and an exception will be thrown when merging the configs.
          */
         REQUIRED,
 
         /**
          * The lowest priority, it can be overridden by any other option value. When two option
-         * values are set as OPTIONAL, the newer value takes precedence over the old one.
+         * values are set with this priority, the newer value takes precedence over the old one.
          */
         OPTIONAL
     }
@@ -278,35 +280,25 @@
     /**
      * Returns if values with these {@link OptionPriority} conflict or not.
      *
-     * Currently it is not allowed to have different values with same ALWAYS_OVERRIDE
-     * priority or to have different values with same REQUIRED priority.
+     * <p>Currently it is not allowed the same option to have different values with priority
+     * {@link OptionPriority#REQUIRED}.
      */
     static boolean hasConflict(@NonNull OptionPriority priority1,
             @NonNull OptionPriority priority2) {
-        if (priority1 == OptionPriority.ALWAYS_OVERRIDE
-                && priority2 == OptionPriority.ALWAYS_OVERRIDE) {
-            return true;
-        }
-
-        if (priority1 == OptionPriority.REQUIRED
-                && priority2 == OptionPriority.REQUIRED) {
-            return true;
-        }
-
-        return false;
+        return priority1 == OptionPriority.REQUIRED
+                && priority2 == OptionPriority.REQUIRED;
     }
 
     /**
-     * Merges two configs
+     * Merges two configs.
      *
      * @param extendedConfig the extended config. The options in the extendedConfig will be applied
      *                       on top of the baseConfig based on the option priorities.
-     * @param baseConfig the base config
-     * @return a {@link MutableOptionsBundle} of the merged config
+     * @param baseConfig the base config.
+     * @return a {@link MutableOptionsBundle} of the merged config.
      */
     @NonNull
-    static Config mergeConfigs(@Nullable Config extendedConfig,
-            @Nullable Config baseConfig) {
+    static Config mergeConfigs(@Nullable Config extendedConfig, @Nullable Config baseConfig) {
         if (extendedConfig == null && baseConfig == null) {
             return OptionsBundle.emptyBundle();
         }
@@ -333,12 +325,12 @@
     /**
      * Merges a specific option value from two configs.
      *
-     * @param mergedConfig   the final output config
+     * @param mergedConfig   the final output config.
      * @param baseConfig     the base config contains the option value which might be overridden by
      *                       the corresponding option value in the extend config.
      * @param extendedConfig the extended config contains the option value which might override
      *                       the corresponding option value in the base config.
-     * @param opt            the option to merge
+     * @param opt            the option to merge.
      */
     static void mergeOptionValue(@NonNull MutableOptionsBundle mergedConfig,
             @NonNull Config baseConfig,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
index c389a1d..0bd54ec 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
@@ -27,6 +27,7 @@
 import androidx.camera.core.ExperimentalZeroShutterLag;
 import androidx.camera.core.ExposureState;
 import androidx.camera.core.FocusMeteringAction;
+import androidx.camera.core.PreviewCapabilities;
 import androidx.camera.core.ZoomState;
 import androidx.lifecycle.LiveData;
 
@@ -192,4 +193,20 @@
     public CameraSelector getCameraSelector() {
         return mCameraInfoInternal.getCameraSelector();
     }
+
+    @Override
+    public boolean isPreviewStabilizationSupported() {
+        return mCameraInfoInternal.isPreviewStabilizationSupported();
+    }
+
+    @Override
+    public boolean isVideoStabilizationSupported() {
+        return mCameraInfoInternal.isVideoStabilizationSupported();
+    }
+
+    @NonNull
+    @Override
+    public PreviewCapabilities getPreviewCapabilities() {
+        return mCameraInfoInternal.getPreviewCapabilities();
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
index fdd0390..a3d1fd8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
@@ -29,6 +29,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.DynamicRange;
 import androidx.camera.core.Logger;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.internal.compat.workaround.SurfaceSorter;
 
 import com.google.auto.value.AutoValue;
@@ -431,6 +432,30 @@
         }
 
         /**
+         * Set the preview stabilization mode of the SessionConfig.
+         * @param mode {@link StabilizationMode}
+         */
+        @NonNull
+        public Builder setPreviewStabilization(@StabilizationMode.Mode int mode) {
+            if (mode != StabilizationMode.UNSPECIFIED) {
+                mCaptureConfigBuilder.setPreviewStabilization(mode);
+            }
+            return this;
+        }
+
+        /**
+         * Set the video stabilization mode of the SessionConfig.
+         * @param mode {@link StabilizationMode}
+         */
+        @NonNull
+        public Builder setVideoStabilization(@StabilizationMode.Mode int mode) {
+            if (mode != StabilizationMode.UNSPECIFIED) {
+                mCaptureConfigBuilder.setVideoStabilization(mode);
+            }
+            return this;
+        }
+
+        /**
          * Adds a tag to the SessionConfig with a key. For tracking the source.
          */
         @NonNull
@@ -748,6 +773,8 @@
             }
 
             setOrVerifyExpectFrameRateRange(captureConfig.getExpectedFrameRateRange());
+            setPreviewStabilizationMode(captureConfig.getPreviewStabilizationMode());
+            setVideoStabilizationMode(captureConfig.getVideoStabilizationMode());
 
             TagBundle tagBundle = sessionConfig.getRepeatingCaptureConfig().getTagBundle();
             mCaptureConfigBuilder.addAllTags(tagBundle);
@@ -810,6 +837,18 @@
             }
         }
 
+        private void setPreviewStabilizationMode(@StabilizationMode.Mode int mode) {
+            if (mode != StabilizationMode.UNSPECIFIED) {
+                mCaptureConfigBuilder.setPreviewStabilization(mode);
+            }
+        }
+
+        private void setVideoStabilizationMode(@StabilizationMode.Mode int mode) {
+            if (mode != StabilizationMode.UNSPECIFIED) {
+                mCaptureConfigBuilder.setVideoStabilization(mode);
+            }
+        }
+
         private List<DeferrableSurface> getSurfaces() {
             List<DeferrableSurface> surfaces = new ArrayList<>();
             for (OutputConfig outputConfig : mOutputConfigs) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
index 7e13965..435c05e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
@@ -24,6 +24,7 @@
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ExtendableBuilder;
 import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.internal.TargetConfig;
 import androidx.camera.core.internal.UseCaseEventConfig;
 
@@ -100,6 +101,17 @@
     Option<UseCaseConfigFactory.CaptureType> OPTION_CAPTURE_TYPE = Option.create(
             "camerax.core.useCase.captureType", UseCaseConfigFactory.CaptureType.class);
 
+    /**
+     * Option: camerax.core.useCase.previewStabilizationMode
+     */
+    Option<Integer> OPTION_PREVIEW_STABILIZATION_MODE =
+            Option.create("camerax.core.useCase.previewStabilizationMode", int.class);
+
+    /**
+     * Option: camerax.core.useCase.videoStabilizationMode
+     */
+    Option<Integer> OPTION_VIDEO_STABILIZATION_MODE =
+            Option.create("camerax.core.useCase.videoStabilizationMode", int.class);
 
     // *********************************************************************************************
 
@@ -328,6 +340,23 @@
     }
 
     /**
+     * @return The preview stabilization mode of this UseCaseConfig.
+     */
+    @StabilizationMode.Mode
+    default int getPreviewStabilizationMode() {
+        return retrieveOption(OPTION_PREVIEW_STABILIZATION_MODE,
+                StabilizationMode.UNSPECIFIED);
+    }
+
+    /**
+     * @return The video stabilization mode of this UseCaseConfig.
+     */
+    @StabilizationMode.Mode
+    default int getVideoStabilizationMode() {
+        return retrieveOption(OPTION_VIDEO_STABILIZATION_MODE, StabilizationMode.UNSPECIFIED);
+    }
+
+    /**
      * Builder for a {@link UseCase}.
      *
      * @param <T> The type of the object which will be built by {@link #build()}.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/stabilization/StabilizationMode.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/stabilization/StabilizationMode.java
new file mode 100644
index 0000000..f5aab65
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/stabilization/StabilizationMode.java
@@ -0,0 +1,51 @@
+/*
+ * 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.camera.core.impl.stabilization;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class for preview or video stabilization mode.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(21)
+public class StabilizationMode {
+
+    /* Not specified */
+    public static final int UNSPECIFIED = 0;
+    /* Off */
+    public static final int OFF = 1;
+    /* On */
+    public static final int ON = 2;
+
+    private StabilizationMode() {
+    }
+
+    /**
+     *
+     */
+    @IntDef({UNSPECIFIED, OFF, ON})
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public @interface Mode {
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index eff1afc..1c0a7c4 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -25,7 +25,6 @@
 import static androidx.camera.core.streamsharing.StreamSharing.isStreamSharing;
 import static androidx.core.util.Preconditions.checkArgument;
 import static androidx.core.util.Preconditions.checkState;
-
 import static java.util.Collections.emptyList;
 import static java.util.Objects.requireNonNull;
 
@@ -62,6 +61,7 @@
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.CameraMode;
 import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.PreviewConfig;
 import androidx.camera.core.impl.RestrictedCameraControl;
 import androidx.camera.core.impl.RestrictedCameraControl.CameraOperation;
 import androidx.camera.core.impl.RestrictedCameraInfo;
@@ -71,6 +71,7 @@
 import androidx.camera.core.impl.SurfaceConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.streamsharing.StreamSharing;
 import androidx.core.util.Preconditions;
@@ -677,6 +678,7 @@
             SupportedOutputSizesSorter supportedOutputSizesSorter = new SupportedOutputSizesSorter(
                     cameraInfoInternal,
                     sensorRect != null ? rectToSize(sensorRect) : null);
+            boolean isPreviewStabilizationOn = false;
             for (UseCase useCase : newUseCases) {
                 ConfigPair configPair = configPairMap.get(useCase);
                 // Combine with default configuration.
@@ -687,6 +689,12 @@
                 configToSupportedSizesMap.put(combinedUseCaseConfig,
                         supportedOutputSizesSorter.getSortedSupportedOutputSizes(
                                 combinedUseCaseConfig));
+
+                if (useCase.getCurrentConfig() instanceof PreviewConfig) {
+                    isPreviewStabilizationOn =
+                            ((PreviewConfig) useCase.getCurrentConfig())
+                                    .getPreviewStabilizationMode() == StabilizationMode.ON;
+                }
             }
 
             // Get suggested stream specifications and update the use case session configuration
@@ -695,7 +703,8 @@
                     mCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
                             cameraMode,
                             cameraId, existingSurfaces,
-                            configToSupportedSizesMap);
+                            configToSupportedSizesMap,
+                            isPreviewStabilizationOn);
 
             for (Map.Entry<UseCaseConfig<?>, UseCase> entry : configToUseCaseMap.entrySet()) {
                 suggestedStreamSpecs.put(entry.getValue(),
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LargeJpegImageQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LargeJpegImageQuirk.java
index c6ad529..471095e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LargeJpegImageQuirk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LargeJpegImageQuirk.java
@@ -28,16 +28,16 @@
 
 /**
  * <p>QuirkSummary
- *     Bug Id: 288828159
+ *     Bug Id: 288828159, 299069235
  *     Description: Quirk required to check whether the captured JPEG image contains redundant
  *                  0's padding data. For example, Samsung A5 (2017) series devices have the
  *                  problem and result in the output JPEG image to be extremely large (about 32 MB).
- *     Device(s): Samsung Galaxy A5 (2017), A52, A70, A72 and S7 series devices
+ *     Device(s): Samsung Galaxy A5 (2017), A52, A70, A72, S7 series devices and Vivo S16 device
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public final class LargeJpegImageQuirk implements Quirk {
 
-    private static final Set<String> DEVICE_MODELS = new HashSet<>(Arrays.asList(
+    private static final Set<String> SAMSUNG_DEVICE_MODELS = new HashSet<>(Arrays.asList(
             // Samsung Galaxy A5 series devices
             "SM-A520F",
             "SM-A520L",
@@ -70,7 +70,22 @@
             "SM-S906B"
     ));
 
+    private static final Set<String> VIVO_DEVICE_MODELS = new HashSet<>(Arrays.asList(
+            // Vivo S16
+            "V2244A"
+    ));
+
     static boolean load() {
-        return DEVICE_MODELS.contains(Build.MODEL.toUpperCase(Locale.US));
+        return isSamsungProblematicDevice() || isVivoProblematicDevice();
+    }
+
+    private static boolean isSamsungProblematicDevice() {
+        return "Samsung".equalsIgnoreCase(Build.BRAND) && SAMSUNG_DEVICE_MODELS.contains(
+                Build.MODEL.toUpperCase(Locale.US));
+    }
+
+    private static boolean isVivoProblematicDevice() {
+        return "Vivo".equalsIgnoreCase(Build.BRAND) && VIVO_DEVICE_MODELS.contains(
+                Build.MODEL.toUpperCase(Locale.US));
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParser.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParser.java
index a9b122a..4a0d527 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParser.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParser.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.core.internal.compat.quirk.LargeJpegImageQuirk;
 
@@ -41,12 +42,23 @@
             return bytes.length;
         }
 
+        int jfifEoiMarkEndPosition = getJfifEoiMarkEndPosition(bytes);
+
+        return jfifEoiMarkEndPosition != -1 ? jfifEoiMarkEndPosition : bytes.length;
+    }
+
+    /**
+     * Returns the end position of JFIF EOI mark. Returns -1 while JFIF EOI mark can't be found
+     * in the provided byte array.
+     */
+    @VisibleForTesting
+    public static int getJfifEoiMarkEndPosition(@NonNull byte[] bytes) {
         // Parses the JFIF segments from the start of the JPEG image data
         int markPosition = 0x2;
         while (true) {
             // Breaks the while-loop and return null if the mark byte can't be correctly found.
             if (markPosition + 4 > bytes.length || bytes[markPosition] != ((byte) 0xff)) {
-                return bytes.length;
+                return -1;
             }
 
             int segmentLength =
@@ -65,7 +77,7 @@
         while (true) {
             // Breaks the while-loop and return null if EOI mark can't be found
             if (eoiPosition + 2 > bytes.length) {
-                return bytes.length;
+                return -1;
             }
 
             if (bytes[eoiPosition] == ((byte) 0xff) && bytes[eoiPosition + 1] == ((byte) 0xd9)) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
index 33a2a1e..7147eb4 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
@@ -21,7 +21,9 @@
 import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
 import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_DYNAMIC_RANGE;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_PREVIEW_STABILIZATION_MODE;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_VIDEO_STABILIZATION_MODE;
 import static androidx.camera.core.impl.utils.Threads.checkMainThread;
 import static androidx.camera.core.impl.utils.TransformUtils.getRotatedSize;
 import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
@@ -57,6 +59,7 @@
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.processing.SurfaceEdge;
 import androidx.camera.core.processing.SurfaceProcessorNode.OutConfig;
 
@@ -158,6 +161,21 @@
                     + " a dynamic range that satisfies all children.");
         }
         mutableConfig.insertOption(OPTION_INPUT_DYNAMIC_RANGE, dynamicRange);
+
+        // Merge Preview stabilization and video stabilization configs.
+        for (UseCase useCase : mChildren) {
+            if (useCase.getCurrentConfig().getVideoStabilizationMode()
+                    != StabilizationMode.UNSPECIFIED) {
+                mutableConfig.insertOption(OPTION_VIDEO_STABILIZATION_MODE,
+                        useCase.getCurrentConfig().getVideoStabilizationMode());
+            }
+
+            if (useCase.getCurrentConfig().getPreviewStabilizationMode()
+                    != StabilizationMode.UNSPECIFIED) {
+                mutableConfig.insertOption(OPTION_PREVIEW_STABILIZATION_MODE,
+                        useCase.getCurrentConfig().getPreviewStabilizationMode());
+            }
+        }
     }
 
     void bindChildren() {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 57850a9..4d2bc37 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -780,6 +780,13 @@
         assertThat(preview.targetFrameRate).isEqualTo(Range(15, 30))
     }
 
+    @Test
+    fun canSetPreviewStabilization() {
+        val preview = Preview.Builder().setPreviewStabilizationEnabled(true)
+            .build()
+        assertThat(preview.isPreviewStabilizationEnabled).isTrue()
+    }
+
     private fun bindToLifecycleAndGetSurfaceRequest(): SurfaceRequest {
         return bindToLifecycleAndGetResult(null).first
     }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/ConfigTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/ConfigTest.java
index 96b086a..e78d689f 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/ConfigTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/ConfigTest.java
@@ -89,8 +89,8 @@
     }
 
     @Test
-    public void hasConflict_whenTwoValueAreALWAYSOVERRIDE() {
-        assertThat(Config.hasConflict(ALWAYS_OVERRIDE, ALWAYS_OVERRIDE)).isTrue();
+    public void noConflict_whenTwoValueAreALWAYSOVERRIDE() {
+        assertThat(Config.hasConflict(ALWAYS_OVERRIDE, ALWAYS_OVERRIDE)).isFalse();
     }
 
     @Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/MutableOptionsBundleTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/MutableOptionsBundleTest.java
index 1db4883..be0e91c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/MutableOptionsBundleTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/MutableOptionsBundleTest.java
@@ -108,13 +108,18 @@
         assertThat(config2.retrieveOptionWithPriority(OPTION_2, OPTIONAL)).isEqualTo(VALUE_1);
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test
     public void insertOption_ALWAYSOVERRIDE_ALWAYSOVERRIDE() {
         MutableOptionsBundle mutOpts = MutableOptionsBundle.create();
 
         mutOpts.insertOption(OPTION_1, ALWAYS_OVERRIDE, VALUE_1);
-        // should throw an Error
         mutOpts.insertOption(OPTION_1, ALWAYS_OVERRIDE, VALUE_2);
+
+        assertThat(mutOpts.retrieveOption(OPTION_1)).isEqualTo(VALUE_2);
+        Config.OptionPriority highestPriority = Collections.min(mutOpts.getPriorities(OPTION_1));
+        assertThat(highestPriority).isEqualTo(ALWAYS_OVERRIDE);
+        assertThat(mutOpts.retrieveOptionWithPriority(OPTION_1, highestPriority))
+                .isEqualTo(VALUE_2);
     }
 
     @Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParserTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParserTest.kt
index cdd2576..127b136 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParserTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParserTest.kt
@@ -73,31 +73,34 @@
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class InvalidJpegDataParserTest(
+    private val brand: String,
     private val model: String,
     private val data: ByteArray,
     private val validDataLength: Int,
-    ) {
+) {
 
     companion object {
         @JvmStatic
-        @ParameterizedRobolectricTestRunner.Parameters(name = "model={0}, data={1}, length={2}")
+        @ParameterizedRobolectricTestRunner.Parameters(
+            name = "brand={0}, model={1}, data={2}, length={3}")
         fun data() = mutableListOf<Array<Any?>>().apply {
-            add(arrayOf("SM-A520F", problematicJpegByteArray, 18))
-            add(arrayOf("SM-A520F", problematicJpegByteArray2, 18))
-            add(arrayOf("SM-A520F", correctJpegByteArray1, 18))
-            add(arrayOf("SM-A520F", correctJpegByteArray2, 18))
-            add(arrayOf("SM-A520F", invalidVeryShortData, 2))
-            add(arrayOf("SM-A520F", invalidNoSosData, 28))
-            add(arrayOf("SM-A520F", invalidNoEoiData, 28))
-            add(arrayOf("fake-model", problematicJpegByteArray, 42))
-            add(arrayOf("fake-model", problematicJpegByteArray2, 64))
-            add(arrayOf("fake-model", correctJpegByteArray1, 28))
-            add(arrayOf("fake-model", correctJpegByteArray2, 18))
+            add(arrayOf("SAMSUNG", "SM-A520F", problematicJpegByteArray, 18))
+            add(arrayOf("SAMSUNG", "SM-A520F", problematicJpegByteArray2, 18))
+            add(arrayOf("SAMSUNG", "SM-A520F", correctJpegByteArray1, 18))
+            add(arrayOf("SAMSUNG", "SM-A520F", correctJpegByteArray2, 18))
+            add(arrayOf("SAMSUNG", "SM-A520F", invalidVeryShortData, 2))
+            add(arrayOf("SAMSUNG", "SM-A520F", invalidNoSosData, 28))
+            add(arrayOf("SAMSUNG", "SM-A520F", invalidNoEoiData, 28))
+            add(arrayOf("fake-brand", "fake-model", problematicJpegByteArray, 42))
+            add(arrayOf("fake-brand", "fake-model", problematicJpegByteArray2, 64))
+            add(arrayOf("fake-brand", "fake-model", correctJpegByteArray1, 28))
+            add(arrayOf("fake-brand", "fake-model", correctJpegByteArray2, 18))
         }
     }
 
     @Test
     fun canGetValidJpegDataLength() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", brand)
         ReflectionHelpers.setStaticField(Build::class.java, "MODEL", model)
         assertThat(InvalidJpegDataParser().getValidDataLength(data)).isEqualTo(validDataLength)
     }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
index 3b5c6ea..3743f65 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
@@ -31,6 +31,7 @@
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
 import androidx.camera.core.ImageProxy
+import androidx.camera.core.Preview
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.CameraCaptureCallback
 import androidx.camera.core.impl.CameraCaptureResult
@@ -41,6 +42,7 @@
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
+import androidx.camera.core.impl.stabilization.StabilizationMode
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.impl.utils.futures.Futures
@@ -55,6 +57,8 @@
 import androidx.camera.testing.impl.fakes.FakeUseCase
 import androidx.camera.testing.impl.fakes.FakeUseCaseConfig
 import androidx.camera.testing.impl.fakes.FakeUseCaseConfigFactory
+import androidx.camera.video.Recorder
+import androidx.camera.video.VideoCapture
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.ListenableFuture
 import kotlinx.coroutines.CompletableDeferred
@@ -479,4 +483,34 @@
         assertThat(config.captureTypes[0]).isEqualTo(CaptureType.PREVIEW)
         assertThat(config.captureTypes[1]).isEqualTo(CaptureType.PREVIEW)
     }
+
+    @Test
+    fun getParentPreviewStabilizationMode_isPreviewChildMode() {
+        val preview = Preview.Builder().setPreviewStabilizationEnabled(true).build()
+        val videoCapture = VideoCapture.Builder(Recorder.Builder().build())
+            .setVideoStabilizationEnabled(false).build()
+
+        streamSharing =
+            StreamSharing(camera, setOf(preview, videoCapture), useCaseConfigFactory)
+        assertThat(
+            streamSharing.mergeConfigs(
+                camera.cameraInfoInternal, /*extendedConfig*/null, /*cameraDefaultConfig*/null
+            ).previewStabilizationMode
+        ).isEqualTo(StabilizationMode.ON)
+    }
+
+    @Test
+    fun getParentVideoStabilizationMode_isVideoCaptureChildMode() {
+        val preview = Preview.Builder().setPreviewStabilizationEnabled(false).build()
+        val videoCapture = VideoCapture.Builder(Recorder.Builder().build())
+            .setVideoStabilizationEnabled(true).build()
+
+        streamSharing =
+            StreamSharing(camera, setOf(preview, videoCapture), useCaseConfigFactory)
+        assertThat(
+            streamSharing.mergeConfigs(
+                camera.cameraInfoInternal, /*extendedConfig*/null, /*cameraDefaultConfig*/null
+            ).videoStabilizationMode
+        ).isEqualTo(StabilizationMode.ON)
+    }
 }
diff --git a/camera/camera-effects/build.gradle b/camera/camera-effects/build.gradle
index ea5a94c..cedb467 100644
--- a/camera/camera-effects/build.gradle
+++ b/camera/camera-effects/build.gradle
@@ -24,6 +24,7 @@
 dependencies {
     api(project(":camera:camera-core"))
     implementation(libs.autoValueAnnotations)
+    implementation("androidx.concurrent:concurrent-futures:1.0.0")
 
     annotationProcessor(libs.autoValue)
 
diff --git a/camera/camera-effects/src/androidTest/java/androidx/camera/effects/internal/SurfaceProcessorImplDeviceTest.kt b/camera/camera-effects/src/androidTest/java/androidx/camera/effects/internal/SurfaceProcessorImplDeviceTest.kt
new file mode 100644
index 0000000..1cf40e9
--- /dev/null
+++ b/camera/camera-effects/src/androidTest/java/androidx/camera/effects/internal/SurfaceProcessorImplDeviceTest.kt
@@ -0,0 +1,421 @@
+/*
+ * 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.camera.effects.internal
+
+import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.graphics.SurfaceTexture
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Size
+import android.view.Surface
+import androidx.camera.core.CameraEffect.PREVIEW
+import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.SurfaceRequest.TransformationInfo
+import androidx.camera.core.impl.utils.TransformUtils.sizeToRect
+import androidx.camera.effects.Frame
+import androidx.camera.effects.OverlayEffect
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.impl.TestImageUtil.getAverageDiff
+import androidx.core.util.Consumer
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumentation tests for [SurfaceProcessorImpl].
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class SurfaceProcessorImplDeviceTest {
+
+    companion object {
+        private const val ROTATION_DEGREES = 90
+        private val TRANSFORM = Matrix().apply {
+            postRotate(90F)
+        }
+        private const val INPUT_COLOR = Color.GREEN
+        private const val OVERLAY_COLOR = Color.RED
+
+        // The timeout is set to 200ms to qualify for @SmallTest.
+        private const val TIMEOUT_MILLIS = 200L
+        private const val THREAD_NAME = "GL_THREAD"
+    }
+
+    private val size = Size(640, 480)
+    private val cropRect = sizeToRect(size)
+    private lateinit var surfaceRequest: SurfaceRequest
+    private lateinit var outputTexture: SurfaceTexture
+    private lateinit var outputSurface: Surface
+    private lateinit var outputTexture2: SurfaceTexture
+    private lateinit var outputSurface2: Surface
+    private lateinit var surfaceOutput: SurfaceOutput
+    private lateinit var surfaceOutput2: SurfaceOutput
+    private lateinit var processor: SurfaceProcessorImpl
+    private lateinit var transformationInfo: TransformationInfo
+    private lateinit var glThread: HandlerThread
+    private lateinit var glHandler: Handler
+
+    @Before
+    fun setUp() {
+        glThread = HandlerThread(THREAD_NAME)
+        glThread.start()
+        glHandler = Handler(glThread.looper)
+
+        transformationInfo = TransformationInfo.of(
+            cropRect,
+            ROTATION_DEGREES,
+            Surface.ROTATION_90,
+            true,
+            TRANSFORM,
+            true
+        )
+        surfaceRequest = SurfaceRequest(size, FakeCamera()) {}
+        surfaceRequest.updateTransformationInfo(transformationInfo)
+        outputTexture = SurfaceTexture(0)
+        outputTexture.detachFromGLContext()
+        outputSurface = Surface(outputTexture)
+        outputTexture2 = SurfaceTexture(1)
+        outputTexture2.detachFromGLContext()
+        outputSurface2 = Surface(outputTexture2)
+        surfaceOutput = SurfaceOutputImpl(outputSurface, size)
+        surfaceOutput2 = SurfaceOutputImpl(outputSurface2, size)
+    }
+
+    @After
+    fun tearDown() {
+        outputTexture.release()
+        outputSurface.release()
+        outputTexture2.release()
+        outputSurface2.release()
+        if (::processor.isInitialized) {
+            processor.release()
+        }
+        glThread.quitSafely()
+    }
+
+    @Test
+    fun onDrawListenerReturnsFalse_notDrawnToOutput() = runBlocking {
+        // Act: return false in the on draw listener.
+        val latch = fillFramesAndWaitForOutput(0, 1) {
+            it.setOnDrawListener { false }
+        }
+        // Assert: output is not drawn.
+        assertThat(latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isFalse()
+    }
+
+    @Test
+    fun onDrawListener_receivesTransformationInfo() = runBlocking {
+        // Arrange.
+        var frameReceived: Frame? = null
+        // Act: fill frames and wait draw frame listener.
+        val latch = fillFramesAndWaitForOutput(0, 1) { processor ->
+            processor.setOnDrawListener { frame ->
+                frameReceived = frame
+                true
+            }
+        }
+        // Assert: draw frame listener receives correct transformation info.
+        assertThat(latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(frameReceived!!.size).isEqualTo(size)
+        assertThat(frameReceived!!.cropRect).isEqualTo(transformationInfo.cropRect)
+        assertThat(frameReceived!!.mirroring).isEqualTo(transformationInfo.mirroring)
+        assertThat(frameReceived!!.sensorToBufferTransform)
+            .isEqualTo(transformationInfo.sensorToBufferTransform)
+        assertThat(frameReceived!!.rotationDegrees).isEqualTo(ROTATION_DEGREES)
+    }
+
+    @Test
+    fun canvasInvalidated_overlayDrawnToOutput(): Unit = runBlocking {
+        val latch = fillFramesAndWaitForOutput(0, 1) { processor ->
+            processor.setOnDrawListener { frame ->
+                // Act: invalidate overlay canvas and draw color.
+                frame.invalidateOverlayCanvas().drawColor(OVERLAY_COLOR)
+                true
+            }
+        }
+        // Assert: output receives frame with overlay color.
+        assertThat(latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue()
+        assertOutputColor(OVERLAY_COLOR)
+    }
+
+    @Test
+    fun canvasNotInvalidated_overlayNotDrawnToOutput() = runBlocking {
+        val latch = fillFramesAndWaitForOutput(0, 1) { processor ->
+            processor.setOnDrawListener { frame ->
+                // Act: draw color on overlay canvas without invalidating.
+                frame.overlayCanvas.drawColor(OVERLAY_COLOR)
+                true
+            }
+        }
+        // Assert: output receives frame with input color
+        assertThat(latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue()
+        assertOutputColor(INPUT_COLOR)
+    }
+
+    @Test
+    fun zeroQueueDepth_inputDrawnToOutput() = runBlocking {
+        // Assert: output receives frame when queue depth == 0.
+        val latch = fillFramesAndWaitForOutput(0, 1)
+        assertThat(latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue()
+    }
+
+    @Test
+    fun nonZeroQueueDepth_inputNotDrawnToOutputBeforeFilledUp() = runBlocking {
+        // Assert: output does not receive frame when frame count = queue depth.
+        val latch = fillFramesAndWaitForOutput(3, 3)
+        assertThat(latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isFalse()
+    }
+
+    @Test
+    fun nonZeroQueueDepth_inputDrawnToOutputAfterFilledUp() = runBlocking {
+        // Assert: output receives frame when frame count > queue depth.
+        val latch = fillFramesAndWaitForOutput(3, 4)
+        assertThat(latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue()
+    }
+
+    @Test
+    fun replaceOutputSurface_noFrameFromPreviousCycle() = runBlocking {
+        // Arrange: setup processor with buffer depth == 1 and fill it full.
+        processor = SurfaceProcessorImpl(1, glHandler)
+        withContext(processor.glExecutor.asCoroutineDispatcher()) {
+            processor.onInputSurface(surfaceRequest)
+            processor.onOutputSurface(surfaceOutput)
+        }
+        val inputSurface = surfaceRequest.deferrableSurface.surface.get()
+        drawSurface(inputSurface)
+
+        // Act: replace output surface so the cached frame is no longer valid. The cached frame
+        // should be marked empty and not blocking the pipeline.
+        val countDownLatch = getTextureUpdateLatch(outputTexture2)
+        withContext(processor.glExecutor.asCoroutineDispatcher()) {
+            processor.onOutputSurface(surfaceOutput2)
+        }
+
+        // Assert: draw the input surface twice and the output surface should receive a frame. It
+        // confirms that there is no frame from the previous cycle blocking the pipeline.
+        drawSurface(inputSurface)
+        drawSurface(inputSurface)
+        assertThat(countDownLatch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue()
+    }
+
+    @Test
+    fun drawCachedFrame_frameDrawnToOutput() = runBlocking {
+        // Arrange: draw the input and get the cached frame.
+        val latch = fillFramesAndWaitForOutput(1, 1)
+        val cachedFrame = processor.buffer.frames.single()
+
+        // Act: draw the cached frame.
+        val drawFuture = processor.drawFrame(cachedFrame.timestampNs)
+
+        // Assert: the future completes with RESULT_SUCCESS and the output receives the frame.
+        assertThat(drawFuture.get()).isEqualTo(OverlayEffect.RESULT_SUCCESS)
+        assertThat(latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue()
+    }
+
+    @Test
+    fun drawMissingFrame_futureCompletesWithNotFound() = runBlocking {
+        // Arrange: draw the input and get the cached frame.
+        val latch = fillFramesAndWaitForOutput(1, 1)
+        val frame = processor.buffer.frames.single()
+
+        // Act: draw the frame with a wrong timestamp.
+        val drawFuture = processor.drawFrame(frame.timestampNs - 1)
+
+        // Assert: the future completes with RESULT_FRAME_NOT_FOUND and the output does not receive
+        // the frame.
+        assertThat(drawFuture.get()).isEqualTo(OverlayEffect.RESULT_FRAME_NOT_FOUND)
+        assertThat(latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isFalse()
+    }
+
+    @Test
+    fun drawFrameAndCancel_futureCompletesWithCanceled() = runBlocking {
+        // Arrange: draw the input and drop the incoming frames.
+        val latch = fillFramesAndWaitForOutput(1, 1) {
+            it.setOnDrawListener {
+                false
+            }
+        }
+        val frame = processor.buffer.frames.single()
+
+        // Act: draw the frame.
+        val drawFuture = processor.drawFrame(frame.timestampNs)
+
+        // Assert: the future completes with RESULT_CANCELLED_BY_CALLER and the output does not
+        // receive the frame.
+        assertThat(drawFuture.get()).isEqualTo(OverlayEffect.RESULT_CANCELLED_BY_CALLER)
+        assertThat(latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isFalse()
+    }
+
+    @Test
+    fun drawFrameAfterReplacingOutput_futureCompletesWithInvalidSurface() = runBlocking {
+        // Arrange: setup processor with buffer depth == 1 and fill it full.
+        processor = SurfaceProcessorImpl(1, glHandler)
+        withContext(processor.glExecutor.asCoroutineDispatcher()) {
+            processor.onInputSurface(surfaceRequest)
+            processor.onOutputSurface(surfaceOutput)
+        }
+        val inputSurface = surfaceRequest.deferrableSurface.surface.get()
+        drawSurface(inputSurface)
+        val frame = processor.buffer.frames.single()
+
+        // Arrange: replace the output Surface so the buffered frame is associated with an invalid
+        // surface.
+        val latch = getTextureUpdateLatch(outputTexture2)
+        withContext(processor.glExecutor.asCoroutineDispatcher()) {
+            processor.onOutputSurface(surfaceOutput2)
+        }
+
+        // Act: draw the buffered frame.
+        val drawFuture = processor.drawFrame(frame.timestampNs)
+
+        // Assert: the future completes with RESULT_INVALID_SURFACE and the output does not
+        // receive the frame.
+        assertThat(drawFuture.get()).isEqualTo(OverlayEffect.RESULT_INVALID_SURFACE)
+        assertThat(latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isFalse()
+    }
+
+    @Test
+    fun drawFrameAfterRelease_futureCompletesWithException(): Unit = runBlocking {
+        // Arrange: draw the input and get the cached frame.
+        fillFramesAndWaitForOutput(1, 1)
+        processor.release()
+
+        // Act: release the processor and draw a frame.
+        val drawFuture = processor.drawFrame(0)
+
+        // Assert: the future completes with an exception.
+        try {
+            drawFuture.get()
+        } catch (e: ExecutionException) {
+            assertThat(e.cause).isInstanceOf(IllegalStateException::class.java)
+        }
+    }
+
+    /**
+     * Renders the input surface to a bitmap and asserts that the color of the bitmap.
+     */
+    private suspend fun assertOutputColor(color: Int) {
+        val matrix = FloatArray(16)
+        android.opengl.Matrix.setIdentityM(matrix, 0)
+        withContext(processor.glExecutor.asCoroutineDispatcher()) {
+            val bitmap = processor.glRendererForTesting
+                .renderInputToBitmap(size.width, size.height, matrix)
+            assertThat(
+                getAverageDiff(
+                    bitmap,
+                    Rect(0, 0, size.width, size.height),
+                    color
+                )
+            ).isEqualTo(0)
+        }
+    }
+
+    /**
+     * Creates a processor and draws frames to the input surface.
+     *
+     * @param queueDepth The queue depth of the processor.
+     * @param frameCount The number of frames to draw.
+     * @return True if the output surface receives a frame.
+     */
+    private suspend fun fillFramesAndWaitForOutput(
+        queueDepth: Int,
+        frameCount: Int,
+        configureProcessor: (SurfaceProcessorImpl) -> Unit = {},
+    ): CountDownLatch {
+        // Arrange: Create a processor.
+        processor = SurfaceProcessorImpl(queueDepth, glHandler)
+        configureProcessor(processor)
+        withContext(processor.glExecutor.asCoroutineDispatcher()) {
+            processor.onInputSurface(surfaceRequest)
+            processor.onOutputSurface(surfaceOutput)
+        }
+        val countDownLatch = getTextureUpdateLatch(outputTexture)
+
+        // Act: Draw frames to the input surface.
+        val inputSurface = surfaceRequest.deferrableSurface.surface.get()
+
+        repeat(frameCount) {
+            drawSurface(inputSurface)
+        }
+
+        return countDownLatch
+    }
+
+    /**
+     * Draws a frame to the surface and block the thread until the gl thread finishes processing.
+     */
+    private suspend fun drawSurface(surface: Surface) {
+        val canvas = surface.lockCanvas(null)
+        canvas.drawColor(INPUT_COLOR)
+        surface.unlockCanvasAndPost(canvas)
+        // Drain the GL thread to ensure the processor caches or draws the frame. Otherwise, the
+        // input SurfaceTexture's onSurfaceAvailable callback may only get called once for
+        // multiple drawings.
+        withContext(processor.glExecutor.asCoroutineDispatcher()) {
+        }
+    }
+
+    private fun getTextureUpdateLatch(surfaceTexture: SurfaceTexture): CountDownLatch {
+        val countDownLatch = CountDownLatch(1)
+        surfaceTexture.setOnFrameAvailableListener {
+            countDownLatch.countDown()
+        }
+        return countDownLatch
+    }
+
+    private class SurfaceOutputImpl(private val surface: Surface, val surfaceSize: Size) :
+        SurfaceOutput {
+
+        override fun close() {
+        }
+
+        override fun getSurface(
+            executor: Executor,
+            listener: Consumer<SurfaceOutput.Event>
+        ): Surface {
+            return surface
+        }
+
+        override fun getTargets(): Int {
+            return PREVIEW or VIDEO_CAPTURE
+        }
+
+        override fun getSize(): Size {
+            return surfaceSize
+        }
+
+        override fun updateTransformMatrix(updated: FloatArray, original: FloatArray) {
+        }
+    }
+}
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/Frame.java b/camera/camera-effects/src/main/java/androidx/camera/effects/Frame.java
new file mode 100644
index 0000000..ee6b8a9
--- /dev/null
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/Frame.java
@@ -0,0 +1,175 @@
+/*
+ * 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.camera.effects;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraCharacteristics;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.ImageInfo;
+import androidx.camera.core.SurfaceRequest;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Represents a frame that will be rendered next.
+ *
+ * <p>This class can be used to overlay graphics or data on camera output. It contains
+ * information for drawing an overlay, including sensor-to-buffer transform, size, crop rect,
+ * rotation, mirroring, and timestamp. It also provides a {@link Canvas} for the drawing.
+ *
+ * TODO(b/297509601): Make it public API in 1.4.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(21)
+@AutoValue
+public abstract class Frame {
+
+    private boolean mIsOverlayDirty = false;
+
+    /**
+     * Internal API to create a frame.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static Frame of(
+            @NonNull Canvas overlayCanvas,
+            long timestampNs,
+            @NonNull Size size,
+            @NonNull SurfaceRequest.TransformationInfo transformationInfo) {
+        return new AutoValue_Frame(transformationInfo.getSensorToBufferTransform(), size,
+                transformationInfo.getCropRect(), transformationInfo.getRotationDegrees(),
+                transformationInfo.getMirroring(), timestampNs, overlayCanvas);
+    }
+
+    /**
+     * Returns the sensor to image buffer transform matrix.
+     *
+     * <p>The value is a mapping from sensor coordinates to buffer coordinates, which is,
+     * from the rect of {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE} to the
+     * rect defined by {@code (0, 0, #getSize()#getWidth(), #getSize()#getHeight())}.
+     *
+     * <p>The value can be set on the {@link Canvas} using {@link Canvas#setMatrix} API. This
+     * transforms the {@link Canvas} to the sensor coordinate system.
+     *
+     * @see SurfaceRequest.TransformationInfo#getSensorToBufferTransform()
+     */
+    @NonNull
+    public abstract Matrix getSensorToBufferTransform();
+
+    /**
+     * Returns the resolution of the frame.
+     *
+     * @see SurfaceRequest#getResolution()
+     */
+    @NonNull
+    public abstract Size getSize();
+
+    /**
+     * Returns the crop rect rectangle.
+     *
+     * <p>The value represents how the frame will be cropped by the CameraX pipeline. The crop
+     * rectangle specifies the region of valid pixels in the frame, using coordinates from (0, 0)
+     * to the (width, height) of {@link #getSize()}. Only the overlay drawn within the bound of
+     * the crop rect will be visible to the end users.
+     *
+     * <p>The crop rect is applied before the rotating and mirroring. The order of the operations
+     * is as follows: 1) cropping, 2) rotating and 3) mirroring.
+     *
+     * @see SurfaceRequest.TransformationInfo#getCropRect()
+     */
+    @NonNull
+    public abstract Rect getCropRect();
+
+    /**
+     * Returns the rotation degrees of the frame.
+     *
+     * <p>This is a clockwise rotation in degrees that needs to be applied to the frame. The
+     * rotation will be determined by {@link CameraCharacteristics} and UseCase configuration.
+     * The app must draw the overlay according to the rotation degrees to ensure it is
+     * displayed correctly to the end users.
+     *
+     * <p>The rotation is applied after the cropping but before the mirroring. The order of the
+     * operations is as follows: 1) cropping, 2) rotating and 3) mirroring.
+     *
+     * @see SurfaceRequest.TransformationInfo#getRotationDegrees()
+     */
+    public abstract int getRotationDegrees();
+
+    /**
+     * Returns whether the buffer will be mirrored.
+     *
+     * <p>This flag indicates whether the buffer will be mirrored by the pipeline vertically. For
+     * example, for front camera preview, the buffer is usually mirrored before displayed to end
+     * users.
+     *
+     * <p>The mirroring is applied after the cropping and the rotating. The order of the
+     * operations is as follows: 1) cropping, 2) rotating and 3) mirroring.
+     *
+     * @see SurfaceRequest.TransformationInfo#getMirroring()
+     */
+    public abstract boolean getMirroring();
+
+    /**
+     * Returns the timestamp of the frame in nanoseconds.
+     *
+     * @see SurfaceTexture#getTimestamp()
+     * @see ImageInfo#getTimestamp()
+     */
+    public abstract long getTimestampNs();
+
+    /**
+     * Invalidates and returns the overlay canvas.
+     *
+     * <p>Call this method to get the {@link Canvas} for drawing an overlay on top of the frame.
+     * The {@link Canvas} is backed by a {@link Bitmap} with the sizes equals {@link #getSize()} and
+     * the format equals {@link Bitmap.Config#ARGB_8888}. To draw object in camera sensor
+     * coordinates, apply {@link #getSensorToBufferTransform()} via
+     * {@link Canvas#setMatrix(Matrix)} before drawing.
+     *
+     * <p>Only call this method if the caller needs to draw overlay on the frame. Calling this
+     * method will upload the {@link Bitmap} to GPU for blending.
+     */
+    @NonNull
+    public Canvas invalidateOverlayCanvas() {
+        mIsOverlayDirty = true;
+        return getOverlayCanvas();
+    }
+
+    /**
+     * Internal API to check whether the overlay canvas is dirty.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public boolean isOverlayDirty() {
+        return mIsOverlayDirty;
+    }
+
+
+    /**
+     * Internal API to get the overlay canvas without invalidating it.
+     */
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public abstract Canvas getOverlayCanvas();
+}
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/OverlayEffect.java b/camera/camera-effects/src/main/java/androidx/camera/effects/OverlayEffect.java
new file mode 100644
index 0000000..642f8e6
--- /dev/null
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/OverlayEffect.java
@@ -0,0 +1,102 @@
+/*
+ * 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.camera.effects;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.arch.core.util.Function;
+import androidx.camera.core.CameraEffect;
+import androidx.camera.core.UseCase;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A {@link CameraEffect} for drawing overlay on top of the camera frames.
+ * TODO(b/297509601): Make it public API in 1.4.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(21)
+public class OverlayEffect {
+
+    /**
+     * {@link #drawFrame(long)} result code
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @IntDef(value = {
+            RESULT_SUCCESS,
+            RESULT_FRAME_NOT_FOUND,
+            RESULT_INVALID_SURFACE,
+            RESULT_CANCELLED_BY_CALLER})
+    public @interface DrawFrameResult {
+    }
+
+    /**
+     * The {@link #drawFrame(long)} call was successful. The frame with the exact timestamp was
+     * drawn to the output surface.
+     */
+    public static final int RESULT_SUCCESS = 1;
+
+    /**
+     * The {@link #drawFrame(long)} call failed because the frame with the exact timestamp was
+     * not found in the queue. It could be one of the following reasons:
+     *
+     * <ul>
+     * <li>the timestamp was incorrect, or
+     * <li>the frame was not yet available, or
+     * <li>the frame was removed because {@link #drawFrame} had been called with a newer
+     * timestamp, or
+     * <li>the frame was removed due to the queue is full.
+     * </ul>
+     *
+     * If it's the last case, the caller may avoid this issue by increasing the queue depth.
+     */
+    public static final int RESULT_FRAME_NOT_FOUND = 2;
+
+    /**
+     * The {@link #drawFrame(long)} call failed because the output surface is missing, or the
+     * output surface no longer matches the frame. It could be because the {@link UseCase}
+     * was unbound, causing the original surface to be replaced or disabled.
+     */
+    public static final int RESULT_INVALID_SURFACE = 3;
+
+    /**
+     * The {@link #drawFrame(long)} call failed because the caller cancelled the drawing. This
+     * happens when the listener provided via {@link #setOnDrawListener(Function)} returned false.
+     */
+    public static final int RESULT_CANCELLED_BY_CALLER = 4;
+
+    /**
+     * TODO(b/297509601): add JavaDoc
+     */
+    @NonNull
+    public ListenableFuture<Integer> drawFrame(long timestampNs) {
+        throw new UnsupportedOperationException("Not implemented yet");
+    }
+
+    /**
+     * TODO(b/297509601): add JavaDoc
+     */
+    public void setOnDrawListener(@NonNull Function<Frame, Boolean> onDrawListener) {
+        throw new UnsupportedOperationException("Not implemented yet");
+    }
+}
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/Portrait.java b/camera/camera-effects/src/main/java/androidx/camera/effects/Portrait.java
deleted file mode 100644
index 7a355f8..0000000
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/Portrait.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.effects;
-
-import androidx.annotation.RestrictTo;
-
-/**
- * Provides a portrait post-processing effect.
- *
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class Portrait {
-    // TODO: implement this
-}
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/SurfaceProcessorImpl.java b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/SurfaceProcessorImpl.java
new file mode 100644
index 0000000..e46f4b5
--- /dev/null
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/SurfaceProcessorImpl.java
@@ -0,0 +1,366 @@
+/*
+ * 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.camera.effects.internal;
+
+import static androidx.core.util.Preconditions.checkArgument;
+import static androidx.core.util.Preconditions.checkState;
+import static java.util.Objects.requireNonNull;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.SurfaceTexture;
+import android.os.Handler;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import androidx.arch.core.util.Function;
+import androidx.camera.core.SurfaceOutput;
+import androidx.camera.core.SurfaceProcessor;
+import androidx.camera.core.SurfaceRequest;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.effects.Frame;
+import androidx.camera.effects.OverlayEffect;
+import androidx.camera.effects.opengl.GlRenderer;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.core.util.Pair;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Implementation of {@link SurfaceProcessor} that applies an overlay to the input surface.
+ *
+ * <p>This implementation only expects one input surface and one output surface.
+ */
+@RequiresApi(21)
+public class SurfaceProcessorImpl implements SurfaceProcessor,
+        SurfaceTexture.OnFrameAvailableListener {
+
+    // GL thread and handler.
+    private final Handler mGlHandler;
+    private final Executor mGlExecutor;
+
+    // GL renderer.
+    private final GlRenderer mGlRenderer = new GlRenderer();
+    private final int mQueueDepth;
+
+    // Transform matrices.
+    private final float[] mSurfaceTransform = new float[16];
+    private final float[] mTextureTransform = new float[16];
+
+    // Surfaces and buffers.
+    @Nullable
+    private Size mInputSize = null;
+    @Nullable
+    private TextureFrameBuffer mBuffer = null;
+    @Nullable
+    private Bitmap mOverlayBitmap;
+    @Nullable
+    private Canvas mOverlayCanvas;
+    @Nullable
+    private Pair<SurfaceOutput, Surface> mOutputSurfacePair = null;
+    @Nullable
+    private SurfaceRequest.TransformationInfo mTransformationInfo = null;
+    @Nullable
+    private Function<Frame, Boolean> mOnDrawListener;
+
+    private boolean mIsReleased = false;
+
+    public SurfaceProcessorImpl(int queueDepth, @NonNull Handler glHandler) {
+        mGlHandler = glHandler;
+        mGlExecutor = CameraXExecutors.newHandlerExecutor(mGlHandler);
+        mQueueDepth = queueDepth;
+        runOnGlThread(mGlRenderer::init);
+    }
+
+    @Override
+    public void onInputSurface(@NonNull SurfaceRequest surfaceRequest) {
+        checkGlThread();
+        if (mIsReleased) {
+            surfaceRequest.willNotProvideSurface();
+            return;
+        }
+
+        // Configure input surface and listen for frame updates.
+        SurfaceTexture surfaceTexture = new SurfaceTexture(mGlRenderer.getInputTextureId());
+        surfaceTexture.setDefaultBufferSize(surfaceRequest.getResolution().getWidth(),
+                surfaceRequest.getResolution().getHeight());
+        Surface surface = new Surface(surfaceTexture);
+        surfaceRequest.provideSurface(surface, mGlExecutor, result -> {
+            // TODO(b/297509601): maybe release the buffer to free up memory.
+            surfaceTexture.setOnFrameAvailableListener(null);
+            surfaceTexture.release();
+            surface.release();
+        });
+        surfaceTexture.setOnFrameAvailableListener(this, mGlHandler);
+
+        // Listen for transformation updates.
+        mTransformationInfo = null;
+        surfaceRequest.setTransformationInfoListener(mGlExecutor, transformationInfo ->
+                mTransformationInfo = transformationInfo);
+
+        // Configure buffers based on the input size.
+        createBufferAndOverlay(surfaceRequest.getResolution());
+    }
+
+    @Override
+    public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
+        checkGlThread();
+        if (mIsReleased) {
+            surfaceOutput.close();
+            return;
+        }
+
+        Surface surface = surfaceOutput.getSurface(mGlExecutor, result -> {
+            surfaceOutput.close();
+            // When the output surface is closed, unregister if it's the same Surface.
+            if (mOutputSurfacePair != null && mOutputSurfacePair.first == surfaceOutput) {
+                mGlRenderer.unregisterOutputSurface(requireNonNull(mOutputSurfacePair.second));
+                mOutputSurfacePair = null;
+            }
+        });
+
+        // Only one output Surface is allowed. Unregister the existing Surface before registering
+        // the new one.
+        if (mOutputSurfacePair != null) {
+            mGlRenderer.unregisterOutputSurface(requireNonNull(mOutputSurfacePair.second));
+        }
+        mGlRenderer.registerOutputSurface(surface);
+        mOutputSurfacePair = Pair.create(surfaceOutput, surface);
+    }
+
+    @Override
+    public void onFrameAvailable(@NonNull SurfaceTexture surfaceTexture) {
+        checkGlThread();
+        if (mIsReleased) {
+            return;
+        }
+        if (mOutputSurfacePair == null) {
+            // Output surface not ready. Skip.
+            return;
+        }
+
+        // Get the GL transform.
+        surfaceTexture.updateTexImage();
+        surfaceTexture.getTransformMatrix(mTextureTransform);
+        Surface surface = requireNonNull(mOutputSurfacePair.second);
+        SurfaceOutput surfaceOutput = requireNonNull(mOutputSurfacePair.first);
+        surfaceOutput.updateTransformMatrix(mSurfaceTransform, mTextureTransform);
+
+        if (requireNonNull(mBuffer).getLength() == 0) {
+            // There is no buffer. Render directly to the output surface.
+            if (drawOverlay(surfaceTexture.getTimestamp())) {
+                mGlRenderer.renderInputToSurface(
+                        surfaceTexture.getTimestamp(),
+                        mSurfaceTransform,
+                        requireNonNull(surface));
+            }
+        } else {
+            // Cache the frame to the buffer.
+            TextureFrame frameToFill = mBuffer.getFrameToFill();
+            if (!frameToFill.isEmpty()) {
+                // The buffer is full. Release the oldest frame and free up a slot.
+                drawFrameAndMarkEmpty(frameToFill);
+            }
+            mGlRenderer.renderInputToQueueTexture(frameToFill.getTextureId());
+            frameToFill.markFilled(surfaceTexture.getTimestamp(), mSurfaceTransform, surface);
+        }
+    }
+
+    /**
+     * Releases the processor and all the resources it holds.
+     *
+     * <p>Once released, the processor can no longer be used.
+     */
+    public void release() {
+        runOnGlThread(() -> {
+            if (!mIsReleased) {
+                if (mOutputSurfacePair != null) {
+                    requireNonNull(mOutputSurfacePair.first).close();
+                    mOutputSurfacePair = null;
+                }
+                mGlRenderer.release();
+                mBuffer = null;
+                mOverlayBitmap = null;
+                mOverlayCanvas = null;
+                mInputSize = null;
+                mIsReleased = true;
+            }
+        });
+    }
+
+    /**
+     * Gets the {@link Executor} used by OpenGL.
+     */
+    @NonNull
+    public Executor getGlExecutor() {
+        return mGlExecutor;
+    }
+
+    /**
+     * Sets the listener that listens to frame updates and draws overlay.
+     *
+     * <p>CameraX invokes this {@link Function} on the GL thread each time a frame is drawn. The
+     * caller can use implement the {@link Function} to draw overlay on the frame.
+     *
+     * <p>The {@link Function} accepts a {@link Frame} object which provides information on how to
+     * draw the overlay. The return value of the {@link Function} indicates whether the frame
+     * should be drawn. If false, the frame will be dropped.
+     */
+    public void setOnDrawListener(@Nullable Function<Frame, Boolean> onDrawListener) {
+        runOnGlThread(() -> mOnDrawListener = onDrawListener);
+    }
+
+    /**
+     * Draws the buffered frame with the given timestamp.
+     *
+     * <p>The {@link ListenableFuture} completes with a {@link OverlayEffect.DrawFrameResult}
+     * value. If this is called after the processor is released, the future completes with an
+     * exception.
+     */
+    @NonNull
+    public ListenableFuture<Integer> drawFrame(long timestampNs) {
+        return CallbackToFutureAdapter.getFuture(completer -> {
+            runOnGlThread(() -> {
+                if (mIsReleased) {
+                    completer.setException(new IllegalStateException("Effect is released"));
+                    return;
+                }
+                TextureFrame frame = requireNonNull(mBuffer).getFrameToRender(timestampNs);
+                if (frame != null) {
+                    completer.set(drawFrameAndMarkEmpty(frame));
+                } else {
+                    // No frame with the given timestamp. Return false to the app.
+                    completer.set(OverlayEffect.RESULT_FRAME_NOT_FOUND);
+                }
+            });
+            return "drawFrameFuture";
+        });
+    }
+
+    // *** Private methods ***
+
+    private void runOnGlThread(@NonNull Runnable runnable) {
+        if (isGlThread()) {
+            runnable.run();
+        } else {
+            mGlHandler.post(runnable);
+        }
+    }
+
+    private void createBufferAndOverlay(@NonNull Size inputSize) {
+        checkGlThread();
+        if (inputSize.equals(mInputSize)) {
+            // Input size unchanged. No need to reallocate buffers.
+            return;
+        }
+        mInputSize = inputSize;
+
+        // Create a buffer of textures with the same size as the input.
+        int[] textureIds = mGlRenderer.createBufferTextureIds(mQueueDepth, mInputSize);
+        mBuffer = new TextureFrameBuffer(textureIds);
+
+        // Create the overlay Bitmap with the same size as the input.
+        mOverlayBitmap = Bitmap.createBitmap(inputSize.getWidth(), inputSize.getHeight(),
+                Bitmap.Config.ARGB_8888);
+        mOverlayCanvas = new Canvas(mOverlayBitmap);
+        mOverlayCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
+        mGlRenderer.uploadOverlay(mOverlayBitmap);
+    }
+
+    /**
+     * Renders a buffered frame to the output surface.
+     *
+     * @return the draw result.
+     */
+    @OverlayEffect.DrawFrameResult
+    private int drawFrameAndMarkEmpty(@NonNull TextureFrame frame) {
+        checkGlThread();
+        checkArgument(!frame.isEmpty());
+        try {
+            if (mOutputSurfacePair == null || mOutputSurfacePair.second != frame.getSurface()) {
+                return OverlayEffect.RESULT_INVALID_SURFACE;
+            }
+            // Only draw if frame is associated with the current output surface.
+            if (drawOverlay(frame.getTimestampNs())) {
+                mGlRenderer.renderQueueTextureToSurface(
+                        frame.getTextureId(),
+                        frame.getTimestampNs(),
+                        frame.getTransform(),
+                        frame.getSurface());
+                return OverlayEffect.RESULT_SUCCESS;
+            }
+            return OverlayEffect.RESULT_CANCELLED_BY_CALLER;
+        } finally {
+            frame.markEmpty();
+        }
+    }
+
+    /**
+     * Requests the app to draw overlay.
+     *
+     * <p>This method invokes app's callback to draw overlay and upload the result to GPU.
+     *
+     * <p>The caller should only render the frame if this method returns true.
+     */
+    @SuppressWarnings("unused")
+    private boolean drawOverlay(long timestampNs) {
+        checkGlThread();
+        if (mTransformationInfo == null || mOnDrawListener == null) {
+            return true;
+        }
+        Frame frame = Frame.of(
+                requireNonNull(mOverlayCanvas),
+                timestampNs,
+                requireNonNull(mInputSize),
+                mTransformationInfo);
+        if (!mOnDrawListener.apply(frame)) {
+            // The caller wants to drop the frame.
+            return false;
+        }
+        if (frame.isOverlayDirty()) {
+            mGlRenderer.uploadOverlay(requireNonNull(mOverlayBitmap));
+        }
+        return true;
+    }
+
+    private void checkGlThread() {
+        checkState(isGlThread(), "Must be called on GL thread");
+    }
+
+    private boolean isGlThread() {
+        return Thread.currentThread() == mGlHandler.getLooper().getThread();
+    }
+
+    @VisibleForTesting
+    @NonNull
+    GlRenderer getGlRendererForTesting() {
+        return mGlRenderer;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    TextureFrameBuffer getBuffer() {
+        return requireNonNull(mBuffer);
+    }
+}
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrame.java b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrame.java
index 76a5726..d284439 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrame.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrame.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.camera.effects.opengl.GlRenderer;
 
 /**
@@ -32,6 +33,7 @@
  * <p>The frame can be empty or filled. A filled frame contains valid information on how to
  * render it. An empty frame can be filled with new content.
  */
+@RequiresApi(21)
 class TextureFrame {
 
     private static final long NO_VALUE = Long.MIN_VALUE;
@@ -41,8 +43,9 @@
     private long mTimestampNs = NO_VALUE;
     @Nullable
     private Surface mSurface;
-    @Nullable
-    private float[] mTransform;
+
+    @NonNull
+    private final float[] mTransform = new float[16];
 
     /**
      * Creates a frame that is backed by a texture ID.
@@ -54,7 +57,7 @@
     /**
      * Checks if the frame is empty.
      *
-     * <p>A empty frame means that the texture does not have valid content. It can be filled
+     * <p>An empty frame means that the texture does not have valid content. It can be filled
      * with new content.
      */
     boolean isEmpty() {
@@ -69,25 +72,24 @@
     void markEmpty() {
         checkState(!isEmpty(), "Frame is already empty");
         mTimestampNs = NO_VALUE;
-        mTransform = null;
         mSurface = null;
     }
 
     /**
      * Marks the frame as filled.
      *
-     * <p>Call this method when a valid camera frame is copied to the texture with
+     * <p>Call this method when a valid camera frame has been copied to the texture with
      * {@link GlRenderer#renderInputToQueueTexture}. Once filled, the frame should not be
      * written into until it's made empty again.
      *
-     * @param timestampNs the timestamp of the camera frame.
+     * @param timestampNs the timestamp of the camera frame in nanoseconds.
      * @param transform   the transform to apply when rendering the frame.
      * @param surface     the output surface to which the frame should render.
      */
-    void markFilled(long timestampNs, float[] transform, Surface surface) {
+    void markFilled(long timestampNs, @NonNull float[] transform, @NonNull Surface surface) {
         checkState(isEmpty(), "Frame is already filled");
         mTimestampNs = timestampNs;
-        mTransform = transform;
+        System.arraycopy(transform, 0, mTransform, 0, transform.length);
         mSurface = surface;
     }
 
@@ -117,7 +119,7 @@
      */
     @NonNull
     float[] getTransform() {
-        return requireNonNull(mTransform);
+        return mTransform;
     }
 
     /**
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrameBuffer.java b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrameBuffer.java
index d68839d..e75b292 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrameBuffer.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrameBuffer.java
@@ -20,11 +20,16 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
 import androidx.camera.effects.opengl.GlRenderer;
 
 /**
  * A buffer of {@link TextureFrame}.
+ *
+ * <p>This class is not thread safe. It is expected to be called from a single GL thread.
  */
+@RequiresApi(21)
 class TextureFrameBuffer {
 
     @NonNull
@@ -97,4 +102,10 @@
         }
         return requireNonNull(oldestFrame);
     }
+
+    @VisibleForTesting
+    @NonNull
+    TextureFrame[] getFrames() {
+        return mFrames;
+    }
 }
diff --git a/camera/camera-effects/src/test/java/androidx/camera/effects/internal/FrameBufferTest.kt b/camera/camera-effects/src/test/java/androidx/camera/effects/internal/FrameBufferTest.kt
deleted file mode 100644
index 636c909..0000000
--- a/camera/camera-effects/src/test/java/androidx/camera/effects/internal/FrameBufferTest.kt
+++ /dev/null
@@ -1,141 +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.camera.effects.internal
-
-import android.graphics.SurfaceTexture
-import android.opengl.Matrix
-import android.os.Build
-import android.view.Surface
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-
-/**
- * Unit tests for [TextureFrameBuffer].
- */
-@RunWith(RobolectricTestRunner::class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-class FrameBufferTest {
-
-    companion object {
-        private const val TIMESTAMP_1 = 11L
-        private const val TIMESTAMP_2 = 22L
-    }
-
-    private lateinit var surfaceTexture1: SurfaceTexture
-    private lateinit var surface1: Surface
-    private val transform1 = FloatArray(16).apply {
-        Matrix.setIdentityM(this, 0)
-    }
-
-    private lateinit var surfaceTexture2: SurfaceTexture
-    private lateinit var surface2: Surface
-    private val transform2 = FloatArray(16).apply {
-        Matrix.setIdentityM(this, 0)
-        Matrix.rotateM(this, 0, 90f, 0f, 0f, 1f)
-    }
-
-    @Before
-    fun setUp() {
-        surfaceTexture1 = SurfaceTexture(1)
-        surface1 = Surface(surfaceTexture1)
-        surfaceTexture2 = SurfaceTexture(2)
-        surface2 = Surface(surfaceTexture2)
-    }
-
-    @After
-    fun tearDown() {
-        surface1.release()
-        surfaceTexture1.release()
-        surface2.release()
-        surfaceTexture2.release()
-    }
-
-    @Test
-    fun getLength_returnsCorrectLength() {
-        // Arrange: create 3 textures.
-        val textures = intArrayOf(1, 2, 3)
-        // Act: create the buffer.
-        val buffer = TextureFrameBuffer(textures)
-        // Assert: the length is 3.
-        assertThat(buffer.length).isEqualTo(3)
-    }
-
-    @Test
-    fun getFrameToRender_returnsNullIfNotFound() {
-        // Arrange: create a buffer with 1 texture and mark them occupied.
-        val buffer = TextureFrameBuffer(intArrayOf(1))
-        buffer.frameToFill.markFilled(TIMESTAMP_1, transform1, surface1)
-        // Act and assert: get frame with a different timestamp and it should be null.
-        assertThat(buffer.getFrameToRender(TIMESTAMP_2)).isNull()
-    }
-
-    @Test
-    fun getFrameToRender_markOlderFramesVacant() {
-        // Arrange: create a buffer with two textures and mark them occupied.
-        val buffer = TextureFrameBuffer(intArrayOf(1, 2))
-        val frames = fillBufferWithTwoFrames(buffer)
-
-        // Act: get frame2 for rendering.
-        assertThat(buffer.getFrameToRender(TIMESTAMP_2)).isSameInstanceAs(frames.second)
-
-        // Assert: frame1 is vacant now.
-        assertThat(frames.second.isEmpty).isFalse()
-        assertThat(frames.first.isEmpty).isTrue()
-    }
-
-    @Test
-    fun getFrameToFill_returnsOldestFrameWhenFull() {
-        // Arrange: create a full buffer.
-        val buffer = TextureFrameBuffer(intArrayOf(1, 2))
-        val frames = fillBufferWithTwoFrames(buffer)
-        // Act and assert: get frame to fill and it should be the first frame.
-        assertThat(buffer.frameToFill).isSameInstanceAs(frames.first)
-    }
-
-    @Test
-    fun getFrameToFill_returnsEmptyFrameWhenNotFull() {
-        // Arrange: create a full buffer.
-        val buffer = TextureFrameBuffer(intArrayOf(1, 2))
-        val frames = fillBufferWithTwoFrames(buffer)
-        // Mark the second frame empty.
-        frames.second.markEmpty()
-        // Act and assert: get frame to fill and it should be the second frame.
-        assertThat(buffer.frameToFill).isSameInstanceAs(frames.second)
-    }
-
-    private fun fillBufferWithTwoFrames(
-        buffer: TextureFrameBuffer
-    ): Pair<TextureFrame, TextureFrame> {
-        assertThat(buffer.length).isEqualTo(2)
-        // Fill frame 1.
-        val frame1 = buffer.frameToFill
-        assertThat(frame1.textureId).isEqualTo(1)
-        frame1.markFilled(TIMESTAMP_1, transform1, surface1)
-        // Fill frame 2.
-        val frame2 = buffer.frameToFill
-        assertThat(frame2.textureId).isEqualTo(2)
-        frame2.markFilled(TIMESTAMP_2, transform2, surface2)
-        return Pair(frame1, frame2)
-    }
-}
diff --git a/camera/camera-effects/src/test/java/androidx/camera/effects/internal/TextureFrameBufferTest.kt b/camera/camera-effects/src/test/java/androidx/camera/effects/internal/TextureFrameBufferTest.kt
new file mode 100644
index 0000000..757373c
--- /dev/null
+++ b/camera/camera-effects/src/test/java/androidx/camera/effects/internal/TextureFrameBufferTest.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.camera.effects.internal
+
+import android.graphics.SurfaceTexture
+import android.opengl.Matrix
+import android.os.Build
+import android.view.Surface
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+/**
+ * Unit tests for [TextureFrameBuffer].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class TextureFrameBufferTest {
+
+    companion object {
+        private const val TIMESTAMP_1 = 11L
+        private const val TIMESTAMP_2 = 22L
+    }
+
+    private lateinit var surfaceTexture1: SurfaceTexture
+    private lateinit var surface1: Surface
+    private val transform1 = FloatArray(16).apply {
+        Matrix.setIdentityM(this, 0)
+    }
+
+    private lateinit var surfaceTexture2: SurfaceTexture
+    private lateinit var surface2: Surface
+    private val transform2 = FloatArray(16).apply {
+        Matrix.setIdentityM(this, 0)
+        Matrix.rotateM(this, 0, 90f, 0f, 0f, 1f)
+    }
+
+    @Before
+    fun setUp() {
+        surfaceTexture1 = SurfaceTexture(1)
+        surface1 = Surface(surfaceTexture1)
+        surfaceTexture2 = SurfaceTexture(2)
+        surface2 = Surface(surfaceTexture2)
+    }
+
+    @After
+    fun tearDown() {
+        surface1.release()
+        surfaceTexture1.release()
+        surface2.release()
+        surfaceTexture2.release()
+    }
+
+    @Test
+    fun getLength_returnsCorrectLength() {
+        // Arrange: create 3 textures.
+        val textures = intArrayOf(1, 2, 3)
+        // Act: create the buffer.
+        val buffer = TextureFrameBuffer(textures)
+        // Assert: the length is 3.
+        assertThat(buffer.length).isEqualTo(3)
+    }
+
+    @Test
+    fun getFrameToRender_returnsFilledFrame() {
+        // Arrange: create a buffer with 1 texture and mark them filled.
+        val buffer = TextureFrameBuffer(intArrayOf(1))
+        buffer.frameToFill.markFilled(TIMESTAMP_1, transform1, surface1)
+
+        // Act: get frame with the same timestamp.
+        val frame = buffer.getFrameToRender(TIMESTAMP_1)!!
+
+        // Assert: the frame has the correct values.
+        assertThat(frame.textureId).isEqualTo(1)
+        assertThat(frame.timestampNs).isEqualTo(TIMESTAMP_1)
+        assertThat(frame.transform.contentEquals(transform1)).isTrue()
+        assertThat(frame.transform).isNotSameInstanceAs(transform1)
+        assertThat(frame.surface).isSameInstanceAs(surface1)
+    }
+
+    @Test
+    fun getFrameToRender_returnsNullIfNotFound() {
+        // Arrange: create a buffer with 1 texture and mark them filled.
+        val buffer = TextureFrameBuffer(intArrayOf(1))
+        buffer.frameToFill.markFilled(TIMESTAMP_1, transform1, surface1)
+        // Act and assert: get frame with a different timestamp and it should be null.
+        assertThat(buffer.getFrameToRender(TIMESTAMP_2)).isNull()
+    }
+
+    @Test
+    fun getFrameToRender_markOlderFramesEmpty() {
+        // Arrange: create a buffer with two textures and mark them filled.
+        val buffer = TextureFrameBuffer(intArrayOf(1, 2))
+        val frames = fillBufferWithTwoFrames(buffer)
+
+        // Act: get frame2 for rendering.
+        assertThat(buffer.getFrameToRender(TIMESTAMP_2)).isSameInstanceAs(frames.second)
+
+        // Assert: frame1 is empty now.
+        assertThat(frames.second.isEmpty).isFalse()
+        assertThat(frames.first.isEmpty).isTrue()
+    }
+
+    @Test
+    fun getFrameToFill_returnsOldestFrameWhenFull() {
+        // Arrange: create a full buffer.
+        val buffer = TextureFrameBuffer(intArrayOf(1, 2))
+        val frames = fillBufferWithTwoFrames(buffer)
+        // Act and assert: get frame to fill and it should be the first frame.
+        assertThat(buffer.frameToFill).isSameInstanceAs(frames.first)
+    }
+
+    @Test
+    fun getFrameToFill_returnsEmptyFrameWhenNotFull() {
+        // Arrange: create a full buffer.
+        val buffer = TextureFrameBuffer(intArrayOf(1, 2))
+        val frames = fillBufferWithTwoFrames(buffer)
+        // Mark the second frame empty.
+        frames.second.markEmpty()
+        // Act and assert: get frame to fill and it should be the second frame.
+        assertThat(buffer.frameToFill).isSameInstanceAs(frames.second)
+    }
+
+    private fun fillBufferWithTwoFrames(
+        buffer: TextureFrameBuffer
+    ): Pair<TextureFrame, TextureFrame> {
+        assertThat(buffer.length).isEqualTo(2)
+        // Fill frame 1.
+        val frame1 = buffer.frameToFill
+        assertThat(frame1.textureId).isEqualTo(1)
+        frame1.markFilled(TIMESTAMP_1, transform1, surface1)
+        // Fill frame 2.
+        val frame2 = buffer.frameToFill
+        assertThat(frame2.textureId).isEqualTo(2)
+        frame2.markFilled(TIMESTAMP_2, transform2, surface2)
+        return Pair(frame1, frame2)
+    }
+}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/AdvancedExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/AdvancedExtenderImpl.java
index 05259d3..03c6ba2 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/AdvancedExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/AdvancedExtenderImpl.java
@@ -132,7 +132,8 @@
      * both JPEG and YUV_420_888 format output.
      *
      * <p>The returned sizes must be smaller than or equal to the provided capture size and have the
-     * same aspect ratio as the given capture size.
+     * same aspect ratio as the given capture size. If no supported resolution exists for the
+     * provided capture size then an empty map is returned.
      *
      * @since 1.4
      */
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceImpl.java
index e7ea060..40b5446 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceImpl.java
@@ -29,7 +29,7 @@
  */
 public interface OutputSurfaceImpl {
     /**
-     * Gets the surface.
+     * Gets the surface. It returns null if output surface is not specified.
      */
     @Nullable
     Surface getSurface();
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
index d73811b..b0b4d0c 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
@@ -72,6 +72,7 @@
 import androidx.camera.extensions.impl.advanced.Camera2OutputConfigImplBuilder
 import androidx.camera.extensions.impl.advanced.Camera2SessionConfigImpl
 import androidx.camera.extensions.impl.advanced.Camera2SessionConfigImplBuilder
+import androidx.camera.extensions.impl.advanced.OutputSurfaceConfigurationImpl
 import androidx.camera.extensions.impl.advanced.OutputSurfaceImpl
 import androidx.camera.extensions.impl.advanced.RequestProcessorImpl
 import androidx.camera.extensions.impl.advanced.SessionProcessorImpl
@@ -180,19 +181,19 @@
             // Directly use output surface
             previewConfigBlock = { outputSurfaceImpl ->
                 Camera2OutputConfigImplBuilder
-                    .newSurfaceConfig(outputSurfaceImpl.surface)
+                    .newSurfaceConfig(outputSurfaceImpl.surface!!)
                     .build()
             },
             // Directly use output surface
             captureConfigBlock = { outputSurfaceImpl ->
                 Camera2OutputConfigImplBuilder
-                    .newSurfaceConfig(outputSurfaceImpl.surface)
+                    .newSurfaceConfig(outputSurfaceImpl.surface!!)
                     .build()
             },
             // Directly use output surface
             analysisConfigBlock = { outputSurfaceImpl ->
                 Camera2OutputConfigImplBuilder
-                    .newSurfaceConfig(outputSurfaceImpl.surface)
+                    .newSurfaceConfig(outputSurfaceImpl.surface!!)
                     .build()
             }
         )
@@ -270,13 +271,13 @@
             // Directly use output surface
             previewConfigBlock = { outputSurfaceImpl ->
                 Camera2OutputConfigImplBuilder
-                    .newSurfaceConfig(outputSurfaceImpl.surface)
+                    .newSurfaceConfig(outputSurfaceImpl.surface!!)
                     .build()
             },
             // Directly use output surface
             captureConfigBlock = { outputSurfaceImpl ->
                 Camera2OutputConfigImplBuilder
-                    .newSurfaceConfig(outputSurfaceImpl.surface)
+                    .newSurfaceConfig(outputSurfaceImpl.surface!!)
                     .build()
             },
             // Directly use output surface with shared ImageReader surface.
@@ -286,7 +287,7 @@
                 ).build()
                 sharedConfigId = sharedConfig.id
 
-                Camera2OutputConfigImplBuilder.newSurfaceConfig(outputSurfaceImpl.surface)
+                Camera2OutputConfigImplBuilder.newSurfaceConfig(outputSurfaceImpl.surface!!)
                     .addSurfaceSharingOutputConfig(sharedConfig)
                     .build()
             },
@@ -334,14 +335,14 @@
             // Directly use output surface
             previewConfigBlock = { outputSurfaceImpl ->
                 Camera2OutputConfigImplBuilder
-                    .newSurfaceConfig(outputSurfaceImpl.surface)
+                    .newSurfaceConfig(outputSurfaceImpl.surface!!)
                     .setPhysicalCameraId(physicalCameraId)
                     .build()
             },
             // Directly use output surface
             captureConfigBlock = {
                 Camera2OutputConfigImplBuilder
-                    .newSurfaceConfig(it.surface)
+                    .newSurfaceConfig(it.surface!!)
                     .build()
             },
             // Has intermediate image reader to process YUV
@@ -476,12 +477,12 @@
 class FakeSessionProcessImpl(
     var previewConfigBlock: (OutputSurfaceImpl) -> Camera2OutputConfigImpl = { outputSurfaceImpl ->
         Camera2OutputConfigImplBuilder
-            .newSurfaceConfig(outputSurfaceImpl.surface)
+            .newSurfaceConfig(outputSurfaceImpl.surface!!)
             .build()
     },
     var captureConfigBlock: (OutputSurfaceImpl) -> Camera2OutputConfigImpl = { outputSurfaceImpl ->
             Camera2OutputConfigImplBuilder
-                .newSurfaceConfig(outputSurfaceImpl.surface)
+                .newSurfaceConfig(outputSurfaceImpl.surface!!)
                 .build()
     },
     var analysisConfigBlock: ((OutputSurfaceImpl) -> Camera2OutputConfigImpl)? = null,
@@ -497,15 +498,15 @@
         CompletableDeferred<MutableMap<CaptureRequest.Key<*>, Any>>()
 
     override fun initSession(
-        cameraId: String?,
-        cameraCharacteristicsMap: MutableMap<String, CameraCharacteristics>?,
-        context: Context?,
-        previewSurfaceConfig: OutputSurfaceImpl?,
-        captureSurfaceConfig: OutputSurfaceImpl?,
+        cameraId: String,
+        cameraCharacteristicsMap: MutableMap<String, CameraCharacteristics>,
+        context: Context,
+        previewSurfaceConfig: OutputSurfaceImpl,
+        captureSurfaceConfig: OutputSurfaceImpl,
         analysisSurfaceConfig: OutputSurfaceImpl?
     ): Camera2SessionConfigImpl {
-        captureOutputConfig = captureConfigBlock.invoke(captureSurfaceConfig!!)
-        previewOutputConfig = previewConfigBlock.invoke(previewSurfaceConfig!!)
+        captureOutputConfig = captureConfigBlock.invoke(captureSurfaceConfig)
+        previewOutputConfig = previewConfigBlock.invoke(previewSurfaceConfig)
         analysisSurfaceConfig?.let {
             analysisOutputConfig = analysisConfigBlock?.invoke(it)
         }
@@ -519,22 +520,36 @@
         val sessionBuilder = Camera2SessionConfigImplBuilder().apply {
             addOutputConfig(previewOutputConfig)
             addOutputConfig(captureOutputConfig)
-            analysisOutputConfig?.let { addOutputConfig(analysisOutputConfig) }
+            analysisOutputConfig?.let { addOutputConfig(it) }
         }
         return sessionBuilder.build()
     }
 
+    override fun initSession(
+        cameraId: String,
+        cameraCharacteristicsMap: MutableMap<String, CameraCharacteristics>,
+        context: Context,
+        surfaceConfigs: OutputSurfaceConfigurationImpl
+    ): Camera2SessionConfigImpl {
+        return initSession(
+            cameraId, cameraCharacteristicsMap, context,
+            surfaceConfigs.previewOutputSurface,
+            surfaceConfigs.imageCaptureOutputSurface,
+            surfaceConfigs.imageAnalysisOutputSurface
+        )
+    }
+
     override fun deInitSession() {
     }
 
-    override fun setParameters(parameters: MutableMap<CaptureRequest.Key<*>, Any>?) {
+    override fun setParameters(parameters: MutableMap<CaptureRequest.Key<*>, Any>) {
     }
 
     override fun startTrigger(
-        triggers: MutableMap<CaptureRequest.Key<*>, Any>?,
-        callback: SessionProcessorImpl.CaptureCallback?
+        triggers: MutableMap<CaptureRequest.Key<*>, Any>,
+        callback: SessionProcessorImpl.CaptureCallback
     ): Int {
-        startTriggerParametersDeferred.complete(triggers!!)
+        startTriggerParametersDeferred.complete(triggers)
         return 0
     }
 
@@ -545,15 +560,15 @@
             .isEqualTo(parameters)
     }
 
-    override fun onCaptureSessionStart(requestProcessor: RequestProcessorImpl?) {
+    override fun onCaptureSessionStart(requestProcessor: RequestProcessorImpl) {
         this.requestProcessor = requestProcessor
-        onCaptureSessionStarted?.invoke(requestProcessor!!)
+        onCaptureSessionStarted?.invoke(requestProcessor)
     }
 
     override fun onCaptureSessionEnd() {
     }
 
-    override fun startRepeating(callback: SessionProcessorImpl.CaptureCallback?): Int {
+    override fun startRepeating(callback: SessionProcessorImpl.CaptureCallback): Int {
         val idList = ArrayList<Int>()
         idList.add(previewOutputConfig.id)
         analysisOutputConfig?.let { idList.add(it.id) }
@@ -565,42 +580,42 @@
         val request = RequestProcessorRequest(idList, mapOf(), CameraDevice.TEMPLATE_PREVIEW)
         requestProcessor!!.setRepeating(request, object : RequestProcessorImpl.Callback {
             override fun onCaptureStarted(
-                request: RequestProcessorImpl.Request?,
+                request: RequestProcessorImpl.Request,
                 frameNumber: Long,
                 timestamp: Long
             ) {
-                callback?.onCaptureStarted(currentSequenceId, timestamp)
+                callback.onCaptureStarted(currentSequenceId, timestamp)
             }
 
             override fun onCaptureProgressed(
-                request: RequestProcessorImpl.Request?,
-                partialResult: CaptureResult?
+                request: RequestProcessorImpl.Request,
+                partialResult: CaptureResult
             ) {
             }
 
             @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
             override fun onCaptureCompleted(
-                request: RequestProcessorImpl.Request?,
-                totalCaptureResult: TotalCaptureResult?
+                request: RequestProcessorImpl.Request,
+                totalCaptureResult: TotalCaptureResult
             ) {
-                callback?.onCaptureProcessStarted(currentSequenceId)
-                callback?.onCaptureCompleted(
-                    totalCaptureResult!!.get(CaptureResult.SENSOR_TIMESTAMP)!!,
+                callback.onCaptureProcessStarted(currentSequenceId)
+                callback.onCaptureCompleted(
+                    totalCaptureResult.get(CaptureResult.SENSOR_TIMESTAMP)!!,
                     currentSequenceId, mapOf()
                 )
-                callback?.onCaptureSequenceCompleted(currentSequenceId)
+                callback.onCaptureSequenceCompleted(currentSequenceId)
             }
 
             override fun onCaptureFailed(
-                request: RequestProcessorImpl.Request?,
-                captureFailure: CaptureFailure?
+                request: RequestProcessorImpl.Request,
+                captureFailure: CaptureFailure
             ) {
-                callback?.onCaptureFailed(currentSequenceId)
-                callback?.onCaptureSequenceAborted(currentSequenceId)
+                callback.onCaptureFailed(currentSequenceId)
+                callback.onCaptureSequenceAborted(currentSequenceId)
             }
 
             override fun onCaptureBufferLost(
-                request: RequestProcessorImpl.Request?,
+                request: RequestProcessorImpl.Request,
                 frameNumber: Long,
                 outputStreamId: Int
             ) {
@@ -619,7 +634,7 @@
         requestProcessor?.stopRepeating()
     }
 
-    override fun startCapture(callback: SessionProcessorImpl.CaptureCallback?): Int {
+    override fun startCapture(callback: SessionProcessorImpl.CaptureCallback): Int {
         val idList = ArrayList<Int>()
         idList.add(captureOutputConfig.id)
 
@@ -627,55 +642,63 @@
         val request = RequestProcessorRequest(idList, mapOf(), CameraDevice.TEMPLATE_STILL_CAPTURE)
         requestProcessor?.submit(request, object : RequestProcessorImpl.Callback {
             override fun onCaptureStarted(
-                request: RequestProcessorImpl.Request?,
+                request: RequestProcessorImpl.Request,
                 frameNumber: Long,
                 timestamp: Long
             ) {
-                callback?.onCaptureStarted(currentSequenceId, timestamp)
+                callback.onCaptureStarted(currentSequenceId, timestamp)
             }
 
             override fun onCaptureProgressed(
-                request: RequestProcessorImpl.Request?,
-                partialResult: CaptureResult?
+                request: RequestProcessorImpl.Request,
+                partialResult: CaptureResult
             ) {
             }
 
             @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
             override fun onCaptureCompleted(
-                request: RequestProcessorImpl.Request?,
-                totalCaptureResult: TotalCaptureResult?
+                request: RequestProcessorImpl.Request,
+                totalCaptureResult: TotalCaptureResult
             ) {
-                callback?.onCaptureCompleted(
-                    totalCaptureResult!!.get(CaptureResult.SENSOR_TIMESTAMP)!!, currentSequenceId,
+                callback.onCaptureCompleted(
+                    totalCaptureResult.get(CaptureResult.SENSOR_TIMESTAMP)!!, currentSequenceId,
                     mapOf()
                 )
             }
 
             override fun onCaptureFailed(
-                request: RequestProcessorImpl.Request?,
-                captureFailure: CaptureFailure?
+                request: RequestProcessorImpl.Request,
+                captureFailure: CaptureFailure
             ) {
-                callback?.onCaptureFailed(currentSequenceId)
+                callback.onCaptureFailed(currentSequenceId)
             }
 
             override fun onCaptureBufferLost(
-                request: RequestProcessorImpl.Request?,
+                request: RequestProcessorImpl.Request,
                 frameNumber: Long,
                 outputStreamId: Int
             ) {
             }
 
             override fun onCaptureSequenceCompleted(sequenceId: Int, frameNumber: Long) {
-                callback?.onCaptureSequenceCompleted(currentSequenceId)
+                callback.onCaptureSequenceCompleted(currentSequenceId)
             }
 
             override fun onCaptureSequenceAborted(sequenceId: Int) {
-                callback?.onCaptureSequenceAborted(currentSequenceId)
+                callback.onCaptureSequenceAborted(currentSequenceId)
             }
         })
         return currentSequenceId
     }
 
+    override fun startCaptureWithPostview(callback: SessionProcessorImpl.CaptureCallback): Int {
+        return startCapture(callback)
+    }
+
+    override fun getRealtimeCaptureLatency(): Pair<Long, Long>? {
+        return null
+    }
+
     override fun abortCapture(captureSequenceId: Int) {
         requestProcessor?.abortCaptures()
     }
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt
index abd3bda..9bc767e 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt
@@ -17,6 +17,7 @@
 package androidx.camera.extensions.internal.compat.workaround
 
 import androidx.camera.extensions.internal.compat.workaround.OnEnableDisableSessionDurationCheck.MIN_DURATION_FOR_ENABLE_DISABLE_SESSION
+import androidx.camera.testing.impl.AndroidUtil
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
@@ -24,6 +25,8 @@
 import kotlin.system.measureTimeMillis
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
+import org.junit.Assume.assumeFalse
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -35,6 +38,11 @@
         const val TOLERANCE = 60L
     }
 
+    @Before
+    fun setUp() {
+        assumeFalse(AndroidUtil.isEmulatorAndAPI21())
+    }
+
     @Test
     fun enabled_ensureMinimalDuration() = runBlocking {
         // Arrange
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
index c4c0e32..ddd974c 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -23,6 +23,7 @@
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.CaptureResult
 import android.hardware.camera2.TotalCaptureResult
+import android.hardware.camera2.params.SessionConfiguration
 import android.media.Image
 import android.media.ImageWriter
 import android.os.Build
@@ -788,6 +789,10 @@
 
         var onEnableSessionCaptureStage: CaptureStageImpl? = null
         var onDisableSessionCaptureStage: CaptureStageImpl? = null
+
+        override fun onSessionType(): Int {
+            return SessionConfiguration.SESSION_REGULAR
+        }
     }
 
     private class FakePreviewExtenderImpl(
@@ -902,13 +907,34 @@
             fakeCaptureProcessorImpl?.close()
             recordInvoking("onDeInit")
         }
+
+        override fun onSessionType(): Int {
+            return SessionConfiguration.SESSION_REGULAR
+        }
+
+        override fun getSupportedPostviewResolutions(captureSize: Size):
+            MutableList<Pair<Int, Array<Size>>>? {
+            return null
+        }
+
+        override fun isCaptureProcessProgressAvailable(): Boolean {
+            return false
+        }
+
+        override fun getRealtimeCaptureLatency(): Pair<Long, Long>? {
+            return null;
+        }
+
+        override fun isPostviewAvailable(): Boolean {
+            return false
+        }
     }
 
     private class FakeCaptureProcessorImpl(
         val throwErrorOnProcess: Boolean = false
     ) : CaptureProcessorImpl {
         private var imageWriter: ImageWriter? = null
-        override fun process(results: MutableMap<Int, Pair<Image, TotalCaptureResult>>?) {
+        override fun process(results: MutableMap<Int, Pair<Image, TotalCaptureResult>>) {
             if (throwErrorOnProcess) {
                 throw RuntimeException("Process failed")
             }
@@ -917,8 +943,8 @@
         }
 
         override fun process(
-            results: MutableMap<Int, Pair<Image, TotalCaptureResult>>?,
-            resultCallback: ProcessResultImpl?,
+            results: MutableMap<Int, Pair<Image, TotalCaptureResult>>,
+            resultCallback: ProcessResultImpl,
             executor: Executor?
         ) {
             process(results)
@@ -930,6 +956,19 @@
 
         override fun onResolutionUpdate(size: Size) {}
         override fun onImageFormatUpdate(imageFormat: Int) {}
+
+        override fun onPostviewOutputSurface(surface: Surface) {}
+
+        override fun onResolutionUpdate(size: Size, postviewSize: Size) {}
+
+        override fun processWithPostview(
+            results: MutableMap<Int, Pair<Image, TotalCaptureResult>>,
+            resultCallback: ProcessResultImpl,
+            executor: Executor?
+        ) {
+            process(results, resultCallback, executor)
+        }
+
         fun close() {
             imageWriter?.close()
             imageWriter = null
@@ -938,15 +977,15 @@
 
     private class FakePreviewImageProcessorImpl : PreviewImageProcessorImpl {
         private var imageWriter: ImageWriter? = null
-        override fun process(image: Image?, result: TotalCaptureResult?) {
+        override fun process(image: Image, result: TotalCaptureResult) {
             val emptyImage = imageWriter!!.dequeueInputImage()
             imageWriter!!.queueInputImage(emptyImage)
         }
 
         override fun process(
-            image: Image?,
-            result: TotalCaptureResult?,
-            resultCallback: ProcessResultImpl?,
+            image: Image,
+            result: TotalCaptureResult,
+            resultCallback: ProcessResultImpl,
             executor: Executor?
         ) {
             process(image, result)
@@ -958,6 +997,7 @@
 
         override fun onResolutionUpdate(size: Size) {}
         override fun onImageFormatUpdate(imageFormat: Int) {}
+
         fun close() {
             imageWriter?.close()
             imageWriter = null
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessorTest.kt
index 0a1d529..b1d29eb 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessorTest.kt
@@ -227,15 +227,15 @@
 
     private class FakePreviewImageProcessorImpl : PreviewImageProcessorImpl {
         private var imageWriter: ImageWriter? = null
-        override fun process(image: Image?, result: TotalCaptureResult?) {
+        override fun process(image: Image, result: TotalCaptureResult) {
             val emptyImage = imageWriter!!.dequeueInputImage()
             imageWriter!!.queueInputImage(emptyImage)
         }
 
         override fun process(
-            image: Image?,
-            result: TotalCaptureResult?,
-            resultCallback: ProcessResultImpl?,
+            image: Image,
+            result: TotalCaptureResult,
+            resultCallback: ProcessResultImpl,
             executor: Executor?
         ) {
             val blankImage = imageWriter!!.dequeueInputImage()
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt
index 6e33c32..edadb08 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt
@@ -416,7 +416,7 @@
             throwExceptionDuringProcess = true
         }
         override fun process(
-            results: MutableMap<Int, android.util.Pair<Image, TotalCaptureResult>>?
+            results: MutableMap<Int, android.util.Pair<Image, TotalCaptureResult>>
         ) {
             if (throwExceptionDuringProcess) {
                 throw RuntimeException("Process failed")
@@ -426,8 +426,8 @@
         }
 
         override fun process(
-            results: MutableMap<Int, android.util.Pair<Image, TotalCaptureResult>>?,
-            resultCallback: ProcessResultImpl?,
+            results: MutableMap<Int, android.util.Pair<Image, TotalCaptureResult>>,
+            resultCallback: ProcessResultImpl,
             executor: Executor?
         ) {
             process(results)
@@ -443,6 +443,20 @@
         override fun onImageFormatUpdate(imageFormat: Int) {
         }
 
+        override fun onPostviewOutputSurface(surface: Surface) {
+        }
+
+        override fun onResolutionUpdate(size: Size, postviewSize: Size) {
+        }
+
+        override fun processWithPostview(
+            results: MutableMap<Int, android.util.Pair<Image, TotalCaptureResult>>,
+            resultCallback: ProcessResultImpl,
+            executor: Executor?
+        ) {
+            process(results, resultCallback, executor)
+        }
+
         fun close() {
             imageWriter?.close()
         }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
index b9a149e..508f54d8 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
@@ -314,13 +314,25 @@
     @Override
     public Size[] getSupportedYuvAnalysisResolutions() {
         ImageAnalysisAvailability imageAnalysisAvailability = new ImageAnalysisAvailability();
-        if (imageAnalysisAvailability.isUnavailable(mCameraId, mMode)) {
+        boolean hasPreviewProcessor = mPreviewExtenderImpl.getProcessorType()
+                == PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR;
+        boolean hasImageCaptureProcessor = mImageCaptureExtenderImpl.getCaptureProcessor() != null;
+        if (!imageAnalysisAvailability.isAvailable(mCameraId, getHardwareLevel(), mMode,
+                hasPreviewProcessor, hasImageCaptureProcessor)) {
             return new Size[0];
         }
         Preconditions.checkNotNull(mCameraInfo, "VendorExtender#init() must be called first");
         return getOutputSizes(ImageFormat.YUV_420_888);
     }
 
+    private int getHardwareLevel() {
+        Integer hardwareLevel =
+                mCameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
+
+        return hardwareLevel != null ? hardwareLevel :
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
+    }
+
     @NonNull
     private List<CaptureRequest.Key> getSupportedParameterKeys(Context context) {
         if (ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_3)) {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirksLoader.java
index fc7a6a0..74a8f42 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirksLoader.java
@@ -56,6 +56,10 @@
             quirks.add(new ImageAnalysisUnavailableQuirk());
         }
 
+        if (ExtraSupportedSurfaceCombinationsQuirk.load()) {
+            quirks.add(new ExtraSupportedSurfaceCombinationsQuirk());
+        }
+
         return quirks;
     }
 }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtraSupportedSurfaceCombinationsQuirk.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtraSupportedSurfaceCombinationsQuirk.java
new file mode 100644
index 0000000..023d5b8
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtraSupportedSurfaceCombinationsQuirk.java
@@ -0,0 +1,218 @@
+/*
+ * 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.camera.extensions.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+import androidx.camera.extensions.internal.compat.workaround.ImageAnalysisAvailability;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * <p>QuirkSummary
+ *     Bug Id: b/194149215
+ *     Description: Quirk required to include extra supported surface combinations which are
+ *                  additional to the guaranteed supported configurations. An example is the
+ *                  Samsung A51's LIMITED-level camera device can support additional YUV/640x480
+ *                  + PRIV/PREVIEW + YUV/MAXIMUM and YUV/640x480 + YUV/PREVIEW + YUV/MAXIMUM
+ *                  configurations.
+ *     Device(s): Some Samsung devices
+ *     @see ImageAnalysisAvailability
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ExtraSupportedSurfaceCombinationsQuirk implements Quirk {
+
+    /**
+     * All devices in the list can support YUV/640x480 + PRIV/PREVIEW + YUV/MAXIMUM and
+     * YUV/640x480 + YUV/PREVIEW + YUV/MAXIMUM configurations.
+     */
+    private static final Set<String> SUPPORT_EXTRA_FULL_CONFIGURATIONS_SAMSUNG_MODELS =
+            new HashSet<>(Arrays.asList(
+                    "SM-A515F", // Galaxy A51
+                    "SM-A515U", // Galaxy A51
+                    "SM-A515U1", // Galaxy A51
+                    "SM-A515W", // Galaxy A51
+                    "SM-S515DL", // Galaxy A51
+                    "SC-54A", // Galaxy A51 5G
+                    "SCG07", // Galaxy A51 5G
+                    "SM-A5160", // Galaxy A51 5G
+                    "SM-A516B", // Galaxy A51 5G
+                    "SM-A516N", // Galaxy A51 5G
+                    "SM-A516U", // Galaxy A51 5G
+                    "SM-A516U1", // Galaxy A51 5G
+                    "SM-A516V", // Galaxy A51 5G
+                    "SM-A715F", // Galaxy A71
+                    "SM-A715W", // Galaxy A71
+                    "SM-A7160", // Galaxy A71 5G
+                    "SM-A716B", // Galaxy A71 5G
+                    "SM-A716U", // Galaxy A71 5G
+                    "SM-A716U1", // Galaxy A71 5G
+                    "SM-A716V", // Galaxy A71 5G
+                    "SM-A8050", // Galaxy A80
+                    "SM-A805F", // Galaxy A80
+                    "SM-A805N", // Galaxy A80
+                    "SCV44", // Galaxy Fold
+                    "SM-F9000", // Galaxy Fold
+                    "SM-F900F", // Galaxy Fold
+                    "SM-F900U", // Galaxy Fold
+                    "SM-F900U1", // Galaxy Fold
+                    "SM-F900W", // Galaxy Fold
+                    "SM-F907B", // Galaxy Fold 5G
+                    "SM-F907N", // Galaxy Fold 5G
+                    "SM-N970F", // Galaxy Note10
+                    "SM-N9700", // Galaxy Note10
+                    "SM-N970U", // Galaxy Note10
+                    "SM-N970U1", // Galaxy Note10
+                    "SM-N970W", // Galaxy Note10
+                    "SM-N971N", // Galaxy Note10 5G
+                    "SM-N770F", // Galaxy Note10 Lite
+                    "SC-01M", // Galaxy Note10+
+                    "SCV45", // Galaxy Note10+
+                    "SM-N9750", // Galaxy Note10+
+                    "SM-N975C", // Galaxy Note10+
+                    "SM-N975U", // Galaxy Note10+
+                    "SM-N975U1", // Galaxy Note10+
+                    "SM-N975W", // Galaxy Note10+
+                    "SM-N975F", // Galaxy Note10+
+                    "SM-N976B", // Galaxy Note10+ 5G
+                    "SM-N976N", // Galaxy Note10+ 5G
+                    "SM-N9760", // Galaxy Note10+ 5G
+                    "SM-N976Q", // Galaxy Note10+ 5G
+                    "SM-N976V", // Galaxy Note10+ 5G
+                    "SM-N976U", // Galaxy Note10+ 5G
+                    "SM-N9810", // Galaxy Note20 5G
+                    "SM-N981N", // Galaxy Note20 5G
+                    "SM-N981U", // Galaxy Note20 5G
+                    "SM-N981U1", // Galaxy Note20 5G
+                    "SM-N981W", // Galaxy Note20 5G
+                    "SM-N981B", // Galaxy Note20 5G
+                    "SC-53A", // Galaxy Note20 Ultra 5G
+                    "SCG06", // Galaxy Note20 Ultra 5G
+                    "SM-N9860", // Galaxy Note20 Ultra 5G
+                    "SM-N986N", // Galaxy Note20 Ultra 5G
+                    "SM-N986U", // Galaxy Note20 Ultra 5G
+                    "SM-N986U1", // Galaxy Note20 Ultra 5G
+                    "SM-N986W", // Galaxy Note20 Ultra 5G
+                    "SM-N986B", // Galaxy Note20 Ultra 5G
+                    "SC-03L", // Galaxy S10
+                    "SCV41", // Galaxy S10
+                    "SM-G973F", // Galaxy S10
+                    "SM-G973N", // Galaxy S10
+                    "SM-G9730", // Galaxy S10
+                    "SM-G9738", // Galaxy S10
+                    "SM-G973C", // Galaxy S10
+                    "SM-G973U", // Galaxy S10
+                    "SM-G973U1", // Galaxy S10
+                    "SM-G973W", // Galaxy S10
+                    "SM-G977B", // Galaxy S10 5G
+                    "SM-G977N", // Galaxy S10 5G
+                    "SM-G977P", // Galaxy S10 5G
+                    "SM-G977T", // Galaxy S10 5G
+                    "SM-G977U", // Galaxy S10 5G
+                    "SM-G770F", // Galaxy S10 Lite
+                    "SM-G770U1", // Galaxy S10 Lite
+                    "SC-04L", // Galaxy S10+
+                    "SCV42", // Galaxy S10+
+                    "SM-G975F", // Galaxy S10+
+                    "SM-G975N", // Galaxy S10+
+                    "SM-G9750", // Galaxy S10+
+                    "SM-G9758", // Galaxy S10+
+                    "SM-G975U", // Galaxy S10+
+                    "SM-G975U1", // Galaxy S10+
+                    "SM-G975W", // Galaxy S10+
+                    "SC-05L", // Galaxy S10+ Olympic Games Edition
+                    "SM-G970F", // Galaxy S10e
+                    "SM-G970N", // Galaxy S10e
+                    "SM-G9700", // Galaxy S10e
+                    "SM-G9708", // Galaxy S10e
+                    "SM-G970U", // Galaxy S10e
+                    "SM-G970U1", // Galaxy S10e
+                    "SM-G970W", // Galaxy S10e
+                    "SC-51A", // Galaxy S20 5G
+                    "SC51Aa", // Galaxy S20 5G
+                    "SCG01", // Galaxy S20 5G
+                    "SM-G9810", // Galaxy S20 5G
+                    "SM-G981N", // Galaxy S20 5G
+                    "SM-G981U", // Galaxy S20 5G
+                    "SM-G981U1", // Galaxy S20 5G
+                    "SM-G981V", // Galaxy S20 5G
+                    "SM-G981W", // Galaxy S20 5G
+                    "SM-G981B", // Galaxy S20 5G
+                    "SCG03", // Galaxy S20 Ultra 5G
+                    "SM-G9880", // Galaxy S20 Ultra 5G
+                    "SM-G988N", // Galaxy S20 Ultra 5G
+                    "SM-G988Q", // Galaxy S20 Ultra 5G
+                    "SM-G988U", // Galaxy S20 Ultra 5G
+                    "SM-G988U1", // Galaxy S20 Ultra 5G
+                    "SM-G988W", // Galaxy S20 Ultra 5G
+                    "SM-G988B", // Galaxy S20 Ultra 5G
+                    "SC-52A", // Galaxy S20+ 5G
+                    "SCG02", // Galaxy S20+ 5G
+                    "SM-G9860", // Galaxy S20+ 5G
+                    "SM-G986N", // Galaxy S20+ 5G
+                    "SM-G986U", // Galaxy S20+ 5G
+                    "SM-G986U1", // Galaxy S20+ 5G
+                    "SM-G986W", // Galaxy S20+ 5G
+                    "SM-G986B", // Galaxy S20+ 5G
+                    "SCV47", // Galaxy Z Flip
+                    "SM-F7000", // Galaxy Z Flip
+                    "SM-F700F", // Galaxy Z Flip
+                    "SM-F700N", // Galaxy Z Flip
+                    "SM-F700U", // Galaxy Z Flip
+                    "SM-F700U1", // Galaxy Z Flip
+                    "SM-F700W", // Galaxy Z Flip
+                    "SCG04", // Galaxy Z Flip 5G
+                    "SM-F7070", // Galaxy Z Flip 5G
+                    "SM-F707B", // Galaxy Z Flip 5G
+                    "SM-F707N", // Galaxy Z Flip 5G
+                    "SM-F707U", // Galaxy Z Flip 5G
+                    "SM-F707U1", // Galaxy Z Flip 5G
+                    "SM-F707W", // Galaxy Z Flip 5G
+                    "SM-F9160", // Galaxy Z Fold2 5G
+                    "SM-F916B", // Galaxy Z Fold2 5G
+                    "SM-F916N", // Galaxy Z Fold2 5G
+                    "SM-F916Q", // Galaxy Z Fold2 5G
+                    "SM-F916U", // Galaxy Z Fold2 5G
+                    "SM-F916U1", // Galaxy Z Fold2 5G
+                    "SM-F916W"// Galaxy Z Fold2 5G
+            ));
+
+    /**
+     * Currently, once the quirk is loaded, it means that the device can support YUV/640x480 +
+     * PRIV/PREVIEW + YUV/MAXIMUM and YUV/640x480 + YUV/PREVIEW + YUV/MAXIMUM configurations even
+     * if it is LIMITED level device.
+     */
+    static boolean load() {
+        return supportExtraFullConfigurationsSamsungDevice();
+    }
+
+    private static boolean supportExtraFullConfigurationsSamsungDevice() {
+        if (!"samsung".equalsIgnoreCase(Build.BRAND)) {
+            return false;
+        }
+
+        String capitalModelName = Build.MODEL.toUpperCase(Locale.US);
+
+        return SUPPORT_EXTRA_FULL_CONFIGURATIONS_SAMSUNG_MODELS.contains(capitalModelName);
+    }
+}
+
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ImageAnalysisUnavailableQuirk.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ImageAnalysisUnavailableQuirk.java
index a26fd3c..fddb34d 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ImageAnalysisUnavailableQuirk.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ImageAnalysisUnavailableQuirk.java
@@ -26,89 +26,67 @@
 import androidx.camera.core.Preview;
 import androidx.camera.core.impl.Quirk;
 import androidx.camera.extensions.ExtensionMode;
+import androidx.camera.extensions.internal.compat.workaround.ImageAnalysisAvailability;
 
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.Set;
-
 /**
  * <p>QuirkSummary
- * Bug Id: b/290007642,
- * Description: When enabling Extensions on devices that implement the Basic Extender,
- * ImageAnalysis is assumed to be supported always. But this might be false on some devices like
- * Samsung Galaxy S23 Ultra 5G. This might cause preview black screen or unable to capture image
- * issues.
- * Device(s): Samsung Galaxy S23 Ultra 5G, Z Fold3 5G or A52s 5G devices
+ *     Bug Id: b/290007642,
+ *     Description: When enabling Extensions on devices that implement the Basic Extender,
+ *                  ImageAnalysis is assumed to be supported always. But this might be false on
+ *                  some devices like Samsung Galaxy S23 Ultra 5G, even if the device hardware
+ *                  level is FULL or above that should be able to support the additional
+ *                  ImageAnalysis no matter the Preview and ImageCapture have capture processor
+ *                  or not. This might cause preview black screen or unable to capture image issues.
+ *     Device(s): Samsung Galaxy S23 Ultra 5G, Z Fold3 5G, A52s 5G or S22 Ultra devices
+ *     @see ImageAnalysisAvailability
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class ImageAnalysisUnavailableQuirk implements Quirk {
-
     private static final Set<Pair<String, String>> KNOWN_DEVICES = new HashSet<>(
             Arrays.asList(
                     Pair.create("samsung", "dm3q"), // Samsung Galaxy S23 Ultra 5G
                     Pair.create("samsung", "q2q"), // Samsung Galaxy Z Fold3 5G
                     Pair.create("samsung", "a52sxq"), // Samsung Galaxy A52s 5G
-                    Pair.create("samsung", "b0q"), // Samsung Galaxy S22 Ultra
-                    Pair.create("samsung", "gts8uwifi") // Samsung Galaxy Tab S8 Ultra
+                    Pair.create("samsung", "b0q") // Samsung Galaxy S22 Ultra
             ));
-
     private final Set<Pair<String, Integer>> mUnavailableCombinations = new HashSet<>();
-
     ImageAnalysisUnavailableQuirk() {
-        if (Build.BRAND.equalsIgnoreCase("SAMSUNG") && Build.DEVICE.equalsIgnoreCase("dm3q")) {
+        if (Build.BRAND.equalsIgnoreCase("SAMSUNG") && Build.DEVICE.equalsIgnoreCase(
+                "dm3q")) { // Samsung Galaxy S23 Ultra 5G
             mUnavailableCombinations.addAll(Arrays.asList(
-                    Pair.create("1", ExtensionMode.BOKEH),
+                    Pair.create("1", ExtensionMode.BOKEH), // LEVEL_FULL
                     Pair.create("1", ExtensionMode.FACE_RETOUCH),
-                    Pair.create("2", ExtensionMode.BOKEH),
-                    Pair.create("2", ExtensionMode.FACE_RETOUCH),
-                    Pair.create("3", ExtensionMode.BOKEH),
+                    Pair.create("3", ExtensionMode.BOKEH), // LEVEL_FULL
                     Pair.create("3", ExtensionMode.FACE_RETOUCH)
             ));
         } else if (Build.BRAND.equalsIgnoreCase("SAMSUNG") && Build.DEVICE.equalsIgnoreCase(
-                "q2q")) {
+                "q2q")) { // Samsung Galaxy Z Fold3 5G
             mUnavailableCombinations.addAll(Arrays.asList(
-                    Pair.create("0", ExtensionMode.BOKEH),
-                    Pair.create("0", ExtensionMode.FACE_RETOUCH),
-                    Pair.create("1", ExtensionMode.BOKEH),
-                    Pair.create("1", ExtensionMode.FACE_RETOUCH),
-                    Pair.create("2", ExtensionMode.BOKEH),
-                    Pair.create("2", ExtensionMode.FACE_RETOUCH),
-                    Pair.create("3", ExtensionMode.BOKEH),
-                    Pair.create("3", ExtensionMode.FACE_RETOUCH)
+                    Pair.create("0", ExtensionMode.BOKEH), // LEVEL_3
+                    Pair.create("0", ExtensionMode.FACE_RETOUCH)
             ));
         } else if (Build.BRAND.equalsIgnoreCase("SAMSUNG") && Build.DEVICE.equalsIgnoreCase(
-                "a52sxq")) {
+                "a52sxq")) { // Samsung Galaxy A52s 5G
             mUnavailableCombinations.addAll(Arrays.asList(
-                    Pair.create("0", ExtensionMode.BOKEH),
-                    Pair.create("0", ExtensionMode.FACE_RETOUCH),
-                    Pair.create("1", ExtensionMode.BOKEH),
-                    Pair.create("1", ExtensionMode.FACE_RETOUCH),
-                    Pair.create("2", ExtensionMode.BOKEH),
-                    Pair.create("2", ExtensionMode.FACE_RETOUCH),
-                    Pair.create("3", ExtensionMode.BOKEH),
-                    Pair.create("3", ExtensionMode.FACE_RETOUCH)
+                    Pair.create("0", ExtensionMode.BOKEH), // LEVEL_3
+                    Pair.create("0", ExtensionMode.FACE_RETOUCH)
             ));
         } else if (Build.BRAND.equalsIgnoreCase("SAMSUNG") && Build.DEVICE.equalsIgnoreCase(
-                "b0q")) {
+                "b0q")) { // Samsung Galaxy A52s 5G
             mUnavailableCombinations.addAll(Arrays.asList(
-                    Pair.create("3", ExtensionMode.BOKEH),
+                    Pair.create("3", ExtensionMode.BOKEH), // FULL
                     Pair.create("3", ExtensionMode.FACE_RETOUCH)
             ));
-        } else if (Build.BRAND.equalsIgnoreCase("SAMSUNG") && Build.DEVICE.equalsIgnoreCase(
-                "gts8uwifi")) {
-            mUnavailableCombinations.addAll(Arrays.asList(
-                    Pair.create("2", ExtensionMode.BOKEH),
-                    Pair.create("2", ExtensionMode.FACE_RETOUCH)
-            ));
         }
     }
-
     static boolean load() {
         return KNOWN_DEVICES.contains(Pair.create(Build.BRAND.toLowerCase(Locale.US),
                 Build.DEVICE.toLowerCase(Locale.US)));
     }
-
     /**
      * Returns whether {@link ImageAnalysis} is unavailable to be bound together with
      * {@link Preview} and {@link ImageCapture} for the specified camera id and extensions mode.
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ImageAnalysisAvailability.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ImageAnalysisAvailability.java
index 83d68c1..9500758 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ImageAnalysisAvailability.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ImageAnalysisAvailability.java
@@ -16,6 +16,10 @@
 
 package androidx.camera.extensions.internal.compat.workaround;
 
+import static android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_3;
+import static android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_FULL;
+import static android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.ImageAnalysis;
@@ -23,31 +27,71 @@
 import androidx.camera.core.Preview;
 import androidx.camera.extensions.ExtensionMode;
 import androidx.camera.extensions.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.extensions.internal.compat.quirk.ExtraSupportedSurfaceCombinationsQuirk;
 import androidx.camera.extensions.internal.compat.quirk.ImageAnalysisUnavailableQuirk;
 
 /**
  * Workaround to check whether {@link ImageAnalysis} can be bound together with {@link Preview} and
- * {@link ImageCapture} when
- * enabling extensions.
+ * {@link ImageCapture} when enabling extensions.
  *
- * @see ImageAnalysisUnavailableQuirk
+ * <p>This is used by the BasicVendorExtender to check whether the device can support to bind the
+ * additional ImageAnalysis UseCase.
+ *
+ * @see ImageAnalysisUnavailableQuirk, ExtraSupportedSurfaceCombinationsQuirk
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class ImageAnalysisAvailability {
     ImageAnalysisUnavailableQuirk mImageAnalysisUnavailableQuirk =
             DeviceQuirks.get(ImageAnalysisUnavailableQuirk.class);
 
+    ExtraSupportedSurfaceCombinationsQuirk mExtraSupportedSurfaceCombinationsQuirk =
+            DeviceQuirks.get(ExtraSupportedSurfaceCombinationsQuirk.class);
+
     /**
-     * Returns whether {@link ImageAnalysis} is unavailable to be bound together with
+     * Returns whether {@link ImageAnalysis} is available to be bound together with
      * {@link Preview} and {@link ImageCapture} for the specified camera id and extensions mode.
      *
      * @param cameraId the camera id to query
+     * @param hardwareLevel the camera device hardware level
      * @param mode the extensions mode to query
-     * @return {@code true} if {@link ImageAnalysis} is unavailable. Otherwise, returns {@code
+     * @param hasPreviewProcessor whether PreviewExtenderImpl has processor
+     * @param hasImageCaptureProcessor whether ImageCaptureExtenderImpl has processor
+     * @return {@code true} if {@link ImageAnalysis} is available. Otherwise, returns {@code
      * false}.
      */
-    public boolean isUnavailable(@NonNull String cameraId, @ExtensionMode.Mode int mode) {
-        return mImageAnalysisUnavailableQuirk != null
-                && mImageAnalysisUnavailableQuirk.isUnavailable(cameraId, mode);
+    public boolean isAvailable(@NonNull String cameraId, int hardwareLevel,
+            @ExtensionMode.Mode int mode, boolean hasPreviewProcessor,
+            boolean hasImageCaptureProcessor) {
+        // When ImageAnalysisUnavailableQuirk is loaded and its isUnavailable() function returns
+        // true, directly return false that means the device can't support to bind ImageAnalysis
+        // when enabling the extensions mode.
+        if (mImageAnalysisUnavailableQuirk != null
+                && mImageAnalysisUnavailableQuirk.isUnavailable(cameraId, mode)) {
+            return false;
+        }
+
+        // No matter what the device hardware is and no matter the Preview and the ImageCapture
+        // have processor or not, once ExtraSupportedSurfaceCombinationsQuirk can be loaded, the
+        // device can support to bind ImageAnalysis when enabling the extensions mode.
+        if (mExtraSupportedSurfaceCombinationsQuirk != null) {
+            return true;
+        }
+
+        if (!hasPreviewProcessor && !hasImageCaptureProcessor) {
+            // Required configuration: PRIV + JPEG + YUV
+            // Required HW level: any
+            return true;
+        } else if (hasPreviewProcessor && !hasImageCaptureProcessor) {
+            // Required configuration: YUV + JPEG + YUV
+            // Required HW level: LIMITED level or above
+            return hardwareLevel == INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+                    || hardwareLevel == INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+                    || hardwareLevel == INFO_SUPPORTED_HARDWARE_LEVEL_3;
+        } else {
+            // Required configuration: PRIV + YUV + YUV or YUV + YUV + YUV
+            // Required HW level: FULL level or above
+            return hardwareLevel == INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+                    || hardwareLevel == INFO_SUPPORTED_HARDWARE_LEVEL_3;
+        }
     }
 }
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ImageAnalysisAvailabilityTest.kt b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ImageAnalysisAvailabilityTest.kt
index bba6c8f..92166ed 100644
--- a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ImageAnalysisAvailabilityTest.kt
+++ b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ImageAnalysisAvailabilityTest.kt
@@ -16,8 +16,12 @@
 
 package androidx.camera.extensions.internal.compat.workaround
 
+import android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_3
+import android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+import android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
 import android.os.Build
-import androidx.camera.extensions.ExtensionMode
+import androidx.camera.extensions.ExtensionMode.BOKEH
+import androidx.camera.extensions.ExtensionMode.FACE_RETOUCH
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -38,56 +42,69 @@
         // Set up device properties
         ReflectionHelpers.setStaticField(Build::class.java, "BRAND", config.brand)
         ReflectionHelpers.setStaticField(Build::class.java, "DEVICE", config.device)
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", config.model)
         val imageAnalysisAvailability = ImageAnalysisAvailability()
-        assertThat(imageAnalysisAvailability.isUnavailable(config.cameraId, config.mode)).isEqualTo(
-            config.isUnavailable
+        assertThat(imageAnalysisAvailability.isAvailable(
+            config.cameraId,
+            config.hardwareLevel,
+            config.mode,
+            config.hasPreviewProcessor,
+            config.hasImageCaptureProcessor)).isEqualTo(
+            config.isAvailable
         )
     }
 
     class TestConfig(
         val brand: String,
         val device: String,
+        val model: String,
         val cameraId: String,
+        val hardwareLevel: Int,
         val mode: Int,
-        val isUnavailable: Boolean
+        val hasPreviewProcessor: Boolean,
+        val hasImageCaptureProcessor: Boolean,
+        val isAvailable: Boolean
     )
 
     companion object {
         @JvmStatic
         @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
         fun createTestSet(): List<TestConfig> {
+            val levelLimited = INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+            val levelFull = INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+            val level3 = INFO_SUPPORTED_HARDWARE_LEVEL_3
             return listOf(
                 // Samsung Galaxy S23 Ultra 5G tests
-                TestConfig("Samsung", "dm3q", "0", ExtensionMode.BOKEH, false),
-                TestConfig("Samsung", "dm3q", "1", ExtensionMode.BOKEH, true),
-                TestConfig("Samsung", "dm3q", "1", ExtensionMode.FACE_RETOUCH, true),
-                TestConfig("Samsung", "dm3q", "1", ExtensionMode.HDR, false),
+                TestConfig("Samsung", "dm3q", "", "0", level3, BOKEH, true, true, true),
+                TestConfig("Samsung", "dm3q", "", "1", levelFull, BOKEH, true, true, false),
+                TestConfig("Samsung", "dm3q", "", "1", levelFull, FACE_RETOUCH, true, true, false),
+                TestConfig("Samsung", "dm3q", "", "2", levelLimited, BOKEH, true, true, false),
 
                 // Samsung Galaxy Z Fold3 5G
-                TestConfig("Samsung", "q2q", "0", ExtensionMode.BOKEH, true),
-                TestConfig("Samsung", "q2q", "0", ExtensionMode.FACE_RETOUCH, true),
-                TestConfig("Samsung", "q2q", "0", ExtensionMode.HDR, false),
+                TestConfig("Samsung", "q2q", "", "0", level3, BOKEH, true, true, false),
+                TestConfig("Samsung", "q2q", "", "0", level3, FACE_RETOUCH, true, true, false),
+                TestConfig("Samsung", "q2q", "", "1", levelLimited, BOKEH, true, true, false),
 
                 // Samsung Galaxy A52s 5G
-                TestConfig("Samsung", "a52sxq", "0", ExtensionMode.BOKEH, true),
-                TestConfig("Samsung", "a52sxq", "0", ExtensionMode.FACE_RETOUCH, true),
-                TestConfig("Samsung", "a52sxq", "0", ExtensionMode.HDR, false),
+                TestConfig("Samsung", "a52sxq", "", "0", level3, BOKEH, true, true, false),
+                TestConfig("Samsung", "a52sxq", "", "0", level3, BOKEH, true, true, false),
+                TestConfig("Samsung", "a52sxq", "", "1", levelLimited, BOKEH, true, true, false),
 
-                // Samsung Galaxy S22 Ultra
-                TestConfig("Samsung", "b0q", "0", ExtensionMode.BOKEH, false),
-                TestConfig("Samsung", "b0q", "3", ExtensionMode.BOKEH, true),
-                TestConfig("Samsung", "b0q", "3", ExtensionMode.FACE_RETOUCH, true),
-                TestConfig("Samsung", "b0q", "3", ExtensionMode.HDR, false),
+                // Samsung Galaxy S22 Ultra tests
+                TestConfig("Samsung", "b0q", "", "0", level3, BOKEH, true, true, true),
+                TestConfig("Samsung", "b0q", "", "3", levelFull, BOKEH, true, true, false),
+                TestConfig("Samsung", "b0q", "", "3", levelFull, FACE_RETOUCH, true, true, false),
 
-                // Samsung Galaxy Tab S8 Ultra
-                TestConfig("Samsung", "gts8uwifi", "0", ExtensionMode.BOKEH, false),
-                TestConfig("Samsung", "gts8uwifi", "2", ExtensionMode.BOKEH, true),
-                TestConfig("Samsung", "gts8uwifi", "2", ExtensionMode.FACE_RETOUCH, true),
-                TestConfig("Samsung", "gts8uwifi", "2", ExtensionMode.HDR, false),
+                // Samsung Galaxy A51 (support extra full level surface combinations)
+                TestConfig("Samsung", "", "SM-A515F", "1", levelLimited, BOKEH, true, true, true),
 
-                // Other cases should be kept available.
-                TestConfig("", "", "0", ExtensionMode.BOKEH, false),
-                TestConfig("", "", "1", ExtensionMode.NONE, false),
+                // Other cases should be determined by hardware level and processors.
+                TestConfig("", "", "", "0", level3, BOKEH, true, true, true),
+                TestConfig("", "", "", "0", levelFull, BOKEH, true, true, true),
+                TestConfig("", "", "", "0", levelFull, BOKEH, false, true, true),
+                TestConfig("", "", "", "0", levelLimited, BOKEH, true, true, false),
+                TestConfig("", "", "", "0", levelLimited, BOKEH, true, false, true),
+                TestConfig("", "", "", "0", levelLimited, BOKEH, false, true, false),
             )
         }
     }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index 7a4b53a..8b28e6c 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -299,6 +299,16 @@
         return mIntrinsicZoomRatio;
     }
 
+    @Override
+    public boolean isPreviewStabilizationSupported() {
+        return false;
+    }
+
+    @Override
+    public boolean isVideoStabilizationSupported() {
+        return false;
+    }
+
     /** Adds a quirk to the list of this camera's quirks. */
     @SuppressWarnings("unused")
     public void addCameraQuirk(@NonNull final Quirk quirk) {
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/E2ETestUtil.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/E2ETestUtil.kt
deleted file mode 100644
index e454f702..0000000
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/E2ETestUtil.kt
+++ /dev/null
@@ -1,158 +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.camera.testing.impl
-
-import android.content.ContentResolver
-import android.content.ContentValues
-import android.os.Environment.DIRECTORY_DOCUMENTS
-import android.os.Environment.DIRECTORY_MOVIES
-import android.os.Environment.getExternalStoragePublicDirectory
-import android.provider.MediaStore
-import androidx.annotation.RequiresApi
-import androidx.camera.core.Logger
-import androidx.camera.video.FileOutputOptions
-import androidx.camera.video.MediaStoreOutputOptions
-import androidx.camera.video.internal.compat.quirk.DeviceQuirks
-import androidx.camera.video.internal.compat.quirk.MediaStoreVideoCannotWrite
-import java.io.File
-import java.io.FileOutputStream
-import java.io.OutputStreamWriter
-
-private const val TAG = "E2ETestUtil"
-private const val EXTENSION_MP4 = "mp4"
-private const val EXTENSION_TEXT = "txt"
-
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-object E2ETestUtil {
-
-    /**
-     * Write the given text to the external storage.
-     *
-     * @param text the text to write to the external storage.
-     * @param fileName the file name to save the text.
-     * @param extension the file extension to save the text, [EXTENSION_TEXT] will be used by
-     * default.
-     *
-     * @return the [FileOutputOptions] instance.
-     */
-    @JvmStatic
-    fun writeTextToExternalFile(
-        text: String,
-        fileName: String,
-        extension: String = EXTENSION_TEXT
-    ) {
-        val fileNameWithExtension = "$fileName.$extension"
-        val folder = getExternalStoragePublicDirectory(DIRECTORY_DOCUMENTS)
-        if (!folder.exists() && !folder.mkdirs()) {
-            Logger.e(TAG, "Failed to create directory: $folder")
-        }
-
-        val file = File(folder, fileNameWithExtension)
-        FileOutputStream(file).use { fos ->
-            OutputStreamWriter(fos).use { writer ->
-                writer.write(text)
-                writer.flush()
-                fos.fd.sync()
-                writer.close()
-                fos.close()
-            }
-        }
-        Logger.d(TAG, "Export test information to: ${file.path}")
-    }
-
-    /**
-     * Check if the media store is available to save video recordings.
-     *
-     * @return true if the media store can be used, false otherwise.
-     * @see MediaStoreVideoCannotWrite
-     */
-    @JvmStatic
-    fun canDeviceWriteToMediaStore(): Boolean {
-        return DeviceQuirks.get(MediaStoreVideoCannotWrite::class.java) == null
-    }
-
-    /**
-     * Create a [FileOutputOptions] for video recording with some default values.
-     *
-     * @param fileName the file name of the video recording.
-     * @param extension the file extension of the video recording, [EXTENSION_MP4] will be used by
-     * default.
-     *
-     * @return the [FileOutputOptions] instance.
-     */
-    @JvmStatic
-    fun generateVideoFileOutputOptions(
-        fileName: String,
-        extension: String = EXTENSION_MP4
-    ): FileOutputOptions {
-        val fileNameWithExtension = "$fileName.$extension"
-        val folder = getExternalStoragePublicDirectory(DIRECTORY_MOVIES)
-        if (!folder.exists() && !folder.mkdirs()) {
-            Logger.e(TAG, "Failed to create directory: $folder")
-        }
-        return FileOutputOptions.Builder(File(folder, fileNameWithExtension)).build()
-    }
-
-    /**
-     * Create a [MediaStoreOutputOptions] for video recording with some default values.
-     *
-     * @param contentResolver the [ContentResolver] instance.
-     * @param fileName the file name of the video recording.
-     *
-     * @return the [MediaStoreOutputOptions] instance.
-     */
-    @JvmStatic
-    fun generateVideoMediaStoreOptions(
-        contentResolver: ContentResolver,
-        fileName: String
-    ): MediaStoreOutputOptions {
-        val contentValues = generateVideoContentValues(fileName)
-
-        return MediaStoreOutputOptions.Builder(
-            contentResolver,
-            MediaStore.Video.Media.EXTERNAL_CONTENT_URI
-        ).setContentValues(contentValues).build()
-    }
-
-    /**
-     * Check if the given file name string is valid.
-     *
-     * Currently a file name is invalid if:
-     * - it is `null`.
-     * - its length is zero.
-     * - it contains only whitespace character.
-     *
-     * @param fileName the file name string to be checked.
-     *
-     * @return `true` if the given file name is valid, otherwise `false`.
-     */
-    @JvmStatic
-    fun isFileNameValid(fileName: String?): Boolean {
-        return !fileName.isNullOrBlank()
-    }
-
-    private fun generateVideoContentValues(fileName: String): ContentValues {
-        val res = ContentValues()
-        res.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
-        res.put(MediaStore.Video.Media.TITLE, fileName)
-        res.put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
-        res.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000)
-        res.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis())
-
-        return res
-    }
-}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/FileUtil.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/FileUtil.kt
new file mode 100644
index 0000000..b482047
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/FileUtil.kt
@@ -0,0 +1,221 @@
+/*
+ * 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.camera.testing.impl
+
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.net.Uri
+import android.os.Environment.DIRECTORY_DOCUMENTS
+import android.os.Environment.DIRECTORY_MOVIES
+import android.os.Environment.getExternalStoragePublicDirectory
+import android.provider.MediaStore
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.camera.core.Logger
+import androidx.camera.video.FileOutputOptions
+import androidx.camera.video.MediaStoreOutputOptions
+import androidx.camera.video.internal.compat.quirk.DeviceQuirks
+import androidx.camera.video.internal.compat.quirk.MediaStoreVideoCannotWrite
+import java.io.File
+import java.io.FileOutputStream
+import java.io.OutputStreamWriter
+
+private const val TAG = "FileUtil"
+private const val EXTENSION_MP4 = "mp4"
+private const val EXTENSION_TEXT = "txt"
+
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+object FileUtil {
+
+    /**
+     * Write the given text to the external storage.
+     *
+     * @param text the text to write to the external storage.
+     * @param fileName the file name to save the text.
+     * @param extension the file extension to save the text, [EXTENSION_TEXT] will be used by
+     * default.
+     *
+     * @return the [FileOutputOptions] instance.
+     */
+    @JvmStatic
+    fun writeTextToExternalFile(
+        text: String,
+        fileName: String,
+        extension: String = EXTENSION_TEXT
+    ) {
+        val fileNameWithExtension = "$fileName.$extension"
+        val folder = getExternalStoragePublicDirectory(DIRECTORY_DOCUMENTS)
+        if (!folder.exists() && !folder.mkdirs()) {
+            Logger.e(TAG, "Failed to create directory: $folder")
+        }
+
+        val file = File(folder, fileNameWithExtension)
+        FileOutputStream(file).use { fos ->
+            OutputStreamWriter(fos).use { writer ->
+                writer.write(text)
+                writer.flush()
+                fos.fd.sync()
+                writer.close()
+                fos.close()
+            }
+        }
+        Logger.d(TAG, "Export test information to: ${file.path}")
+    }
+
+    /**
+     * Check if the media store is available to save video recordings.
+     *
+     * @return true if the media store can be used, false otherwise.
+     * @see MediaStoreVideoCannotWrite
+     */
+    @JvmStatic
+    fun canDeviceWriteToMediaStore(): Boolean {
+        return DeviceQuirks.get(MediaStoreVideoCannotWrite::class.java) == null
+    }
+
+    /**
+     * Create a [FileOutputOptions] for video recording with some default values.
+     *
+     * @param fileName the file name of the video recording.
+     * @param extension the file extension of the video recording, [EXTENSION_MP4] will be used by
+     * default.
+     *
+     * @return the [FileOutputOptions] instance.
+     */
+    @JvmStatic
+    fun generateVideoFileOutputOptions(
+        fileName: String,
+        extension: String = EXTENSION_MP4
+    ): FileOutputOptions {
+        val fileNameWithExtension = "$fileName.$extension"
+        val folder = getExternalStoragePublicDirectory(DIRECTORY_MOVIES)
+        if (!createFolder(folder)) {
+            Logger.e(TAG, "Failed to create directory: $folder")
+        }
+        return FileOutputOptions.Builder(File(folder, fileNameWithExtension)).build()
+    }
+
+    /**
+     * Create a [MediaStoreOutputOptions] for video recording with some default values.
+     *
+     * @param contentResolver the [ContentResolver] instance.
+     * @param fileName the file name of the video recording.
+     *
+     * @return the [MediaStoreOutputOptions] instance.
+     */
+    @JvmStatic
+    fun generateVideoMediaStoreOptions(
+        contentResolver: ContentResolver,
+        fileName: String
+    ): MediaStoreOutputOptions = MediaStoreOutputOptions.Builder(
+        contentResolver,
+        MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+    ).setContentValues(generateVideoContentValues(fileName)).build()
+
+    /**
+     * Check if the given file name string is valid.
+     *
+     * Currently a file name is invalid if:
+     * - it is `null`.
+     * - its length is zero.
+     * - it contains only whitespace character.
+     *
+     * @param fileName the file name string to be checked.
+     *
+     * @return `true` if the given file name is valid, otherwise `false`.
+     */
+    @JvmStatic
+    fun isFileNameValid(fileName: String?): Boolean {
+        return !fileName.isNullOrBlank()
+    }
+
+    /**
+     * Creates parent folder for the input file path.
+     *
+     * @param filePath the input file path to create its parent folder.
+     * @return `true` if the parent folder already exists or is created successfully.
+     * `false` if the existing parent folder path is not a folder or failed to create.
+     */
+    @JvmStatic
+    fun createParentFolder(filePath: String): Boolean {
+        return createParentFolder(File(filePath))
+    }
+
+    /**
+     * Creates parent folder for the input file.
+     *
+     * @param file the input file to create its parent folder
+     * @return `true` if the parent folder already exists or is created successfully.
+     * `false` if the existing parent folder path is not a folder or failed to create.
+     */
+    @JvmStatic
+    fun createParentFolder(file: File): Boolean = file.parentFile?.let {
+        createFolder(it)
+    } ?: false
+
+    /**
+     * Creates folder for the input file.
+     *
+     * @param file the input file to create folder
+     * @return `true` if the folder already exists or is created successfully.
+     * `false` if the existing folder path is not a folder or failed to create.
+     */
+    @JvmStatic
+    fun createFolder(file: File): Boolean = if (file.exists()) {
+        file.isDirectory
+    } else {
+        file.mkdirs()
+    }
+
+    /**
+     * Gets the absolute path from a Uri.
+     *
+     * @param resolver   the content resolver.
+     * @param contentUri the content uri.
+     * @return the file path of the content uri or null if not found.
+     */
+    @JvmStatic
+    fun getAbsolutePathFromUri(resolver: ContentResolver, contentUri: Uri): String? {
+        // MediaStore.Video.Media.DATA was deprecated in API level 29.
+        val column = MediaStore.Video.Media.DATA
+        try {
+            resolver.query(contentUri, arrayOf(column), null, null, null)!!.use { cursor ->
+                val columnIndex = cursor.getColumnIndexOrThrow(column)
+                cursor.moveToFirst()
+                return cursor.getString(columnIndex)
+            }
+        } catch (e: RuntimeException) {
+            Log.e(
+                TAG,
+                String.format(
+                    "Failed in getting absolute path for Uri %s with Exception %s",
+                    contentUri, e
+                ), e
+            )
+            return null
+        }
+    }
+
+    private fun generateVideoContentValues(fileName: String) = ContentValues().apply {
+        put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
+        put(MediaStore.Video.Media.TITLE, fileName)
+        put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
+        val currentTimeMs = System.currentTimeMillis()
+        put(MediaStore.Video.Media.DATE_ADDED, currentTimeMs / 1000)
+        put(MediaStore.Video.Media.DATE_TAKEN, currentTimeMs)
+    }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraDeviceSurfaceManager.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraDeviceSurfaceManager.java
index c0a5fe5..4813cdc 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraDeviceSurfaceManager.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraDeviceSurfaceManager.java
@@ -18,9 +18,7 @@
 
 import static android.graphics.ImageFormat.JPEG;
 import static android.graphics.ImageFormat.YUV_420_888;
-
 import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
-
 import static com.google.common.primitives.Ints.asList;
 
 import android.util.Pair;
@@ -90,7 +88,8 @@
             @CameraMode.Mode int cameraMode,
             @NonNull String cameraId,
             @NonNull List<AttachedSurfaceInfo> existingSurfaces,
-            @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap) {
+            @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap,
+            boolean isPreviewStabilizationOn) {
         List<UseCaseConfig<?>> newUseCaseConfigs =
                 new ArrayList<>(newUseCaseConfigsSupportedSizeMap.keySet());
         checkSurfaceCombo(existingSurfaces, newUseCaseConfigs);
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
index 453a9af..1ac4332 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
@@ -17,12 +17,9 @@
 package androidx.camera.testing.fakes;
 
 import static android.graphics.ImageFormat.YUV_420_888;
-
 import static androidx.camera.core.impl.SurfaceConfig.ConfigSize.PREVIEW;
 import static androidx.camera.core.impl.SurfaceConfig.ConfigType.YUV;
-
 import static com.google.common.truth.Truth.assertThat;
-
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
 
@@ -96,7 +93,8 @@
                 CameraMode.DEFAULT,
                 FAKE_CAMERA_ID0,
                 emptyList(),
-                createConfigOutputSizesMap(preview, analysis));
+                createConfigOutputSizesMap(preview, analysis),
+                false);
     }
 
     @Test(expected = IllegalArgumentException.class)
@@ -114,7 +112,8 @@
         mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
                 CameraMode.DEFAULT,
                 FAKE_CAMERA_ID0,
-                singletonList(analysis), createConfigOutputSizesMap(preview, video));
+                singletonList(analysis), createConfigOutputSizesMap(preview, video),
+                false);
     }
 
     @Test(expected = IllegalArgumentException.class)
@@ -125,7 +124,8 @@
         mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
                 CameraMode.DEFAULT,
                 FAKE_CAMERA_ID0,
-                Collections.emptyList(), createConfigOutputSizesMap(preview, video, analysis));
+                Collections.emptyList(), createConfigOutputSizesMap(preview, video, analysis),
+                false);
     }
 
     @Test
@@ -134,12 +134,14 @@
                 mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
                         CameraMode.DEFAULT,
                         FAKE_CAMERA_ID0,
-                        emptyList(), createConfigOutputSizesMap(mFakeUseCaseConfig)).first;
+                        emptyList(), createConfigOutputSizesMap(mFakeUseCaseConfig),
+                        false).first;
         Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecCamera1 =
                 mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
                         CameraMode.DEFAULT,
                         FAKE_CAMERA_ID1,
-                        emptyList(), createConfigOutputSizesMap(mFakeUseCaseConfig)).first;
+                        emptyList(), createConfigOutputSizesMap(mFakeUseCaseConfig),
+                        false).first;
 
         assertThat(suggestedStreamSpecsCamera0.get(mFakeUseCaseConfig)).isEqualTo(
                 StreamSpec.builder(new Size(FAKE_WIDTH0, FAKE_HEIGHT0)).build());
diff --git a/camera/camera-testlib-extensions/build.gradle b/camera/camera-testlib-extensions/build.gradle
index 849e115..7e445ca 100644
--- a/camera/camera-testlib-extensions/build.gradle
+++ b/camera/camera-testlib-extensions/build.gradle
@@ -24,6 +24,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
+    implementation(project(":camera:camera-core"))
 }
 
 android {
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
index 96e9f73..930f5a2 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
@@ -15,12 +15,12 @@
  */
 package androidx.camera.extensions.impl;
 
-import android.annotation.SuppressLint;
 import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.SessionConfiguration;
 import android.media.Image;
 import android.media.ImageWriter;
 import android.os.Build;
@@ -49,7 +49,6 @@
  * @since 1.0
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@SuppressLint("UnknownNullness")
 public final class AutoImageCaptureExtenderImpl implements ImageCaptureExtenderImpl {
     private static final String TAG = "AutoICExtender";
     private static final int DEFAULT_STAGE_ID = 0;
@@ -79,6 +78,7 @@
         return CameraCharacteristicAvailability.isEffectAvailable(cameraCharacteristics, EFFECT);
     }
 
+    @NonNull
     @Override
     public List<CaptureStageImpl> getCaptureStages() {
         // Placeholder set of CaptureRequest.Key values
@@ -89,6 +89,7 @@
         return captureStages;
     }
 
+    @Nullable
     @Override
     public CaptureProcessorImpl getCaptureProcessor() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -110,6 +111,7 @@
 
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onPresetSession() {
         // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
@@ -126,6 +128,7 @@
         return captureStage;
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onEnableSession() {
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
@@ -136,6 +139,7 @@
         return captureStage;
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onDisableSession() {
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
@@ -151,6 +155,7 @@
         return 3;
     }
 
+    @Nullable
     @Override
     public List<Pair<Integer, Size[]>> getSupportedResolutions() {
         return null;
@@ -163,6 +168,33 @@
         return new Range<>(300L, 1000L);
     }
 
+    @Override
+    public int onSessionType() {
+        return SessionConfiguration.SESSION_REGULAR;
+    }
+
+    @Nullable
+    @Override
+    public List<Pair<Integer, Size[]>> getSupportedPostviewResolutions(@NonNull Size captureSize) {
+        return null;
+    }
+
+    @Override
+    public boolean isCaptureProcessProgressAvailable() {
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public Pair<Long, Long> getRealtimeCaptureLatency() {
+        return null;
+    }
+
+    @Override
+    public boolean isPostviewAvailable() {
+        return false;
+    }
+
     @RequiresApi(23)
     static final class AutoImageCaptureExtenderCaptureProcessorImpl implements
             CaptureProcessorImpl {
@@ -174,7 +206,7 @@
         }
 
         @Override
-        public void process(Map<Integer, Pair<Image, TotalCaptureResult>> results) {
+        public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results) {
             Log.d(TAG, "Started auto CaptureProcessor");
 
             Pair<Image, TotalCaptureResult> result = results.get(DEFAULT_STAGE_ID);
@@ -213,9 +245,26 @@
         }
 
         @Override
-        public void process(Map<Integer, Pair<Image, TotalCaptureResult>> results,
-                ProcessResultImpl resultCallback, Executor executor) {
+        public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            process(results);
+        }
 
+        @Override
+        public void onPostviewOutputSurface(@NonNull Surface surface) {
+
+        }
+
+        @Override
+        public void onResolutionUpdate(@NonNull Size size, @NonNull Size postviewSize) {
+
+        }
+
+        @Override
+        public void processWithPostview(
+                @NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            throw new UnsupportedOperationException("Postview is not supported");
         }
     }
 
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
index 18c9c9d..e719b0e 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
@@ -18,6 +18,7 @@
 import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.SessionConfiguration;
 import android.os.Build;
 import android.util.Pair;
 import android.util.Size;
@@ -146,4 +147,9 @@
 
         return captureStage;
     }
+
+    @Override
+    public int onSessionType() {
+        return SessionConfiguration.SESSION_REGULAR;
+    }
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
index 4257f2c..0d74482 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
@@ -22,6 +22,7 @@
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.SessionConfiguration;
 import android.hardware.camera2.params.StreamConfigurationMap;
 import android.media.Image;
 import android.media.ImageWriter;
@@ -84,6 +85,7 @@
         return CameraCharacteristicAvailability.isEffectAvailable(cameraCharacteristics, EFFECT);
     }
 
+    @NonNull
     @Override
     public List<CaptureStageImpl> getCaptureStages() {
         // Placeholder set of CaptureRequest.Key values
@@ -94,6 +96,7 @@
         return captureStages;
     }
 
+    @Nullable
     @Override
     public CaptureProcessorImpl getCaptureProcessor() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -115,6 +118,7 @@
 
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onPresetSession() {
         // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
@@ -132,6 +136,12 @@
     }
 
     @Override
+    public int onSessionType() {
+        return SessionConfiguration.SESSION_REGULAR;
+    }
+
+    @Nullable
+    @Override
     public CaptureStageImpl onEnableSession() {
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
         // placeholder set of CaptureRequest.Key values
@@ -141,6 +151,7 @@
         return captureStage;
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onDisableSession() {
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
@@ -156,6 +167,7 @@
         return 3;
     }
 
+    @Nullable
     @Override
     public List<Pair<Integer, Size[]>> getSupportedResolutions() {
         List<Pair<Integer, Size[]>> formatResolutionsPairList = new ArrayList<>();
@@ -188,6 +200,28 @@
         return new Range<>(300L, 1000L);
     }
 
+    @Nullable
+    @Override
+    public List<Pair<Integer, Size[]>> getSupportedPostviewResolutions(@NonNull Size captureSize) {
+        return null;
+    }
+
+    @Override
+    public boolean isCaptureProcessProgressAvailable() {
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public Pair<Long, Long> getRealtimeCaptureLatency() {
+        return null;
+    }
+
+    @Override
+    public boolean isPostviewAvailable() {
+        return false;
+    }
+
     @RequiresApi(23)
     static final class BeautyImageCaptureExtenderCaptureProcessorImpl implements
             CaptureProcessorImpl {
@@ -199,7 +233,7 @@
         }
 
         @Override
-        public void process(Map<Integer, Pair<Image, TotalCaptureResult>> results) {
+        public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results) {
             Log.d(TAG, "Started beauty CaptureProcessor");
 
             Pair<Image, TotalCaptureResult> result = results.get(DEFAULT_STAGE_ID);
@@ -228,8 +262,9 @@
         }
 
         @Override
-        public void process(Map<Integer, Pair<Image, TotalCaptureResult>> results,
-                ProcessResultImpl resultCallback, Executor executor) {
+        public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            process(results);
         }
 
         @Override
@@ -241,6 +276,23 @@
         public void onImageFormatUpdate(int imageFormat) {
 
         }
+
+        @Override
+        public void onPostviewOutputSurface(@NonNull Surface surface) {
+
+        }
+
+        @Override
+        public void onResolutionUpdate(@NonNull Size size, @NonNull Size postviewSize) {
+
+        }
+
+        @Override
+        public void processWithPostview(
+                @NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            throw new UnsupportedOperationException("Postview is not supported");
+        }
     }
 
     @NonNull
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
index 1665c36..d45d71e 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
@@ -19,6 +19,7 @@
 import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.SessionConfiguration;
 import android.hardware.camera2.params.StreamConfigurationMap;
 import android.os.Build;
 import android.util.Pair;
@@ -168,4 +169,9 @@
 
         return captureStage;
     }
+
+    @Override
+    public int onSessionType() {
+        return SessionConfiguration.SESSION_REGULAR;
+    }
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
index fdb7e01..962d4bf 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
@@ -21,6 +21,7 @@
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.SessionConfiguration;
 import android.media.Image;
 import android.media.ImageWriter;
 import android.os.Build;
@@ -79,6 +80,7 @@
         return CameraCharacteristicAvailability.isEffectAvailable(cameraCharacteristics, EFFECT);
     }
 
+    @NonNull
     @Override
     public List<CaptureStageImpl> getCaptureStages() {
         // Placeholder set of CaptureRequest.Key values
@@ -89,6 +91,7 @@
         return captureStages;
     }
 
+    @Nullable
     @Override
     public CaptureProcessorImpl getCaptureProcessor() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -110,6 +113,7 @@
 
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onPresetSession() {
         // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
@@ -126,6 +130,7 @@
         return captureStage;
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onEnableSession() {
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
@@ -136,6 +141,7 @@
         return captureStage;
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onDisableSession() {
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
@@ -151,6 +157,7 @@
         return 3;
     }
 
+    @Nullable
     @Override
     public List<Pair<Integer, Size[]>> getSupportedResolutions() {
         return null;
@@ -162,6 +169,33 @@
         return new Range<>(300L, 1000L);
     }
 
+    @Override
+    public int onSessionType() {
+        return SessionConfiguration.SESSION_REGULAR;
+    }
+
+    @Nullable
+    @Override
+    public List<Pair<Integer, Size[]>> getSupportedPostviewResolutions(@NonNull Size captureSize) {
+        return null;
+    }
+
+    @Override
+    public boolean isCaptureProcessProgressAvailable() {
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public Pair<Long, Long> getRealtimeCaptureLatency() {
+        return null;
+    }
+
+    @Override
+    public boolean isPostviewAvailable() {
+        return false;
+    }
+
     @RequiresApi(23)
     static final class BokehImageCaptureExtenderCaptureProcessorImpl implements
             CaptureProcessorImpl {
@@ -173,7 +207,7 @@
         }
 
         @Override
-        public void process(Map<Integer, Pair<Image, TotalCaptureResult>> results) {
+        public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results) {
             Log.d(TAG, "Started bokeh CaptureProcessor");
 
             Pair<Image, TotalCaptureResult> result = results.get(DEFAULT_STAGE_ID);
@@ -202,9 +236,9 @@
         }
 
         @Override
-        public void process(Map<Integer, Pair<Image, TotalCaptureResult>> results,
-                ProcessResultImpl resultCallback, Executor executor) {
-
+        public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            process(results);
         }
 
         @Override
@@ -216,6 +250,23 @@
         public void onImageFormatUpdate(int imageFormat) {
 
         }
+
+        @Override
+        public void onPostviewOutputSurface(@NonNull Surface surface) {
+
+        }
+
+        @Override
+        public void onResolutionUpdate(@NonNull Size size, @NonNull Size postviewSize) {
+
+        }
+
+        @Override
+        public void processWithPostview(
+                @NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            throw new UnsupportedOperationException("Postview is not supported");
+        }
     }
 
     @NonNull
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
index 99fb8d9..d93e393 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
@@ -19,6 +19,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.SessionConfiguration;
 import android.os.Build;
 import android.util.Pair;
 import android.util.Size;
@@ -186,4 +187,9 @@
 
         return captureStage;
     }
+
+    @Override
+    public int onSessionType() {
+        return SessionConfiguration.SESSION_REGULAR;
+    }
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
index 8518283..2e27560 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
@@ -21,9 +21,11 @@
 import android.hardware.camera2.TotalCaptureResult;
 import android.media.Image;
 import android.util.Pair;
+import android.util.Size;
 import android.view.Surface;
 
-import androidx.annotation.RequiresApi;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import java.util.Map;
 import java.util.concurrent.Executor;
@@ -34,7 +36,6 @@
  * @since 1.0
  */
 @SuppressLint("UnknownNullness")
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public interface CaptureProcessorImpl extends ProcessorImpl {
     /**
      * Process a set images captured that were requested.
@@ -46,7 +47,30 @@
      *                process. The {@link Image} that are contained within the map will become
      *                invalid after this method completes, so no references to them should be kept.
      */
-    void process(Map<Integer, Pair<Image, TotalCaptureResult>> results);
+    void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results);
+
+    /**
+     * Informs the CaptureProcessorImpl where it should write the postview output to.
+     * This will only be invoked once if a valid postview surface was set.
+     *
+     * @param surface A valid {@link ImageFormat#YUV_420_888} {@link Surface}
+     *                that the CaptureProcessorImpl should write data into.
+     * @since 1.4
+     */
+    void onPostviewOutputSurface(@NonNull Surface surface);
+
+    /**
+     * Invoked when the Camera Framework changes the configured output resolution for
+     * still capture and postview.
+     *
+     * <p>After this call, {@link CaptureProcessorImpl} should expect any {@link Image} received as
+     * input for still capture and postview to be at the specified resolutions.
+     *
+     * @param size for the surface for still capture.
+     * @param postviewSize for the surface for postview.
+     * @since 1.4
+     */
+    void onResolutionUpdate(@NonNull Size size, @NonNull Size postviewSize);
 
     /**
      * Process a set images captured that were requested.
@@ -64,6 +88,32 @@
      *                       run on any arbitrary executor.
      * @since 1.3
      */
-    void process(Map<Integer, Pair<Image, TotalCaptureResult>> results,
-            ProcessResultImpl resultCallback, Executor executor);
+    void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+            @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor);
+
+    /**
+     * Process a set images captured that were requested for both postview and
+     * still capture.
+     *
+     * <p> This processing method will be called if a postview was requested, therefore the
+     * processed postview should be written to the
+     * {@link Surface} received by {@link #onPostviewOutputSurface(Surface, int)}.
+     * The final result of the processing step should be written to the {@link Surface} that was
+     * received by {@link #onOutputSurface(Surface, int)}. Since postview should be available
+     * before the capture, it should be processed and written to the surface before
+     * the final capture is processed.
+     *
+     * @param results             The map of {@link ImageFormat#YUV_420_888} format images and
+     *                            metadata to process. The {@link Image} that are contained within
+     *                            the map will become invalid after this method completes, so no
+     *                            references to them should be kept.
+     * @param resultCallback      Capture result callback to be called once the capture result
+     *                            values of the processed image are ready.
+     * @param executor            The executor to run the callback on. If null then the callback
+     *                            will run on any arbitrary executor.
+     * @throws RuntimeException   if postview feature is not supported
+     * @since 1.4
+     */
+    void processWithPostview(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+            @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor);
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/CaptureStageImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/CaptureStageImpl.java
index fc86020..c4f4a47 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/CaptureStageImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/CaptureStageImpl.java
@@ -20,7 +20,6 @@
 import android.util.Pair;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
 
 import java.util.List;
 
@@ -29,7 +28,6 @@
  *
  * @since 1.0
  */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public interface CaptureStageImpl {
     /** Returns the identifier for the {@link CaptureStageImpl}. */
     int getId();
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
index b13f602..2d97454 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
@@ -17,7 +17,6 @@
 package androidx.camera.extensions.impl;
 
 import android.content.Context;
-import android.hardware.camera2.CameraCaptureSession;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CaptureRequest;
@@ -25,14 +24,12 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
 
 /**
  * Provides interfaces that the OEM needs to implement to handle the state change.
  *
  * @since 1.0
  */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public interface ExtenderStateListener {
 
     /**
@@ -40,6 +37,7 @@
      * where the use case is started and would be able to allocate resources here. After onInit() is
      * called, the camera ID, cameraCharacteristics and context will not change until onDeInit()
      * has been called.
+     *
      * @param cameraId The camera2 id string of the camera.
      * @param cameraCharacteristics The {@link CameraCharacteristics} of the camera.
      * @param context The {@link Context} used for CameraX.
@@ -56,12 +54,11 @@
 
     /**
      * This will be invoked before creating a
-     * {@link CameraCaptureSession}. The {@link CaptureRequest}
+     * {@link android.hardware.camera2.CameraCaptureSession}. The {@link CaptureRequest}
      * parameters returned via {@link CaptureStageImpl} will be passed to the camera device as
      * part of the capture session initialization via
-     * {@link SessionConfiguration#setSessionParameters(CaptureRequest)} which only supported
-     * from API level 28. The valid parameter is a subset of the available capture request
-     * parameters.
+     * {@link SessionConfiguration#setSessionParameters(CaptureRequest)} which only supported from
+     * API level 28. The valid parameter is a subset of the available capture request parameters.
      *
      * @return The request information to set the session wide camera parameters.
      */
@@ -69,7 +66,7 @@
     CaptureStageImpl onPresetSession();
 
     /**
-     * This will be invoked once after the {@link CameraCaptureSession}
+     * This will be invoked once after the {@link android.hardware.camera2.CameraCaptureSession}
      * has been created. The {@link CaptureRequest} parameters returned via
      * {@link CaptureStageImpl} will be used to generate a single request to the current
      * configured {@link CameraDevice}. The generated request will be submitted to camera before
@@ -81,7 +78,7 @@
     CaptureStageImpl onEnableSession();
 
     /**
-     * This will be invoked before the {@link CameraCaptureSession} is
+     * This will be invoked before the {@link android.hardware.camera2.CameraCaptureSession} is
      * closed. The {@link CaptureRequest} parameters returned via {@link CaptureStageImpl} will
      * be used to generate a single request to the currently configured {@link CameraDevice}. The
      * generated request will be submitted to camera before the CameraCaptureSession is closed.
@@ -90,4 +87,21 @@
      */
     @Nullable
     CaptureStageImpl onDisableSession();
+
+    /**
+     * This will be invoked before the {@link android.hardware.camera2.CameraCaptureSession} is
+     * initialized and must return a valid camera session type
+     * {@link android.hardware.camera2.params.SessionConfiguration#getSessionType}
+     * to be used to configure camera capture session. Both the preview and the image capture
+     * extender must return the same session type value for a specific extension type. If there
+     * is inconsistency between the session type values from preview and image extenders, then
+     * the session configuration will fail.
+     *
+     * @return Camera capture session type. Regular and vendor specific types are supported but
+     * not high speed values. The extension can return -1 in which case the camera capture session
+     * will be configured to use the default regular type.
+     *
+     * @since 1.4
+     */
+    int onSessionType();
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java
index 212bea0..5235e59 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java
@@ -32,7 +32,7 @@
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class ExtensionVersionImpl {
     private static final String TAG = "ExtenderVersionImpl";
-    private static final String VERSION = "1.2.0";
+    private static final String VERSION = "1.4.0";
 
     public ExtensionVersionImpl() {
     }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionsTestlibControl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionsTestlibControl.java
new file mode 100644
index 0000000..6cf35b5
--- /dev/null
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionsTestlibControl.java
@@ -0,0 +1,68 @@
+/*
+ * 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.camera.extensions.impl;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * An internal utility class that allows tests to specify whether to enable basic extender or
+ * advanced extender of this testlib.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ExtensionsTestlibControl {
+    public enum ImplementationType {
+        ADVANCED_EXTENDER,
+        BASIC_EXTENDER
+    }
+
+    private static ExtensionsTestlibControl sInstance;
+    private static Object sLock = new Object();
+
+    private ExtensionsTestlibControl() {
+    }
+
+    /**
+     * Gets the singleton instance.
+     */
+    @NonNull
+    public static ExtensionsTestlibControl getInstance() {
+        synchronized (sLock) {
+            if (sInstance == null) {
+                sInstance = new ExtensionsTestlibControl();
+            }
+            return sInstance;
+        }
+    }
+
+    private ImplementationType mImplementationType = ImplementationType.BASIC_EXTENDER;
+
+    /**
+     * Set the implementation type.
+     */
+    public void setImplementationType(@NonNull ImplementationType type) {
+        mImplementationType = type;
+    }
+
+    /**
+     * Gets the implementation type;
+     */
+    @NonNull
+    public ImplementationType getImplementationType() {
+        return mImplementationType;
+    }
+}
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
index 6a16dff..0ba4752 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
@@ -19,10 +19,12 @@
 
 import android.annotation.SuppressLint;
 import android.content.Context;
+import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.SessionConfiguration;
 import android.media.Image;
 import android.media.ImageWriter;
 import android.os.Build;
@@ -38,6 +40,7 @@
 
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Executor;
@@ -106,6 +109,7 @@
         return false;
     }
 
+    @NonNull
     @Override
     public List<CaptureStageImpl> getCaptureStages() {
         // Under exposed capture stage
@@ -133,6 +137,8 @@
         return captureStages;
     }
 
+
+    @Nullable
     @Override
     public CaptureProcessorImpl getCaptureProcessor() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -154,6 +160,7 @@
 
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onPresetSession() {
         // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
@@ -166,12 +173,14 @@
         return captureStage;
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onEnableSession() {
         SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
         return captureStage;
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onDisableSession() {
         SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
@@ -183,6 +192,7 @@
         return 4;
     }
 
+    @Nullable
     @Override
     public List<Pair<Integer, Size[]>> getSupportedResolutions() {
         return null;
@@ -197,6 +207,7 @@
     @RequiresApi(23)
     static final class HdrImageCaptureExtenderCaptureProcessorImpl implements CaptureProcessorImpl {
         private ImageWriter mImageWriter;
+        private Surface mPostViewSurface;
 
         @Override
         public void onOutputSurface(@NonNull Surface surface, int imageFormat) {
@@ -204,9 +215,23 @@
         }
 
         @Override
-        public void process(Map<Integer, Pair<Image, TotalCaptureResult>> results) {
+        public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results) {
+            processInternal(results, null, null, false);
+        }
+
+        @Override
+        public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            processInternal(results, resultCallback, executor, false);
+        }
+
+        public void processInternal(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @Nullable ProcessResultImpl resultCallback, @Nullable Executor executor,
+                boolean hasPostview) {
             Log.d(TAG, "Started HDR CaptureProcessor");
 
+            Executor executorForCallback = executor != null ? executor : (cmd) -> cmd.run();
+
             // Check for availability of all requested images
             if (!results.containsKey(UNDER_STAGE_ID)) {
                 Log.w(TAG,
@@ -238,6 +263,9 @@
             // Do processing here
             // The sample here simply returns the normal image result
             Image normalImage = imageDataPairs.get(NORMAL_STAGE_ID).first;
+            if (hasPostview) {
+                YuvToJpegConverter.writeYuvToJpegSurface(normalImage, mPostViewSurface);
+            }
 
             if (outputImage.getWidth() != normalImage.getWidth()
                     || outputImage.getHeight() != normalImage.getHeight()) {
@@ -248,6 +276,12 @@
                         outputImage.getHeight()));
             }
 
+            if (resultCallback != null) {
+                executorForCallback.execute(() -> {
+                    resultCallback.onCaptureProcessProgressed(10);
+                });
+            }
+
             try {
                 // copy y plane
                 Image.Plane inYPlane = normalImage.getPlanes()[0];
@@ -266,6 +300,11 @@
                     }
                 }
 
+                if (resultCallback != null) {
+                    executorForCallback.execute(
+                            () -> resultCallback.onCaptureProcessProgressed(50));
+                }
+
                 // Copy UV
                 for (int i = 1; i < 3; i++) {
                     Image.Plane inPlane = normalImage.getPlanes()[i];
@@ -294,16 +333,35 @@
             }
 
             mImageWriter.queueInputImage(outputImage);
+            if (resultCallback != null) {
+                executorForCallback.execute(
+                        () -> resultCallback.onCaptureProcessProgressed(100));
+            }
+
+            TotalCaptureResult captureResult = results.get(NORMAL_STAGE_ID).second;
+
+            if (resultCallback != null) {
+                executorForCallback.execute(
+                        () -> resultCallback.onCaptureCompleted(
+                                captureResult.get(CaptureResult.SENSOR_TIMESTAMP),
+                                getFilteredResults(captureResult)));
+            }
 
             Log.d(TAG, "Completed HDR CaptureProcessor");
         }
 
-        @Override
-        public void process(Map<Integer, Pair<Image, TotalCaptureResult>> results,
-                ProcessResultImpl resultCallback, Executor executor) {
+        @SuppressWarnings("unchecked")
+        private List<Pair<CaptureResult.Key, Object>> getFilteredResults(
+                TotalCaptureResult captureResult) {
+            List<Pair<CaptureResult.Key, Object>> list = new ArrayList<>();
+            for (CaptureResult.Key key : captureResult.getKeys()) {
+                list.add(new Pair<>(key, captureResult.get(key)));
+            }
 
+            return list;
         }
 
+
         @Override
         public void onResolutionUpdate(@NonNull Size size) {
 
@@ -313,6 +371,24 @@
         public void onImageFormatUpdate(int imageFormat) {
 
         }
+
+        @Override
+        public void onPostviewOutputSurface(@NonNull Surface surface) {
+            mPostViewSurface = surface;
+        }
+
+        @Override
+        public void onResolutionUpdate(@NonNull Size size, @NonNull Size postviewSize) {
+            onResolutionUpdate(size);
+        }
+
+        @Override
+        public void processWithPostview(
+                @NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            Log.d(TAG, "processWithPostview");
+            processInternal(results, resultCallback, executor, true);
+        }
     }
 
     @NonNull
@@ -321,6 +397,34 @@
         return null;
     }
 
+    @Override
+    public int onSessionType() {
+        return SessionConfiguration.SESSION_REGULAR;
+    }
+
+    @Nullable
+    @Override
+    public List<Pair<Integer, Size[]>> getSupportedPostviewResolutions(@NonNull Size captureSize) {
+        Pair<Integer, Size[]> pair = new Pair<>(ImageFormat.JPEG, new Size[] {captureSize});
+        return Arrays.asList(pair);
+    }
+
+    @Override
+    public boolean isCaptureProcessProgressAvailable() {
+        return true;
+    }
+
+    @Nullable
+    @Override
+    public Pair<Long, Long> getRealtimeCaptureLatency() {
+        return null;
+    }
+
+    @Override
+    public boolean isPostviewAvailable() {
+        return true;
+    }
+
     @NonNull
     @Override
     public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
index defe8e9..60d2888 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.SessionConfiguration;
 import android.media.Image;
 import android.os.Build;
 import android.os.Handler;
@@ -47,6 +48,8 @@
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public final class HdrPreviewExtenderImpl implements PreviewExtenderImpl {
+    private static final String TAG = "HdrPreviewExtenderImpl";
+
     private static final int DEFAULT_STAGE_ID = 0;
 
     @Nullable
@@ -174,7 +177,7 @@
         @Override
         public void process(Image image, TotalCaptureResult result,
                 ProcessResultImpl resultCallback, Executor executor) {
-
+            process(image, result);
         }
 
         @Override
@@ -225,4 +228,9 @@
             }
         }
     }
+
+    @Override
+    public int onSessionType() {
+        return SessionConfiguration.SESSION_REGULAR;
+    }
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
index f235fbe..dbcd1ef 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
@@ -20,14 +20,12 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.params.StreamConfigurationMap;
 import android.util.Pair;
 import android.util.Range;
 import android.util.Size;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
 
 import java.util.List;
 
@@ -36,7 +34,6 @@
  *
  * @since 1.0
  */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public interface ImageCaptureExtenderImpl extends ExtenderStateListener {
     /**
      * Indicates whether the extension is supported on the device.
@@ -66,7 +63,7 @@
     CaptureProcessorImpl getCaptureProcessor();
 
     /** The set of captures that are needed to create an image with the effect. */
-    @Nullable
+    @NonNull
     List<CaptureStageImpl> getCaptureStages();
 
     /**
@@ -81,18 +78,31 @@
      * <p>Pair list composed with {@link ImageFormat} and {@link Size} array will be returned.
      *
      * <p>The returned resolutions should be subset of the supported sizes retrieved from
-     * {@link StreamConfigurationMap} for the camera device. If the
+     * {@link android.hardware.camera2.params.StreamConfigurationMap} for the camera device. If the
      * returned list is not null, it will be used to find the best resolutions combination for
      * the bound use cases.
      *
      * @return the customized supported resolutions, or null to support all sizes retrieved from
-     * {@link StreamConfigurationMap}.
+     *         {@link android.hardware.camera2.params.StreamConfigurationMap}.
      * @since 1.1
      */
     @Nullable
     List<Pair<Integer, Size[]>> getSupportedResolutions();
 
     /**
+     * Returns supported output format/size map for postview image. OEM is required to support
+     * both JPEG and YUV_420_888 format output.
+     *
+     * <p>Pair list composed with {@link ImageFormat} and {@link Size} array will be returned.
+     * The sizes must be smaller than or equal to the provided capture size and have the same
+     * aspect ratio as the given capture size.
+     *
+     * @since 1.4
+     */
+    @Nullable
+    List<Pair<Integer, Size[]>> getSupportedPostviewResolutions(@NonNull Size captureSize);
+
+    /**
      * Returns the estimated capture latency range in milliseconds for the target capture
      * resolution.
      *
@@ -172,6 +182,15 @@
     List<CaptureResult.Key> getAvailableCaptureResultKeys();
 
     /**
+     * Advertise support for {@link ProcessResultImpl#onCaptureProcessProgressed}.
+     *
+     * @return {@code true} in case the process progress callback is supported and is expected to
+     * be triggered, {@code false} otherwise.
+     * @since 1.4
+     */
+    boolean isCaptureProcessProgressAvailable();
+
+    /**
      * Returns the dynamically calculated capture latency pair in milliseconds.
      *
      * <p>In contrast to {@link #getEstimatedCaptureLatencyRange} this method is guaranteed to be
@@ -190,7 +209,16 @@
      * @since 1.4
      */
     @Nullable
-    default Pair<Long, Long> getRealtimeCaptureLatency() {
-        return null;
-    };
+    Pair<Long, Long> getRealtimeCaptureLatency();
+
+    /**
+     * Indicates whether the extension supports the postview for still capture feature.
+     * If the extension is using HAL processing, false should be returned since the
+     * postview feature is not currently supported for this case.
+     *
+     * @return {@code true} in case postview for still capture is supported
+     * {@code false} otherwise.
+     * @since 1.4
+     */
+    boolean isPostviewAvailable();
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
index 98c171e..b3e38b7 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
@@ -21,6 +21,7 @@
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.SessionConfiguration;
 import android.media.Image;
 import android.media.ImageWriter;
 import android.os.Build;
@@ -79,6 +80,7 @@
         return CameraCharacteristicAvailability.isEffectAvailable(cameraCharacteristics, EFFECT);
     }
 
+    @NonNull
     @Override
     public List<CaptureStageImpl> getCaptureStages() {
         // Placeholder set of CaptureRequest.Key values
@@ -89,6 +91,7 @@
         return captureStages;
     }
 
+    @Nullable
     @Override
     public CaptureProcessorImpl getCaptureProcessor() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -110,6 +113,7 @@
 
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onPresetSession() {
         // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
@@ -126,6 +130,7 @@
         return captureStage;
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onEnableSession() {
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
@@ -136,6 +141,7 @@
         return captureStage;
     }
 
+    @Nullable
     @Override
     public CaptureStageImpl onDisableSession() {
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
@@ -151,6 +157,7 @@
         return 3;
     }
 
+    @Nullable
     @Override
     public List<Pair<Integer, Size[]>> getSupportedResolutions() {
         return null;
@@ -163,6 +170,33 @@
         return new Range<>(300L, 1000L);
     }
 
+    @Override
+    public int onSessionType() {
+        return SessionConfiguration.SESSION_REGULAR;
+    }
+
+    @Nullable
+    @Override
+    public List<Pair<Integer, Size[]>> getSupportedPostviewResolutions(@NonNull Size captureSize) {
+        return null;
+    }
+
+    @Override
+    public boolean isCaptureProcessProgressAvailable() {
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public Pair<Long, Long> getRealtimeCaptureLatency() {
+        return null;
+    }
+
+    @Override
+    public boolean isPostviewAvailable() {
+        return false;
+    }
+
     @RequiresApi(23)
     static final class NightImageCaptureExtenderCaptureProcessorImpl
             implements CaptureProcessorImpl {
@@ -174,7 +208,7 @@
         }
 
         @Override
-        public void process(Map<Integer, Pair<Image, TotalCaptureResult>> results) {
+        public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results) {
             Log.d(TAG, "Started night CaptureProcessor");
 
             Pair<Image, TotalCaptureResult> result = results.get(DEFAULT_STAGE_ID);
@@ -203,8 +237,9 @@
         }
 
         @Override
-        public void process(Map<Integer, Pair<Image, TotalCaptureResult>> results,
-                ProcessResultImpl resultCallback, Executor executor) {
+        public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            process(results);
         }
 
         @Override
@@ -214,6 +249,23 @@
         @Override
         public void onImageFormatUpdate(int imageFormat) {
         }
+
+        @Override
+        public void onPostviewOutputSurface(@NonNull Surface surface) {
+
+        }
+
+        @Override
+        public void onResolutionUpdate(@NonNull Size size, @NonNull Size postviewSize) {
+
+        }
+
+        @Override
+        public void processWithPostview(
+                @NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            throw new UnsupportedOperationException("Postview is not supported");
+        }
     }
 
     @NonNull
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
index 46512ee..754a9f5 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
@@ -18,6 +18,7 @@
 import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.SessionConfiguration;
 import android.os.Build;
 import android.util.Pair;
 import android.util.Size;
@@ -146,4 +147,9 @@
 
         return captureStage;
     }
+
+    @Override
+    public int onSessionType() {
+        return SessionConfiguration.SESSION_REGULAR;
+    }
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NoOpCaptureProcessorImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NoOpCaptureProcessorImpl.java
index a84fed7..a78fa30 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NoOpCaptureProcessorImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NoOpCaptureProcessorImpl.java
@@ -23,6 +23,7 @@
 import android.view.Surface;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
 import java.util.Map;
@@ -51,8 +52,23 @@
     }
 
     @Override
-    public void process(Map<Integer, Pair<Image, TotalCaptureResult>> results,
-            ProcessResultImpl resultCallback, Executor executor) {
+    public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+            @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+    }
+
+    @Override
+    public void onPostviewOutputSurface(@NonNull Surface surface) {
+
+    }
+
+    @Override
+    public void onResolutionUpdate(@NonNull Size size, @NonNull Size postviewSize) {
+
+    }
+
+    @Override
+    public void processWithPostview(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+            @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
 
     }
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java
index 93cbcb9..d921151 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java
@@ -18,14 +18,12 @@
 
 import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCharacteristics;
-import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.TotalCaptureResult;
 import android.util.Pair;
 import android.util.Size;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
 
 import java.util.List;
 
@@ -34,7 +32,6 @@
  *
  * @since 1.0
  */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public interface PreviewExtenderImpl extends ExtenderStateListener {
     /** The different types of the preview processing. */
     enum ProcessorType {
@@ -61,7 +58,8 @@
      *
      * <p>This should be called before any other method on the extender. The exception is {@link
      * #isExtensionAvailable(String, CameraCharacteristics)}.
-     *  @param cameraId The camera2 id string of the camera.
+     *
+     * @param cameraId The camera2 id string of the camera.
      * @param cameraCharacteristics The {@link CameraCharacteristics} of the camera.
      */
     void init(@NonNull String cameraId, @NonNull CameraCharacteristics cameraCharacteristics);
@@ -70,7 +68,7 @@
      * The set of parameters required to produce the effect on the preview stream.
      *
      * <p> This will be the initial set of parameters used for the preview
-     * {@link CaptureRequest}. If the {@link ProcessorType} is defined as
+     * {@link android.hardware.camera2.CaptureRequest}. If the {@link ProcessorType} is defined as
      * {@link ProcessorType#PROCESSOR_TYPE_REQUEST_UPDATE_ONLY} then this will be updated when
      * the {@link RequestUpdateProcessorImpl#process(TotalCaptureResult)} from {@link
      * #getProcessor()} has been called, this should be updated to reflect the new {@link
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
index ff6ed88..c4bf2c8 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
@@ -16,12 +16,12 @@
 
 package androidx.camera.extensions.impl;
 
-import android.annotation.SuppressLint;
 import android.graphics.ImageFormat;
 import android.hardware.camera2.TotalCaptureResult;
 import android.media.Image;
 
-import androidx.annotation.RequiresApi;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import java.util.concurrent.Executor;
 
@@ -31,8 +31,6 @@
  *
  * @since 1.0
  */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@SuppressLint("UnknownNullness")
 public interface PreviewImageProcessorImpl extends ProcessorImpl {
     /**
      * Processes the requested image capture.
@@ -44,7 +42,7 @@
      *               invalid after the method completes so no reference to it should be kept.
      * @param result The metadata associated with the image to process.
      */
-    void process(Image image, TotalCaptureResult result);
+    void process(@NonNull Image image, @NonNull TotalCaptureResult result);
 
     /**
      * Processes the requested image capture.
@@ -62,6 +60,7 @@
      *                       run on any arbitrary executor.
      * @since 1.3
      */
-    void process(Image image, TotalCaptureResult result, ProcessResultImpl resultCallback,
-            Executor executor);
+    void process(@NonNull Image image, @NonNull TotalCaptureResult result,
+            @NonNull ProcessResultImpl resultCallback,
+            @Nullable Executor executor);
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ProcessResultImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ProcessResultImpl.java
index d0e3605..0cc10c5 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ProcessResultImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ProcessResultImpl.java
@@ -16,18 +16,17 @@
 
 package androidx.camera.extensions.impl;
 
-import android.annotation.SuppressLint;
 import android.hardware.camera2.CaptureResult;
 import android.util.Pair;
 
+import androidx.annotation.NonNull;
+
 import java.util.List;
 
 /**
  * Allows clients to receive information about the capture result values of processed frames.
  *
- * @since 1.3
  */
-@SuppressLint("UnknownNullness")
 public interface ProcessResultImpl {
     /**
      * Capture result callback that needs to be called when the process capture results are
@@ -40,6 +39,23 @@
      *                             must also be passed as part of this callback. Both Camera2 and
      *                             CameraX guarantee that those two settings and results are always
      *                             supported and applied by the corresponding framework.
+     * @since 1.3
      */
-    void onCaptureCompleted(long shutterTimestamp, List<Pair<CaptureResult.Key, Object>> result);
+    void onCaptureCompleted(long shutterTimestamp,
+            @NonNull List<Pair<CaptureResult.Key, Object>> result);
+
+    /**
+     * Capture progress callback that needs to be called when the process capture is
+     * ongoing and includes the estimated progress of the processing.
+     *
+     * <p>Extensions must ensure that they always call this callback with monotonically increasing
+     * values.</p>
+     *
+     * <p>Extensions are allowed to trigger this callback multiple times but at the minimum the
+     * callback is expected to be called once when processing is done with value 100.</p>
+     *
+     * @param progress             Value between 0 and 100.
+     * @since 1.4
+     */
+    default void onCaptureProcessProgressed(int progress) {}
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ProcessorImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ProcessorImpl.java
index a339cca..e5ca19e 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ProcessorImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ProcessorImpl.java
@@ -20,15 +20,12 @@
 import android.view.Surface;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
 
 /**
  * Processes an input image stream and produces an output image stream.
  *
  * @since 1.0
  */
-@SuppressWarnings("unused")
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public interface ProcessorImpl {
     /**
      * Updates where the ProcessorImpl should write the output to.
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpl.java
index 8a6a2e2..ac3bfb3 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpl.java
@@ -19,14 +19,12 @@
 import android.hardware.camera2.TotalCaptureResult;
 
 import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
 
 /**
  * Processes a {@link TotalCaptureResult} to update a CaptureStage.
  *
  * @since 1.0
  */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public interface RequestUpdateProcessorImpl extends ProcessorImpl {
     /**
      * Process the {@link TotalCaptureResult} to update the {@link CaptureStageImpl}
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/YuvToJpegConverter.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/YuvToJpegConverter.java
new file mode 100644
index 0000000..0681b97
--- /dev/null
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/YuvToJpegConverter.java
@@ -0,0 +1,101 @@
+/*
+ * 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.camera.extensions.impl;
+
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.graphics.YuvImage;
+import android.media.Image;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.ImageProcessingUtil;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+
+@RequiresApi(23)
+class YuvToJpegConverter {
+    private YuvToJpegConverter() {}
+    public static void writeYuvToJpegSurface(@NonNull Image yuvMediaImage,
+            @NonNull Surface jpegSurface) {
+        byte[] yuvBytes = yuv_420_888toNv21(yuvMediaImage);
+        YuvImage yuvImage = new YuvImage(yuvBytes, ImageFormat.NV21, yuvMediaImage.getWidth(),
+                yuvMediaImage.getHeight(), null);
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        yuvImage.compressToJpeg(
+                new Rect(0, 0, yuvMediaImage.getWidth(), yuvMediaImage.getHeight()),
+                95, byteArrayOutputStream);
+        ImageProcessingUtil.writeJpegBytesToSurface(jpegSurface,
+                byteArrayOutputStream.toByteArray());
+        yuvMediaImage.getPlanes()[0].getBuffer().rewind();
+    }
+
+    @NonNull
+    private static byte[] yuv_420_888toNv21(@NonNull Image image) {
+        Image.Plane yPlane = image.getPlanes()[0];
+        Image.Plane uPlane = image.getPlanes()[1];
+        Image.Plane vPlane = image.getPlanes()[2];
+
+        ByteBuffer yBuffer = yPlane.getBuffer();
+        ByteBuffer uBuffer = uPlane.getBuffer();
+        ByteBuffer vBuffer = vPlane.getBuffer();
+        yBuffer.rewind();
+        uBuffer.rewind();
+        vBuffer.rewind();
+
+        int ySize = yBuffer.remaining();
+
+        int position = 0;
+        byte[] nv21 = new byte[ySize + (image.getWidth() * image.getHeight() / 2)];
+
+        // Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
+        for (int row = 0; row < image.getHeight(); row++) {
+            yBuffer.get(nv21, position, image.getWidth());
+            position += image.getWidth();
+            yBuffer.position(
+                    Math.min(ySize, yBuffer.position() - image.getWidth() + yPlane.getRowStride()));
+        }
+
+        int chromaHeight = image.getHeight() / 2;
+        int chromaWidth = image.getWidth() / 2;
+        int vRowStride = vPlane.getRowStride();
+        int uRowStride = uPlane.getRowStride();
+        int vPixelStride = vPlane.getPixelStride();
+        int uPixelStride = uPlane.getPixelStride();
+
+        // Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
+        // perform faster bulk gets from the byte buffers.
+        byte[] vLineBuffer = new byte[vRowStride];
+        byte[] uLineBuffer = new byte[uRowStride];
+        for (int row = 0; row < chromaHeight; row++) {
+            vBuffer.get(vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining()));
+            uBuffer.get(uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining()));
+            int vLineBufferPosition = 0;
+            int uLineBufferPosition = 0;
+            for (int col = 0; col < chromaWidth; col++) {
+                nv21[position++] = vLineBuffer[vLineBufferPosition];
+                nv21[position++] = uLineBuffer[uLineBufferPosition];
+                vLineBufferPosition += vPixelStride;
+                uLineBufferPosition += uPixelStride;
+            }
+        }
+
+        return nv21;
+    }
+}
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/AdvancedExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/AdvancedExtenderImpl.java
index 465bfe8..03c6ba2 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/AdvancedExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/AdvancedExtenderImpl.java
@@ -16,13 +16,14 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.util.Range;
 import android.util.Size;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.camera.extensions.impl.ExtensionVersionImpl;
 
 import java.util.List;
@@ -50,7 +51,6 @@
  *
  * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public interface AdvancedExtenderImpl {
 
     /**
@@ -64,8 +64,8 @@
      *                           physical camera ids and their CameraCharacteristics.
      * @return true if the extension is supported, otherwise false
      */
-    boolean isExtensionAvailable(String cameraId,
-            Map<String, CameraCharacteristics> characteristicsMap);
+    boolean isExtensionAvailable(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> characteristicsMap);
 
     /**
      * Initializes the extender to be used with the specified camera.
@@ -80,7 +80,8 @@
      *                           If the camera is logical camera, it will also contain associated
      *                           physical camera ids and their CameraCharacteristics.
      */
-    void init(String cameraId, Map<String, CameraCharacteristics> characteristicsMap);
+    void init(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> characteristicsMap);
 
     /**
      * Returns the estimated capture latency range in milliseconds for the
@@ -97,8 +98,9 @@
      * @return the range of estimated minimal and maximal capture latency in milliseconds.
      * Returns null if no capture latency info can be provided.
      */
-    Range<Long> getEstimatedCaptureLatencyRange(String cameraId,
-            Size captureOutputSize, int imageFormat);
+    @Nullable
+    Range<Long> getEstimatedCaptureLatencyRange(@NonNull String cameraId,
+            @Nullable Size captureOutputSize, int imageFormat);
 
     /**
      * Returns supported output format/size map for preview. The format could be PRIVATE or
@@ -111,7 +113,8 @@
      * the HAL. Alternatively OEM can configure a intermediate YUV surface of the same size and
      * writes the output to the preview output surface.
      */
-    Map<Integer, List<Size>> getSupportedPreviewOutputResolutions(String cameraId);
+    @NonNull
+    Map<Integer, List<Size>> getSupportedPreviewOutputResolutions(@NonNull String cameraId);
 
     /**
      * Returns supported output format/size map for image capture. OEM is required to support
@@ -121,7 +124,21 @@
      * format/size could be either added in CameraCaptureSession with HAL processing OR it
      * configures intermediate surfaces(YUV/RAW..) and writes the output to the output surface.
      */
-    Map<Integer, List<Size>> getSupportedCaptureOutputResolutions(String cameraId);
+    @NonNull
+    Map<Integer, List<Size>> getSupportedCaptureOutputResolutions(@NonNull String cameraId);
+
+    /**
+     * Returns supported output format/size map for postview image. OEM is required to support
+     * both JPEG and YUV_420_888 format output.
+     *
+     * <p>The returned sizes must be smaller than or equal to the provided capture size and have the
+     * same aspect ratio as the given capture size. If no supported resolution exists for the
+     * provided capture size then an empty map is returned.
+     *
+     * @since 1.4
+     */
+    @NonNull
+    Map<Integer, List<Size>> getSupportedPostviewResolutions(@NonNull Size captureSize);
 
     /**
      * Returns supported output sizes for Image Analysis (YUV_420_888 format).
@@ -130,12 +147,14 @@
      * output surfaces. If imageAnalysis YUV surface is not supported, OEM should return null or
      * empty list.
      */
-    List<Size> getSupportedYuvAnalysisResolutions(String cameraId);
+    @Nullable
+    List<Size> getSupportedYuvAnalysisResolutions(@NonNull String cameraId);
 
     /**
      * Returns a processor for activating extension sessions. It implements all the interactions
      * required for starting a extension and cleanup.
      */
+    @NonNull
     SessionProcessorImpl createSessionProcessor();
 
     /**
@@ -169,6 +188,7 @@
      * are not supported.
      * @since 1.3
      */
+    @NonNull
     List<CaptureRequest.Key> getAvailableCaptureRequestKeys();
 
     /**
@@ -184,5 +204,26 @@
      * an empty list if capture results are not supported.
      * @since 1.3
      */
+    @NonNull
     List<CaptureResult.Key> getAvailableCaptureResultKeys();
+
+    /**
+     * Advertise support for {@link SessionProcessorImpl#onCaptureProcessProgressed}.
+     *
+     * @return {@code true} in case the process progress callback is supported and is expected to
+     * be triggered, {@code false} otherwise.
+     * @since 1.4
+     */
+    boolean isCaptureProcessProgressAvailable();
+
+    /**
+     * Indicates whether the extension supports the postview for still capture feature.
+     * If the extension is using HAL processing, false should be returned since the
+     * postview feature is not currently supported for this case.
+     *
+     * @return {@code true} in case postview for still capture is supported
+     * {@code false} otherwise.
+     * @since 1.4
+     */
+    boolean isPostviewAvailable();
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/AutoAdvancedExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/AutoAdvancedExtenderImpl.java
index 7065a8f..9f567ba 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/AutoAdvancedExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/AutoAdvancedExtenderImpl.java
@@ -16,13 +16,15 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.util.Range;
 import android.util.Size;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import java.util.List;
 import java.util.Map;
 
@@ -33,58 +35,82 @@
  *
  * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public class AutoAdvancedExtenderImpl implements AdvancedExtenderImpl {
     public AutoAdvancedExtenderImpl() {
     }
 
     @Override
-    public boolean isExtensionAvailable(String cameraId,
-            Map<String, CameraCharacteristics> characteristicsMap) {
-        return false;
+    public boolean isExtensionAvailable(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
-    public void init(String cameraId,
-            Map<String, CameraCharacteristics> characteristicsMap) {
+    public void init(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @Nullable
     public Range<Long> getEstimatedCaptureLatencyRange(
-            String cameraId, Size size, int imageFormat) {
-        return null;
+            @NonNull String cameraId, @Nullable Size size, int imageFormat) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public Map<Integer, List<Size>> getSupportedPreviewOutputResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public Map<Integer, List<Size>> getSupportedCaptureOutputResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
+    public Map<Integer, List<Size>> getSupportedPostviewResolutions(
+            @NonNull Size captureSize) {
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    @Nullable
     public List<Size> getSupportedYuvAnalysisResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public SessionProcessorImpl createSessionProcessor() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
     }
-}
+
+    @Override
+    public boolean isCaptureProcessProgressAvailable() {
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    public boolean isPostviewAvailable() {
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/BeautyAdvancedExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/BeautyAdvancedExtenderImpl.java
index b553ed6..40bbb93 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/BeautyAdvancedExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/BeautyAdvancedExtenderImpl.java
@@ -16,13 +16,15 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.util.Range;
 import android.util.Size;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import java.util.List;
 import java.util.Map;
 
@@ -33,58 +35,82 @@
  *
  * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public class BeautyAdvancedExtenderImpl implements AdvancedExtenderImpl {
     public BeautyAdvancedExtenderImpl() {
     }
 
     @Override
-    public boolean isExtensionAvailable(String cameraId,
-            Map<String, CameraCharacteristics> characteristicsMap) {
-        return false;
+    public boolean isExtensionAvailable(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
-    public void init(String cameraId,
-            Map<String, CameraCharacteristics> characteristicsMap) {
+    public void init(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @Nullable
     public Range<Long> getEstimatedCaptureLatencyRange(
-            String cameraId, Size size, int imageFormat) {
-        return null;
+            @NonNull String cameraId, @Nullable Size size, int imageFormat) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public Map<Integer, List<Size>> getSupportedPreviewOutputResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public Map<Integer, List<Size>> getSupportedCaptureOutputResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
+    public Map<Integer, List<Size>> getSupportedPostviewResolutions(
+            @NonNull Size captureSize) {
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    @Nullable
     public List<Size> getSupportedYuvAnalysisResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public SessionProcessorImpl createSessionProcessor() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    public boolean isCaptureProcessProgressAvailable() {
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    public boolean isPostviewAvailable() {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/BokehAdvancedExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/BokehAdvancedExtenderImpl.java
index c1df4f4..4093211 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/BokehAdvancedExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/BokehAdvancedExtenderImpl.java
@@ -16,13 +16,15 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.util.Range;
 import android.util.Size;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import java.util.List;
 import java.util.Map;
 
@@ -33,58 +35,82 @@
  *
  * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public class BokehAdvancedExtenderImpl implements AdvancedExtenderImpl {
     public BokehAdvancedExtenderImpl() {
     }
 
     @Override
-    public boolean isExtensionAvailable(String cameraId,
-            Map<String, CameraCharacteristics> characteristicsMap) {
-        return false;
+    public boolean isExtensionAvailable(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
-    public void init(String cameraId,
-            Map<String, CameraCharacteristics> characteristicsMap) {
+    public void init(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @Nullable
     public Range<Long> getEstimatedCaptureLatencyRange(
-            String cameraId, Size size, int imageFormat) {
-        return null;
+            @NonNull String cameraId, @Nullable Size size, int imageFormat) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public Map<Integer, List<Size>> getSupportedPreviewOutputResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public Map<Integer, List<Size>> getSupportedCaptureOutputResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
-    public List<Size> getSupportedYuvAnalysisResolutions(
-            String cameraId) {
-        return null;
+    @NonNull
+    public Map<Integer, List<Size>> getSupportedPostviewResolutions(
+            @NonNull Size captureSize) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @Nullable
+    public List<Size> getSupportedYuvAnalysisResolutions(@NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    @NonNull
     public SessionProcessorImpl createSessionProcessor() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    @NonNull
+    public boolean isCaptureProcessProgressAvailable() {
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    public boolean isPostviewAvailable() {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2OutputConfigImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2OutputConfigImpl.java
index 68de01b..e20ac87 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2OutputConfigImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2OutputConfigImpl.java
@@ -16,15 +16,16 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
+import androidx.annotation.Nullable;
 
 import java.util.List;
 
 /**
  * A config representing a {@link android.hardware.camera2.params.OutputConfiguration} where
  * Surface will be created by the information in this config.
+ *
+ * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public interface Camera2OutputConfigImpl {
     /**
      * Gets thd id of this output config. The id can be used to identify the stream in vendor
@@ -41,11 +42,13 @@
     /**
      * Gets the physical camera id. Returns null if not specified.
      */
+    @Nullable
     String getPhysicalCameraId();
 
     /**
      * If non-null, enable surface sharing and add the surface constructed by the return
      * Camera2OutputConfig.
      */
+    @Nullable
     List<Camera2OutputConfigImpl> getSurfaceSharingOutputConfigs();
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2OutputConfigImplBuilder.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2OutputConfigImplBuilder.java
index a66d3ce..4de3ede 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2OutputConfigImplBuilder.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2OutputConfigImplBuilder.java
@@ -16,11 +16,13 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.hardware.camera2.params.OutputConfiguration;
 import android.util.Size;
 import android.view.Surface;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -28,15 +30,15 @@
 /**
  * A builder implementation to help OEM build the {@link Camera2OutputConfigImpl} instance.
  */
-@SuppressLint("UnknownNullness")
 public class Camera2OutputConfigImplBuilder {
     static AtomicInteger sLastId = new AtomicInteger(0);
     private OutputConfigImplImpl mOutputConfig;
     private int mSurfaceGroupId = OutputConfiguration.SURFACE_GROUP_ID_NONE;
+    private int mOutputConfigId = -1;
     private String mPhysicalCameraId;
     private List<Camera2OutputConfigImpl> mSurfaceSharingConfigs;
 
-    private Camera2OutputConfigImplBuilder(OutputConfigImplImpl outputConfig) {
+    private Camera2OutputConfigImplBuilder(@NonNull OutputConfigImplImpl outputConfig) {
         mOutputConfig = outputConfig;
     }
 
@@ -48,8 +50,9 @@
      * Creates a {@link Camera2OutputConfigImpl} that represents a {@link android.media.ImageReader}
      * with the given parameters.
      */
+    @NonNull
     public static Camera2OutputConfigImplBuilder newImageReaderConfig(
-            Size size, int imageFormat, int maxImages) {
+            @NonNull Size size, int imageFormat, int maxImages) {
         return new Camera2OutputConfigImplBuilder(
                 new ImageReaderOutputConfigImplImpl(size, imageFormat, maxImages));
     }
@@ -58,6 +61,7 @@
      * Creates a {@link Camera2OutputConfigImpl} that represents a MultiResolutionImageReader with
      * the given parameters.
      */
+    @NonNull
     public static Camera2OutputConfigImplBuilder newMultiResolutionImageReaderConfig(
             int imageFormat, int maxImages) {
         return new Camera2OutputConfigImplBuilder(
@@ -67,15 +71,17 @@
     /**
      * Creates a {@link Camera2OutputConfigImpl} that contains the Surface directly.
      */
-    public static Camera2OutputConfigImplBuilder newSurfaceConfig(Surface surface) {
+    @NonNull
+    public static Camera2OutputConfigImplBuilder newSurfaceConfig(@NonNull Surface surface) {
         return new Camera2OutputConfigImplBuilder(new SurfaceOutputConfigImplImpl(surface));
     }
 
     /**
      * Adds a {@link Camera2SessionConfigImpl} to be shared with current config.
      */
+    @NonNull
     public Camera2OutputConfigImplBuilder addSurfaceSharingOutputConfig(
-            Camera2OutputConfigImpl camera2OutputConfig) {
+            @NonNull Camera2OutputConfigImpl camera2OutputConfig) {
         if (mSurfaceSharingConfigs == null) {
             mSurfaceSharingConfigs = new ArrayList<>();
         }
@@ -87,7 +93,8 @@
     /**
      * Sets a physical camera id.
      */
-    public Camera2OutputConfigImplBuilder setPhysicalCameraId(String physicalCameraId) {
+    @NonNull
+    public Camera2OutputConfigImplBuilder setPhysicalCameraId(@Nullable String physicalCameraId) {
         mPhysicalCameraId = physicalCameraId;
         return this;
     }
@@ -95,16 +102,32 @@
     /**
      * Sets surface group id.
      */
+    @NonNull
     public Camera2OutputConfigImplBuilder setSurfaceGroupId(int surfaceGroupId) {
         mSurfaceGroupId = surfaceGroupId;
         return this;
     }
 
     /**
+     * Sets Output Config id (Optional: Atomic Integer will be used if this function is not called)
+     */
+    @NonNull
+    public Camera2OutputConfigImplBuilder setOutputConfigId(int outputConfigId) {
+        mOutputConfigId = outputConfigId;
+        return this;
+    }
+
+    /**
      * Build a {@link Camera2OutputConfigImpl} instance.
      */
+    @NonNull
     public Camera2OutputConfigImpl build() {
-        mOutputConfig.setId(getNextId());
+        // Sets an output config id otherwise an output config id will be generated
+        if (mOutputConfigId == -1) {
+            mOutputConfig.setId(getNextId());
+        } else {
+            mOutputConfig.setId(mOutputConfigId);
+        }
         mOutputConfig.setPhysicalCameraId(mPhysicalCameraId);
         mOutputConfig.setSurfaceGroup(mSurfaceGroupId);
         mOutputConfig.setSurfaceSharingConfigs(mSurfaceSharingConfigs);
@@ -135,11 +158,13 @@
         }
 
         @Override
+        @Nullable
         public String getPhysicalCameraId() {
             return mPhysicalCameraId;
         }
 
         @Override
+        @Nullable
         public List<Camera2OutputConfigImpl> getSurfaceSharingOutputConfigs() {
             return mSurfaceSharingConfigs;
         }
@@ -152,25 +177,26 @@
             mSurfaceGroup = surfaceGroup;
         }
 
-        public void setPhysicalCameraId(String physicalCameraId) {
+        public void setPhysicalCameraId(@Nullable String physicalCameraId) {
             mPhysicalCameraId = physicalCameraId;
         }
 
         public void setSurfaceSharingConfigs(
-                List<Camera2OutputConfigImpl> surfaceSharingConfigs) {
+                @Nullable List<Camera2OutputConfigImpl> surfaceSharingConfigs) {
             mSurfaceSharingConfigs = surfaceSharingConfigs;
         }
     }
 
     private static class SurfaceOutputConfigImplImpl extends OutputConfigImplImpl
             implements SurfaceOutputConfigImpl {
-        private Surface mSurface;
+        private final Surface mSurface;
 
-        SurfaceOutputConfigImplImpl(Surface surface) {
+        SurfaceOutputConfigImplImpl(@NonNull Surface surface) {
             mSurface = surface;
         }
 
         @Override
+        @NonNull
         public Surface getSurface() {
             return mSurface;
         }
@@ -178,17 +204,18 @@
 
     private static class ImageReaderOutputConfigImplImpl extends OutputConfigImplImpl
             implements ImageReaderOutputConfigImpl {
-        private Size mSize;
-        private int mImageFormat;
-        private int mMaxImages;
+        private final Size mSize;
+        private final int mImageFormat;
+        private final int mMaxImages;
 
-        ImageReaderOutputConfigImplImpl(Size size, int imageFormat, int maxImages) {
+        ImageReaderOutputConfigImplImpl(@NonNull Size size, int imageFormat, int maxImages) {
             mSize = size;
             mImageFormat = imageFormat;
             mMaxImages = maxImages;
         }
 
         @Override
+        @NonNull
         public Size getSize() {
             return mSize;
         }
@@ -206,8 +233,8 @@
 
     private static class MultiResolutionImageReaderOutputConfigImplImpl extends OutputConfigImplImpl
             implements MultiResolutionImageReaderOutputConfigImpl {
-        private int mImageFormat;
-        private int mMaxImages;
+        private final int mImageFormat;
+        private final int mMaxImages;
 
         MultiResolutionImageReaderOutputConfigImplImpl(int imageFormat, int maxImages) {
             mImageFormat = imageFormat;
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2SessionConfigImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2SessionConfigImpl.java
index d121717..e0559fb 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2SessionConfigImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2SessionConfigImpl.java
@@ -16,26 +16,30 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.hardware.camera2.CaptureRequest;
 
+import androidx.annotation.NonNull;
+
 import java.util.List;
 import java.util.Map;
 
 /**
  * A config representing a {@link android.hardware.camera2.params.SessionConfiguration}
+ *
+ * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public interface Camera2SessionConfigImpl {
     /**
      * Returns all the {@link Camera2OutputConfigImpl}s that will be used to create
      * {@link android.hardware.camera2.params.OutputConfiguration}.
      */
+    @NonNull
     List<Camera2OutputConfigImpl> getOutputConfigs();
 
     /**
      * Gets all the parameters to create the session parameters with.
      */
+    @NonNull
     Map<CaptureRequest.Key<?>, Object> getSessionParameters();
 
     /**
@@ -43,4 +47,17 @@
      * {@link android.hardware.camera2.params.SessionConfiguration#setSessionParameters}.
      */
     int getSessionTemplateId();
-}
+
+
+    /**
+     * Retrieves the session type to be used when initializing the
+     * {@link android.hardware.camera2.CameraCaptureSession}.
+     *
+     * @return Camera capture session type. Regular and vendor specific types are supported but
+     * not high speed values. The extension can return -1 in which case the camera capture session
+     * will be configured to use the default regular type.
+     *
+     * @since 1.4
+     */
+    int getSessionType();
+}
\ No newline at end of file
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2SessionConfigImplBuilder.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2SessionConfigImplBuilder.java
index a301166..d1bb69a 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2SessionConfigImplBuilder.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/Camera2SessionConfigImplBuilder.java
@@ -16,10 +16,11 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-
-import android.annotation.SuppressLint;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.SessionConfiguration;
+
+import androidx.annotation.NonNull;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -29,9 +30,9 @@
 /**
  * A builder implementation to help OEM build the {@link Camera2SessionConfigImpl} instance.
  */
-@SuppressLint("UnknownNullness")
 public class Camera2SessionConfigImplBuilder {
     private int mSessionTemplateId = CameraDevice.TEMPLATE_PREVIEW;
+    private int mSessionType = SessionConfiguration.SESSION_REGULAR;
     Map<CaptureRequest.Key<?>, Object> mSessionParameters = new HashMap<>();
     List<Camera2OutputConfigImpl> mCamera2OutputConfigs = new ArrayList<>();
 
@@ -41,8 +42,9 @@
     /**
      * Adds a output config.
      */
+    @NonNull
     public Camera2SessionConfigImplBuilder addOutputConfig(
-            Camera2OutputConfigImpl outputConfig) {
+            @NonNull Camera2OutputConfigImpl outputConfig) {
         mCamera2OutputConfigs.add(outputConfig);
         return this;
     }
@@ -50,8 +52,9 @@
     /**
      * Sets session parameters.
      */
+    @NonNull
     public <T> Camera2SessionConfigImplBuilder addSessionParameter(
-            CaptureRequest.Key<T> key, T value) {
+            @NonNull CaptureRequest.Key<T> key, @NonNull T value) {
         mSessionParameters.put(key, value);
         return this;
     }
@@ -59,6 +62,7 @@
     /**
      * Sets the template id for session parameters request.
      */
+    @NonNull
     public Camera2SessionConfigImplBuilder setSessionTemplateId(int templateId) {
         mSessionTemplateId = templateId;
         return this;
@@ -74,6 +78,7 @@
     /**
      * Gets the session parameters.
      */
+    @NonNull
     public Map<CaptureRequest.Key<?>, Object> getSessionParameters() {
         return mSessionParameters;
     }
@@ -81,35 +86,48 @@
     /**
      * Gets all the output configs.
      */
+    @NonNull
     public List<Camera2OutputConfigImpl> getCamera2OutputConfigs() {
         return mCamera2OutputConfigs;
     }
 
     /**
+     * Gets the camera capture session type.
+     */
+    public int getSessionType() {
+        return mSessionType;
+    }
+
+    /**
      * Builds a {@link Camera2SessionConfigImpl} instance.
      */
+    @NonNull
     public Camera2SessionConfigImpl build() {
         return new Camera2SessionConfigImplImpl(this);
     }
 
     private static class Camera2SessionConfigImplImpl implements
             Camera2SessionConfigImpl {
-        int mSessionTemplateId;
-        Map<CaptureRequest.Key<?>, Object> mSessionParameters;
-        List<Camera2OutputConfigImpl> mCamera2OutputConfigs;
+        private final int mSessionTemplateId;
+        private final int mSessionType;
+        private final Map<CaptureRequest.Key<?>, Object> mSessionParameters;
+        private final List<Camera2OutputConfigImpl> mCamera2OutputConfigs;
 
-        Camera2SessionConfigImplImpl(Camera2SessionConfigImplBuilder builder) {
+        Camera2SessionConfigImplImpl(@NonNull Camera2SessionConfigImplBuilder builder) {
             mSessionTemplateId = builder.getSessionTemplateId();
             mSessionParameters = builder.getSessionParameters();
             mCamera2OutputConfigs = builder.getCamera2OutputConfigs();
+            mSessionType = builder.getSessionType();
         }
 
         @Override
+        @NonNull
         public List<Camera2OutputConfigImpl> getOutputConfigs() {
             return mCamera2OutputConfigs;
         }
 
         @Override
+        @NonNull
         public Map<CaptureRequest.Key<?>, Object> getSessionParameters() {
             return mSessionParameters;
         }
@@ -118,6 +136,11 @@
         public int getSessionTemplateId() {
             return mSessionTemplateId;
         }
+
+        @Override
+        public int getSessionType() {
+            return mSessionType;
+        }
     }
 }
 
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/HdrAdvancedExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/HdrAdvancedExtenderImpl.java
index 1621f3e..3d682ec 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/HdrAdvancedExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/HdrAdvancedExtenderImpl.java
@@ -16,13 +16,15 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.util.Range;
 import android.util.Size;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import java.util.List;
 import java.util.Map;
 
@@ -33,59 +35,82 @@
  *
  * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public class HdrAdvancedExtenderImpl implements AdvancedExtenderImpl {
     public HdrAdvancedExtenderImpl() {
     }
 
     @Override
-    public boolean isExtensionAvailable(String cameraId,
-            Map<String, CameraCharacteristics> characteristicsMap) {
-        return false;
+    public boolean isExtensionAvailable(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
-    public void init(String cameraId,
-            Map<String, CameraCharacteristics> characteristicsMap) {
+    public void init(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @Nullable
     public Range<Long> getEstimatedCaptureLatencyRange(
-            String cameraId, Size size, int imageFormat) {
-        return null;
+            @NonNull String cameraId, @Nullable Size size, int imageFormat) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public Map<Integer, List<Size>> getSupportedPreviewOutputResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
 
     @Override
+    @NonNull
     public Map<Integer, List<Size>> getSupportedCaptureOutputResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
-    public List<Size> getSupportedYuvAnalysisResolutions(
-            String cameraId) {
-        return null;
+    @NonNull
+    public Map<Integer, List<Size>> getSupportedPostviewResolutions(
+            @NonNull Size captureSize) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @Nullable
+    public List<Size> getSupportedYuvAnalysisResolutions(@NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    @NonNull
     public SessionProcessorImpl createSessionProcessor() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    public boolean isCaptureProcessProgressAvailable() {
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    public boolean isPostviewAvailable() {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/ImageProcessorImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/ImageProcessorImpl.java
index ce17c4f..841c6ac 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/ImageProcessorImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/ImageProcessorImpl.java
@@ -22,12 +22,13 @@
  * A interface to receive and process the upcoming next available Image.
  *
  * <p>Implemented by OEM.
+ *
+ * @since 1.2
  */
 @SuppressLint("UnknownNullness")
 public interface ImageProcessorImpl {
     /**
-     * The reference count will be decremented when this method returns. If an extension wants
-     * to hold onto the image it should increment the reference count in this method and
+     * The reference count will not be decremented when this method returns. Extensions must
      * decrement it when the image is no longer needed.
      *
      * <p>If OEM is not closing(decrement) the image fast enough, the imageReference passed
@@ -50,5 +51,5 @@
             long timestampNs,
             ImageReferenceImpl imageReference,
             String physicalCameraId
-            );
+    );
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/ImageReaderOutputConfigImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/ImageReaderOutputConfigImpl.java
index ca4dcaf..2144588 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/ImageReaderOutputConfigImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/ImageReaderOutputConfigImpl.java
@@ -16,17 +16,20 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.util.Size;
 
+import androidx.annotation.NonNull;
+
 /**
  * Surface will be created by constructing a ImageReader.
+ *
+ * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public interface ImageReaderOutputConfigImpl extends Camera2OutputConfigImpl {
     /**
      * Returns the size of the surface.
      */
+    @NonNull
     Size getSize();
 
     /**
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/ImageReferenceImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/ImageReferenceImpl.java
index 95f2c3b..647fbf9 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/ImageReferenceImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/ImageReferenceImpl.java
@@ -16,17 +16,19 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.media.Image;
 
+import androidx.annotation.Nullable;
+
 /**
  * A Image reference container that enables the Image sharing between Camera2/CameraX and OEM
  * using reference counting. The wrapped Image will be closed once the reference count
  * reaches 0.
  *
  * <p>Implemented by Camera2/CameraX.
+ *
+ * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public interface ImageReferenceImpl {
 
     /**
@@ -46,5 +48,6 @@
      * Return the Android image. This object MUST not be closed directly.
      * Returns null when the reference count is zero.
      */
+    @Nullable
     Image get();
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/MultiResolutionImageReaderOutputConfigImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/MultiResolutionImageReaderOutputConfigImpl.java
index c3ad61b..ccc229d 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/MultiResolutionImageReaderOutputConfigImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/MultiResolutionImageReaderOutputConfigImpl.java
@@ -18,6 +18,8 @@
 
 /**
  * Surface will be created by constructing a MultiResolutionImageReader.
+ *
+ * @since 1.2
  */
 public interface MultiResolutionImageReaderOutputConfigImpl extends Camera2OutputConfigImpl {
     /**
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/NightAdvancedExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/NightAdvancedExtenderImpl.java
index a5e0775..5c32de8 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/NightAdvancedExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/NightAdvancedExtenderImpl.java
@@ -16,13 +16,15 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.util.Range;
 import android.util.Size;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import java.util.List;
 import java.util.Map;
 
@@ -33,58 +35,82 @@
  *
  * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public class NightAdvancedExtenderImpl implements AdvancedExtenderImpl {
     public NightAdvancedExtenderImpl() {
     }
 
     @Override
-    public boolean isExtensionAvailable(String cameraId,
-            Map<String, CameraCharacteristics> characteristicsMap) {
-        return false;
+    public boolean isExtensionAvailable(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
-    public void init(String cameraId,
-            Map<String, CameraCharacteristics> characteristicsMap) {
+    public void init(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @Nullable
     public Range<Long> getEstimatedCaptureLatencyRange(
-            String cameraId, Size size, int imageFormat) {
-        return null;
+            @NonNull String cameraId, @Nullable Size size, int imageFormat) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public Map<Integer, List<Size>> getSupportedPreviewOutputResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public Map<Integer, List<Size>> getSupportedCaptureOutputResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
+    public Map<Integer, List<Size>> getSupportedPostviewResolutions(
+            @NonNull Size captureSize) {
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    @Nullable
     public List<Size> getSupportedYuvAnalysisResolutions(
-            String cameraId) {
-        return null;
+            @NonNull String cameraId) {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public SessionProcessorImpl createSessionProcessor() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 
     @Override
+    @NonNull
     public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
-        return null;
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    public boolean isCaptureProcessProgressAvailable() {
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+
+    @Override
+    public boolean isPostviewAvailable() {
+        throw new RuntimeException("Stub, replace with implementation.");
     }
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceConfigurationImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceConfigurationImpl.java
new file mode 100644
index 0000000..1e427b1
--- /dev/null
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceConfigurationImpl.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.impl.advanced;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * For specifying the output surface configurations for the extension.
+ *
+ * @since 1.4
+ */
+public interface OutputSurfaceConfigurationImpl {
+    /**
+     * gets the preview {@link OutputSurfaceImpl}, which may contain a <code>null</code> surface
+     * if the app doesn't specify the preview output surface.
+     */
+    @NonNull
+    OutputSurfaceImpl getPreviewOutputSurface();
+
+    /**
+     * gets the still capture {@link OutputSurfaceImpl} which may contain a <code>null</code>
+     * surface if the app doesn't specify the still capture output surface.
+     */
+    @NonNull
+    OutputSurfaceImpl getImageCaptureOutputSurface();
+
+    /**
+     * gets the image analysis {@link OutputSurfaceImpl}.
+     */
+    @Nullable
+    OutputSurfaceImpl getImageAnalysisOutputSurface();
+
+    /**
+     * gets the postview {@link OutputSurfaceImpl} which may contain a <code>null</code> surface
+     * if the app doesn't specify the postview output surface.
+     */
+    @Nullable
+    OutputSurfaceImpl getPostviewOutputSurface();
+}
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceImpl.java
index f692029..40b5446 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceImpl.java
@@ -16,23 +16,29 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.util.Size;
 import android.view.Surface;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 /**
  * For specifying output surface of the extension.
+ *
+ * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public interface OutputSurfaceImpl {
     /**
-     * Gets the surface.
+     * Gets the surface. It returns null if output surface is not specified.
      */
+    @Nullable
     Surface getSurface();
 
+
     /**
      * Gets the size.
      */
+    @NonNull
     Size getSize();
 
     /**
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/RequestProcessorImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/RequestProcessorImpl.java
index 5185333..5fc8a6f 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/RequestProcessorImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/RequestProcessorImpl.java
@@ -16,45 +16,47 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.hardware.camera2.CaptureFailure;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.TotalCaptureResult;
 
+import androidx.annotation.NonNull;
+
 import java.util.List;
 import java.util.Map;
 
 /**
  * An Interface to execute Camera2 capture requests.
+ *
+ * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public interface RequestProcessorImpl {
     /**
      * Sets a {@link ImageProcessorImpl} to receive {@link ImageReferenceImpl} to process.
      */
-    void setImageProcessor(int outputconfigId, ImageProcessorImpl imageProcessor);
+    void setImageProcessor(int outputconfigId, @NonNull ImageProcessorImpl imageProcessor);
 
     /**
      * Submits a request.
      * @return the id of the capture sequence or -1 in case the processor encounters a fatal error
      *         or receives an invalid argument.
      */
-    int submit(Request request, Callback callback);
+    int submit(@NonNull Request request, @NonNull Callback callback);
 
     /**
      * Submits a list of requests.
      * @return the id of the capture sequence or -1 in case the processor encounters a fatal error
      *         or receives an invalid argument.
      */
-    int submit(List<Request> requests, Callback callback);
+    int submit(@NonNull List<Request> requests, @NonNull Callback callback);
 
     /**
      * Set repeating requests.
      * @return the id of the capture sequence or -1 in case the processor encounters a fatal error
      *         or receives an invalid argument.
      */
-    int setRepeating(Request request, Callback callback);
+    int setRepeating(@NonNull Request request, @NonNull Callback callback);
 
 
     /**
@@ -76,16 +78,19 @@
          * Gets the target ids of {@link Camera2OutputConfigImpl} which identifies corresponding
          * Surface to be the targeted for the request.
          */
+        @NonNull
         List<Integer> getTargetOutputConfigIds();
 
         /**
          * Gets all the parameters.
          */
+        @NonNull
         Map<CaptureRequest.Key<?>, Object> getParameters();
 
         /**
          * Gets the template id.
          */
+        @NonNull
         Integer getTemplateId();
     }
 
@@ -94,24 +99,24 @@
      */
     interface Callback {
         void onCaptureStarted(
-                Request request,
+                @NonNull Request request,
                 long frameNumber,
                 long timestamp);
 
         void onCaptureProgressed(
-                Request request,
-                CaptureResult partialResult);
+                @NonNull Request request,
+                @NonNull CaptureResult partialResult);
 
         void onCaptureCompleted(
-                Request request,
-                TotalCaptureResult totalCaptureResult);
+                @NonNull Request request,
+                @NonNull TotalCaptureResult totalCaptureResult);
 
         void onCaptureFailed(
-                Request request,
-                CaptureFailure captureFailure);
+                @NonNull Request request,
+                @NonNull CaptureFailure captureFailure);
 
         void onCaptureBufferLost(
-                Request request,
+                @NonNull Request request,
                 long frameNumber,
                 int outputStreamId);
 
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
index e3ace72..1915015 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
@@ -16,7 +16,6 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
@@ -24,6 +23,7 @@
 import android.util.Pair;
 import android.view.Surface;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import java.util.Map;
@@ -59,17 +59,69 @@
  *
  * (6) {@link #deInitSession}: called when CameraCaptureSession is closed.
  * </pre>
+ *
+ * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public interface SessionProcessorImpl {
     /**
      * Initializes the session for the extension. This is where the OEMs allocate resources for
      * preparing a CameraCaptureSession. After initSession() is called, the camera ID,
      * cameraCharacteristics and context will not change until deInitSession() has been called.
      *
-     * <p>CameraX specifies the output surface configurations for preview, image capture and image
-     * analysis[optional]. And OEM returns a {@link Camera2SessionConfigImpl} which consists of a
-     * list of {@link Camera2OutputConfigImpl} and session parameters. The
+     * <p>CameraX / Camera2 specifies the output surface configurations for preview using
+     * {@link OutputSurfaceConfigurationImpl#getPreviewOutputSurface}, image capture using
+     * {@link OutputSurfaceConfigurationImpl#getImageCaptureOutputSurface}, and image analysis
+     * [optional] using {@link OutputSurfaceConfigurationImpl#getImageAnalysisOutputSurface}.
+     * And OEM returns a {@link Camera2SessionConfigImpl} which consists of a list of
+     * {@link Camera2OutputConfigImpl} and session parameters. The {@link Camera2SessionConfigImpl}
+     * will be used to configure the CameraCaptureSession.
+     *
+     * <p>OEM is responsible for outputting correct camera images output to these output surfaces.
+     * OEM can have the following options to enable the output:
+     * <pre>
+     * (1) Add these output surfaces in CameraCaptureSession directly using
+     * {@link Camera2OutputConfigImplBuilder#newSurfaceConfig(Surface)} }. Processing is done in
+     * HAL.
+     *
+     * (2) Use surface sharing with other surface by calling
+     * {@link Camera2OutputConfigImplBuilder#addSurfaceSharingOutputConfig(Camera2OutputConfigImpl)}
+     * to add the output surface to the other {@link Camera2OutputConfigImpl}.
+     *
+     * (3) Process output from other surfaces (RAW, YUV..) and write the result to the output
+     * surface. The output surface won't be contained in the returned
+     * {@link Camera2SessionConfigImpl}.
+     * </pre>
+     *
+     * <p>{@link Camera2OutputConfigImplBuilder} and {@link Camera2SessionConfigImplBuilder}
+     * implementations are provided in the stub for OEM to construct the
+     * {@link Camera2OutputConfigImpl} and {@link Camera2SessionConfigImpl} instances.
+     *
+     * @param surfaceConfigs contains output surfaces for preview, image capture, and an
+     *                       optional output config for image analysis (YUV_420_888).
+     * @return a {@link Camera2SessionConfigImpl} consisting of a list of
+     * {@link Camera2OutputConfigImpl} and session parameters which will decide the
+     * {@link android.hardware.camera2.params.SessionConfiguration} for configuring the
+     * CameraCaptureSession. Please note that the OutputConfiguration list may not be part of any
+     * supported or mandatory stream combination BUT OEM must ensure this list will always
+     * produce a valid camera capture session.
+     *
+     * @since 1.4
+     */
+    @NonNull
+    Camera2SessionConfigImpl initSession(
+            @NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> cameraCharacteristicsMap,
+            @NonNull Context context,
+            @NonNull OutputSurfaceConfigurationImpl surfaceConfigs);
+
+    /**
+     * Initializes the session for the extension. This is where the OEMs allocate resources for
+     * preparing a CameraCaptureSession. After initSession() is called, the camera ID,
+     * cameraCharacteristics and context will not change until deInitSession() has been called.
+     *
+     * <p>CameraX / Camera 2 specifies the output surface configurations for preview, image capture
+     * and image analysis[optional]. And OEM returns a {@link Camera2SessionConfigImpl} which
+     * consists of a list of {@link Camera2OutputConfigImpl} and session parameters. The
      * {@link Camera2SessionConfigImpl} will be used to configure the CameraCaptureSession.
      *
      * <p>OEM is responsible for outputting correct camera images output to these output surfaces.
@@ -92,8 +144,12 @@
      * implementations are provided in the stub for OEM to construct the
      * {@link Camera2OutputConfigImpl} and {@link Camera2SessionConfigImpl} instances.
      *
-     * @param previewSurfaceConfig       output surface for preview
-     * @param imageCaptureSurfaceConfig  output surface for image capture.
+     * @param previewSurfaceConfig       output surface for preview, which may contain a
+     *                                   <code>null</code> surface if the app doesn't specify the
+     *                                   preview surface.
+     * @param imageCaptureSurfaceConfig  output surface for still capture, which may contain a
+     *                                   <code>null</code> surface if the app doesn't specify the
+     *                                   still capture surface.
      * @param imageAnalysisSurfaceConfig an optional output config for image analysis
      *                                   (YUV_420_888).
      * @return a {@link Camera2SessionConfigImpl} consisting of a list of
@@ -103,13 +159,14 @@
      * supported or mandatory stream combination BUT OEM must ensure this list will always
      * produce a valid camera capture session.
      */
+    @NonNull
     Camera2SessionConfigImpl initSession(
-            String cameraId,
-            Map<String, CameraCharacteristics> cameraCharacteristicsMap,
-            Context context,
-            OutputSurfaceImpl previewSurfaceConfig,
-            OutputSurfaceImpl imageCaptureSurfaceConfig,
-            OutputSurfaceImpl imageAnalysisSurfaceConfig);
+            @NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> cameraCharacteristicsMap,
+            @NonNull Context context,
+            @NonNull OutputSurfaceImpl previewSurfaceConfig,
+            @NonNull OutputSurfaceImpl imageCaptureSurfaceConfig,
+            @Nullable OutputSurfaceImpl imageAnalysisSurfaceConfig);
 
     /**
      * Notify to de-initialize the extension. This callback will be invoked after
@@ -124,7 +181,7 @@
      * expected that the OEM would (eventually) update the repeating request if the keys are
      * supported. Setting a value to null explicitly un-sets the value.
      */
-    void setParameters(Map<CaptureRequest.Key<?>, Object> parameters);
+    void setParameters(@NonNull Map<CaptureRequest.Key<?>, Object> parameters);
 
     /**
      * CameraX / Camera2 will call this interface in response to client requests involving
@@ -140,7 +197,8 @@
      *
      * @since 1.3
      */
-    int startTrigger(Map<CaptureRequest.Key<?>, Object> triggers, CaptureCallback callback);
+    int startTrigger(@NonNull Map<CaptureRequest.Key<?>, Object> triggers,
+            @NonNull CaptureCallback callback);
 
     /**
      * This will be invoked once after the {@link android.hardware.camera2.CameraCaptureSession}
@@ -148,7 +206,7 @@
      * requests or set repeating requests. This ExtensionRequestProcessor will be valid to use
      * until onCaptureSessionEnd is called.
      */
-    void onCaptureSessionStart(RequestProcessorImpl requestProcessor);
+    void onCaptureSessionStart(@NonNull RequestProcessorImpl requestProcessor);
 
     /**
      * This will be invoked before the {@link android.hardware.camera2.CameraCaptureSession} is
@@ -165,7 +223,7 @@
      * @param callback a callback to report the status.
      * @return the id of the capture sequence.
      */
-    int startRepeating(CaptureCallback callback);
+    int startRepeating(@NonNull CaptureCallback callback);
 
     /**
      * Stop the repeating request. To prevent OEM from not calling stopRepeating, CameraX will
@@ -188,7 +246,27 @@
      * @param callback a callback to report the status.
      * @return the id of the capture sequence.
      */
-    int startCapture(CaptureCallback callback);
+    int startCapture(@NonNull CaptureCallback callback);
+
+    /**
+     * Start a multi-frame capture with a postview. {@link #startCapture(CaptureCallback)}
+     * will be used for captures without a postview request.
+     *
+     * Postview will be available before the capture. Upon postview completion,
+     * {@code OnImageAvailableListener#onImageAvailable} will be called on the ImageReader
+     * that creates the postview output surface. When the capture is completed,
+     * {@link CaptureCallback#onCaptureSequenceCompleted} is called and
+     * {@code OnImageAvailableListener#onImageAvailable} will also be called on the ImageReader
+     * that creates the image capture output surface.
+     *
+     * <p>Only one capture can perform at a time. Starting a capture when another capture is
+     * running will cause onCaptureFailed to be called immediately.
+     *
+     * @param callback a callback to report the status.
+     * @return the id of the capture sequence.
+     * @since 1.4
+     */
+    int startCaptureWithPostview(@NonNull CaptureCallback callback);
 
     /**
      * Abort all capture tasks.
@@ -217,9 +295,7 @@
      * @since 1.4
      */
     @Nullable
-    default Pair<Long, Long> getRealtimeCaptureLatency() {
-        return null;
-    };
+    Pair<Long, Long> getRealtimeCaptureLatency();
 
     /**
      * Callback for notifying the status of {@link #startCapture(CaptureCallback)} and
@@ -303,8 +379,24 @@
          *                             as part of this callback. Both Camera2 and CameraX guarantee
          *                             that those two settings and results are always supported and
          *                             applied by the corresponding framework.
+         * @since 1.3
          */
-        void onCaptureCompleted(long timestamp, int captureSequenceId,
-                Map<CaptureResult.Key, Object> result);
+        default void onCaptureCompleted(long timestamp, int captureSequenceId,
+                @NonNull Map<CaptureResult.Key, Object> result) {}
+
+        /**
+         * Capture progress callback that needs to be called when the process capture is
+         * ongoing and includes the estimated progress of the processing.
+         *
+         * <p>Extensions must ensure that they always call this callback with monotonically
+         * increasing values.</p>
+         *
+         * <p>Extensions are allowed to trigger this callback multiple times but at the minimum the
+         * callback is expected to be called once when processing is done with value 100.</p>
+         *
+         * @param progress             Value between 0 and 100.
+         * @since 1.4
+         */
+        default void onCaptureProcessProgressed(int progress) {}
     }
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SurfaceOutputConfigImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SurfaceOutputConfigImpl.java
index 7b8d83c..72e8816 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SurfaceOutputConfigImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SurfaceOutputConfigImpl.java
@@ -16,16 +16,19 @@
 
 package androidx.camera.extensions.impl.advanced;
 
-import android.annotation.SuppressLint;
 import android.view.Surface;
 
+import androidx.annotation.NonNull;
+
 /**
  * Use Surface directly to create the OutputConfiguration.
+ *
+ * @since 1.2
  */
-@SuppressLint("UnknownNullness")
 public interface SurfaceOutputConfigImpl extends Camera2OutputConfigImpl {
     /**
      * Get the {@link Surface}. It'll return valid surface only when type is TYPE_SURFACE.
      */
+    @NonNull
     Surface getSurface();
 }
diff --git a/camera/camera-video/api/current.txt b/camera/camera-video/api/current.txt
index b9cc77a..7f1314e 100644
--- a/camera/camera-video/api/current.txt
+++ b/camera/camera-video/api/current.txt
@@ -2,10 +2,12 @@
 package androidx.camera.video {
 
   @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class AudioStats {
+    method public double getAudioAmplitude();
     method public abstract int getAudioState();
     method public abstract Throwable? getErrorCause();
     method public boolean hasAudio();
     method public boolean hasError();
+    field public static final double AUDIO_AMPLITUDE_NONE = 0.0;
     field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
     field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
     field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
@@ -14,6 +16,9 @@
     field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
   }
 
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAudioApi {
+  }
+
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalPersistentRecording {
   }
 
diff --git a/camera/camera-video/api/restricted_current.txt b/camera/camera-video/api/restricted_current.txt
index b9cc77a..7f1314e 100644
--- a/camera/camera-video/api/restricted_current.txt
+++ b/camera/camera-video/api/restricted_current.txt
@@ -2,10 +2,12 @@
 package androidx.camera.video {
 
   @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class AudioStats {
+    method public double getAudioAmplitude();
     method public abstract int getAudioState();
     method public abstract Throwable? getErrorCause();
     method public boolean hasAudio();
     method public boolean hasError();
+    field public static final double AUDIO_AMPLITUDE_NONE = 0.0;
     field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
     field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
     field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
@@ -14,6 +16,9 @@
     field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
   }
 
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAudioApi {
+  }
+
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalPersistentRecording {
   }
 
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
index 42f8ae0..3e39ad6 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
@@ -570,6 +570,10 @@
                 val profiles = createFakeEncoderProfilesProxy(size.width, size.height)
                 return VideoValidatedEncoderProfilesProxy.from(profiles)
             }
+
+            override fun isStabilizationSupported(): Boolean {
+                return false
+            }
         }
     }
 
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java b/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
index 7f171f5..dde69be 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
@@ -104,7 +104,6 @@
      * Should audio recording be disabled, any attempts to retrieve the amplitude will
      * return this value.
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
     public static final double AUDIO_AMPLITUDE_NONE = 0;
 
     @IntDef({AUDIO_STATE_ACTIVE, AUDIO_STATE_DISABLED, AUDIO_STATE_SOURCE_SILENCED,
@@ -168,8 +167,8 @@
     public abstract Throwable getErrorCause();
 
     /**
-     * Returns the maximum absolute amplitude of the audio most recently sampled. Returns
-     * {@link #AUDIO_AMPLITUDE_NONE} if audio is disabled.
+     * Returns the maximum absolute amplitude of the audio most recently sampled in the past 2
+     * nanoseconds
      *
      * <p>The amplitude is the maximum absolute value over all channels which the audio was
      * most recently sampled from.
@@ -177,10 +176,11 @@
      * <p>Amplitude is a relative measure of the maximum sound pressure/voltage range of the device
      * microphone.
      *
+     * <p>Returns {@link #AUDIO_AMPLITUDE_NONE} if audio is disabled.
      * <p>The amplitude value returned will be a double between {@code 0} and {@code 1}.
+     *
      */
     @OptIn(markerClass = ExperimentalAudioApi.class)
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
     public double getAudioAmplitude() {
         if (getAudioState() == AUDIO_STATE_DISABLED) {
             return AUDIO_AMPLITUDE_NONE;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/ExperimentalAudioApi.java b/camera/camera-video/src/main/java/androidx/camera/video/ExperimentalAudioApi.java
index bf07a6e..99de642 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/ExperimentalAudioApi.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/ExperimentalAudioApi.java
@@ -19,16 +19,14 @@
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
 import androidx.annotation.RequiresOptIn;
-import androidx.annotation.RestrictTo;
 
 import java.lang.annotation.Retention;
 
 /**
- * Denotes that the methods on retrieving audio amplitude data are experimental and may
+ * Denotes that the annotated element relates to an experimental audio feature and may
  * change in a future release.
  */
 @Retention(CLASS)
 @RequiresOptIn
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 public @interface ExperimentalAudioApi {
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
index 7b78e91..b9e560e 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
@@ -76,6 +76,8 @@
 
     private final EncoderProfilesProvider mProfilesProvider;
 
+    private boolean mIsStabilizationSupported = false;
+
     // Mappings of DynamicRange to recording capability information. The mappings are divided
     // into two collections based on the key's (DynamicRange) category, one for specified
     // DynamicRange and one for others. Specified DynamicRange means that its bit depth and
@@ -130,6 +132,9 @@
                 mCapabilitiesMapForFullySpecifiedDynamicRange.put(dynamicRange, capabilities);
             }
         }
+
+        // Video stabilization
+        mIsStabilizationSupported = cameraInfoInternal.isVideoStabilizationSupported();
     }
 
     /**
@@ -166,6 +171,11 @@
         return capabilities != null && capabilities.isQualitySupported(quality);
     }
 
+    @Override
+    public boolean isStabilizationSupported() {
+        return mIsStabilizationSupported;
+    }
+
     @Nullable
     @Override
     public VideoValidatedEncoderProfilesProxy getProfiles(@NonNull Quality quality,
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
index 02b857e..8f9fd80 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.video;
 
+import android.hardware.camera2.CaptureRequest;
 import android.util.Size;
 
 import androidx.annotation.NonNull;
@@ -114,6 +115,17 @@
     boolean isQualitySupported(@NonNull Quality quality, @NonNull DynamicRange dynamicRange);
 
     /**
+     * Returns if video stabilization is supported on the device.
+     *
+     * @return true if {@link CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE_ON} is supported,
+     * otherwise false.
+     *
+     * @see CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    boolean isStabilizationSupported();
+
+    /**
      * Gets the corresponding {@link VideoValidatedEncoderProfilesProxy} of the input quality and
      * dynamic range.
      *
@@ -193,5 +205,10 @@
                 @NonNull DynamicRange dynamicRange) {
             return false;
         }
+
+        @Override
+        public boolean isStabilizationSupported() {
+            return false;
+        }
     };
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 1b93298..8e8bbd6 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -35,6 +35,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_TARGET_FRAME_RATE;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_VIDEO_STABILIZATION_MODE;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
 import static androidx.camera.core.impl.utils.Threads.isMainThread;
 import static androidx.camera.core.impl.utils.TransformUtils.rectToString;
@@ -104,6 +105,7 @@
 import androidx.camera.core.impl.Timebase;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.impl.utils.Threads;
 import androidx.camera.core.impl.utils.TransformUtils;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
@@ -282,6 +284,14 @@
     }
 
     /**
+     * Returns whether video stabilization is enabled.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public boolean isVideoStabilizationEnabled() {
+        return getCurrentConfig().getVideoStabilizationMode() == StabilizationMode.ON;
+    }
+
+    /**
      * Sets the desired rotation of the output video.
      *
      * <p>Valid values include: {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
@@ -684,6 +694,7 @@
         // Use the frame rate range directly from the StreamSpec here (don't resolve it to the
         // default if unresolved).
         sessionConfigBuilder.setExpectedFrameRateRange(streamSpec.getExpectedFrameRateRange());
+        sessionConfigBuilder.setVideoStabilization(config.getVideoStabilizationMode());
         sessionConfigBuilder.addErrorListener(
                 (sessionConfig, error) -> resetPipeline(cameraId, config, streamSpec));
         if (USE_TEMPLATE_PREVIEW_BY_QUIRK) {
@@ -894,7 +905,7 @@
 
         sessionConfigBuilder.clearSurfaces();
         DynamicRange dynamicRange = streamSpec.getDynamicRange();
-        if (!isStreamError) {
+        if (!isStreamError && mDeferrableSurface != null) {
             if (isStreamActive) {
                 sessionConfigBuilder.addSurface(mDeferrableSurface, dynamicRange);
             } else {
@@ -1798,6 +1809,20 @@
             return this;
         }
 
+        /**
+         * Enable video stabilization.
+         *
+         * @param enabled True if enable, otherwise false.
+         * @return the current Builder.
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder<T> setVideoStabilizationEnabled(boolean enabled) {
+            getMutableConfig().insertOption(OPTION_VIDEO_STABILIZATION_MODE,
+                    enabled ? StabilizationMode.ON : StabilizationMode.OFF);
+            return this;
+        }
+
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         @Override
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewDelayWhenVideoCaptureIsBoundQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewDelayWhenVideoCaptureIsBoundQuirk.java
index efffa0c..558d22d 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewDelayWhenVideoCaptureIsBoundQuirk.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewDelayWhenVideoCaptureIsBoundQuirk.java
@@ -21,22 +21,20 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.impl.Quirk;
 
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Set;
-
 /**
  * <p>QuirkSummary
  *     Bug Id: b/223643510
  *     Description: Quirk indicates Preview is delayed on some Huawei devices when the Preview uses
- *                  certain resolutions and VideoCapture is bound.
+ *                  certain resolutions and VideoCapture is bound. The quirk applies on all
+ *                  Huawei devices since there is a certain number of unknown devices with this
+ *                  issue.
  *     Device(s): Some Huawei devices.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class PreviewDelayWhenVideoCaptureIsBoundQuirk implements Quirk {
 
-    private static final Set<String> HUAWEI_DEVICE_LIST = new HashSet<>(Arrays.asList(
+    /*
+    Known devices:
             "HWELE",  // P30
             "HW-02L", // P30 Pro
             "HWVOG",  // P30 Pro
@@ -44,15 +42,16 @@
             "HWLYA",  // Mate 20 Pro
             "HWCOL",  // Honor 10
             "HWPAR"   // Nova 3
-    ));
 
-    private static final Set<String> HUAWEI_MODEL_LIST = new HashSet<>(Arrays.asList(
+    Known models:
             "ELS-AN00", "ELS-TN00", "ELS-NX9", "ELS-N04"  // P40 Pro
-    ));
+
+    Known others:
+            mate40
+            honor v2
+     */
 
     static boolean load() {
-        return "Huawei".equalsIgnoreCase(Build.MANUFACTURER)
-                && (HUAWEI_DEVICE_LIST.contains(Build.DEVICE.toUpperCase(Locale.US))
-                || HUAWEI_MODEL_LIST.contains(Build.MODEL.toUpperCase(Locale.US)));
+        return "Huawei".equalsIgnoreCase(Build.MANUFACTURER);
     }
 }
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/QualitySelectorTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/QualitySelectorTest.kt
index 9fef206..78c7b9a 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/QualitySelectorTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/QualitySelectorTest.kt
@@ -438,6 +438,10 @@
             override fun isQualitySupported(quality: Quality, dynamicRange: DynamicRange): Boolean {
                 throw UnsupportedOperationException("Not supported.")
             }
+
+            override fun isStabilizationSupported(): Boolean {
+                return false
+            }
         }
     }
 }
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index 6f0e1a9..734deea 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -1354,6 +1354,14 @@
         ).isEqualTo(newImplementationOptionValue)
     }
 
+    @Test
+    fun canSetVideoStabilization() {
+        val videoCapture = VideoCapture.Builder(Recorder.Builder().build())
+            .setVideoStabilizationEnabled(true)
+            .build()
+        assertThat(videoCapture.isVideoStabilizationEnabled).isTrue()
+    }
+
     private fun testSurfaceRequestContainsExpected(
         quality: Quality = HD, // HD maps to 1280x720 (4:3)
         videoEncoderInfo: VideoEncoderInfo = createVideoEncoderInfo(),
@@ -1708,6 +1716,10 @@
                     return videoCapabilitiesMap[dynamicRange]?.isQualitySupported(quality) ?: false
                 }
 
+                override fun isStabilizationSupported(): Boolean {
+                    return false
+                }
+
                 override fun getProfiles(
                     quality: Quality,
                     dynamicRange: DynamicRange
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index d92bad2..b76e3c6 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -655,13 +655,17 @@
     /**
      * Captures a new still image and saves to a file along with application specified metadata.
      *
-     * <p> The callback will be called only once for every invocation of this method.
+     * <p>The callback will be called only once for every invocation of this method.
      *
-     * <p> By default, the saved image is mirrored to match the output of the preview if front
+     * <p>By default, the saved image is mirrored to match the output of the preview if front
      * camera is used. To override this behavior, the app needs to explicitly set the flag to
      * {@code false} using {@link ImageCapture.Metadata#setReversedHorizontal} and
      * {@link ImageCapture.OutputFileOptions.Builder#setMetadata}.
      *
+     * <p>The saved image is cropped to match the aspect ratio of the {@link PreviewView}. To
+     * take a picture with the maximum available resolution, make sure that the
+     * {@link PreviewView}'s aspect ratio is 4:3.
+     *
      * @param outputFileOptions  Options to store the newly captured image.
      * @param executor           The executor in which the callback methods will be run.
      * @param imageSavedCallback Callback to be called for the newly captured image.
@@ -1773,8 +1777,8 @@
      * <p>Valid zoom values range from {@link ZoomState#getMinZoomRatio()} to
      * {@link ZoomState#getMaxZoomRatio()}.
      *
-     * <p> No-ops if the camera is not ready. The {@link ListenableFuture} completes successfully
-     * in this case.
+     * <p>If the value is set before the camera is ready, {@link CameraController} waits for the
+     * camera to be ready and then sets the zoom ratio.
      *
      * @param zoomRatio The requested zoom ratio.
      * @return a {@link ListenableFuture} which is finished when camera is set to the given ratio.
@@ -1797,13 +1801,13 @@
     /**
      * Sets current zoom by a linear zoom value ranging from 0f to 1.0f.
      *
-     * <p> LinearZoom 0f represents the minimum zoom while linearZoom 1.0f represents the maximum
+     * <p>LinearZoom 0f represents the minimum zoom while linearZoom 1.0f represents the maximum
      * zoom. The advantage of linearZoom is that it ensures the field of view (FOV) varies
      * linearly with the linearZoom value, for use with slider UI elements (while
      * {@link #setZoomRatio(float)} works well for pinch-zoom gestures).
      *
-     * <p> No-ops if the camera is not ready. The {@link ListenableFuture} completes successfully
-     * in this case.
+     * <p>If the value is set before the camera is ready, {@link CameraController} waits for the
+     * camera to be ready and then sets the linear zoom.
      *
      * @return a {@link ListenableFuture} which is finished when camera is set to the given ratio.
      * It fails with {@link CameraControl.OperationCanceledException} if there is newer value
@@ -1840,8 +1844,8 @@
     /**
      * Enable the torch or disable the torch.
      *
-     * <p> No-ops if the camera is not ready. The {@link ListenableFuture} completes successfully
-     * in this case.
+     * <p>If the value is set before the camera is ready, {@link CameraController} waits for the
+     * camera to be ready and then enables the torch.
      *
      * @param torchEnabled true to turn on the torch, false to turn it off.
      * @return A {@link ListenableFuture} which is successful when the torch was changed to the
diff --git a/camera/integration-tests/coretestapp/build.gradle b/camera/integration-tests/coretestapp/build.gradle
index 2e5a792..410eb09 100644
--- a/camera/integration-tests/coretestapp/build.gradle
+++ b/camera/integration-tests/coretestapp/build.gradle
@@ -72,6 +72,7 @@
     implementation(libs.guavaAndroid)
     implementation(libs.espressoIdlingResource)
     implementation("androidx.appcompat:appcompat:1.3.0")
+    implementation("androidx.lifecycle:lifecycle-service:2.2.0")
     // MLKit library: Barcode scanner
     implementation(libs.mlkitBarcode) {
         exclude group: "androidx.fragment"
diff --git a/camera/integration-tests/coretestapp/lint-baseline.xml b/camera/integration-tests/coretestapp/lint-baseline.xml
index 4cbca68..e580691 100644
--- a/camera/integration-tests/coretestapp/lint-baseline.xml
+++ b/camera/integration-tests/coretestapp/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="cli" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="BanThreadSleep"
@@ -12,108 +12,90 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="CameraInfoInternal can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                if (cameraInfo instanceof CameraInfoInternal) {"
-        errorLine2="                                          ~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                    if (canDeviceWriteToMediaStore()) {"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="DeviceQuirks.getAll can only be called from within the same library (androidx.camera:camera-video)"
-        errorLine1="                    Quirks deviceQuirks = DeviceQuirks.getAll();"
-        errorLine2="                                                       ~~~~~~">
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="CameraInfoInternal can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    Quirks cameraQuirks = ((CameraInfoInternal) cameraInfo).getCameraQuirks();"
-        errorLine2="                                            ~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+        errorLine2="                                                               ~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="CameraInfoInternal.getCameraQuirks can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    Quirks cameraQuirks = ((CameraInfoInternal) cameraInfo).getCameraQuirks();"
-        errorLine2="                                                                            ~~~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+        errorLine2="                                                                                     ~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    if (deviceQuirks.contains(CrashWhenTakingPhotoWithAutoFlashAEModeQuirk.class)"
-        errorLine2="                                     ~~~~~~~~">
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                this, generateVideoFileOutputOptions(fileName, extension));"
+        errorLine2="                                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    if (deviceQuirks.contains(CrashWhenTakingPhotoWithAutoFlashAEModeQuirk.class)"
-        errorLine2="                                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                this, generateVideoFileOutputOptions(fileName, extension));"
+        errorLine2="                                                                     ~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                            || cameraQuirks.contains(ImageCaptureFailWithAutoFlashQuirk.class)"
-        errorLine2="                                            ~~~~~~~~">
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                this, generateVideoFileOutputOptions(fileName, extension));"
+        errorLine2="                                                                               ~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                            || cameraQuirks.contains(ImageCaptureFailWithAutoFlashQuirk.class)"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        videoFilePath = getAbsolutePathFromUri("
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                            || cameraQuirks.contains(ImageCaptureFlashNotFireQuirk.class)) {"
-        errorLine2="                                            ~~~~~~~~">
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                getApplicationContext().getContentResolver(),"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                            || cameraQuirks.contains(ImageCaptureFlashNotFireQuirk.class)) {"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="DeviceQuirks.get can only be called from within the same library (androidx.camera:camera-video)"
-        errorLine1="                    if (DeviceQuirks.get(MediaStoreVideoCannotWrite.class) != null) {"
-        errorLine2="                                     ~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="DeviceQuirks.get can only be called from within the same library (androidx.camera:camera-video)"
-        errorLine1="                    if (DeviceQuirks.get(MediaStoreVideoCannotWrite.class) != null) {"
-        errorLine2="                                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                uri"
+        errorLine2="                                ~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
@@ -183,6 +165,69 @@
 
     <issue
         id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (createParentFolder(pictureFolder)) {"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (createParentFolder(pictureFolder)) {"
+        errorLine2="                               ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                getAbsolutePathFromUri(getApplicationContext().getContentResolver(),"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                getAbsolutePathFromUri(getApplicationContext().getContentResolver(),"
+        errorLine2="                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        MediaStore.Video.Media.EXTERNAL_CONTENT_URI);"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (videoFilePath == null || !createParentFolder(videoFilePath)) {"
+        errorLine2="                                      ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (videoFilePath == null || !createParentFolder(videoFilePath)) {"
+        errorLine2="                                                         ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
         message="CameraXExecutors.mainThreadExecutor can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                            CameraXExecutors.mainThreadExecutor());"
         errorLine2="                                             ~~~~~~~~~~~~~~~~~~">
@@ -192,6 +237,159 @@
 
     <issue
         id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (!createParentFolder(pictureFolder)) {"
+        errorLine2="             ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (!createParentFolder(pictureFolder)) {"
+        errorLine2="                                ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="            if (canDeviceWriteToMediaStore()) {"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+        errorLine2="                                                       ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+        errorLine2="                                                                             ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        this, generateVideoFileOutputOptions(fileName, extension));"
+        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        this, generateVideoFileOutputOptions(fileName, extension));"
+        errorLine2="                                                             ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        this, generateVideoFileOutputOptions(fileName, extension));"
+        errorLine2="                                                                       ~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        String videoFilePath = getAbsolutePathFromUri(getContentResolver(),"
+        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        String videoFilePath = getAbsolutePathFromUri(getContentResolver(),"
+        errorLine2="                                                      ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                MediaStore.Video.Media.EXTERNAL_CONTENT_URI);"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (videoFilePath == null || !createParentFolder(videoFilePath)) {"
+        errorLine2="                                      ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (videoFilePath == null || !createParentFolder(videoFilePath)) {"
+        errorLine2="                                                         ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        videoFilePath = getAbsolutePathFromUri("
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                getApplicationContext().getContentResolver(),"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                uri"
+        errorLine2="                                ~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
         message="TransformationInfo.hasCameraTransform can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                                    mHasCameraTransform = transformationInfo.hasCameraTransform();"
         errorLine2="                                                                             ~~~~~~~~~~~~~~~~~~">
@@ -264,34 +462,34 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (E2ETestUtil.canDeviceWriteToMediaStore()) {"
-        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (FileUtil.canDeviceWriteToMediaStore()) {"
+        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    E2ETestUtil.generateVideoMediaStoreOptions(this.getContentResolver(),"
-        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                    FileUtil.generateVideoMediaStoreOptions(this.getContentResolver(),"
+        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    E2ETestUtil.generateVideoMediaStoreOptions(this.getContentResolver(),"
-        errorLine2="                                                               ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                    FileUtil.generateVideoMediaStoreOptions(this.getContentResolver(),"
+        errorLine2="                                                            ~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                            videoFileName));"
         errorLine2="                            ~~~~~~~~~~~~~">
         <location
@@ -300,52 +498,52 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    E2ETestUtil.generateVideoFileOutputOptions(videoFileName, &quot;mp4&quot;));"
-        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                    FileUtil.generateVideoFileOutputOptions(videoFileName, &quot;mp4&quot;));"
+        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    E2ETestUtil.generateVideoFileOutputOptions(videoFileName, &quot;mp4&quot;));"
-        errorLine2="                                                               ~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                    FileUtil.generateVideoFileOutputOptions(videoFileName, &quot;mp4&quot;));"
+        errorLine2="                                                            ~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    E2ETestUtil.generateVideoFileOutputOptions(videoFileName, &quot;mp4&quot;));"
-        errorLine2="                                                                              ~~~~~">
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                    FileUtil.generateVideoFileOutputOptions(videoFileName, &quot;mp4&quot;));"
+        errorLine2="                                                                           ~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        E2ETestUtil.writeTextToExternalFile(information,"
-        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        FileUtil.writeTextToExternalFile(information,"
+        errorLine2="                 ~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        E2ETestUtil.writeTextToExternalFile(information,"
-        errorLine2="                                            ~~~~~~~~~~~">
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        FileUtil.writeTextToExternalFile(information,"
+        errorLine2="                                         ~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                generateFileName(INFO_FILE_PREFIX, false), &quot;txt&quot;);"
         errorLine2="                ~~~~~~~~~~~~~~~~">
         <location
@@ -354,7 +552,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                generateFileName(INFO_FILE_PREFIX, false), &quot;txt&quot;);"
         errorLine2="                                                           ~~~~~">
         <location
@@ -363,36 +561,36 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (!isUnique &amp;&amp; !E2ETestUtil.isFileNameValid(prefix)) {"
-        errorLine2="                                      ~~~~~~~~~~~~~~~">
+        message="FileUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (!isUnique &amp;&amp; !FileUtil.isFileNameValid(prefix)) {"
+        errorLine2="                                   ~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (!isUnique &amp;&amp; !E2ETestUtil.isFileNameValid(prefix)) {"
-        errorLine2="                                                      ~~~~~~">
+        message="FileUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (!isUnique &amp;&amp; !FileUtil.isFileNameValid(prefix)) {"
+        errorLine2="                                                   ~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (E2ETestUtil.isFileNameValid(prefix)) {"
-        errorLine2="                        ~~~~~~~~~~~~~~~">
+        message="FileUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (FileUtil.isFileNameValid(prefix)) {"
+        errorLine2="                     ~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (E2ETestUtil.isFileNameValid(prefix)) {"
-        errorLine2="                                        ~~~~~~">
+        message="FileUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (FileUtil.isFileNameValid(prefix)) {"
+        errorLine2="                                     ~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXServiceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXServiceTest.kt
new file mode 100644
index 0000000..5ce4624
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXServiceTest.kt
@@ -0,0 +1,251 @@
+/*
+ * 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.camera.integration.core
+
+import android.Manifest
+import android.app.ActivityManager
+import android.app.Service
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.Build
+import android.os.IBinder
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.CameraSelector.LENS_FACING_BACK
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.UseCase
+import androidx.camera.integration.core.CameraXService.ACTION_BIND_USE_CASES
+import androidx.camera.integration.core.CameraXService.ACTION_START_RECORDING
+import androidx.camera.integration.core.CameraXService.ACTION_STOP_RECORDING
+import androidx.camera.integration.core.CameraXService.ACTION_TAKE_PICTURE
+import androidx.camera.integration.core.CameraXService.EXTRA_IMAGE_ANALYSIS_ENABLED
+import androidx.camera.integration.core.CameraXService.EXTRA_IMAGE_CAPTURE_ENABLED
+import androidx.camera.integration.core.CameraXService.EXTRA_VIDEO_CAPTURE_ENABLED
+import androidx.camera.testing.impl.CameraPipeConfigTestRule
+import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.CameraUtil.hasCameraWithLensFacing
+import androidx.camera.testing.impl.mocks.MockConsumer
+import androidx.camera.testing.impl.mocks.helpers.ArgumentCaptor
+import androidx.camera.testing.impl.mocks.helpers.CallTimes
+import androidx.camera.video.VideoCapture
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.rule.GrantPermissionRule
+import androidx.testutils.LifecycleOwnerUtils
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class CameraXServiceTest(
+    private val implName: String,
+    private val cameraXConfig: CameraXConfig
+) {
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+        CameraUtil.PreTestCameraIdList(cameraXConfig)
+    )
+
+    @get:Rule
+    val permissionRule: GrantPermissionRule =
+        GrantPermissionRule.grant(
+            Manifest.permission.WRITE_EXTERNAL_STORAGE,
+            Manifest.permission.RECORD_AUDIO,
+        )
+
+    @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() = listOf(
+            arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+            arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+        )
+    }
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val activityManager =
+        context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+    private lateinit var serviceConnection: ServiceConnection
+    private lateinit var service: CameraXService
+
+    @Before
+    fun setUp() = runBlocking {
+        assumeTrue(hasCameraWithLensFacing(LENS_FACING_BACK))
+        assumeFalse(isBackgroundRestricted())
+
+        service = bindService()
+
+        // Ensure service is started.
+        LifecycleOwnerUtils.waitUntilState(service, Lifecycle.State.STARTED)
+    }
+
+    @After
+    fun tearDown() {
+        if (this::service.isInitialized) {
+            service.deleteSavedMediaFiles()
+            context.unbindService(serviceConnection)
+            context.stopService(createServiceIntent())
+
+            // Ensure service is destroyed
+            LifecycleOwnerUtils.waitUntilState(service, Lifecycle.State.DESTROYED)
+        }
+    }
+
+    @Test
+    fun canStartServiceAsForeground() {
+        assertThat(isForegroundService(service)).isTrue()
+    }
+
+    @Test
+    fun canBindUseCases() {
+        // Arrange: set up onUseCaseBound callback.
+        val useCaseCallback = MockConsumer<Collection<UseCase>>()
+        service.setOnUseCaseBoundCallback(useCaseCallback)
+
+        // Act: bind VideoCapture and ImageCapture.
+        context.startService(createServiceIntent(ACTION_BIND_USE_CASES).apply {
+            putExtra(EXTRA_VIDEO_CAPTURE_ENABLED, true)
+            putExtra(EXTRA_IMAGE_CAPTURE_ENABLED, true)
+        })
+
+        // Assert: verify bound UseCases.
+        val captor = ArgumentCaptor<Collection<UseCase>>()
+        useCaseCallback.verifyAcceptCall(Collection::class.java, false, 3000L, CallTimes(1), captor)
+        assertThat(captor.value!!.map { it.javaClass }).containsExactly(
+            VideoCapture::class.java,
+            ImageCapture::class.java
+        )
+
+        // Act: rebind by ImageAnalysis.
+        useCaseCallback.clearAcceptCalls()
+        context.startService(createServiceIntent(ACTION_BIND_USE_CASES).apply {
+            putExtra(EXTRA_IMAGE_ANALYSIS_ENABLED, true)
+        })
+
+        // Assert: verify bound UseCases.
+        useCaseCallback.verifyAcceptCall(Collection::class.java, false, 3000L, CallTimes(1), captor)
+        assertThat(captor.value!!.map { it.javaClass }).containsExactly(
+            ImageAnalysis::class.java,
+        )
+    }
+
+    @Test
+    fun canReceiveAnalysisFrame() {
+        // Arrange.
+        context.startService(createServiceIntent(ACTION_BIND_USE_CASES).apply {
+            putExtra(EXTRA_IMAGE_ANALYSIS_ENABLED, true)
+        })
+
+        // Act.
+        val latch = service.acquireAnalysisFrameCountDownLatch()
+
+        // Assert.
+        assertThat(latch.await(15, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @Test
+    fun canTakePicture() {
+        // Arrange.
+        context.startService(createServiceIntent(ACTION_BIND_USE_CASES).apply {
+            putExtra(EXTRA_IMAGE_CAPTURE_ENABLED, true)
+        })
+
+        // Act.
+        val latch = service.acquireTakePictureCountDownLatch()
+        context.startService(createServiceIntent(ACTION_TAKE_PICTURE))
+
+        // Assert.
+        assertThat(latch.await(15, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @Test
+    fun canRecordVideo() = runBlocking {
+        // Arrange.
+        context.startService(createServiceIntent(ACTION_BIND_USE_CASES).apply {
+            putExtra(EXTRA_VIDEO_CAPTURE_ENABLED, true)
+        })
+
+        // Act.
+        val latch = service.acquireRecordVideoCountDownLatch()
+        context.startService(createServiceIntent(ACTION_START_RECORDING))
+
+        delay(3000L)
+
+        context.startService(createServiceIntent(ACTION_STOP_RECORDING))
+
+        // Assert.
+        assertThat(latch.await(15, TimeUnit.SECONDS)).isTrue()
+    }
+
+    private fun createServiceIntent(action: String? = null) =
+        Intent(context, CameraXService::class.java).apply {
+            action?.let { setAction(it) }
+        }
+
+    private fun isForegroundService(service: Service): Boolean {
+        val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+        @Suppress("DEPRECATION")
+        for (serviceInfo in manager.getRunningServices(Int.MAX_VALUE)) {
+            if (service.javaClass.name == serviceInfo.service.className) {
+                return serviceInfo.foreground
+            }
+        }
+        return false
+    }
+
+    private suspend fun bindService(): CameraXService {
+        val serviceDeferred = CompletableDeferred<CameraXService>()
+        serviceConnection = object : ServiceConnection {
+            override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+                val binder = service as CameraXService.CameraXServiceBinder
+                serviceDeferred.complete(binder.service)
+            }
+
+            override fun onServiceDisconnected(name: ComponentName?) {
+            }
+        }
+        context.bindService(createServiceIntent(), serviceConnection, Service.BIND_AUTO_CREATE)
+        return withTimeout(3000L) {
+            serviceDeferred.await()
+        }
+    }
+
+    private fun isBackgroundRestricted(): Boolean =
+        if (Build.VERSION.SDK_INT >= 28) activityManager.isBackgroundRestricted else false
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 94eb4bd..e10ff38 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -64,6 +64,7 @@
 import androidx.camera.core.impl.utils.CameraOrientationUtil
 import androidx.camera.core.impl.utils.Exif
 import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability
+import androidx.camera.core.internal.compat.workaround.InvalidJpegDataParser
 import androidx.camera.core.resolutionselector.AspectRatioStrategy
 import androidx.camera.core.resolutionselector.ResolutionFilter
 import androidx.camera.core.resolutionselector.ResolutionSelector
@@ -1309,7 +1310,14 @@
         val useCase = builder.build()
         var camera: Camera
         withContext(Dispatchers.Main) {
-            camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
+            camera = cameraProvider.bindToLifecycle(
+                fakeLifecycleOwner,
+                BACK_SELECTOR,
+                useCase,
+                Preview.Builder().build().apply {
+                    setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
+                }
+            )
         }
 
         val callback = FakeImageCaptureCallback(capturesCount = 1)
@@ -1341,7 +1349,14 @@
         val useCase = builder.build()
         var camera: Camera
         withContext(Dispatchers.Main) {
-            camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
+            camera = cameraProvider.bindToLifecycle(
+                fakeLifecycleOwner,
+                BACK_SELECTOR,
+                useCase,
+                Preview.Builder().build().apply {
+                    setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
+                }
+            )
         }
 
         val saveLocation = temporaryFolder.newFile("test.jpg")
@@ -1365,7 +1380,14 @@
         val useCase = builder.build()
         var camera: Camera
         withContext(Dispatchers.Main) {
-            camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
+            camera = cameraProvider.bindToLifecycle(
+                fakeLifecycleOwner,
+                BACK_SELECTOR,
+                useCase,
+                Preview.Builder().build().apply {
+                    setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
+                }
+            )
         }
 
         val callback = FakeImageCaptureCallback(capturesCount = 1)
@@ -1396,7 +1418,14 @@
         val useCase = builder.build()
         var camera: Camera
         withContext(Dispatchers.Main) {
-            camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
+            camera = cameraProvider.bindToLifecycle(
+                fakeLifecycleOwner,
+                BACK_SELECTOR,
+                useCase,
+                Preview.Builder().build().apply {
+                    setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
+                }
+            )
         }
 
         val callback = FakeImageCaptureCallback(capturesCount = 1)
@@ -1500,6 +1529,22 @@
         assertThat(imageProperties.format).isEqualTo(ImageFormat.JPEG)
     }
 
+    @Test
+    fun canCaptureImage_whenOnlyImageCaptureBound_withYuvBufferFormat() {
+        val cameraHwLevel = CameraUtil.getCameraCharacteristics(CameraSelector.LENS_FACING_BACK)
+            ?.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
+        assumeTrue(
+            "TODO(b/298138582): Check if MeteringRepeating will need to be added while" +
+                " choosing resolution for ImageCapture",
+            cameraHwLevel != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY &&
+                cameraHwLevel != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+
+        canTakeImages(ImageCapture.Builder().apply {
+            setBufferFormat(ImageFormat.YUV_420_888)
+        })
+    }
+
     private fun getCameraSelectorWithSessionProcessor(
         cameraSelector: CameraSelector,
         sessionProcessor: SessionProcessor
@@ -1758,6 +1803,89 @@
         assertThat(imageProperties.size).isEqualTo(maxHighResolutionOutputSize)
     }
 
+    /**
+     * See b/288828159 for the detailed info of the issue
+     */
+    @Test
+    fun jpegImageZeroPaddingDataDetectionTest(): Unit = runBlocking {
+        val imageCapture = ImageCapture.Builder().build()
+
+        withContext(Dispatchers.Main) {
+            cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
+        }
+
+        val latch = CountdownDeferred(1)
+        var errors: Exception? = null
+
+        val callback = object : ImageCapture.OnImageCapturedCallback() {
+            override fun onCaptureSuccess(image: ImageProxy) {
+                val planes = image.planes
+                val buffer = planes[0].buffer
+                val data = ByteArray(buffer.capacity())
+                buffer.rewind()
+                buffer[data]
+
+                image.close()
+
+                val invalidJpegDataParser = InvalidJpegDataParser()
+
+                // Only checks the unnecessary zero padding data when the device is not included in
+                // the LargeJpegImageQuirk device list. InvalidJpegDataParser#getValidDataLength()
+                // should have returned the valid data length to avoid the extremely large JPEG
+                // file issue.
+                if (invalidJpegDataParser.getValidDataLength(data) == data.size &&
+                    containsZeroPaddingDataAfterEoi(data)
+                ) {
+                    errors = Exception("UNNECESSARY_JPEG_ZERO_PADDING_DATA_DETECTED!")
+                }
+
+                latch.countDown()
+            }
+
+            override fun onError(exception: ImageCaptureException) {
+                errors = exception
+                latch.countDown()
+            }
+        }
+
+        imageCapture.takePicture(mainExecutor, callback)
+
+        // Wait for the signal that the image has been captured.
+        assertThat(withTimeoutOrNull(CAPTURE_TIMEOUT) {
+            latch.await()
+        }).isNotNull()
+        assertThat(errors).isNull()
+    }
+
+    /**
+     * This util function is only used to detect the unnecessary zero padding data after EOI. It
+     * will directly return false when it fails to parse the JPEG byte array data.
+     */
+    private fun containsZeroPaddingDataAfterEoi(bytes: ByteArray): Boolean {
+        val jfifEoiMarkEndPosition = InvalidJpegDataParser.getJfifEoiMarkEndPosition(bytes)
+
+        // Directly returns false when EOI mark can't be found.
+        if (jfifEoiMarkEndPosition == -1) {
+            return false
+        }
+
+        // Will check 1mb data to know whether unnecessary zero padding data exists or not.
+        // Directly returns false when the data length is long enough
+        val dataLengthToDetect = 1_000_000
+        if (jfifEoiMarkEndPosition + dataLengthToDetect > bytes.size) {
+            return false
+        }
+
+        // Checks that there are at least continuous 1mb of unnecessary zero padding data after EOI
+        for (position in jfifEoiMarkEndPosition..jfifEoiMarkEndPosition + dataLengthToDetect) {
+            if (bytes[position] != 0x00.toByte()) {
+                return false
+            }
+        }
+
+        return true
+    }
+
     private fun createNonRotatedConfiguration(): ImageCaptureConfig {
         // Create a configuration with target rotation that matches the sensor rotation.
         // This assumes a back-facing camera (facing away from screen)
diff --git a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
index 425ca58..5ee37e2 100644
--- a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
@@ -76,6 +76,18 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <service
+            android:name=".CameraXService"
+            android:exported="true"
+            android:foregroundServiceType="camera|microphone"
+            android:label="CameraX Service">
+            <intent-filter>
+                <action android:name="androidx.camera.integration.core.intent.action.BIND_USE_CASES" />
+                <action android:name="androidx.camera.integration.core.intent.action.TAKE_PICTURE" />
+                <action android:name="androidx.camera.integration.core.intent.action.START_RECORDING" />
+                <action android:name="androidx.camera.integration.core.intent.action.STOP_RECORDING" />
+            </intent-filter>
+        </service>
     </application>
 
     <uses-feature android:glEsVersion="0x00020000" />
@@ -85,4 +97,7 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
 </manifest>
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index c653b5f..e20ec12 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -25,21 +25,24 @@
 import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
 import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
+import static androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore;
+import static androidx.camera.testing.impl.FileUtil.createParentFolder;
+import static androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions;
+import static androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions;
+import static androidx.camera.testing.impl.FileUtil.getAbsolutePathFromUri;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE;
+import static java.util.Objects.requireNonNull;
 
 import android.Manifest;
 import android.annotation.SuppressLint;
-import android.content.ContentResolver;
 import android.content.ContentValues;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
-import android.database.Cursor;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.display.DisplayManager;
 import android.media.MediaScannerConnection;
@@ -126,8 +129,6 @@
 import androidx.camera.video.VideoCapabilities;
 import androidx.camera.video.VideoCapture;
 import androidx.camera.video.VideoRecordEvent;
-import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
-import androidx.camera.video.internal.compat.quirk.MediaStoreVideoCannotWrite;
 import androidx.core.content.ContextCompat;
 import androidx.core.math.MathUtils;
 import androidx.core.util.Consumer;
@@ -320,6 +321,8 @@
     private Button mZoomIn2XToggle;
     private Button mZoomResetToggle;
     private Toast mEvToast = null;
+    private Toast mPSToast = null;
+    private ToggleButton mPreviewStabilizationToggle;
 
     private OpenGLRenderer mPreviewRenderer;
     private DisplayManager.DisplayListener mDisplayListener;
@@ -328,6 +331,7 @@
     private DynamicRange mDynamicRange = DynamicRange.SDR;
     private final Set<DynamicRange> mSelectableDynamicRanges = new HashSet<>();
     private int mVideoMirrorMode = MIRROR_MODE_ON_FRONT_ONLY;
+    private boolean mIsPreviewStabilizationOn = false;
 
     SessionMediaUriSet mSessionImagesUriSet = new SessionMediaUriSet();
     SessionMediaUriSet mSessionVideosUriSet = new SessionMediaUriSet();
@@ -357,6 +361,9 @@
 
         @Override
         public void onSuccess(@Nullable Integer result) {
+            if (result == null) {
+                return;
+            }
             CameraInfo cameraInfo = getCameraInfo();
             if (cameraInfo != null) {
                 ExposureState exposureState = cameraInfo.getExposureState();
@@ -478,7 +485,7 @@
     @VisibleForTesting
     public void resetViewIdlingResource() {
         mPreviewFrameCount.set(0);
-        // Make the view idling resource non-idle, until required framecount achieved.
+        // Make the view idling resource non-idle, until required frame count achieved.
         if (mViewIdlingResource.isIdleNow()) {
             mViewIdlingResource.increment();
         }
@@ -541,6 +548,7 @@
         return mPhotoToggle.isChecked() && cameraInfo != null && cameraInfo.hasFlashUnit();
     }
 
+    @SuppressLint("RestrictedApiAndroidX")
     private boolean isFlashTestSupported(@ImageCapture.FlashMode int flashMode) {
         switch (flashMode) {
             case FLASH_MODE_OFF:
@@ -548,7 +556,8 @@
             case FLASH_MODE_AUTO:
                 CameraInfo cameraInfo = getCameraInfo();
                 if (cameraInfo instanceof CameraInfoInternal) {
-                    Quirks deviceQuirks = DeviceQuirks.getAll();
+                    Quirks deviceQuirks =
+                            androidx.camera.camera2.internal.compat.quirk.DeviceQuirks.getAll();
                     Quirks cameraQuirks = ((CameraInfoInternal) cameraInfo).getCameraQuirks();
                     if (deviceQuirks.contains(CrashWhenTakingPhotoWithAutoFlashAEModeQuirk.class)
                             || cameraQuirks.contains(ImageCaptureFailWithAutoFlashQuirk.class)
@@ -594,14 +603,17 @@
                 case IDLE:
                     createDefaultVideoFolderIfNotExist();
                     final PendingRecording pendingRecording;
-                    if (DeviceQuirks.get(MediaStoreVideoCannotWrite.class) != null) {
-                        // Use FileOutputOption for devices in MediaStoreVideoCannotWrite Quirk.
-                        pendingRecording = getVideoCapture().getOutput().prepareRecording(
-                                this, getNewVideoFileOutputOptions());
-                    } else {
+                    String fileName = "video_" + System.currentTimeMillis();
+                    String extension = "mp4";
+                    if (canDeviceWriteToMediaStore()) {
                         // Use MediaStoreOutputOptions for public share media storage.
                         pendingRecording = getVideoCapture().getOutput().prepareRecording(
-                                this, getNewVideoOutputMediaStoreOptions());
+                                this,
+                                generateVideoMediaStoreOptions(getContentResolver(), fileName));
+                    } else {
+                        // Use FileOutputOption for devices in MediaStoreVideoCannotWrite Quirk.
+                        pendingRecording = getVideoCapture().getOutput().prepareRecording(
+                                this, generateVideoFileOutputOptions(fileName, extension));
                     }
 
                     resetVideoSavedIdlingResource();
@@ -804,32 +816,6 @@
         }
     }
 
-    @NonNull
-    private MediaStoreOutputOptions getNewVideoOutputMediaStoreOptions() {
-        String videoFileName = "video_" + System.currentTimeMillis();
-        ContentValues contentValues = new ContentValues();
-        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
-        contentValues.put(MediaStore.Video.Media.TITLE, videoFileName);
-        contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName);
-        contentValues.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
-        contentValues.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
-        return new MediaStoreOutputOptions.Builder(getContentResolver(),
-                MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
-                .setContentValues(contentValues)
-                .build();
-    }
-
-    @NonNull
-    private FileOutputOptions getNewVideoFileOutputOptions() {
-        String videoFileName = "video_" + System.currentTimeMillis() + ".mp4";
-        File videoFolder = Environment.getExternalStoragePublicDirectory(
-                Environment.DIRECTORY_MOVIES);
-        if (!videoFolder.exists() && !videoFolder.mkdirs()) {
-            Log.e(TAG, "Failed to create directory: " + videoFolder);
-        }
-        return new FileOutputOptions.Builder(new File(videoFolder, videoFileName)).build();
-    }
-
     private void updateRecordingStats(@NonNull RecordingStats stats) {
         double durationMs = TimeUnit.NANOSECONDS.toMillis(stats.getRecordedDurationNanos());
         // Show megabytes in International System of Units (SI)
@@ -888,7 +874,7 @@
                                                 Toast.LENGTH_SHORT).show());
                                         if (mSessionImagesUriSet != null) {
                                             mSessionImagesUriSet.add(
-                                                    Objects.requireNonNull(
+                                                    requireNonNull(
                                                             outputFileResults.getSavedUri()));
                                         }
                                     }
@@ -954,7 +940,7 @@
                     Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
                 }
             } catch (IllegalArgumentException e) {
-                Toast.makeText(this, "Failed to swich Camera. Error:" + e.getMessage(),
+                Toast.makeText(this, "Failed to switch Camera. Error:" + e.getMessage(),
                         Toast.LENGTH_SHORT).show();
             }
         });
@@ -1006,8 +992,8 @@
 
     private void setUpTorchButton() {
         mTorchButton.setOnClickListener(v -> {
-            Objects.requireNonNull(getCameraInfo());
-            Objects.requireNonNull(getCameraControl());
+            requireNonNull(getCameraInfo());
+            requireNonNull(getCameraControl());
             Integer torchState = getCameraInfo().getTorchState().getValue();
             boolean toggledState = !Objects.equals(torchState, TorchState.ON);
             Log.d(TAG, "Set camera torch: " + toggledState);
@@ -1027,8 +1013,8 @@
 
     private void setUpEVButton() {
         mPlusEV.setOnClickListener(v -> {
-            Objects.requireNonNull(getCameraInfo());
-            Objects.requireNonNull(getCameraControl());
+            requireNonNull(getCameraInfo());
+            requireNonNull(getCameraControl());
 
             ExposureState exposureState = getCameraInfo().getExposureState();
             Range<Integer> range = exposureState.getExposureCompensationRange();
@@ -1046,8 +1032,8 @@
         });
 
         mDecEV.setOnClickListener(v -> {
-            Objects.requireNonNull(getCameraInfo());
-            Objects.requireNonNull(getCameraControl());
+            requireNonNull(getCameraInfo());
+            requireNonNull(getCameraControl());
 
             ExposureState exposureState = getCameraInfo().getExposureState();
             Range<Integer> range = exposureState.getExposureCompensationRange();
@@ -1073,6 +1059,14 @@
         mEvToast.show();
     }
 
+    void showPreviewStabilizationToast(String message) {
+        if (mPSToast != null) {
+            mPSToast.cancel();
+        }
+        mPSToast = Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT);
+        mPSToast.show();
+    }
+
     private void updateAppUIForE2ETest(@NonNull String testCase) {
         if (getSupportActionBar() != null) {
             getSupportActionBar().hide();
@@ -1118,19 +1112,19 @@
                 ViewGroup.LayoutParams lp = viewFinderStub.getLayoutParams();
                 if (orientation == Configuration.ORIENTATION_PORTRAIT) {
                     lp.width = displayMetrics.widthPixels;
-                    lp.height = (int) (displayMetrics.widthPixels / ratio.getDenominator()
-                            * ratio.getNumerator());
+                    lp.height = displayMetrics.widthPixels / ratio.getDenominator()
+                            * ratio.getNumerator();
                 } else {
                     lp.height = displayMetrics.heightPixels;
-                    lp.width = (int) (displayMetrics.heightPixels / ratio.getDenominator()
-                            * ratio.getNumerator());
+                    lp.width = displayMetrics.heightPixels / ratio.getDenominator()
+                            * ratio.getNumerator();
                 }
                 viewFinderStub.setLayoutParams(lp);
             }
         }
     }
 
-    @SuppressLint("NullAnnotationGroup")
+    @SuppressLint({"NullAnnotationGroup", "RestrictedApiAndroidX"})
     @OptIn(markerClass = androidx.camera.core.ExperimentalZeroShutterLag.class)
     private void updateButtonsUi() {
         mRecordUi.setEnabled(mVideoToggle.isChecked());
@@ -1140,6 +1134,8 @@
                 && getCameraInfo().isZslSupported() ? View.VISIBLE : View.GONE);
         mZslToggle.setEnabled(mPhotoToggle.isChecked());
         mCameraDirectionButton.setEnabled(getCameraInfo() != null);
+        mPreviewStabilizationToggle.setEnabled(mCamera != null
+                && mCamera.getCameraInfo().getPreviewCapabilities().isStabilizationSupported());
         mTorchButton.setEnabled(isFlashAvailable());
         // Flash button
         mFlashButton.setEnabled(mPhotoToggle.isChecked() && isFlashAvailable());
@@ -1182,6 +1178,7 @@
         setUpTorchButton();
         setUpEVButton();
         setUpZoomButton();
+        setUpPreviewStabilizationButton();
         mCaptureQualityToggle.setOnCheckedChangeListener(mOnCheckedChangeListener);
         mZslToggle.setOnCheckedChangeListener(mOnCheckedChangeListener);
     }
@@ -1277,6 +1274,7 @@
         mPlusEV = findViewById(R.id.plus_ev_toggle);
         mDecEV = findViewById(R.id.dec_ev_toggle);
         mZslToggle = findViewById(R.id.zsl_toggle);
+        mPreviewStabilizationToggle = findViewById(R.id.preview_stabilization);
         mZoomSeekBar = findViewById(R.id.seekBar);
         mZoomRatioLabel = findViewById(R.id.zoomRatio);
         mZoomIn2XToggle = findViewById(R.id.zoom_in_2x_toggle);
@@ -1327,7 +1325,7 @@
         };
 
         DisplayManager dpyMgr =
-                Objects.requireNonNull((DisplayManager) getSystemService(Context.DISPLAY_SERVICE));
+                requireNonNull(ContextCompat.getSystemService(this, DisplayManager.class));
         dpyMgr.registerDisplayListener(mDisplayListener, new Handler(Looper.getMainLooper()));
 
         StrictMode.VmPolicy vmPolicy =
@@ -1444,7 +1442,7 @@
     public void onDestroy() {
         super.onDestroy();
         DisplayManager dpyMgr =
-                Objects.requireNonNull((DisplayManager) getSystemService(Context.DISPLAY_SERVICE));
+                requireNonNull(ContextCompat.getSystemService(this, DisplayManager.class));
         dpyMgr.unregisterDisplayListener(mDisplayListener);
         mPreviewRenderer.shutdown();
         mImageCaptureExecutorService.shutdown();
@@ -1575,23 +1573,22 @@
         // Remove ImageAnalysis to check whether the new use cases combination can be supported.
         if (mAnalysisToggle.isChecked()) {
             mAnalysisToggle.setChecked(false);
-            if (isCheckedUseCasesCombinationSupported()) {
-                return;
-            }
+            // No need to do further use case combination check since Preview + ImageCapture
+            // should be always supported.
         }
-
-        // Preview + ImageCapture should be always supported.
     }
 
     /**
      * Builds all use cases based on current settings and return as an array.
      */
+    @SuppressLint("RestrictedApiAndroidX")
     private List<UseCase> buildUseCases() {
         List<UseCase> useCases = new ArrayList<>();
         if (mPreviewToggle.isChecked()) {
             Preview preview = new Preview.Builder()
                     .setTargetName("Preview")
                     .setTargetAspectRatio(mTargetAspectRatio)
+                    .setPreviewStabilizationEnabled(mIsPreviewStabilizationOn)
                     .build();
             resetViewIdlingResource();
             // Use the listener of the future to make sure the Preview setup the new surface.
@@ -1685,7 +1682,7 @@
                             new ActivityResultContracts.RequestMultiplePermissions(),
                             result -> {
                                 for (String permission : REQUIRED_PERMISSIONS) {
-                                    if (!Objects.requireNonNull(result.get(permission))) {
+                                    if (!requireNonNull(result.get(permission))) {
                                         Toast.makeText(getApplicationContext(),
                                                         "Camera permission denied.",
                                                         Toast.LENGTH_SHORT)
@@ -1718,10 +1715,8 @@
     void createDefaultPictureFolderIfNotExist() {
         File pictureFolder = Environment.getExternalStoragePublicDirectory(
                 Environment.DIRECTORY_PICTURES);
-        if (!pictureFolder.exists()) {
-            if (!pictureFolder.mkdir()) {
-                Log.e(TAG, "Failed to create directory: " + pictureFolder);
-            }
+        if (createParentFolder(pictureFolder)) {
+            Log.e(TAG, "Failed to create directory: " + pictureFolder);
         }
     }
 
@@ -1730,17 +1725,8 @@
         String videoFilePath =
                 getAbsolutePathFromUri(getApplicationContext().getContentResolver(),
                         MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
-
-        // If cannot get the video path, just skip checking and create folder.
-        if (videoFilePath == null) {
-            return;
-        }
-        File videoFile = new File(videoFilePath);
-
-        if (videoFile.getParentFile() != null && !videoFile.getParentFile().exists()) {
-            if (!videoFile.getParentFile().mkdir()) {
-                Log.e(TAG, "Failed to create directory: " + videoFile);
-            }
+        if (videoFilePath == null || !createParentFolder(videoFilePath)) {
+            Log.e(TAG, "Failed to create parent directory for: " + videoFilePath);
         }
     }
 
@@ -1776,13 +1762,14 @@
     ScaleGestureDetector.SimpleOnScaleGestureListener mScaleGestureListener =
             new ScaleGestureDetector.SimpleOnScaleGestureListener() {
                 @Override
-                public boolean onScale(ScaleGestureDetector detector) {
+                public boolean onScale(@NonNull ScaleGestureDetector detector) {
                     if (mCamera == null) {
                         return true;
                     }
 
                     CameraInfo cameraInfo = mCamera.getCameraInfo();
-                    float newZoom = cameraInfo.getZoomState().getValue().getZoomRatio()
+                    float newZoom =
+                            requireNonNull(cameraInfo.getZoomState().getValue()).getZoomRatio()
                             * detector.getScaleFactor();
                     setZoomRatio(newZoom);
                     return true;
@@ -1792,7 +1779,7 @@
     GestureDetector.OnGestureListener onTapGestureListener =
             new GestureDetector.SimpleOnGestureListener() {
                 @Override
-                public boolean onSingleTapUp(MotionEvent e) {
+                public boolean onSingleTapUp(@NonNull MotionEvent e) {
                     if (mCamera == null) {
                         return false;
                     }
@@ -1832,7 +1819,8 @@
 
         mZoomSeekBar.setMax(MAX_SEEKBAR_VALUE);
         mZoomSeekBar.setProgress(
-                (int) (cameraInfo.getZoomState().getValue().getLinearZoom() * MAX_SEEKBAR_VALUE));
+                (int) (requireNonNull(cameraInfo.getZoomState().getValue()).getLinearZoom()
+                        * MAX_SEEKBAR_VALUE));
         mZoomSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
             @Override
             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
@@ -1882,7 +1870,7 @@
     private boolean is2XZoomSupported() {
         CameraInfo cameraInfo = getCameraInfo();
         return cameraInfo != null
-                && cameraInfo.getZoomState().getValue().getMaxZoomRatio() >= 2.0f;
+                && requireNonNull(cameraInfo.getZoomState().getValue()).getMaxZoomRatio() >= 2.0f;
     }
 
     private void setUpZoomButton() {
@@ -1891,6 +1879,16 @@
         mZoomResetToggle.setOnClickListener(v -> setZoomRatio(1.0f));
     }
 
+    private void setUpPreviewStabilizationButton() {
+        mPreviewStabilizationToggle.setOnClickListener(v -> {
+            mIsPreviewStabilizationOn = !mIsPreviewStabilizationOn;
+            if (mIsPreviewStabilizationOn) {
+                showPreviewStabilizationToast("Preview Stabilization On, FOV changes");
+            }
+            tryBindUseCases();
+        });
+    }
+
     void setZoomRatio(float newZoom) {
         if (mCamera == null) {
             return;
@@ -1899,7 +1897,7 @@
         CameraInfo cameraInfo = mCamera.getCameraInfo();
         CameraControl cameraControl = mCamera.getCameraControl();
         float clampedNewZoom = MathUtils.clamp(newZoom,
-                cameraInfo.getZoomState().getValue().getMinZoomRatio(),
+                requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio(),
                 cameraInfo.getZoomState().getValue().getMaxZoomRatio());
 
         Log.d(TAG, "setZoomRatio ratio: " + clampedNewZoom);
@@ -1930,34 +1928,6 @@
         });
     }
 
-    /** Gets the absolute path from a Uri. */
-    @Nullable
-    public String getAbsolutePathFromUri(@NonNull ContentResolver resolver,
-            @NonNull Uri contentUri) {
-        Cursor cursor = null;
-        try {
-            // We should include in any Media collections.
-            String[] proj;
-            int columnIndex;
-            // MediaStore.Video.Media.DATA was deprecated in API level 29.
-            proj = new String[]{MediaStore.Video.Media.DATA};
-            cursor = resolver.query(contentUri, proj, null, null, null);
-            columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA);
-
-            cursor.moveToFirst();
-            return cursor.getString(columnIndex);
-        } catch (RuntimeException e) {
-            Log.e(TAG, String.format(
-                    "Failed in getting absolute path for Uri %s with Exception %s",
-                    contentUri, e));
-            return "";
-        } finally {
-            if (cursor != null) {
-                cursor.close();
-            }
-        }
-    }
-
     private class SessionMediaUriSet {
         private final Set<Uri> mSessionMediaUris;
 
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXService.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXService.java
new file mode 100644
index 0000000..ac8e9cf
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXService.java
@@ -0,0 +1,482 @@
+/*
+ * 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.camera.integration.core;
+
+import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
+import static androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore;
+import static androidx.camera.testing.impl.FileUtil.createParentFolder;
+import static androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions;
+import static androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions;
+import static androidx.camera.testing.impl.FileUtil.getAbsolutePathFromUri;
+import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED;
+import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED;
+import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE;
+import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE;
+import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Environment;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.UseCase;
+import androidx.camera.core.UseCaseGroup;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.video.FileOutputOptions;
+import androidx.camera.video.MediaStoreOutputOptions;
+import androidx.camera.video.OutputOptions;
+import androidx.camera.video.PendingRecording;
+import androidx.camera.video.Recorder;
+import androidx.camera.video.Recording;
+import androidx.camera.video.VideoCapture;
+import androidx.camera.video.VideoRecordEvent;
+import androidx.core.app.NotificationCompat;
+import androidx.core.content.ContextCompat;
+import androidx.core.util.Consumer;
+import androidx.lifecycle.LifecycleService;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.File;
+import java.text.Format;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * A service used to test background UseCases binding and camera operations.
+ */
+public class CameraXService extends LifecycleService {
+    private static final String TAG = "CameraXService";
+    private static final int NOTIFICATION_ID = 1;
+    private static final String CHANNEL_ID_SERVICE_INFO = "channel_service_info";
+
+    // Actions
+    public static final String ACTION_BIND_USE_CASES =
+            "androidx.camera.integration.core.intent.action.BIND_USE_CASES";
+    public static final String ACTION_TAKE_PICTURE =
+            "androidx.camera.integration.core.intent.action.TAKE_PICTURE";
+    public static final String ACTION_START_RECORDING =
+            "androidx.camera.integration.core.intent.action.START_RECORDING";
+    public static final String ACTION_STOP_RECORDING =
+            "androidx.camera.integration.core.intent.action.STOP_RECORDING";
+
+    // Extras
+    public static final String EXTRA_VIDEO_CAPTURE_ENABLED = "EXTRA_VIDEO_CAPTURE_ENABLED";
+    public static final String EXTRA_IMAGE_CAPTURE_ENABLED = "EXTRA_IMAGE_CAPTURE_ENABLED";
+    public static final String EXTRA_IMAGE_ANALYSIS_ENABLED = "EXTRA_IMAGE_ANALYSIS_ENABLED";
+
+    private final IBinder mBinder = new CameraXServiceBinder();
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    //                          Members only accessed on main thread                              //
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    private final Map<Class<?>, UseCase> mBoundUseCases = new HashMap<>();
+    @Nullable
+    private Recording mActiveRecording;
+    //--------------------------------------------------------------------------------------------//
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    //                                   Members for testing                                      //
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    private final Set<Uri> mSavedMediaUri = new HashSet<>();
+
+    @Nullable
+    private Consumer<Collection<UseCase>> mOnUseCaseBoundCallback;
+    @Nullable
+    private CountDownLatch mAnalysisFrameLatch;
+    @Nullable
+    private CountDownLatch mTakePictureLatch;
+    @Nullable
+    private CountDownLatch mRecordVideoLatch;
+    //--------------------------------------------------------------------------------------------//
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        makeForeground();
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(@NonNull Intent intent) {
+        super.onBind(intent);
+        return mBinder;
+    }
+
+    @Override
+    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
+        if (intent != null) {
+            String action = intent.getAction();
+            Log.d(TAG, "onStartCommand: action = " + action + ", extras = " + intent.getExtras());
+            if (ACTION_BIND_USE_CASES.equals(action)) {
+                bindToLifecycle(intent);
+            } else if (ACTION_TAKE_PICTURE.equals(action)) {
+                takePicture();
+            } else if (ACTION_START_RECORDING.equals(action)) {
+                startRecording();
+            } else if (ACTION_STOP_RECORDING.equals(action)) {
+                stopRecording();
+            }
+        }
+        return super.onStartCommand(intent, flags, startId);
+    }
+
+    private void makeForeground() {
+        createNotificationChannel();
+        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this,
+                CHANNEL_ID_SERVICE_INFO)
+                .setSmallIcon(android.R.drawable.ic_menu_camera)
+                .setStyle(new NotificationCompat.DecoratedCustomViewStyle());
+        startForeground(NOTIFICATION_ID, notificationBuilder.build());
+    }
+
+    private void bindToLifecycle(@NonNull Intent intent) {
+        ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
+                ProcessCameraProvider.getInstance(this);
+        ProcessCameraProvider cameraProvider;
+        try {
+            cameraProvider = cameraProviderFuture.get();
+        } catch (ExecutionException | InterruptedException e) {
+            throw new IllegalStateException(e);
+        }
+        cameraProvider.unbindAll();
+        mBoundUseCases.clear();
+        UseCaseGroup useCaseGroup = resolveUseCaseGroup(intent);
+        List<UseCase> boundUseCases = Collections.emptyList();
+        if (useCaseGroup != null) {
+            try {
+                cameraProvider.bindToLifecycle(this, DEFAULT_BACK_CAMERA, useCaseGroup);
+                boundUseCases = useCaseGroup.getUseCases();
+            } catch (IllegalArgumentException e) {
+                Log.w(TAG, "Failed to bind by " + e, e);
+            }
+        }
+        Log.d(TAG, "Bound UseCases: " + boundUseCases);
+        for (UseCase boundUseCase : boundUseCases) {
+            mBoundUseCases.put(boundUseCase.getClass(), boundUseCase);
+        }
+        if (mOnUseCaseBoundCallback != null) {
+            mOnUseCaseBoundCallback.accept(boundUseCases);
+        }
+    }
+
+    @Nullable
+    private UseCaseGroup resolveUseCaseGroup(@NonNull Intent intent) {
+        boolean hasUseCase = false;
+        UseCaseGroup.Builder useCaseGroupBuilder = new UseCaseGroup.Builder();
+
+        if (intent.getBooleanExtra(EXTRA_VIDEO_CAPTURE_ENABLED, false)) {
+            Recorder recorder = new Recorder.Builder().build();
+            VideoCapture<?> videoCapture = new VideoCapture.Builder<>(recorder).build();
+            useCaseGroupBuilder.addUseCase(videoCapture);
+            hasUseCase = true;
+        }
+        if (intent.getBooleanExtra(EXTRA_IMAGE_CAPTURE_ENABLED, false)) {
+            ImageCapture imageCapture = new ImageCapture.Builder().build();
+            useCaseGroupBuilder.addUseCase(imageCapture);
+            hasUseCase = true;
+        }
+        if (intent.getBooleanExtra(EXTRA_IMAGE_ANALYSIS_ENABLED, false)) {
+            ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build();
+            imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this), mAnalyzer);
+            useCaseGroupBuilder.addUseCase(imageAnalysis);
+            hasUseCase = true;
+        }
+
+        return hasUseCase ? useCaseGroupBuilder.build() : null;
+    }
+
+    private void createNotificationChannel() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            NotificationChannel serviceChannel = Api26Impl.newNotificationChannel(
+                    CHANNEL_ID_SERVICE_INFO,
+                    getString(R.string.camerax_service),
+                    NotificationManager.IMPORTANCE_DEFAULT
+            );
+            Api26Impl.createNotificationChannel(getNotificationManager(), serviceChannel);
+        }
+    }
+
+    @NonNull
+    private NotificationManager getNotificationManager() {
+        return checkNotNull(ContextCompat.getSystemService(this, NotificationManager.class));
+    }
+
+    @Nullable
+    private ImageCapture getImageCapture() {
+        return (ImageCapture) mBoundUseCases.get(ImageCapture.class);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Nullable
+    private VideoCapture<Recorder> getVideoCapture() {
+        return (VideoCapture<Recorder>) mBoundUseCases.get(VideoCapture.class);
+    }
+
+    private void takePicture() {
+        ImageCapture imageCapture = getImageCapture();
+        if (imageCapture == null) {
+            Log.w(TAG, "ImageCapture is not bound.");
+            return;
+        }
+        createDefaultPictureFolderIfNotExist();
+        Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US);
+        String fileName = "ServiceTestApp-" + formatter.format(Calendar.getInstance().getTime())
+                + ".jpg";
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
+        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
+        ImageCapture.OutputFileOptions outputFileOptions =
+                new ImageCapture.OutputFileOptions.Builder(
+                        getContentResolver(),
+                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                        contentValues).build();
+        long startTimeMs = SystemClock.elapsedRealtime();
+        imageCapture.takePicture(outputFileOptions,
+                ContextCompat.getMainExecutor(this),
+                new ImageCapture.OnImageSavedCallback() {
+                    @Override
+                    public void onImageSaved(
+                            @NonNull ImageCapture.OutputFileResults outputFileResults) {
+                        long durationMs = SystemClock.elapsedRealtime() - startTimeMs;
+                        Log.d(TAG, "Saved image " + outputFileResults.getSavedUri()
+                                + "  (" + durationMs + " ms)");
+                        mSavedMediaUri.add(outputFileResults.getSavedUri());
+                        if (mTakePictureLatch != null) {
+                            mTakePictureLatch.countDown();
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull ImageCaptureException exception) {
+                        Log.e(TAG, "Failed to save image by " + exception.getImageCaptureError(),
+                                exception);
+                    }
+                });
+    }
+
+    private void createDefaultPictureFolderIfNotExist() {
+        File pictureFolder = Environment.getExternalStoragePublicDirectory(
+                Environment.DIRECTORY_PICTURES);
+        if (!createParentFolder(pictureFolder)) {
+            Log.e(TAG, "Failed to create directory: " + pictureFolder);
+        }
+    }
+
+    private void startRecording() {
+        VideoCapture<Recorder> videoCapture = getVideoCapture();
+        if (videoCapture == null) {
+            Log.w(TAG, "VideoCapture is not bound.");
+            return;
+        }
+
+        createDefaultVideoFolderIfNotExist();
+        if (mActiveRecording == null) {
+            PendingRecording pendingRecording;
+            String fileName = "video_" + System.currentTimeMillis();
+            String extension = "mp4";
+            if (canDeviceWriteToMediaStore()) {
+                // Use MediaStoreOutputOptions for public share media storage.
+                pendingRecording = getVideoCapture().getOutput().prepareRecording(
+                        this,
+                        generateVideoMediaStoreOptions(getContentResolver(), fileName));
+            } else {
+                // Use FileOutputOption for devices in MediaStoreVideoCannotWrite Quirk.
+                pendingRecording = getVideoCapture().getOutput().prepareRecording(
+                        this, generateVideoFileOutputOptions(fileName, extension));
+            }
+            //noinspection MissingPermission
+            mActiveRecording = pendingRecording
+                    .withAudioEnabled()
+                    .start(ContextCompat.getMainExecutor(this), mRecordingListener);
+        } else {
+            Log.e(TAG, "It should stop the active recording before start a new one.");
+        }
+    }
+
+    private void stopRecording() {
+        if (mActiveRecording != null) {
+            mActiveRecording.stop();
+            mActiveRecording = null;
+        }
+    }
+
+    private void createDefaultVideoFolderIfNotExist() {
+        String videoFilePath = getAbsolutePathFromUri(getContentResolver(),
+                MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
+        if (videoFilePath == null || !createParentFolder(videoFilePath)) {
+            Log.e(TAG, "Failed to create parent directory for: " + videoFilePath);
+        }
+    }
+
+    private final ImageAnalysis.Analyzer mAnalyzer = image -> {
+        if (mAnalysisFrameLatch != null) {
+            mAnalysisFrameLatch.countDown();
+        }
+        image.close();
+    };
+
+    private final Consumer<VideoRecordEvent> mRecordingListener = event -> {
+        if (event instanceof VideoRecordEvent.Finalize) {
+            VideoRecordEvent.Finalize finalize = (VideoRecordEvent.Finalize) event;
+
+            switch (finalize.getError()) {
+                case ERROR_NONE:
+                case ERROR_FILE_SIZE_LIMIT_REACHED:
+                case ERROR_DURATION_LIMIT_REACHED:
+                case ERROR_INSUFFICIENT_STORAGE:
+                case ERROR_SOURCE_INACTIVE:
+                    Uri uri = finalize.getOutputResults().getOutputUri();
+                    OutputOptions outputOptions = finalize.getOutputOptions();
+                    String msg;
+                    String videoFilePath;
+                    if (outputOptions instanceof MediaStoreOutputOptions) {
+                        msg = "Saved video " + uri;
+                        videoFilePath = getAbsolutePathFromUri(
+                                getApplicationContext().getContentResolver(),
+                                uri
+                        );
+                    } else if (outputOptions instanceof FileOutputOptions) {
+                        videoFilePath = ((FileOutputOptions) outputOptions).getFile().getPath();
+                        MediaScannerConnection.scanFile(this,
+                                new String[]{videoFilePath}, null,
+                                (path, uri1) -> Log.i(TAG, "Scanned " + path + " -> uri= " + uri1));
+                        msg = "Saved video " + videoFilePath;
+                    } else {
+                        throw new AssertionError("Unknown or unsupported OutputOptions type: "
+                                + outputOptions.getClass().getSimpleName());
+                    }
+                    // The video file path is used in tracing e2e test log. Don't remove it.
+                    Log.d(TAG, "Saved video file: " + videoFilePath);
+
+                    if (finalize.getError() != ERROR_NONE) {
+                        msg += " with code (" + finalize.getError() + ")";
+                    }
+                    Log.d(TAG, msg, finalize.getCause());
+
+                    mSavedMediaUri.add(uri);
+                    if (mRecordVideoLatch != null) {
+                        mRecordVideoLatch.countDown();
+                    }
+                    break;
+                default:
+                    String errMsg = "Video capture failed by (" + finalize.getError() + "): "
+                            + finalize.getCause();
+                    Log.e(TAG, errMsg, finalize.getCause());
+            }
+            mActiveRecording = null;
+        }
+    };
+
+    @RequiresApi(26)
+    static class Api26Impl {
+
+        private Api26Impl() {
+        }
+
+        /** @noinspection SameParameterValue */
+        @DoNotInline
+        @NonNull
+        static NotificationChannel newNotificationChannel(@NonNull String id,
+                @NonNull CharSequence name, int importance) {
+            return new NotificationChannel(id, name, importance);
+        }
+
+        @DoNotInline
+        static void createNotificationChannel(@NonNull NotificationManager manager,
+                @NonNull NotificationChannel channel) {
+            manager.createNotificationChannel(channel);
+        }
+    }
+
+    @VisibleForTesting
+    void setOnUseCaseBoundCallback(@NonNull Consumer<Collection<UseCase>> callback) {
+        mOnUseCaseBoundCallback = callback;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    CountDownLatch acquireAnalysisFrameCountDownLatch() {
+        mAnalysisFrameLatch = new CountDownLatch(3);
+        return mAnalysisFrameLatch;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    CountDownLatch acquireTakePictureCountDownLatch() {
+        mTakePictureLatch = new CountDownLatch(1);
+        return mTakePictureLatch;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    CountDownLatch acquireRecordVideoCountDownLatch() {
+        mRecordVideoLatch = new CountDownLatch(1);
+        return mRecordVideoLatch;
+    }
+
+    @VisibleForTesting
+    void deleteSavedMediaFiles() {
+        deleteUriSet(mSavedMediaUri);
+    }
+
+    private void deleteUriSet(@NonNull Set<Uri> uriSet) {
+        for (Uri uri : uriSet) {
+            try {
+                getContentResolver().delete(uri, null, null);
+            } catch (RuntimeException e) {
+                Log.w(TAG, "Unable to delete uri: " + uri, e);
+            }
+        }
+    }
+
+    class CameraXServiceBinder extends Binder {
+        @NonNull
+        CameraXService getService() {
+            return CameraXService.this;
+        }
+    }
+}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
index 61a1f09..620f7e0 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
@@ -107,7 +107,7 @@
     @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
     @MainThread
     public static boolean isCameraProviderUnInitializedOrSameAsParameter(
-            @NonNull String cameraImplementation) {
+            @Nullable String cameraImplementation) {
 
         if (sConfiguredCameraXCameraImplementation == null) {
             return true;
@@ -116,11 +116,7 @@
                 sConfiguredCameraXCameraImplementation);
         cameraImplementation = getCameraProviderName(cameraImplementation);
 
-        if (currentCameraProvider.equals(cameraImplementation)) {
-            return true;
-        }
-
-        return false;
+        return currentCameraProvider.equals(cameraImplementation);
     }
 
     /**
@@ -129,7 +125,7 @@
      */
     @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
     @MainThread
-    private static String getCameraProviderName(String mCameraProvider) {
+    private static String getCameraProviderName(@Nullable String mCameraProvider) {
         if (mCameraProvider == null) {
             mCameraProvider = CAMERA2_IMPLEMENTATION_OPTION;
         }
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
index c775b3d..976b5f3 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
@@ -33,7 +33,7 @@
 import androidx.camera.core.Logger;
 import androidx.camera.core.Preview;
 import androidx.camera.lifecycle.ProcessCameraProvider;
-import androidx.camera.testing.impl.E2ETestUtil;
+import androidx.camera.testing.impl.FileUtil;
 import androidx.camera.video.ExperimentalPersistentRecording;
 import androidx.camera.video.PendingRecording;
 import androidx.camera.video.Recorder;
@@ -232,14 +232,14 @@
 
         final String videoFileName = generateFileName(VIDEO_FILE_PREFIX, true);
         final PendingRecording pendingRecording;
-        if (E2ETestUtil.canDeviceWriteToMediaStore()) {
+        if (FileUtil.canDeviceWriteToMediaStore()) {
             // Use MediaStoreOutputOptions for public share media storage.
             pendingRecording = mVideoCapture.getOutput().prepareRecording(this,
-                    E2ETestUtil.generateVideoMediaStoreOptions(this.getContentResolver(),
+                    FileUtil.generateVideoMediaStoreOptions(this.getContentResolver(),
                             videoFileName));
         } else {
             pendingRecording = mVideoCapture.getOutput().prepareRecording(this,
-                    E2ETestUtil.generateVideoFileOutputOptions(videoFileName, "mp4"));
+                    FileUtil.generateVideoFileOutputOptions(videoFileName, "mp4"));
         }
         mRecording = pendingRecording
                 .asPersistentRecording() // Perform the recording as a persistent recording.
@@ -274,17 +274,17 @@
 
     private void exportTestInformation() {
         String information = KEY_DEVICE_ORIENTATION + ": " + mDeviceOrientation;
-        E2ETestUtil.writeTextToExternalFile(information,
+        FileUtil.writeTextToExternalFile(information,
                 generateFileName(INFO_FILE_PREFIX, false), "txt");
     }
 
     @NonNull
     private String generateFileName(@Nullable String prefix, boolean isUnique) {
-        if (!isUnique && !E2ETestUtil.isFileNameValid(prefix)) {
+        if (!isUnique && !FileUtil.isFileNameValid(prefix)) {
             throw new IllegalArgumentException("Invalid arguments for generating file name.");
         }
         StringBuilder fileName = new StringBuilder();
-        if (E2ETestUtil.isFileNameValid(prefix)) {
+        if (FileUtil.isFileNameValid(prefix)) {
             fileName.append(prefix);
             if (isUnique) {
                 fileName.append("_");
diff --git a/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml b/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
index a1e8f19..53613a2 100644
--- a/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
+++ b/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
@@ -308,6 +308,20 @@
         app:layout_constraintLeft_toRightOf="@id/plus_ev_toggle"
         app:layout_constraintTop_toBottomOf="@id/VideoToggle" />
 
+    <ToggleButton
+        android:id="@+id/preview_stabilization"
+        android:layout_width="46dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="5dp"
+        android:layout_marginTop="1dp"
+        android:background="@android:drawable/btn_default"
+        android:textOff="@string/toggle_preview_stabilization_off"
+        android:textOn="@string/toggle_preview_stabilization_on"
+        android:textSize="11dp"
+        android:translationZ="1dp"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/direction_toggle" />
+
     <Button
         android:id="@+id/video_quality"
         android:layout_width="46dp"
diff --git a/camera/integration-tests/coretestapp/src/main/res/values/donottranslate-strings.xml b/camera/integration-tests/coretestapp/src/main/res/values/donottranslate-strings.xml
index e835bfa..65335ef 100644
--- a/camera/integration-tests/coretestapp/src/main/res/values/donottranslate-strings.xml
+++ b/camera/integration-tests/coretestapp/src/main/res/values/donottranslate-strings.xml
@@ -16,6 +16,7 @@
   -->
 
 <resources>
+    <string name="camerax_service">CameraX Service</string>
     <string name="fps_counter_template">%1$s fps</string>
     <string name="toggle_preview_on">preview\non</string>
     <string name="toggle_preview_off">preview\noff</string>
@@ -29,6 +30,8 @@
     <string name="toggle_capture_quality_off">MIN</string>
     <string name="toggle_capture_zsl_on">ZSL ON</string>
     <string name="toggle_capture_zsl_off">ZSL OFF</string>
+    <string name="toggle_preview_stabilization_on">PS ON</string>
+    <string name="toggle_preview_stabilization_off">PS OFF</string>
     <string name="toggle_plus_ev">+EV</string>
     <string name="toggle_dec_ev">-EV</string>
     <string name="toggle_zoom_in_2x">2X</string>
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt
index e3dc0e9..49a518d 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt
@@ -39,7 +39,6 @@
 import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 import androidx.test.espresso.matcher.ViewMatchers.withId
 import androidx.test.espresso.matcher.ViewMatchers.withText
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.UiDevice
@@ -146,7 +145,6 @@
     }
 
     // The test makes sure the TextureView surface texture keeps the same after switch.
-    @FlakyTest(bugId = 230873000)
     @Test
     fun testPreviewViewUpdateAfterSwitch() {
         launchActivity(lensFacing, cameraXConfig).use { scenario ->
diff --git a/camera/integration-tests/viewtestapp/lint-baseline.xml b/camera/integration-tests/viewtestapp/lint-baseline.xml
index acea7110..cfacbf5 100644
--- a/camera/integration-tests/viewtestapp/lint-baseline.xml
+++ b/camera/integration-tests/viewtestapp/lint-baseline.xml
@@ -264,7 +264,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="        return if (canDeviceWriteToMediaStore()) {"
         errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -273,7 +273,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                generateVideoMediaStoreOptions(context.contentResolver, fileName)"
         errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -282,7 +282,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                generateVideoMediaStoreOptions(context.contentResolver, fileName)"
         errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -291,7 +291,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                generateVideoMediaStoreOptions(context.contentResolver, fileName)"
         errorLine2="                                                                        ~~~~~~~~">
         <location
@@ -300,7 +300,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="            recorder.prepareRecording(context, generateVideoFileOutputOptions(fileName))"
         errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -309,7 +309,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="            recorder.prepareRecording(context, generateVideoFileOutputOptions(fileName))"
         errorLine2="                                                                              ~~~~~~~~">
         <location
@@ -318,7 +318,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="        writeTextToExternalFile(information, fileName)"
         errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -327,7 +327,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="        writeTextToExternalFile(information, fileName)"
         errorLine2="                                ~~~~~~~~~~~">
         <location
@@ -336,7 +336,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="        writeTextToExternalFile(information, fileName)"
         errorLine2="                                             ~~~~~~~~">
         <location
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt
index a593ec0..6577ce6 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt
@@ -33,10 +33,10 @@
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.lifecycle.ProcessCameraProvider
-import androidx.camera.testing.impl.E2ETestUtil.canDeviceWriteToMediaStore
-import androidx.camera.testing.impl.E2ETestUtil.generateVideoFileOutputOptions
-import androidx.camera.testing.impl.E2ETestUtil.generateVideoMediaStoreOptions
-import androidx.camera.testing.impl.E2ETestUtil.writeTextToExternalFile
+import androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore
+import androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions
+import androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions
+import androidx.camera.testing.impl.FileUtil.writeTextToExternalFile
 import androidx.camera.video.PendingRecording
 import androidx.camera.video.Recorder
 import androidx.camera.video.Recording
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/BaseCarAppActivity.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/BaseCarAppActivity.java
index 4de2e3a..cb737f6 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/BaseCarAppActivity.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/BaseCarAppActivity.java
@@ -135,11 +135,8 @@
                     // cause a mismatch between the insets applied to the content on the hosts side
                     // vs. the actual visible window available on the client side.
                     Insets insets;
-                    // Android U+ (SDK 34+) introduced SYSTEM_OVERLAYS insets, which we pass to the
-                    // host to adjust padding.
-                    if (Build.VERSION.SDK_INT >= 34) {
-                        // TODO(b/287700349): Add tests once Robolectric supports SDK 34.
-                        insets = Api34Impl.getInsets(windowInsets);
+                    if (Build.VERSION.SDK_INT >= 30) {
+                        insets = Api30Impl.getInsets(windowInsets);
                     } else {
                         insets = WindowInsetsCompat.toWindowInsetsCompat(windowInsets)
                                 .getInsets(WindowInsetsCompat.Type.systemBars()
@@ -252,24 +249,17 @@
                 }
             };
 
-    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    private static class Api34Impl {
-
-        private Api34Impl() {
-        }
-
-        static Insets getInsets(WindowInsets windowInsets) {
-            return windowInsets.getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.ime()
-                    | WindowInsets.Type.systemOverlays());
-        }
-    }
-
     @RequiresApi(Build.VERSION_CODES.R)
     private static class Api30Impl {
         private Api30Impl() {
         }
 
         @DoNotInline
+        static Insets getInsets(WindowInsets windowInsets) {
+            return windowInsets.getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.ime());
+        }
+
+        @DoNotInline
         static WindowInsets getDecorViewInsets(WindowInsets insets) {
             return new WindowInsets.Builder(insets).setInsets(
                     WindowInsets.Type.displayCutout(), Insets.NONE).build();
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
index f371f31..a666d03 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
@@ -323,6 +323,34 @@
     }
 
     @Test
+    @Config(minSdk = Build.VERSION_CODES.R)
+    public void testWindowInsets_whenRAndAbove_handlesInsetsCorrectly() {
+        runOnActivity((scenario, activity) -> {
+            IInsetsListener insetsListener = mock(IInsetsListener.class);
+            mRenderServiceDelegate.getCarAppActivity().setInsetsListener(insetsListener);
+            View activityContainer = activity.mActivityContainerView;
+            Insets insets = Insets.of(50, 60, 70, 80);
+            WindowInsets windowInsets = new WindowInsets.Builder().setInsets(
+                    WindowInsets.Type.systemBars(),
+                    insets.toPlatformInsets()).build();
+            activityContainer.onApplyWindowInsets(windowInsets);
+
+            // Verify that system bars insets are handled correctly.
+            verify(insetsListener).onWindowInsetsChanged(eq(insets.toPlatformInsets()),
+                    eq(Insets.NONE.toPlatformInsets()));
+
+            windowInsets = new WindowInsets.Builder().setInsets(
+                    WindowInsets.Type.ime(),
+                    insets.toPlatformInsets()).build();
+            activityContainer.onApplyWindowInsets(windowInsets);
+
+             // Verify that ime insets are handled correctly.
+            verify(insetsListener).onWindowInsetsChanged(eq(insets.toPlatformInsets()),
+                    eq(Insets.NONE.toPlatformInsets()));
+        });
+    }
+
+    @Test
     public void testServiceNotTerminatedWhenConfigurationChanges() {
         runOnActivity((scenario, activity) -> {
             System.out.println("before");
diff --git a/car/app/app-projected/gradle.properties b/car/app/app-projected/gradle.properties
new file mode 100644
index 0000000..a060082
--- /dev/null
+++ b/car/app/app-projected/gradle.properties
@@ -0,0 +1,16 @@
+#
+# 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-samples/navigation/automotive/build.gradle b/car/app/app-samples/navigation/automotive/build.gradle
index 531198c..9984ce7 100644
--- a/car/app/app-samples/navigation/automotive/build.gradle
+++ b/car/app/app-samples/navigation/automotive/build.gradle
@@ -26,6 +26,7 @@
     defaultConfig {
         applicationId "androidx.car.app.sample.navigation"
         minSdkVersion 29
+        targetSdkVersion 33
         versionCode 1
         versionName "1.0"
     }
diff --git a/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
index 47c9404..ee2be84 100644
--- a/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
@@ -37,6 +37,12 @@
   <uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
   <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
+  <!-- SDK 33 onwards, apps require this permission to send any notifications to the system -->
+  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
+  <!-- For the Microphone Recording demos. -->
+  <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
   <!-- Various required feature settings for an automotive app. -->
   <uses-feature
       android:name="android.hardware.type.automotive"
diff --git a/car/app/app-samples/navigation/mobile/build.gradle b/car/app/app-samples/navigation/mobile/build.gradle
index 5e80268..ece98f3 100644
--- a/car/app/app-samples/navigation/mobile/build.gradle
+++ b/car/app/app-samples/navigation/mobile/build.gradle
@@ -25,6 +25,7 @@
     defaultConfig {
         applicationId "androidx.car.app.sample.navigation"
         minSdkVersion 23
+        targetSdkVersion 33
         versionCode 1
         versionName "1.0"
     }
diff --git a/car/app/app-samples/navigation/mobile/lint-baseline.xml b/car/app/app-samples/navigation/mobile/lint-baseline.xml
deleted file mode 100644
index d28831e..0000000
--- a/car/app/app-samples/navigation/mobile/lint-baseline.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
-
-    <issue
-        id="ForegroundServicePermission"
-        message="foregroundServiceType:location requires permission:[android.permission.FOREGROUND_SERVICE_LOCATION] AND any permission in list:[android.permission.ACCESS_COARSE_LOCATION, android.permission.ACCESS_FINE_LOCATION]"
-        errorLine1="    &lt;service"
-        errorLine2="    ^">
-        <location
-            file="src/main/AndroidManifest.xml"/>
-    </issue>
-
-</issues>
diff --git a/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
index e2ec81f..d9473ac 100644
--- a/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
@@ -39,6 +39,8 @@
 
     <!-- For Microphone Recording -->
     <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+
+    <!-- SDK 33 onwards, apps require this permission to send any notifications to the system -->
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 
     <application
diff --git a/car/app/app-samples/showcase/automotive/build.gradle b/car/app/app-samples/showcase/automotive/build.gradle
index 41cf3a4..822a062 100644
--- a/car/app/app-samples/showcase/automotive/build.gradle
+++ b/car/app/app-samples/showcase/automotive/build.gradle
@@ -23,7 +23,7 @@
     defaultConfig {
         applicationId "androidx.car.app.sample.showcase"
         minSdkVersion 29
-        targetSdkVersion 31
+        targetSdkVersion 33
         // Increment this to generate signed builds for uploading to Playstore
         // Make sure this is different from the showcase-mobile version
         versionCode 107
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
index 83fb3b2..28758d6 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
@@ -24,6 +24,9 @@
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
   <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
+  <!-- SDK 33 onwards, apps require this permission to send any notifications to the system -->
+  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
   <!-- For PlaceListMapTemplate -->
   <uses-permission android:name="androidx.car.app.MAP_TEMPLATES"/>
 
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
index e322348..96ad8b9 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
@@ -27,7 +27,7 @@
 
   <uses-sdk
       android:minSdkVersion="29"
-      android:targetSdkVersion="31" />
+      android:targetSdkVersion="33" />
 
   <uses-permission android:name="android.permission.INTERNET"/>
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
@@ -40,8 +40,12 @@
   <uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
   <uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
 
+  <!-- For the Microphone Recording demos. -->
   <uses-permission android:name="android.permission.RECORD_AUDIO"/>
 
+  <!-- SDK 33 onwards, apps require this permission to send any notifications to the system -->
+  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
   <!-- For Access to Car Hardware. -->
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
   <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/navigationtemplates/RoutingDemoModels.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/navigationtemplates/RoutingDemoModels.java
index b51619b..bdc3771 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/navigationtemplates/RoutingDemoModels.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/navigationtemplates/RoutingDemoModels.java
@@ -24,6 +24,7 @@
 import android.text.SpannableString;
 import android.text.Spanned;
 
+import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.car.app.AppManager;
 import androidx.car.app.CarContext;
@@ -189,10 +190,14 @@
             @NonNull CarContext carContext, @NonNull OnClickListener onStopNavigation) {
         ActionStrip.Builder builder = new ActionStrip.Builder();
         if (carContext.getCarAppApiLevel() >= CarAppApiLevels.LEVEL_5) {
+            @ColorInt int actionButtonRed = 0xffb40404;
             builder.addAction(
                     new Action.Builder()
+                            .setFlags(FLAG_PRIMARY)
+                            .setBackgroundColor(
+                                    CarColor.createCustom(actionButtonRed, actionButtonRed))
                             .setOnClickListener(
-                                    () ->  carContext.getCarService(AppManager.class)
+                                    () -> carContext.getCarService(AppManager.class)
                                             .showAlert(createAlert(carContext)))
                             .setIcon(new CarIcon.Builder(
                                     IconCompat.createWithResource(
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-af/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-af/strings.xml
index 5106be1..55ef0b2 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-af/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-af/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Lysoortjie"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Roosteroortjie met lang oortjietitel"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Soekoortjie"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Laai tans oortjie"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Oortjietemplaat laai tans demonstrasie"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Oortjietemplaatdemonstrasie sonder oortjies"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Prente kan nie vir ’n onbekende gasheer gewys word nie"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-am/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-am/strings.xml
index 287b25c..96cfa3f 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-am/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-am/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"የዝርዝር ትር"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"የፍርግርግ ትር ከረጅም ትር ርዕስ ጋር"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"የፍለጋ ትር"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"የመጫኛ ትር"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"የትር ቅንብር ደንብ መጫኛ ቅንጭብ ማሳያ"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"የትር ቅንብር ደንብ ቁጥር ትሮች ቅንጭብ ማሳያ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"ምስሎች ለማይታወቅ አስተናጋጅ ሊታዩ አይችሉም"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ar/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ar/strings.xml
index a3690a9..d98b591 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ar/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ar/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"علامة تبويب قائمة"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"علامة تبويب شبكة مع عنوان طويل لعلامة التبويب"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"علامة تبويب بحث"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"جارٍ تحميل علامة التبويب"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"عرض توضيحي لنموذج علامة تبويب يجري تحميلها"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"عرض توضيحي لنموذج بلا علامات تبويب"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"يتعذّر عرض الصور لمضيف غير معروف."</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-as/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-as/strings.xml
index 9b69f79..2ce2fd1 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-as/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-as/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"সূচীৰ টেব"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"টেবৰ দীঘলীয়া শিৰোনামৰ সৈতে গ্ৰিডৰ টেব"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"সন্ধানৰ টেব"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"টেব ল’ড কৰি থকা হৈছে"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"টেবৰ টেম্পলে’টৰ ল’ড হৈ থকা ডেম’"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"কোনো টেব নথকা টেবৰ টেম্পলে’টৰ ডেম’"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"অজ্ঞাত হ’ষ্টৰ বাবে প্ৰতিচ্ছবি দেখুৱাব নোৱাৰি"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-az/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-az/strings.xml
index 3483510..71468ba 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-az/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-az/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Siyahı Tabı"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Uzun Tab Başlığı ilə Torlu Tab"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Axtarış Tabı"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Tab yüklənir"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Tab Şablonu - Yükləmə Demosu"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Tab Şablonu - Tabın Olmaması Demosu"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Şəkillər naməlum host üçün göstərilə bilməz"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-b+sr+Latn/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-b+sr+Latn/strings.xml
index f5d2c1f..170974b33 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-b+sr+Latn/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-b+sr+Latn/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Kartica liste"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Kartica sa rešetkom i dugačkim naslovom"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Kartica pretrage"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Učitava se kartica"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demonstracija učitavanja šablona kartice"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demonstracija šablona kartice kada nema kartica"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Slike ne mogu da se prikazuju za nepoznat host"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-be/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-be/strings.xml
index 5c80bd0..2cd921b 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-be/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-be/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Укладка \"Спіс\""</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Укладка з доўгай назвай і змесцівам у выглядзе сеткі"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Укладка \"Пошук\""</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Ідзе загрузка ўкладкі"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Дэмаверсія загрузкі шаблона ўкладкі"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Дэмаверсія шаблона без укладак"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Відарысы нельга адлюстроўваць для невядомага хоста"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-bg/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-bg/strings.xml
index 22f3181..43b15e9 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-bg/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-bg/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Раздел „Списъци“"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Решетка от раздели с дълги заглавия"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Раздел „Търсене“"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Разделът се зарежда"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Шаблонен раздел – демонстрация на зареждането"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Шаблонен раздел – демонстрация без раздели"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Изображенията не могат да бъдат показани за неизвестен хост"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-bn/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-bn/strings.xml
index b42e3d6..5111661 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-bn/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-bn/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"তালিকা ট্যাব"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ট্যাবের দীর্ঘ শিরোনাম সহ গ্রিড ট্যাব"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"সার্চ ট্যাব"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"\'ট্যাব\' লোড করা হচ্ছে"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"টেম্পলেট লোড করার সম্পর্কিত ডেমো ট্যাব"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ট্যাব ছাড়াই ট্যাব টেমপ্লেটের ডেমো"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"অজানা হোস্টের জন্য ছবি দেখানো যায় না"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-bs/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-bs/strings.xml
index 9a2e822..e3c97b7 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-bs/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-bs/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Kartica liste"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Kartica mreže s dugim naslovom kartice"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Kartica pretraživanja"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Učitavanje kartice"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demo verzija učitavanja šablona kartice"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demo verzija šablona kartice bez kartice"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Nije moguće prikazati slike za nepoznatog hosta"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ca/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ca/strings.xml
index 26ef199..e7115bc 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ca/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ca/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Pestanya de llista"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Pestanya de quadrícula amb títol llarg"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Pestanya de cerca"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"S\'està carregant la pestanya"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demostració de càrrega d\'una plantilla de pestanya"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demostració d\'una plantilla de pestanya sense pestanyes"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"No es poden mostrar les imatges per a un amfitrió desconegut"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-cs/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-cs/strings.xml
index 4c49f27..e041477 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-cs/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-cs/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Karta se seznamem"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Karta s mřížkou s dlouhým názvem karty"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Karta vyhledávání"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Načítání karty"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Ukázka načítání šablony karty"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Ukázka šablony karty bez karet"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Obrázky nelze zobrazit neznámému hostiteli"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-da/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-da/strings.xml
index 18b72b9..dc1614f 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-da/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-da/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Listefane"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Gitterfane med lang fanetitel"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Søgefane"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Indlæser fanen"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demonstration af indlæsning af faneskabelon"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demonstration af faneskabelon uden faner"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Billeder kan ikke vises for en ukendt host"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-de/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-de/strings.xml
index d84389a..8ebf3ba 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-de/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-de/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Tab „Liste“"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Tab „Raster“ mit langem Tab-Titel"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Tab „Suche“"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Tab wird geladen"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Tab-Vorlage – Demo wird geladen"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Tab-Vorlage – Demo „Keine Tabs“"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Für einen unbekannten Host können keine Bilder angezeigt werden"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-el/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-el/strings.xml
index 3094f92..852f0ea 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-el/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-el/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Καρτέλα λίστας"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Καρτέλα πλέγματος με μεγάλο τίτλο καρτέλας"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Καρτέλα αναζήτησης"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Φόρτωση καρτέλας"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Επίδειξη φόρτωσης προτύπου καρτέλας"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Επίδειξη προτύπου καρτέλας χωρίς καρτέλες"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Δεν είναι δυνατή η εμφάνιση εικόνων για έναν άγνωστο κεντρικό υπολογιστή"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-en-rAU/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-en-rAU/strings.xml
index 5e87d9d0..96cc45b 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-en-rAU/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-en-rAU/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"List tab"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Grid tab with long tab title"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Search tab"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Loading tab"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Tab template loading demo"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Tab template no tabs demo"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Images cannot be displayed for an unknown host"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-en-rCA/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-en-rCA/strings.xml
index 3ac9b76..a645411 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-en-rCA/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-en-rCA/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"List Tab"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Grid Tab with Long Tab Title"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Search Tab"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Loading Tab"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Tab Template Loading Demo"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Tab Template No Tabs Demo"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Images cannot be displayed for an unknown host"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-en-rGB/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-en-rGB/strings.xml
index 5e87d9d0..96cc45b 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-en-rGB/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-en-rGB/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"List tab"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Grid tab with long tab title"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Search tab"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Loading tab"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Tab template loading demo"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Tab template no tabs demo"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Images cannot be displayed for an unknown host"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-en-rIN/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-en-rIN/strings.xml
index 5e87d9d0..96cc45b 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-en-rIN/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-en-rIN/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"List tab"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Grid tab with long tab title"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Search tab"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Loading tab"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Tab template loading demo"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Tab template no tabs demo"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Images cannot be displayed for an unknown host"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-en-rXC/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-en-rXC/strings.xml
index b8771e8..bc10981 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-en-rXC/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-en-rXC/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‎‏‏‎‏‏‎‎‎‎‏‏‏‏‎‎‎‎‏‎‏‏‏‎‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‎‏‎‎‏‏‎‏‎‎‎‎‏‏‎‏‏‎List Tab‎‏‎‎‏‎"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‎‎‎‏‏‏‎‎‎‏‎‎‏‎‏‏‏‎‏‏‎‏‎‏‏‎‏‏‏‎‎‎‏‎‏‎‏‏‏‏‏‎‏‏‏‎‎‎‎‎‏‎‎‎‏‎‎Grid Tab with Long Tab Title‎‏‎‎‏‎"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‏‎‎‏‎‎‎‏‎‏‎‎‎‎‎‏‎‏‏‏‏‎‎‎‏‏‏‎‎‎‎‏‏‎‎‎‏‏‎‏‏‏‎‎‎‎‏‏‎‎‎‎‎‎‎‏‎‏‎Search Tab‎‏‎‎‏‎"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‎‏‎‏‏‏‏‏‎‎‎‏‏‏‎‏‏‎‏‎‏‏‏‏‎‎‎‎‏‏‎‏‎‎‎‏‏‎‏‏‏‎‏‎‏‎‏‏‏‎‏‏‎‏‏‎‏‎Loading Tab‎‏‎‎‏‎"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‎‎‏‎‏‏‏‎‏‏‎‏‎‏‎‏‏‎‏‎‏‏‏‏‎‏‏‎‏‎‎‏‏‏‏‎‎‏‏‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎Tab Template Loading Demo‎‏‎‎‏‎"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‏‎‏‎‏‎‎‏‏‎‎‎‏‏‏‎‎‏‏‎‏‎‎‎‎‎‎‏‎‏‎Tab Template No Tabs Demo‎‏‎‎‏‎"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‎‎‎‎‏‎‎‎‏‏‏‏‏‏‏‎‏‏‎‎‎‏‎‎‏‎‎‎‏‏‎‎‏‎‏‎‏‏‏‎‎‏‏‏‎‎‎‎‏‎‏‏‎‎‏‏‎‎‎Images cannot be displayed for an unknown host‎‏‎‎‏‎"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-es-rUS/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-es-rUS/strings.xml
index b3d11bb..8ff4d62 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-es-rUS/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-es-rUS/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Pestaña de lista"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Pestaña en cuadrícula con título extenso"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Pestaña de búsqueda"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Cargando pestaña"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demostración de carga de plantilla de pestaña"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demostración de plantilla de pestaña sin pestañas"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"No es posible mostrar imágenes de un host desconocido"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-es/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-es/strings.xml
index a850b68..80cd6ee 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-es/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-es/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Pestaña de lista"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Pestaña de cuadrícula con título largo"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Pestaña de búsqueda"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Cargando pestaña"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demo de carga de plantilla de pestañas"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demo de plantilla de pestañas sin pestañas"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Las imágenes no se pueden mostrar en un host desconocido"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-et/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-et/strings.xml
index 8ebc9a6..c84986a 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-et/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-et/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Loendi vaheleht"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Ruudustiku vaheleht pika nimega"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Otsingu vaheleht"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Vahekaardi laadimine"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Vahelehemalli laadimise demo"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Vahelehemalli vahelehtedeta demo"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Tundmatu hosti puhul ei saa pilte kuvada"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-eu/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-eu/strings.xml
index 8cffc42..1c790f3 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-eu/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-eu/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Zerrendaren fitxa"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Saretaren fitxa izenburu luzearekin"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Bilaketen fitxa"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Fitxa kargatzen"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Fitxen txantiloi bat kargatzearen demo-bertsioa"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Fitxen txantiloien fitxarik gabeko demo-bertsioa"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Ostalari ezezagunei ezin zaizkie bistaratu irudiak"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml
index d606dc5..2b1282c 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"برگه فهرست"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"برگه جدول با عنوان برگه طولانی"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"برگه جستجو"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"درحال بار کردن «برگه»"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"نمونه بارگیری الگوی برگه"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"نمونه عدم وجود برگه الگوی برگه"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"به‌دلیل نامشخص بودن میزبان، تصویر نمایش داده نمی‌شود"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-fi/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-fi/strings.xml
index 02f9fb9..d623ed5 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-fi/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-fi/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Luettelovälilehti"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Ruudukkovälilehti ja pitkä välilehden nimi"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Hakuvälilehti"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Ladataan välilehteä"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Välilehtimallin lataamisen esittely"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Välilehtimallin (ei välilehtiä) esittely"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Kuvia ei voida näyttää tuntemattomalle isännälle"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-fr-rCA/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-fr-rCA/strings.xml
index cec9d82..ae27adf 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-fr-rCA/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-fr-rCA/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Onglet Liste"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Onglet de grille avec un titre d\'onglet long"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Onglet Recherche"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Chargement de l\'onglet en cours…"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Démo du chargement du modèle d\'onglet"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Démo sans onglets du modèle d\'onglet"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Impossible d\'afficher les images pour un hôte inconnu"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-fr/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-fr/strings.xml
index 5b87c06..5b45a10 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-fr/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-fr/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Onglet de liste"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Onglet de grille avec titre d\'onglet long"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Onglet de recherche"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Chargement de l\'onglet"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Démonstration de chargement du modèle d\'onglet"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Démonstrations du modèle d\'onglet sans onglets"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Les images d\'un hôte inconnu ne peuvent pas être affichées"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-gl/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-gl/strings.xml
index 54a5b03..5d559ff 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-gl/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-gl/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Pestana de lista"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Pestana de grade con título de pestana longo"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Pestana de busca"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Cargando pestana"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demostración de carga do modelo de pestanas"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demostración do modelo de pestanas sen pestanas"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"As imaxes non se poden mostrar nun host descoñecido"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-gu/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-gu/strings.xml
index dc883fe..cb90252 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-gu/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-gu/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"સૂચિ ટૅબ"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ટૅબનું લાંબું શીર્ષક ધરાવતું ગ્રીડ ટૅબ"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"શોધ ટૅબ"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"ટૅબ લોડ થઈ રહ્યું છે"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ટૅબનો નમૂનો લોડ થવાનો ડેમો"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ટૅબના નમૂનાનો, ટૅબ વિનાનો ડેમો"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"અજાણ્યા હોસ્ટ માટે છબીઓ બતાવી શકાતી નથી"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-hi/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-hi/strings.xml
index 43bdb56..109e8e6 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-hi/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-hi/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"सूची टैब"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"लंबे टैब टाइटल वाले ग्रिड टैब"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"खोज टैब"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"टैब लोड हो रहा है"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"टैब टेंप्लेट लोड करने का डेमो"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"बिना किसी टैब वाले टैब टेंप्लेट का डेमो"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"अनजान होस्ट के लिए इमेज नहीं दिखाई जा सकतीं"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-hr/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-hr/strings.xml
index 8b4d02e..9cb85ff 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-hr/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-hr/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Kartica popisa"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Kartica rešetke s dugačkim naslovom kartice"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Kartica pretraživanja"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Učitavanje kartice"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Pokazna verzija učitavanja predloška kartice"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Pokazna verzija predloška kartice bez kartica"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Slike se ne mogu prikazati za nepoznatog hosta"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-hu/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-hu/strings.xml
index 9c59d5f..2ee0a9c 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-hu/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-hu/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Listalap"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Rácslap hosszú című lappal"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Keresőlap"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Lap betöltése"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Lapsablon bemutatójának betöltése"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Lapsablon lapok nélküli bemutatója"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"A képek nem jeleníthetők meg ismeretlen gazdagépnek"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-hy/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-hy/strings.xml
index c3c1c32..e114756 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-hy/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-hy/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Ցանկերի ներդիր"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Ցանցերի ներդիր՝ երկար վերնագրով"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Որոնումների ներդիր"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Ներդիրի բեռնում"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Ներդիրների ձևանմուշի բեռնման դեմո"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Առանց ներդիրների ձևանմուշի դեմո"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Պատկերները չեն կարող ցուցադրվել անհայտ խնամորդի համար"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-in/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-in/strings.xml
index a59d4c3..ca12300 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-in/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-in/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Tab Daftar"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Tab Petak dengan Judul Tab Panjang"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Tab Penelusuran"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Memuat Tab"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demo Template Tab Memuat Konten"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demo Template Tab Tidak Ada Tab"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Gambar tidak dapat ditampilkan untuk host yang tidak dikenal"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-is/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-is/strings.xml
index fc6c815..dba3f3a 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-is/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-is/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Listaflipi"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Töfluflipi með löngum flipatitli"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Leitarflipi"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Hleður flipa"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Prufuútgáfa hleðslu flipasniðmáts"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Prufuútgáfa flipasniðmáts með engum flipum"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Ekki er hægt að birta myndir fyrir óþekktan hýsil"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-it/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-it/strings.xml
index e2c1ac3..2122097 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-it/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-it/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Scheda elenco"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Scheda griglia con titolo scheda lungo"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Scheda ricerca"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Caricamento scheda in corso…"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demo caricamento modello scheda"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demo nessuna scheda modello scheda"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Non è possibile mostrare immagini per un host sconosciuto"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-iw/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-iw/strings.xml
index 663a81b..ac062b5 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-iw/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-iw/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"כרטיסייה של רשימה"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"כרטיסיית רשת עם שם כרטיסייה ארוך"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"כרטיסיית חיפוש"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"הכרטיסייה נטענת"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"הדגמה של העלאת תבנית הכרטיסייה"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"הדגמה של תבנית הכרטיסייה כשאין כרטיסיות"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"לא ניתן להציג תמונות למארח לא ידוע"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ja/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ja/strings.xml
index 53722f0..b7c907d 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ja/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ja/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"リストタブ"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"タブのタイトルが長いグリッドタブ"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"検索タブ"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"タブを読み込んでいます"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"タブ テンプレートの読み込みのデモ"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"タブ テンプレートのタブなしのデモ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"不明なホストでは画像を表示できません"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ka/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ka/strings.xml
index 16c59e0..c174edd 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ka/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ka/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"სიის ჩანართი"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ბადისებრი ჩანართი გრძელი სათაურით"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"ძიების ჩანართი"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"მიმდინარეობს ჩანართის ჩატვირთვა"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ჩანართის შაბლონის ჩატვირთვის დემო"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ჩანართის შაბლონის უჩანართო დემო"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"სურათების უცნობის მასპინძლისთვის ჩვენება შეუძლებელია"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-kk/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-kk/strings.xml
index 214a0a8..e3b064d 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-kk/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-kk/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Тізім қойындысы"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Қойынды атауы ұзын тор қойындысы"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Іздеу қойындысы"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Қойынды жүктеліп жатыр"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Қойынды үлгісінің демо нұсқасын жүктеу"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Қойынды үлгісі – қойындылардың демо нұсқасы жоқ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Суреттер белгісіз хост үшін көрсетілмейді."</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-km/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-km/strings.xml
index c6d6e72..de18a90 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-km/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-km/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"ផ្ទាំង​​បញ្ជី"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ផ្ទាំងក្រឡាដែលមានចំណងជើងផ្ទាំងវែង"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"ផ្ទាំងស្វែងរក"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"កំពុងផ្ទុកផ្ទាំង"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"គំរូបង្ហាញផ្ទុកទម្រង់គំរូផ្ទាំង"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"គំរូបង្ហាញគ្មានផ្ទាំងទម្រង់គំរូផ្ទាំង"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"មិនអាចបង្ហាញរូបភាព​សម្រាប់ម៉ាស៊ីនដែល​មិនស្គាល់បានទេ"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-kn/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-kn/strings.xml
index 03c20c2..42a4a75 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-kn/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-kn/strings.xml
@@ -162,7 +162,7 @@
     <string name="go_straight" msgid="2301747728609198718">"ನೇರವಾಗಿ ಹೋಗಿ"</string>
     <string name="turn_right" msgid="4710562732720109969">"ಬಲಕ್ಕೆ ತಿರುಗಿ"</string>
     <string name="take_520" msgid="3804796387195842741">"520 ಗೆ ಹೋಗಿ"</string>
-    <string name="gas_station" msgid="1203313937444666161">"ಗ್ಯಾಸ್ ಸ್ಟೇಶನ್"</string>
+    <string name="gas_station" msgid="1203313937444666161">"ಪೆಟ್ರೋಲ್ ಬಂಕ್"</string>
     <string name="short_route" msgid="4831864276538141265">"ಕಡಿಮೆ ದೂರದ ಮಾರ್ಗ"</string>
     <string name="less_busy" msgid="310625272281710983">"ಕಡಿಮೆ ಟ್ರಾಫಿಕ್"</string>
     <string name="hov_friendly" msgid="6956152104754594971">"HOV ಸ್ನೇಹಿ"</string>
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"ಪಟ್ಟಿ ಟ್ಯಾಬ್"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ಲಾಂಗ್ ಟ್ಯಾಬ್ ಶೀರ್ಷಿಕೆಯೊಂದಿಗೆ ಗ್ರಿಡ್ ಟ್ಯಾಬ್"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"ಹುಡುಕಾಟ ಟ್ಯಾಬ್"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"ಟ್ಯಾಬ್ ಅನ್ನು ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ಟ್ಯಾಬ್ ಟೆಂಪ್ಲೇಟ್‌‌ ಲೋಡ್ ಆಗುತ್ತಿರುವ ಡೆಮೋ"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ಟ್ಯಾಬ್ ಟೆಂಪ್ಲೇಟ್‌‌ ಯಾವುದೇ ಟ್ಯಾಬ್‌ಗಳ ಡೆಮೋ ಇಲ್ಲ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"ಅಪರಿಚಿತ ಹೋಸ್ಟ್‌ಗಾಗಿ ಚಿತ್ರಗಳನ್ನು ಪ್ರದರ್ಶಿಸಲಾಗುವುದಿಲ್ಲ"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ko/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ko/strings.xml
index 9bd4030..d3c96b0 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ko/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ko/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"탭 목록"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"긴 탭 제목이 있는 그리드 탭"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"검색 탭"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"탭 로드 중"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"탭 템플릿 로딩 데모"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"탭 없음 탭 템플릿 데모"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"알려지지 않은 호스트의 이미지를 표시할 수 없음"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ky/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ky/strings.xml
index d2cadb3..074c234 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ky/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ky/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Тизме өтмөгү"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Өтмөктүн аталышы узун болгон торчо түрүндөгү өтмөк"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Издөө өтмөгү"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Өтмөк жүктөлүүдө"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Өтмөктүн үлгүсүн жүктөөнүн демо версиясы"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Өтмөктүн үлгүсү, өтмөктөр жок демо версиясы"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Сүрөттөрдү белгисиз башкы түйүн үчүн көрсөтүүгө болбойт"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-lo/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-lo/strings.xml
index c0e7724..6afe958 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-lo/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-lo/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"ແຖບລາຍຊື່"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ແຖບຕາໜ່າງພ້ອມຊື່ແຖບຍາວ"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"ແຖບຊອກຫາ"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"ກຳລັງໂຫຼດແຖບ"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ເດໂມແມ່ແບບແຖບການໂຫຼດ"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ເດໂມແມ່ແບບແຖບທີ່ບໍ່ມີແຖບ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"ບໍ່ສາມາດສະແດງຮູບສຳລັບໂຮສທີ່ບໍ່ຮູ້ຈັກໄດ້"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-lt/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-lt/strings.xml
index 3a8d187..e919358 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-lt/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-lt/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Sąrašo skirtukas"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Tinklelio skirtukas su ilgu tinklelio pavadinimu"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Paieškos skirtukas"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Įkeliamas skirtukas"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Skirtuko šablono įkėlimo demonstracinė versija"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Skirtuko šablono be skirtukų demonstracinė versija"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Vaizdų negalima pateikti nežinomai prieglobai"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-lv/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-lv/strings.xml
index 03b5db9..4963476 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-lv/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-lv/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Saraksta skata cilne"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Režģa skata cilne ar garu cilnes nosaukumu"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Cilne meklēšanai"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Notiek cilnes ielāde…"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Cilnes veidnes ielādes demonstrācija"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demonstrācija cilnes veidnei, kurā nav ciļņu"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Nevar rādīt attēlus nezināmam saimniekdatoram"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-mk/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-mk/strings.xml
index c37b862..5cf182b 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-mk/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-mk/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Картичка во вид на список"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Картичка во вид на решетка со долг наслов на картичката"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Картичка за пребарување"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Картичката се вчитува"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Демо за вчитување шаблон за картичка"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Демо за немање картички за шаблон за картичка"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Сликите не може да се прикажат за непознат хост"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ml/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ml/strings.xml
index ba91977..76b6ae5 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ml/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ml/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"ലിസ്റ്റ് ടാബ്"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"നീളമേറിയ ടാബ് പേരോട് കൂടിയ ഗ്രിഡ് ടാബ്"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"തിരയൽ ടാബ്"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"ലോഡ് ചെയ്യൽ ടാബ്"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ടാബ് ടെംപ്ലേറ്റ് ലോഡ് ചെയ്യൽ ഡെമോ"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ടാബ് ടെംപ്ലേറ്റ് ടാബുകളൊന്നുമില്ല ഡെമോ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"അജ്ഞാത ഹോസ്റ്റിൽ നിന്നുള്ള ചിത്രങ്ങൾ പ്രദർശിപ്പിക്കാനാകില്ല"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-mn/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-mn/strings.xml
index 88059eb..3ffae53 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-mn/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-mn/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Табын жагсаалт"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Табын урт нэртэй хүснэгтэн таб"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Хайлтын таб"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Табыг ачаалж байна"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Табын загварыг ачаалах демо"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Табын загварын ямар ч табуудын демо байхгүй"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Зургийг тодорхойгүй хостод үзүүлэх боломжгүй"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-mr/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-mr/strings.xml
index 288dfda..b1c63b2 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-mr/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-mr/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"सूची टॅब"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"लांब टॅब शीर्षक असलेला ग्रिड टॅब"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"शोध टॅब"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"टॅब लोड करत आहे"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"टॅब टेंप्लेट लोड होत आहे डेमो"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"टॅब टेंप्लेट टॅब नाही डेमो"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"अज्ञात होस्टला इमेज दाखवल्या जाऊ शकत नाहीत"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ms/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ms/strings.xml
index 8a6ac22..9065afd 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ms/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ms/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Tab Senarai"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Tab Grid dengan Tajuk Tab Panjang"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Tab Carian"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Memuatkan Tab"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demo Memuatkan Templat Tab"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demo Templat Tab Tiada Tab"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Imej tidak boleh dipaparkan untuk hos yang tidak diketahui"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-my/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-my/strings.xml
index 9e38964..2c19281 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-my/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-my/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"စာရင်းတဘ်"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"တဘ်ခေါင်းစဉ်အရှည် ပါဝင်သော ဇယားကွက်တဘ်"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"ရှာဖွေရေးတဘ်"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"တဘ် ဖွင့်နေသည်"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"တဘ်ပုံစံဖွင့်နေသည့် သရုပ်ပြချက်"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"တဘ်ပုံစံတွင် တဘ်မရှိသည့် သရုပ်ပြချက်"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"မသိသောဆာဗာပင်ရင်းအတွက် ပုံများကို ပြ၍မရပါ"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-nb/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-nb/strings.xml
index fe2bd0c..53ec16b 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-nb/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-nb/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Listefane"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Rutenettfane med lang fanetittel"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Søkefane"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Laster inn fanen"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Laster inn demo av fanemal"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demo av fanemal uten faner"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Bilder kan ikke vises for en ukjent vert"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ne/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ne/strings.xml
index deed831..79b778d 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ne/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ne/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"लिस्ट ट्याब"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ट्याबको लामो शीर्षक भएको ग्रिड ट्याब"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"सर्च ट्याब"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"ट्याब लोड गरिँदै छ"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ट्याब टेम्प्लेट लोड गर्ने डेमो"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"कुनै पनि ट्याब नभएको ट्याब टेम्प्लेटको डेमो"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"अज्ञात होस्टका हकमा फोटोहरू देखाउन मिल्दैन"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-nl/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-nl/strings.xml
index 7317473..e3fbe2c 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-nl/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-nl/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Lijsttabblad"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Rastertabblad met lange tabbladtitel"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Zoektabblad"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Tabblad laden"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demo van laden van tabbladtemplate"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demo van tabbladtemplate zonder tabbladen"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Afbeeldingen kunnen niet worden getoond voor een onbekende host"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-or/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-or/strings.xml
index f01eced..42cf151 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-or/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-or/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"ଲିଷ୍ଟ ଟାବ"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ଲମ୍ବା ଟାବ ଟାଇଟେଲ ସହ ଗ୍ରୀଡ ଟାବ"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"ସର୍ଚ୍ଚ ଟାବ"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"ଟାବ ଲୋଡ ହେଉଛି"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ଟାବ ଟେମ୍ପଲେଟର ଲୋଡିଂ ଡେମୋ"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ଟାବ ଟେମ୍ପଲେଟ କୌଣସି ଟାବ ବିନା ଡେମୋ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"ଏକ ଅଜଣା ହୋଷ୍ଟ ପାଇଁ ଇମେଜଗୁଡ଼ିକୁ ଡିସପ୍ଲେ କରାଯାଇପାରିବ ନାହିଁ"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-pa/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-pa/strings.xml
index 49faa9c..0d162a5 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-pa/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-pa/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"ਸੂਚੀ ਟੈਬ"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ਲੰਮੇ ਟੈਬ ਸਿਰਲੇਖ ਵਾਲੇ ਗਰਿੱਡ ਟੈਬ"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"ਖੋਜੋ ਟੈਬ"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"ਟੈਬ ਨੂੰ ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ਟੈਬ ਟੈਮਪਲੇਟ ਲੋਡ ਕਰਨ ਦੀ ਪ੍ਰਕਿਰਿਆ ਦਾ ਡੈਮੋ"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ਬਿਨਾ ਕਿਸੇ ਟੈਬ ਵਾਲੇ ਟੈਬ ਟੈਮਪਲੇਟ ਦਾ ਡੈਮੋ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"ਕਿਸੇ ਅਗਿਆਤ ਹੋਸਟ ਲਈ ਚਿੱਤਰ ਨਹੀਂ ਦਿਖਾਏ ਜਾ ਸਕਦੇ"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-pl/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-pl/strings.xml
index 7918b4b..9649d73 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-pl/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-pl/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Karta listy"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Wersja demonstracyjna szablonu kart korzystająca z długich kart"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Karta wyszukiwania"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Ładowanie karty"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Ładowanie wersji demonstracyjnej szablonu kart"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Wersja demonstracyjna szablonu kart bez kart"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Nie można wyświetlać grafiki z nieznanego serwera"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-pt-rBR/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-pt-rBR/strings.xml
index d5d524a..146b7c7 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-pt-rBR/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-pt-rBR/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Guia em lista"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Guia em grade com título longo"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Guia de pesquisa"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Carregando a guia"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demonstração de carregamento do modelo de guia"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demonstração do modelo de guia sem guias"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Não é possível exibir imagens para um host desconhecido"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-pt-rPT/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-pt-rPT/strings.xml
index 57ed9c6..3598722 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-pt-rPT/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-pt-rPT/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Separador de lista"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Separador de grelha com título do separador longo"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Separador de pesquisa"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"A carregar o separador"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demonstração do carregamento do modelo de separador"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demonstração do modelo de separador sem separadores"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Não podem ser apresentadas imagens num anfitrião desconhecido"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-pt/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-pt/strings.xml
index d5d524a..146b7c7 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-pt/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-pt/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Guia em lista"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Guia em grade com título longo"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Guia de pesquisa"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Carregando a guia"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demonstração de carregamento do modelo de guia"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demonstração do modelo de guia sem guias"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Não é possível exibir imagens para um host desconhecido"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ro/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ro/strings.xml
index 256f19e..e5f8a3c 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ro/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ro/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Filă cu listă"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Filă cu grilă și titlu lung"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Filă cu căutare"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Se încarcă fila"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demonstrație cu încărcarea șablonului de file"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demonstrație fără file pentru șablon"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Nu se pot afișa imagini pentru o gazdă necunoscută"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ru/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ru/strings.xml
index 8c5e410..0c20c56 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ru/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ru/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Вкладка списка"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Вкладка сетки с длинным названием"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Вкладка поиска"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Загрузка вкладки…"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Демонстрация загрузки шаблона вкладок"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Демонстрация шаблона без вкладок"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Нельзя показывать изображения для неизвестного хоста."</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-si/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-si/strings.xml
index 4a4cf29..146ca3e 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-si/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-si/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"ලැයිස්තු පටිත්ත"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"දිගු පටිති මාතෘකාව සහිත ජාල පටිත්ත"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"සෙවීම් පටිත්ත"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"පටිත්ත පූරණය වේ"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"පටිති අච්චු පූරණය වීමේ ආදර්ශනය"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"පටිති අච්චු පටිති නැති ආදර්ශනය"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"නොදන්නා සංග්‍රාහකයෙක් සඳහා රූප සංදර්ශනය කළ නොහැකිය"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-sk/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-sk/strings.xml
index 560acd0..d494703 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-sk/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-sk/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Karta zoznamu"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Karta mriežky s dlhým názvom karty"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Karta vyhľadávania"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Načítava sa karta"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Ukážka načítavania šablóny karty"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Ukážka šablón karty bez kariet"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Obrázky nie je možné zobraziť pre neznámeho hostiteľa"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-sl/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-sl/strings.xml
index 6cc0da2..1956308 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-sl/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-sl/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Zavihek s seznamom"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Zavihek z mrežo in dolgim naslovom zavihka"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Zavihek za iskanje"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Nalaganje zavihka"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Predstavitvena različica predloge zavihka za stanje nalaganja"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Predstavitvena različica predloge zavihka za stanje brez zavihkov"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Slik ni mogoče prikazati za neznanega gostitelja."</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-sq/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-sq/strings.xml
index b52261b..23a7b4c 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-sq/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-sq/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Skeda e listës"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Skeda e rrjetës me titull të gjatë skede"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Skeda e \"Kërko\""</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Skeda po ngarkohet"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demonstrimi i ngarkimit të shabllonit të skedës"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demonstrimi i shabllonit të skedës pa skeda"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Imazhet nuk mund të shfaqen për një organizator të panjohur"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-sr/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-sr/strings.xml
index eb4dc26..4e692a8 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-sr/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-sr/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Картица листе"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Картица са решетком и дугачким насловом"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Картица претраге"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Учитава се картица"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Демонстрација учитавања шаблона картице"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Демонстрација шаблона картице када нема картица"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Слике не могу да се приказују за непознат хост"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-sv/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-sv/strings.xml
index e708283..8537e2a 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-sv/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-sv/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Listflik"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Elnätsflik med lång fliktitel"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Sökflik"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Läser in flik"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Laddningsflikmallsdemo"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Flikmall utan flikdemo"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Bilder kan inte visas för en okänd värd"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-sw/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-sw/strings.xml
index dc57142..42be6f4 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-sw/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-sw/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Tab ya Orodha"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Tab ya kupangilia kwa Gridi yenye Jina Refu la Tab"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Tab ya Kutafuta"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Inapakia Kichupo"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Kiolezo cha Kupakia Toleo la Kujaribu la Tab"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Kiolezo cha Tab Kisicho na Toleo la Kujaribu la Tab"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Picha haziwezi kuonyeshwa kwa mpangishi asiyejulikana"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ta/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ta/strings.xml
index f17e770..b56be79 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ta/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ta/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"பட்டியல் டேப் (Tab)"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"நீண்ட தலைப்புள்ள கட்டமான டேப் (Tab)"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"தேடல் டேப் (Tab)"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"பிரிவை ஏற்றுகிறது"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"டேப் (Tab) ஏற்றும்போதுள்ள டெம்ப்ளேட் டெமோ"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"டேப் (Tab)  இல்லாதபோதுள்ள டெம்ப்ளேட் டேப்கள் டெமோ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"அறியப்படாத ஹோஸ்ட்டிற்குப் படங்கள் காட்டப்படாது"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-te/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-te/strings.xml
index c5bf0de..98643fa 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-te/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-te/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"లిస్ట్ ట్యాబ్"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"పొడవైన ట్యాబ్ టైటిల్ గల గ్రిడ్ ట్యాబ్"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"సెర్చ్ ట్యాబ్"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"ట్యాబ్ లోడ్ అవుతోంది"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ట్యాబ్ టెంప్లేట్‌ను లోడ్ చేస్తున్న డెమో"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ట్యాబ్‌లు లేని ట్యాబ్ టెంప్లేట్ డెమో"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"తెలియని హోస్ట్ కోసం ఇమేజ్‌లను ప్రదర్శించడం సాధ్యం కాదు"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-th/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-th/strings.xml
index f19bcc2..72d8466 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-th/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-th/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"แท็บรายการ"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"แท็บตารางกริดที่มีชื่อแท็บยาว"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"แท็บค้นหา"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"กำลังโหลดแท็บ"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"เดโมการโหลดเทมเพลตแท็บ"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"เดโมเทมเพลตแท็บที่ไม่มีแท็บ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"ไม่สามารถแสดงรูปภาพสำหรับโฮสต์ที่ไม่รู้จัก"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-tl/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-tl/strings.xml
index 1b845a3..7db8464 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-tl/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-tl/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Tab ng Listahan"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Grid na Tab na may Mahabang Pamagat ng Tab"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Tab ng Paghahanap"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Nilo-load ang Tab"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demo ng Pag-load ng Template ng Tab"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demo ng Walang Tab na Template ng Tab"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Hindi maipapakita ang mga larawan para sa hindi tukoy na host"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-tr/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-tr/strings.xml
index 6e636da..c56746b 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-tr/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-tr/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Liste Sekmesi"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Uzun Sekme Başlıklı Izgara Sekmesi"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Arama Sekmesi"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Sekme Yükleniyor"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Sekme Şablonu Yükleme Demosu"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Sekme Şablonu Sekme İçermeyen Demo"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Resimler, bilinmeyen ana makine için gösterilemez"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-uk/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-uk/strings.xml
index 6cb6774..3b429dd 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-uk/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-uk/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Вкладка зі списком"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Вкладка із сіткою та довгою назвою"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Вкладка пошуку"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Завантаження вкладки"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Демонстрація шаблона вкладки: завантаження"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Демонстрація шаблона вкладки: немає вкладок"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Зображення не показуються для невідомого хосту"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ur/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ur/strings.xml
index 2fe8e82..d82e510 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ur/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ur/strings.xml
@@ -268,6 +268,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"فہرست ٹیب"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"طویل ٹیب کے عنوان کے ساتھ گرڈ ٹیب"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"تلاش ٹیب"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"ٹیب لوڈ ہو رہا ہے"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ٹیب کی تمثیل ڈیمو لوڈ ہو رہا ہے"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ٹیب کی تمثیل کوئی ٹیبز ڈیمو نہیں"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"نامعلوم میزبان کیلئے تصاویر کو ڈسپلے نہیں کیا جا سکتا"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-uz/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-uz/strings.xml
index b604f15..a4f772bd 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-uz/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-uz/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Roʻyxat varagʻi"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Uzun varaq nomi bilan katakli varaq"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Qidiruv varagʻi"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Varaq yuklanmoqda"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Varaq andozasi yuklanish demo versiyasi"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Varaq andozasi varaqsiz demo versiyasi"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Rasmlar notanish xost uchun chiqarilmaydi"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-vi/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-vi/strings.xml
index 334b6e0..69dd820 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-vi/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-vi/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Thẻ dạng danh sách"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Thẻ dạng lưới có tiêu đề thẻ dài"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Thẻ tìm kiếm"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Đang tải thẻ"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Bản demo mẫu thẻ đang tải"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Bản demo mẫu thẻ không có thẻ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Không thể hiển thị hình ảnh cho một máy chủ không xác định"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-zh-rCN/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-zh-rCN/strings.xml
index 8f96acb..09c29ec 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-zh-rCN/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-zh-rCN/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"列表标签页"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"包含长标签页标题的网格标签页"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"搜索标签页"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"正在加载标签页"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"标签页模板加载演示"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"标签页模板无标签页演示"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"无法为未知主机显示图像"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-zh-rHK/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-zh-rHK/strings.xml
index 5408e2f..a52ba16 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-zh-rHK/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-zh-rHK/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"清單分頁"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"有長分頁標題的格線分頁"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"搜尋分頁"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"正在載入分頁"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"分頁範本載入示範"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"分頁範本無分頁示範"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"無法顯示不明主機的圖片"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-zh-rTW/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-zh-rTW/strings.xml
index 8ace70f..5fe34e8 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-zh-rTW/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-zh-rTW/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"清單分頁"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"有長分頁標題的格線分頁"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"搜尋分頁"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"正在載入分頁"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"分頁範本載入示範"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"分頁範本無分頁示範"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"無法顯示不明主機的圖片"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-zu/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-zu/strings.xml
index 183870c..f37b8a4 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-zu/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-zu/strings.xml
@@ -264,6 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Ithebhu Yohlu"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Ithebhu yegridi enesihloko sethebhu ende"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Sesha, ithebhu"</string>
+    <string name="tab_title_loading" msgid="5385807479734490989">"Ilayisha Ithebhu"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Isifanekiso Sethebhu Silayisha Idemo"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Isifanekiso Sethebhu Ayikho Idemo Yamathebhu"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Imifanekiso ayikwazi ukuboniswa mayelana nomsingathi ongaziwa"</string>
diff --git a/car/app/app-samples/showcase/mobile/build.gradle b/car/app/app-samples/showcase/mobile/build.gradle
index 5466dad..ac1eaeb 100644
--- a/car/app/app-samples/showcase/mobile/build.gradle
+++ b/car/app/app-samples/showcase/mobile/build.gradle
@@ -25,7 +25,7 @@
     defaultConfig {
         applicationId "androidx.car.app.sample.showcase"
         minSdkVersion 23
-        targetSdkVersion 31
+        targetSdkVersion 33
         // Increment this to generate signed builds for uploading to Playstore
         // Make sure this is different from the showcase-automotive version
         versionCode 106
diff --git a/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
index 59a17ab..917662c 100644
--- a/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
@@ -31,6 +31,9 @@
   <uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
   <uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
 
+  <!-- SDK 33 onwards, apps require this permission to send any notifications to the system -->
+  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
   <!-- For Access to Car Hardware. -->
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
   <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
diff --git a/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml
index 1c9c267..6f205ae 100644
--- a/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml
@@ -27,12 +27,15 @@
 
   <uses-sdk
       android:minSdkVersion="23"
-      android:targetSdkVersion="31" />
+      android:targetSdkVersion="33" />
 
   <uses-permission android:name="android.permission.INTERNET"/>
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
   <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
+  <!-- SDK 33 onwards, apps require this permission to send any notifications to the system -->
+  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
   <!-- For PlaceListMapTemplate -->
   <uses-permission android:name="androidx.car.app.MAP_TEMPLATES"/>
 
diff --git a/car/app/app/gradle.properties b/car/app/app/gradle.properties
new file mode 100644
index 0000000..a060082
--- /dev/null
+++ b/car/app/app/gradle.properties
@@ -0,0 +1,16 @@
+#
+# 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 3cc8390d..8492f99 100644
--- a/car/app/app/lint-baseline.xml
+++ b/car/app/app/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="MissingPermission"
@@ -371,15 +371,6 @@
     </issue>
 
     <issue
-        id="UnspecifiedRegisterReceiverFlag"
-        message="`mBroadcastReceiver` \&#xA;is missing `RECEIVER_EXPORTED` or `RECEIVER_NOT_EXPORTED` flag for unprotected \&#xA;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="            } else if (rowObj instanceof ConversationItem) {"
diff --git a/collection/collection-benchmark/build.gradle b/collection/collection-benchmark/build.gradle
index f28fbdd..f255aee 100644
--- a/collection/collection-benchmark/build.gradle
+++ b/collection/collection-benchmark/build.gradle
@@ -62,7 +62,7 @@
             }
         }
 
-        androidTest {
+        androidInstrumentedTest {
             dependsOn(commonTest)
             dependencies {
                 implementation(projectOrArtifact(":benchmark:benchmark-junit4"))
diff --git a/collection/collection-benchmark/src/androidTest/AndroidManifest.xml b/collection/collection-benchmark/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from collection/collection-benchmark/src/androidTest/AndroidManifest.xml
rename to collection/collection-benchmark/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/collection/collection-benchmark/src/androidTest/java/androidx/collection/ArraySetBenchmarkTest.kt b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ArraySetBenchmarkTest.kt
similarity index 100%
rename from collection/collection-benchmark/src/androidTest/java/androidx/collection/ArraySetBenchmarkTest.kt
rename to collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ArraySetBenchmarkTest.kt
diff --git a/collection/collection-benchmark/src/androidTest/java/androidx/collection/CircularArrayBenchmarkTest.kt b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/CircularArrayBenchmarkTest.kt
similarity index 100%
rename from collection/collection-benchmark/src/androidTest/java/androidx/collection/CircularArrayBenchmarkTest.kt
rename to collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/CircularArrayBenchmarkTest.kt
diff --git a/collection/collection-benchmark/src/androidTest/java/androidx/collection/CollectionBenchmarkExt.kt b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/CollectionBenchmarkExt.kt
similarity index 100%
rename from collection/collection-benchmark/src/androidTest/java/androidx/collection/CollectionBenchmarkExt.kt
rename to collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/CollectionBenchmarkExt.kt
diff --git a/collection/collection-benchmark/src/androidTest/java/androidx/collection/LruCacheBenchmarkTest.kt b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/LruCacheBenchmarkTest.kt
similarity index 100%
rename from collection/collection-benchmark/src/androidTest/java/androidx/collection/LruCacheBenchmarkTest.kt
rename to collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/LruCacheBenchmarkTest.kt
diff --git a/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ObjectListBenchmarkTest.kt b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ObjectListBenchmarkTest.kt
new file mode 100644
index 0000000..a6ba28f
--- /dev/null
+++ b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ObjectListBenchmarkTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.collection
+
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ObjectListBenchmarkTest {
+    val ObjectCount = 100
+    private val list: ObjectList<String> = MutableObjectList<String>(ObjectCount).also { list ->
+        repeat(ObjectCount) {
+            list += it.toString()
+        }
+    }
+
+    private val array = Array(ObjectCount) { it.toString() }
+
+    @get:Rule
+    val benchmark = BenchmarkRule()
+
+    @Test
+    fun forEach() {
+        benchmark.measureRepeated {
+            @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
+            var last: String
+            list.forEach { element ->
+                last = element
+            }
+        }
+    }
+
+    @Test
+    fun add() {
+        val mutableList = MutableObjectList<String>(ObjectCount)
+        benchmark.measureRepeated {
+            repeat(ObjectCount) {
+                mutableList += array[it]
+            }
+            mutableList.clear()
+        }
+    }
+
+    @Test
+    fun contains() {
+        benchmark.measureRepeated {
+            repeat(ObjectCount) {
+                list.contains(array[it])
+            }
+        }
+    }
+
+    @Test
+    fun get() {
+        benchmark.measureRepeated {
+            repeat(ObjectCount) {
+                list[it]
+            }
+        }
+    }
+
+    @Test
+    fun addAll() {
+        val mutableList = MutableObjectList<String>(ObjectCount)
+        benchmark.measureRepeated {
+            mutableList += list
+            mutableList.clear()
+        }
+    }
+
+    @Test
+    fun removeStart() {
+        val mutableList = MutableObjectList<String>(ObjectCount)
+        benchmark.measureRepeated {
+            mutableList += list
+            repeat(ObjectCount) {
+                mutableList.removeAt(0)
+            }
+        }
+    }
+
+    @Test
+    fun removeEnd() {
+        val mutableList = MutableObjectList<String>(ObjectCount)
+        benchmark.measureRepeated {
+            mutableList += list
+            for (i in ObjectCount - 1 downTo 0) {
+                mutableList.removeAt(i)
+            }
+        }
+    }
+}
diff --git a/collection/collection-benchmark/src/androidTest/java/androidx/collection/ScatterMapBenchmarkTest.kt b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ScatterMapBenchmarkTest.kt
similarity index 100%
rename from collection/collection-benchmark/src/androidTest/java/androidx/collection/ScatterMapBenchmarkTest.kt
rename to collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ScatterMapBenchmarkTest.kt
diff --git a/collection/collection-benchmark/src/androidTest/java/androidx/collection/ScatterSetBenchmarkTest.kt b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ScatterSetBenchmarkTest.kt
similarity index 100%
rename from collection/collection-benchmark/src/androidTest/java/androidx/collection/ScatterSetBenchmarkTest.kt
rename to collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ScatterSetBenchmarkTest.kt
diff --git a/collection/collection-benchmark/src/androidTest/java/androidx/collection/SimpleArrayMapBenchmarkTest.kt b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/SimpleArrayMapBenchmarkTest.kt
similarity index 100%
rename from collection/collection-benchmark/src/androidTest/java/androidx/collection/SimpleArrayMapBenchmarkTest.kt
rename to collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/SimpleArrayMapBenchmarkTest.kt
diff --git a/collection/collection-benchmark/src/androidTest/java/androidx/collection/SparseArrayFilledBenchmarkTest.kt b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/SparseArrayFilledBenchmarkTest.kt
similarity index 100%
rename from collection/collection-benchmark/src/androidTest/java/androidx/collection/SparseArrayFilledBenchmarkTest.kt
rename to collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/SparseArrayFilledBenchmarkTest.kt
diff --git a/collection/collection-benchmark/src/androidTest/java/androidx/collection/junit.kt b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/junit.kt
similarity index 100%
rename from collection/collection-benchmark/src/androidTest/java/androidx/collection/junit.kt
rename to collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/junit.kt
diff --git a/collection/collection/api/current.txt b/collection/collection/api/current.txt
index 5bcab89..53cd0b5 100644
--- a/collection/collection/api/current.txt
+++ b/collection/collection/api/current.txt
@@ -94,6 +94,120 @@
     property public final int last;
   }
 
+  public abstract sealed class FloatFloatMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(float key);
+    method public final int getCapacity();
+    method public final float getOrDefault(float key, float defaultValue);
+    method public final inline float getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class FloatFloatMapKt {
+    method public static androidx.collection.FloatFloatMap emptyFloatFloatMap();
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf();
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf(float key1, float value1);
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf(float key1, float value1, float key2, float value2);
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf(float key1, float value1, float key2, float value2, float key3, float value3);
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf(float key1, float value1, float key2, float value2, float key3, float value3, float key4, float value4);
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf(float key1, float value1, float key2, float value2, float key3, float value3, float key4, float value4, float key5, float value5);
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf();
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(float key1, float value1);
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(float key1, float value1, float key2, float value2);
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(float key1, float value1, float key2, float value2, float key3, float value3);
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(float key1, float value1, float key2, float value2, float key3, float value3, float key4, float value4);
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(float key1, float value1, float key2, float value2, float key3, float value3, float key4, float value4, float key5, float value5);
+  }
+
+  @kotlin.jvm.JvmInline public final value class FloatFloatPair {
+    ctor public FloatFloatPair(float first, float second);
+    method public inline operator float component1();
+    method public inline operator float component2();
+    method public inline float getFirst();
+    method public inline float getSecond();
+    property public final inline float first;
+    property public final inline float second;
+  }
+
+  public abstract sealed class FloatIntMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(float key);
+    method public final int getCapacity();
+    method public final int getOrDefault(float key, int defaultValue);
+    method public final inline int getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class FloatIntMapKt {
+    method public static androidx.collection.FloatIntMap emptyFloatIntMap();
+    method public static androidx.collection.FloatIntMap floatIntMapOf();
+    method public static androidx.collection.FloatIntMap floatIntMapOf(float key1, int value1);
+    method public static androidx.collection.FloatIntMap floatIntMapOf(float key1, int value1, float key2, int value2);
+    method public static androidx.collection.FloatIntMap floatIntMapOf(float key1, int value1, float key2, int value2, float key3, int value3);
+    method public static androidx.collection.FloatIntMap floatIntMapOf(float key1, int value1, float key2, int value2, float key3, int value3, float key4, int value4);
+    method public static androidx.collection.FloatIntMap floatIntMapOf(float key1, int value1, float key2, int value2, float key3, int value3, float key4, int value4, float key5, int value5);
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf();
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(float key1, int value1);
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(float key1, int value1, float key2, int value2);
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(float key1, int value1, float key2, int value2, float key3, int value3);
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(float key1, int value1, float key2, int value2, float key3, int value3, float key4, int value4);
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(float key1, int value1, float key2, int value2, float key3, int value3, float key4, int value4, float key5, int value5);
+  }
+
   public abstract sealed class FloatList {
     method public final boolean any();
     method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
@@ -122,6 +236,18 @@
     method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
     method public final float last();
     method public final inline float last(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
     method public final int lastIndexOf(float element);
@@ -146,6 +272,110 @@
     method public static inline androidx.collection.MutableFloatList mutableFloatListOf(float... elements);
   }
 
+  public abstract sealed class FloatLongMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(float key);
+    method public final int getCapacity();
+    method public final long getOrDefault(float key, long defaultValue);
+    method public final inline long getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class FloatLongMapKt {
+    method public static androidx.collection.FloatLongMap emptyFloatLongMap();
+    method public static androidx.collection.FloatLongMap floatLongMapOf();
+    method public static androidx.collection.FloatLongMap floatLongMapOf(float key1, long value1);
+    method public static androidx.collection.FloatLongMap floatLongMapOf(float key1, long value1, float key2, long value2);
+    method public static androidx.collection.FloatLongMap floatLongMapOf(float key1, long value1, float key2, long value2, float key3, long value3);
+    method public static androidx.collection.FloatLongMap floatLongMapOf(float key1, long value1, float key2, long value2, float key3, long value3, float key4, long value4);
+    method public static androidx.collection.FloatLongMap floatLongMapOf(float key1, long value1, float key2, long value2, float key3, long value3, float key4, long value4, float key5, long value5);
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf();
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(float key1, long value1);
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(float key1, long value1, float key2, long value2);
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(float key1, long value1, float key2, long value2, float key3, long value3);
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(float key1, long value1, float key2, long value2, float key3, long value3, float key4, long value4);
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(float key1, long value1, float key2, long value2, float key3, long value3, float key4, long value4, float key5, long value5);
+  }
+
+  public abstract sealed class FloatObjectMap<V> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(V value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+    method public final operator V? get(float key);
+    method public final int getCapacity();
+    method public final V getOrDefault(float key, V defaultValue);
+    method public final inline V getOrElse(float key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class FloatObjectMapKt {
+    method public static <V> androidx.collection.FloatObjectMap<V> emptyFloatObjectMap();
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf();
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(float key1, V value1);
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(float key1, V value1, float key2, V value2);
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(float key1, V value1, float key2, V value2, float key3, V value3);
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(float key1, V value1, float key2, V value2, float key3, V value3, float key4, V value4);
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(float key1, V value1, float key2, V value2, float key3, V value3, float key4, V value4, float key5, V value5);
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf();
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(float key1, V value1);
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(float key1, V value1, float key2, V value2);
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(float key1, V value1, float key2, V value2, float key3, V value3);
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(float key1, V value1, float key2, V value2, float key3, V value3, float key4, V value4);
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(float key1, V value1, float key2, V value2, float key3, V value3, float key4, V value4, float key5, V value5);
+  }
+
   public abstract sealed class FloatSet {
     method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -160,6 +390,18 @@
     method @IntRange(from=0L) public final int getSize();
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
     method public final boolean none();
     property @IntRange(from=0L) public final int capacity;
     property @IntRange(from=0L) public final int size;
@@ -179,6 +421,120 @@
     method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float... elements);
   }
 
+  public abstract sealed class IntFloatMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(int key);
+    method public final int getCapacity();
+    method public final float getOrDefault(int key, float defaultValue);
+    method public final inline float getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class IntFloatMapKt {
+    method public static androidx.collection.IntFloatMap emptyIntFloatMap();
+    method public static androidx.collection.IntFloatMap intFloatMapOf();
+    method public static androidx.collection.IntFloatMap intFloatMapOf(int key1, float value1);
+    method public static androidx.collection.IntFloatMap intFloatMapOf(int key1, float value1, int key2, float value2);
+    method public static androidx.collection.IntFloatMap intFloatMapOf(int key1, float value1, int key2, float value2, int key3, float value3);
+    method public static androidx.collection.IntFloatMap intFloatMapOf(int key1, float value1, int key2, float value2, int key3, float value3, int key4, float value4);
+    method public static androidx.collection.IntFloatMap intFloatMapOf(int key1, float value1, int key2, float value2, int key3, float value3, int key4, float value4, int key5, float value5);
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf();
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(int key1, float value1);
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(int key1, float value1, int key2, float value2);
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(int key1, float value1, int key2, float value2, int key3, float value3);
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(int key1, float value1, int key2, float value2, int key3, float value3, int key4, float value4);
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(int key1, float value1, int key2, float value2, int key3, float value3, int key4, float value4, int key5, float value5);
+  }
+
+  public abstract sealed class IntIntMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(int key);
+    method public final int getCapacity();
+    method public final int getOrDefault(int key, int defaultValue);
+    method public final inline int getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class IntIntMapKt {
+    method public static androidx.collection.IntIntMap emptyIntIntMap();
+    method public static androidx.collection.IntIntMap intIntMapOf();
+    method public static androidx.collection.IntIntMap intIntMapOf(int key1, int value1);
+    method public static androidx.collection.IntIntMap intIntMapOf(int key1, int value1, int key2, int value2);
+    method public static androidx.collection.IntIntMap intIntMapOf(int key1, int value1, int key2, int value2, int key3, int value3);
+    method public static androidx.collection.IntIntMap intIntMapOf(int key1, int value1, int key2, int value2, int key3, int value3, int key4, int value4);
+    method public static androidx.collection.IntIntMap intIntMapOf(int key1, int value1, int key2, int value2, int key3, int value3, int key4, int value4, int key5, int value5);
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf();
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(int key1, int value1);
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(int key1, int value1, int key2, int value2);
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(int key1, int value1, int key2, int value2, int key3, int value3);
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(int key1, int value1, int key2, int value2, int key3, int value3, int key4, int value4);
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(int key1, int value1, int key2, int value2, int key3, int value3, int key4, int value4, int key5, int value5);
+  }
+
+  @kotlin.jvm.JvmInline public final value class IntIntPair {
+    ctor public IntIntPair(int first, int second);
+    method public inline operator int component1();
+    method public inline operator int component2();
+    method public int getFirst();
+    method public int getSecond();
+    property public final int first;
+    property public final int second;
+  }
+
   public abstract sealed class IntList {
     method public final boolean any();
     method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
@@ -207,6 +563,18 @@
     method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
     method public final int last();
     method public final inline int last(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
     method public final int lastIndexOf(int element);
@@ -231,6 +599,110 @@
     method public static inline androidx.collection.MutableIntList mutableIntListOf(int... elements);
   }
 
+  public abstract sealed class IntLongMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(int key);
+    method public final int getCapacity();
+    method public final long getOrDefault(int key, long defaultValue);
+    method public final inline long getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class IntLongMapKt {
+    method public static androidx.collection.IntLongMap emptyIntLongMap();
+    method public static androidx.collection.IntLongMap intLongMapOf();
+    method public static androidx.collection.IntLongMap intLongMapOf(int key1, long value1);
+    method public static androidx.collection.IntLongMap intLongMapOf(int key1, long value1, int key2, long value2);
+    method public static androidx.collection.IntLongMap intLongMapOf(int key1, long value1, int key2, long value2, int key3, long value3);
+    method public static androidx.collection.IntLongMap intLongMapOf(int key1, long value1, int key2, long value2, int key3, long value3, int key4, long value4);
+    method public static androidx.collection.IntLongMap intLongMapOf(int key1, long value1, int key2, long value2, int key3, long value3, int key4, long value4, int key5, long value5);
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf();
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(int key1, long value1);
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(int key1, long value1, int key2, long value2);
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(int key1, long value1, int key2, long value2, int key3, long value3);
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(int key1, long value1, int key2, long value2, int key3, long value3, int key4, long value4);
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(int key1, long value1, int key2, long value2, int key3, long value3, int key4, long value4, int key5, long value5);
+  }
+
+  public abstract sealed class IntObjectMap<V> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(V value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+    method public final operator V? get(int key);
+    method public final int getCapacity();
+    method public final V getOrDefault(int key, V defaultValue);
+    method public final inline V getOrElse(int key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class IntObjectMapKt {
+    method public static <V> androidx.collection.IntObjectMap<V> emptyIntObjectMap();
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf();
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(int key1, V value1);
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(int key1, V value1, int key2, V value2);
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(int key1, V value1, int key2, V value2, int key3, V value3);
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(int key1, V value1, int key2, V value2, int key3, V value3, int key4, V value4);
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(int key1, V value1, int key2, V value2, int key3, V value3, int key4, V value4, int key5, V value5);
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf();
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(int key1, V value1);
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(int key1, V value1, int key2, V value2);
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(int key1, V value1, int key2, V value2, int key3, V value3);
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(int key1, V value1, int key2, V value2, int key3, V value3, int key4, V value4);
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(int key1, V value1, int key2, V value2, int key3, V value3, int key4, V value4, int key5, V value5);
+  }
+
   public abstract sealed class IntSet {
     method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -245,6 +717,18 @@
     method @IntRange(from=0L) public final int getSize();
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
     method public final boolean none();
     property @IntRange(from=0L) public final int capacity;
     property @IntRange(from=0L) public final int size;
@@ -264,6 +748,110 @@
     method public static androidx.collection.MutableIntSet mutableIntSetOf(int... elements);
   }
 
+  public abstract sealed class LongFloatMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(long key);
+    method public final int getCapacity();
+    method public final float getOrDefault(long key, float defaultValue);
+    method public final inline float getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class LongFloatMapKt {
+    method public static androidx.collection.LongFloatMap emptyLongFloatMap();
+    method public static androidx.collection.LongFloatMap longFloatMapOf();
+    method public static androidx.collection.LongFloatMap longFloatMapOf(long key1, float value1);
+    method public static androidx.collection.LongFloatMap longFloatMapOf(long key1, float value1, long key2, float value2);
+    method public static androidx.collection.LongFloatMap longFloatMapOf(long key1, float value1, long key2, float value2, long key3, float value3);
+    method public static androidx.collection.LongFloatMap longFloatMapOf(long key1, float value1, long key2, float value2, long key3, float value3, long key4, float value4);
+    method public static androidx.collection.LongFloatMap longFloatMapOf(long key1, float value1, long key2, float value2, long key3, float value3, long key4, float value4, long key5, float value5);
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf();
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(long key1, float value1);
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(long key1, float value1, long key2, float value2);
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(long key1, float value1, long key2, float value2, long key3, float value3);
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(long key1, float value1, long key2, float value2, long key3, float value3, long key4, float value4);
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(long key1, float value1, long key2, float value2, long key3, float value3, long key4, float value4, long key5, float value5);
+  }
+
+  public abstract sealed class LongIntMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(long key);
+    method public final int getCapacity();
+    method public final int getOrDefault(long key, int defaultValue);
+    method public final inline int getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class LongIntMapKt {
+    method public static androidx.collection.LongIntMap emptyLongIntMap();
+    method public static androidx.collection.LongIntMap longIntMapOf();
+    method public static androidx.collection.LongIntMap longIntMapOf(long key1, int value1);
+    method public static androidx.collection.LongIntMap longIntMapOf(long key1, int value1, long key2, int value2);
+    method public static androidx.collection.LongIntMap longIntMapOf(long key1, int value1, long key2, int value2, long key3, int value3);
+    method public static androidx.collection.LongIntMap longIntMapOf(long key1, int value1, long key2, int value2, long key3, int value3, long key4, int value4);
+    method public static androidx.collection.LongIntMap longIntMapOf(long key1, int value1, long key2, int value2, long key3, int value3, long key4, int value4, long key5, int value5);
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf();
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(long key1, int value1);
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(long key1, int value1, long key2, int value2);
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(long key1, int value1, long key2, int value2, long key3, int value3);
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(long key1, int value1, long key2, int value2, long key3, int value3, long key4, int value4);
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(long key1, int value1, long key2, int value2, long key3, int value3, long key4, int value4, long key5, int value5);
+  }
+
   public abstract sealed class LongList {
     method public final boolean any();
     method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
@@ -292,6 +880,18 @@
     method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
     method public final long last();
     method public final inline long last(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
     method public final int lastIndexOf(long element);
@@ -316,6 +916,120 @@
     method public static inline androidx.collection.MutableLongList mutableLongListOf(long... elements);
   }
 
+  public abstract sealed class LongLongMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(long key);
+    method public final int getCapacity();
+    method public final long getOrDefault(long key, long defaultValue);
+    method public final inline long getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class LongLongMapKt {
+    method public static androidx.collection.LongLongMap emptyLongLongMap();
+    method public static androidx.collection.LongLongMap longLongMapOf();
+    method public static androidx.collection.LongLongMap longLongMapOf(long key1, long value1);
+    method public static androidx.collection.LongLongMap longLongMapOf(long key1, long value1, long key2, long value2);
+    method public static androidx.collection.LongLongMap longLongMapOf(long key1, long value1, long key2, long value2, long key3, long value3);
+    method public static androidx.collection.LongLongMap longLongMapOf(long key1, long value1, long key2, long value2, long key3, long value3, long key4, long value4);
+    method public static androidx.collection.LongLongMap longLongMapOf(long key1, long value1, long key2, long value2, long key3, long value3, long key4, long value4, long key5, long value5);
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf();
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(long key1, long value1);
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(long key1, long value1, long key2, long value2);
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(long key1, long value1, long key2, long value2, long key3, long value3);
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(long key1, long value1, long key2, long value2, long key3, long value3, long key4, long value4);
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(long key1, long value1, long key2, long value2, long key3, long value3, long key4, long value4, long key5, long value5);
+  }
+
+  public final class LongLongPair {
+    ctor public LongLongPair(long first, long second);
+    method public inline operator long component1();
+    method public inline operator long component2();
+    method public long getFirst();
+    method public long getSecond();
+    property public final long first;
+    property public final long second;
+  }
+
+  public abstract sealed class LongObjectMap<V> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(V value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+    method public final operator V? get(long key);
+    method public final int getCapacity();
+    method public final V getOrDefault(long key, V defaultValue);
+    method public final inline V getOrElse(long key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class LongObjectMapKt {
+    method public static <V> androidx.collection.LongObjectMap<V> emptyLongObjectMap();
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf();
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(long key1, V value1);
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(long key1, V value1, long key2, V value2);
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(long key1, V value1, long key2, V value2, long key3, V value3);
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(long key1, V value1, long key2, V value2, long key3, V value3, long key4, V value4);
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(long key1, V value1, long key2, V value2, long key3, V value3, long key4, V value4, long key5, V value5);
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf();
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(long key1, V value1);
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(long key1, V value1, long key2, V value2);
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(long key1, V value1, long key2, V value2, long key3, V value3);
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(long key1, V value1, long key2, V value2, long key3, V value3, long key4, V value4);
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(long key1, V value1, long key2, V value2, long key3, V value3, long key4, V value4, long key5, V value5);
+  }
+
   public abstract sealed class LongSet {
     method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -330,6 +1044,18 @@
     method @IntRange(from=0L) public final int getSize();
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
     method public final boolean none();
     property @IntRange(from=0L) public final int capacity;
     property @IntRange(from=0L) public final int size;
@@ -416,6 +1142,42 @@
     method public static inline <K, V> androidx.collection.LruCache<K,V> lruCache(int maxSize, optional kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf, optional kotlin.jvm.functions.Function1<? super K,? extends V> create, optional kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved);
   }
 
+  public final class MutableFloatFloatMap extends androidx.collection.FloatFloatMap {
+    ctor public MutableFloatFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatFloatMap from);
+    method public void put(float key, float value);
+    method public void putAll(androidx.collection.FloatFloatMap from);
+    method public void remove(float key);
+    method public boolean remove(float key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(float key, float value);
+    method public int trim();
+  }
+
+  public final class MutableFloatIntMap extends androidx.collection.FloatIntMap {
+    ctor public MutableFloatIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatIntMap from);
+    method public void put(float key, int value);
+    method public void putAll(androidx.collection.FloatIntMap from);
+    method public void remove(float key);
+    method public boolean remove(float key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(float key, int value);
+    method public int trim();
+  }
+
   public final class MutableFloatList extends androidx.collection.FloatList {
     ctor public MutableFloatList(optional int initialCapacity);
     method public boolean add(float element);
@@ -447,6 +1209,42 @@
     property public final inline int capacity;
   }
 
+  public final class MutableFloatLongMap extends androidx.collection.FloatLongMap {
+    ctor public MutableFloatLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatLongMap from);
+    method public void put(float key, long value);
+    method public void putAll(androidx.collection.FloatLongMap from);
+    method public void remove(float key);
+    method public boolean remove(float key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(float key, long value);
+    method public int trim();
+  }
+
+  public final class MutableFloatObjectMap<V> extends androidx.collection.FloatObjectMap<V> {
+    ctor public MutableFloatObjectMap(optional int initialCapacity);
+    method public void clear();
+    method public inline V getOrPut(float key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatObjectMap<V> from);
+    method public V? put(float key, V value);
+    method public void putAll(androidx.collection.FloatObjectMap<V> from);
+    method public V? remove(float key);
+    method public boolean remove(float key, V value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public operator void set(float key, V value);
+    method public int trim();
+  }
+
   public final class MutableFloatSet extends androidx.collection.FloatSet {
     ctor public MutableFloatSet(optional int initialCapacity);
     method public boolean add(float element);
@@ -465,6 +1263,42 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public final class MutableIntFloatMap extends androidx.collection.IntFloatMap {
+    ctor public MutableIntFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntFloatMap from);
+    method public void put(int key, float value);
+    method public void putAll(androidx.collection.IntFloatMap from);
+    method public void remove(int key);
+    method public boolean remove(int key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(int key, float value);
+    method public int trim();
+  }
+
+  public final class MutableIntIntMap extends androidx.collection.IntIntMap {
+    ctor public MutableIntIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntIntMap from);
+    method public void put(int key, int value);
+    method public void putAll(androidx.collection.IntIntMap from);
+    method public void remove(int key);
+    method public boolean remove(int key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(int key, int value);
+    method public int trim();
+  }
+
   public final class MutableIntList extends androidx.collection.IntList {
     ctor public MutableIntList(optional int initialCapacity);
     method public boolean add(int element);
@@ -496,6 +1330,42 @@
     property public final inline int capacity;
   }
 
+  public final class MutableIntLongMap extends androidx.collection.IntLongMap {
+    ctor public MutableIntLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntLongMap from);
+    method public void put(int key, long value);
+    method public void putAll(androidx.collection.IntLongMap from);
+    method public void remove(int key);
+    method public boolean remove(int key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(int key, long value);
+    method public int trim();
+  }
+
+  public final class MutableIntObjectMap<V> extends androidx.collection.IntObjectMap<V> {
+    ctor public MutableIntObjectMap(optional int initialCapacity);
+    method public void clear();
+    method public inline V getOrPut(int key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntObjectMap<V> from);
+    method public V? put(int key, V value);
+    method public void putAll(androidx.collection.IntObjectMap<V> from);
+    method public V? remove(int key);
+    method public boolean remove(int key, V value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public operator void set(int key, V value);
+    method public int trim();
+  }
+
   public final class MutableIntSet extends androidx.collection.IntSet {
     ctor public MutableIntSet(optional int initialCapacity);
     method public boolean add(int element);
@@ -514,6 +1384,42 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public final class MutableLongFloatMap extends androidx.collection.LongFloatMap {
+    ctor public MutableLongFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongFloatMap from);
+    method public void put(long key, float value);
+    method public void putAll(androidx.collection.LongFloatMap from);
+    method public void remove(long key);
+    method public boolean remove(long key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(long key, float value);
+    method public int trim();
+  }
+
+  public final class MutableLongIntMap extends androidx.collection.LongIntMap {
+    ctor public MutableLongIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongIntMap from);
+    method public void put(long key, int value);
+    method public void putAll(androidx.collection.LongIntMap from);
+    method public void remove(long key);
+    method public boolean remove(long key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(long key, int value);
+    method public int trim();
+  }
+
   public final class MutableLongList extends androidx.collection.LongList {
     ctor public MutableLongList(optional int initialCapacity);
     method public void add(@IntRange(from=0L) int index, long element);
@@ -545,6 +1451,42 @@
     property public final inline int capacity;
   }
 
+  public final class MutableLongLongMap extends androidx.collection.LongLongMap {
+    ctor public MutableLongLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongLongMap from);
+    method public void put(long key, long value);
+    method public void putAll(androidx.collection.LongLongMap from);
+    method public void remove(long key);
+    method public boolean remove(long key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(long key, long value);
+    method public int trim();
+  }
+
+  public final class MutableLongObjectMap<V> extends androidx.collection.LongObjectMap<V> {
+    ctor public MutableLongObjectMap(optional int initialCapacity);
+    method public void clear();
+    method public inline V getOrPut(long key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongObjectMap<V> from);
+    method public V? put(long key, V value);
+    method public void putAll(androidx.collection.LongObjectMap<V> from);
+    method public V? remove(long key);
+    method public boolean remove(long key, V value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public operator void set(long key, V value);
+    method public int trim();
+  }
+
   public final class MutableLongSet extends androidx.collection.LongSet {
     ctor public MutableLongSet(optional int initialCapacity);
     method public boolean add(long element);
@@ -563,11 +1505,122 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public final class MutableObjectFloatMap<K> extends androidx.collection.ObjectFloatMap<K> {
+    ctor public MutableObjectFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+    method public inline operator void minusAssign(Iterable<? extends K> keys);
+    method public inline operator void minusAssign(K key);
+    method public inline operator void minusAssign(K![] keys);
+    method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+    method public inline operator void plusAssign(androidx.collection.ObjectFloatMap<K> from);
+    method public void put(K key, float value);
+    method public void putAll(androidx.collection.ObjectFloatMap<K> from);
+    method public void remove(K key);
+    method public boolean remove(K key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(K key, float value);
+    method public int trim();
+  }
+
+  public final class MutableObjectIntMap<K> extends androidx.collection.ObjectIntMap<K> {
+    ctor public MutableObjectIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+    method public inline operator void minusAssign(Iterable<? extends K> keys);
+    method public inline operator void minusAssign(K key);
+    method public inline operator void minusAssign(K![] keys);
+    method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+    method public inline operator void plusAssign(androidx.collection.ObjectIntMap<K> from);
+    method public void put(K key, int value);
+    method public void putAll(androidx.collection.ObjectIntMap<K> from);
+    method public void remove(K key);
+    method public boolean remove(K key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(K key, int value);
+    method public int trim();
+  }
+
+  public final class MutableObjectList<E> extends androidx.collection.ObjectList<E> {
+    ctor public MutableObjectList(optional int initialCapacity);
+    method public boolean add(E element);
+    method public void add(@IntRange(from=0L) int index, E element);
+    method public boolean addAll(androidx.collection.ObjectList<E> elements);
+    method public boolean addAll(androidx.collection.ScatterSet<E> elements);
+    method public boolean addAll(E![] elements);
+    method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.ObjectList<E> elements);
+    method public boolean addAll(@IntRange(from=0L) int index, E![] elements);
+    method public boolean addAll(@IntRange(from=0L) int index, java.util.Collection<? extends E> elements);
+    method public boolean addAll(Iterable<? extends E> elements);
+    method public boolean addAll(java.util.List<? extends E> elements);
+    method public boolean addAll(kotlin.sequences.Sequence<? extends E> elements);
+    method public java.util.List<E> asList();
+    method public java.util.List<E> asMutableList();
+    method public void clear();
+    method public void ensureCapacity(int capacity);
+    method public inline int getCapacity();
+    method public operator void minusAssign(androidx.collection.ObjectList<E> elements);
+    method public operator void minusAssign(androidx.collection.ScatterSet<E> elements);
+    method public inline operator void minusAssign(E element);
+    method public operator void minusAssign(E![] elements);
+    method public operator void minusAssign(Iterable<? extends E> elements);
+    method public operator void minusAssign(java.util.List<? extends E> elements);
+    method public operator void minusAssign(kotlin.sequences.Sequence<? extends E> elements);
+    method public operator void plusAssign(androidx.collection.ObjectList<E> elements);
+    method public operator void plusAssign(androidx.collection.ScatterSet<E> elements);
+    method public inline operator void plusAssign(E element);
+    method public operator void plusAssign(E![] elements);
+    method public operator void plusAssign(Iterable<? extends E> elements);
+    method public operator void plusAssign(java.util.List<? extends E> elements);
+    method public operator void plusAssign(kotlin.sequences.Sequence<? extends E> elements);
+    method public boolean remove(E element);
+    method public boolean removeAll(androidx.collection.ObjectList<E> elements);
+    method public boolean removeAll(androidx.collection.ScatterSet<E> elements);
+    method public boolean removeAll(E![] elements);
+    method public boolean removeAll(Iterable<? extends E> elements);
+    method public boolean removeAll(java.util.List<? extends E> elements);
+    method public boolean removeAll(kotlin.sequences.Sequence<? extends E> elements);
+    method public E removeAt(@IntRange(from=0L) int index);
+    method public inline void removeIf(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public void removeRange(@IntRange(from=0L) int start, @IntRange(from=0L) int end);
+    method public boolean retainAll(androidx.collection.ObjectList<E> elements);
+    method public boolean retainAll(E![] elements);
+    method public boolean retainAll(Iterable<? extends E> elements);
+    method public boolean retainAll(java.util.Collection<? extends E> elements);
+    method public boolean retainAll(kotlin.sequences.Sequence<? extends E> elements);
+    method public operator E set(@IntRange(from=0L) int index, E element);
+    method public void trim(optional int minCapacity);
+    property public final inline int capacity;
+  }
+
+  public final class MutableObjectLongMap<K> extends androidx.collection.ObjectLongMap<K> {
+    ctor public MutableObjectLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+    method public inline operator void minusAssign(Iterable<? extends K> keys);
+    method public inline operator void minusAssign(K key);
+    method public inline operator void minusAssign(K![] keys);
+    method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+    method public inline operator void plusAssign(androidx.collection.ObjectLongMap<K> from);
+    method public void put(K key, long value);
+    method public void putAll(androidx.collection.ObjectLongMap<K> from);
+    method public void remove(K key);
+    method public boolean remove(K key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(K key, long value);
+    method public int trim();
+  }
+
   public final class MutableScatterMap<K, V> extends androidx.collection.ScatterMap<K,V> {
     ctor public MutableScatterMap(optional int initialCapacity);
     method public java.util.Map<K,V> asMutableMap();
     method public void clear();
     method public inline V getOrPut(K key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ObjectList<K> keys);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
     method public inline operator void minusAssign(Iterable<? extends K> keys);
     method public inline operator void minusAssign(K key);
     method public inline operator void minusAssign(K![] keys);
@@ -594,23 +1647,27 @@
   public final class MutableScatterSet<E> extends androidx.collection.ScatterSet<E> {
     ctor public MutableScatterSet(optional int initialCapacity);
     method public boolean add(E element);
+    method public boolean addAll(androidx.collection.ObjectList<E> elements);
     method public boolean addAll(androidx.collection.ScatterSet<E> elements);
     method public boolean addAll(E![] elements);
     method public boolean addAll(Iterable<? extends E> elements);
     method public boolean addAll(kotlin.sequences.Sequence<? extends E> elements);
     method public java.util.Set<E> asMutableSet();
     method public void clear();
+    method public operator void minusAssign(androidx.collection.ObjectList<E> elements);
     method public operator void minusAssign(androidx.collection.ScatterSet<E> elements);
     method public operator void minusAssign(E element);
     method public operator void minusAssign(E![] elements);
     method public operator void minusAssign(Iterable<? extends E> elements);
     method public operator void minusAssign(kotlin.sequences.Sequence<? extends E> elements);
+    method public operator void plusAssign(androidx.collection.ObjectList<E> elements);
     method public operator void plusAssign(androidx.collection.ScatterSet<E> elements);
     method public operator void plusAssign(E element);
     method public operator void plusAssign(E![] elements);
     method public operator void plusAssign(Iterable<? extends E> elements);
     method public operator void plusAssign(kotlin.sequences.Sequence<? extends E> elements);
     method public boolean remove(E element);
+    method public boolean removeAll(androidx.collection.ObjectList<E> elements);
     method public boolean removeAll(androidx.collection.ScatterSet<E> elements);
     method public boolean removeAll(E![] elements);
     method public boolean removeAll(Iterable<? extends E> elements);
@@ -619,34 +1676,227 @@
     method @IntRange(from=0L) public int trim();
   }
 
-  @kotlin.jvm.JvmInline public final value class PairFloatFloat {
-    ctor public PairFloatFloat(float first, float second);
-    method public inline operator float component1();
-    method public inline operator float component2();
-    method public inline float getFirst();
-    method public inline float getSecond();
-    property public final inline float first;
-    property public final inline float second;
+  public abstract sealed class ObjectFloatMap<K> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(K key);
+    method public final boolean containsKey(K key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(K key);
+    method public final int getCapacity();
+    method public final float getOrDefault(K key, float defaultValue);
+    method public final inline float getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
   }
 
-  @kotlin.jvm.JvmInline public final value class PairIntInt {
-    ctor public PairIntInt(int first, int second);
-    method public inline operator int component1();
-    method public inline operator int component2();
-    method public int getFirst();
-    method public int getSecond();
-    property public final int first;
-    property public final int second;
+  public final class ObjectFloatMapKt {
+    method public static <K> androidx.collection.ObjectFloatMap<K> emptyObjectFloatMap();
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf();
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(K key1, float value1);
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(K key1, float value1, K key2, float value2);
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(K key1, float value1, K key2, float value2, K key3, float value3);
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(K key1, float value1, K key2, float value2, K key3, float value3, K key4, float value4);
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(K key1, float value1, K key2, float value2, K key3, float value3, K key4, float value4, K key5, float value5);
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMap();
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(K key1, float value1);
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(K key1, float value1, K key2, float value2);
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(K key1, float value1, K key2, float value2, K key3, float value3);
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(K key1, float value1, K key2, float value2, K key3, float value3, K key4, float value4);
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(K key1, float value1, K key2, float value2, K key3, float value3, K key4, float value4, K key5, float value5);
   }
 
-  public final class PairLongLong {
-    ctor public PairLongLong(long first, long second);
-    method public inline operator long component1();
-    method public inline operator long component2();
-    method public long getFirst();
-    method public long getSecond();
-    property public final long first;
-    property public final long second;
+  public abstract sealed class ObjectIntMap<K> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(K key);
+    method public final boolean containsKey(K key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(K key);
+    method public final int getCapacity();
+    method public final int getOrDefault(K key, int defaultValue);
+    method public final inline int getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class ObjectIntMapKt {
+    method public static <K> androidx.collection.ObjectIntMap<K> emptyObjectIntMap();
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf();
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(K key1, int value1);
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(K key1, int value1, K key2, int value2);
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(K key1, int value1, K key2, int value2, K key3, int value3);
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(K key1, int value1, K key2, int value2, K key3, int value3, K key4, int value4);
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(K key1, int value1, K key2, int value2, K key3, int value3, K key4, int value4, K key5, int value5);
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMap();
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(K key1, int value1);
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(K key1, int value1, K key2, int value2);
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(K key1, int value1, K key2, int value2, K key3, int value3);
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(K key1, int value1, K key2, int value2, K key3, int value3, K key4, int value4);
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(K key1, int value1, K key2, int value2, K key3, int value3, K key4, int value4, K key5, int value5);
+  }
+
+  public abstract sealed class ObjectList<E> {
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public abstract java.util.List<E> asList();
+    method public final operator boolean contains(E element);
+    method public final boolean containsAll(androidx.collection.ObjectList<E> elements);
+    method public final boolean containsAll(E![] elements);
+    method public final boolean containsAll(Iterable<? extends E> elements);
+    method public final boolean containsAll(java.util.List<? extends E> elements);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final E elementAt(@IntRange(from=0L) int index);
+    method public final inline E elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends E> defaultValue);
+    method public final E first();
+    method public final inline E first(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final inline E? firstOrNull();
+    method public final inline E? firstOrNull(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final inline <R> R fold(R initial, kotlin.jvm.functions.Function2<? super R,? super E,? extends R> operation);
+    method public final inline <R> R foldIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super R,? super E,? extends R> operation);
+    method public final inline <R> R foldRight(R initial, kotlin.jvm.functions.Function2<? super E,? super R,? extends R> operation);
+    method public final inline <R> R foldRightIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super E,? super R,? extends R> operation);
+    method public final inline void forEach(kotlin.jvm.functions.Function1<? super E,kotlin.Unit> block);
+    method public final inline void forEachIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super E,kotlin.Unit> block);
+    method public final inline void forEachReversed(kotlin.jvm.functions.Function1<? super E,kotlin.Unit> block);
+    method public final inline void forEachReversedIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super E,kotlin.Unit> block);
+    method public final operator E get(@IntRange(from=0L) int index);
+    method public final inline kotlin.ranges.IntRange getIndices();
+    method @IntRange(from=-1L) public final inline int getLastIndex();
+    method @IntRange(from=0L) public final int getSize();
+    method public final int indexOf(E element);
+    method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, optional kotlin.jvm.functions.Function1<? super E,? extends java.lang.CharSequence>? transform);
+    method public final E last();
+    method public final inline E last(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final int lastIndexOf(E element);
+    method public final inline E? lastOrNull();
+    method public final inline E? lastOrNull(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final boolean none();
+    method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    property public final inline kotlin.ranges.IntRange indices;
+    property @IntRange(from=-1L) public final inline int lastIndex;
+    property @IntRange(from=0L) public final int size;
+  }
+
+  public final class ObjectListKt {
+    method public static <E> androidx.collection.ObjectList<E> emptyObjectList();
+    method public static inline <E> androidx.collection.MutableObjectList<E> mutableObjectListOf();
+    method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1);
+    method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1, E element2);
+    method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1, E element2, E element3);
+    method public static inline <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E?... elements);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf();
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1, E element2);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1, E element2, E element3);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E?... elements);
+  }
+
+  public abstract sealed class ObjectLongMap<K> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(K key);
+    method public final boolean containsKey(K key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(K key);
+    method public final int getCapacity();
+    method public final long getOrDefault(K key, long defaultValue);
+    method public final inline long getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class ObjectLongMapKt {
+    method public static <K> androidx.collection.ObjectLongMap<K> emptyObjectLongMap();
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf();
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(K key1, long value1);
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(K key1, long value1, K key2, long value2);
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(K key1, long value1, K key2, long value2, K key3, long value3);
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(K key1, long value1, K key2, long value2, K key3, long value3, K key4, long value4);
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(K key1, long value1, K key2, long value2, K key3, long value3, K key4, long value4, K key5, long value5);
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMap();
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(K key1, long value1);
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(K key1, long value1, K key2, long value2);
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(K key1, long value1, K key2, long value2, K key3, long value3);
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(K key1, long value1, K key2, long value2, K key3, long value3, K key4, long value4);
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(K key1, long value1, K key2, long value2, K key3, long value3, K key4, long value4, K key5, long value5);
   }
 
   public abstract sealed class ScatterMap<K, V> {
@@ -669,6 +1919,13 @@
     method public final int getSize();
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, optional kotlin.jvm.functions.Function2<? super K,? super V,? extends java.lang.CharSequence>? transform);
     method public final boolean none();
     property public final int capacity;
     property public final int size;
@@ -696,6 +1953,13 @@
     method @IntRange(from=0L) public final int getSize();
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, optional kotlin.jvm.functions.Function1<? super E,? extends java.lang.CharSequence>? transform);
     method public final boolean none();
     property @IntRange(from=0L) public final int capacity;
     property @IntRange(from=0L) public final int size;
diff --git a/collection/collection/api/restricted_current.txt b/collection/collection/api/restricted_current.txt
index a46df5c..b1ab5c9 100644
--- a/collection/collection/api/restricted_current.txt
+++ b/collection/collection/api/restricted_current.txt
@@ -94,6 +94,130 @@
     property public final int last;
   }
 
+  public abstract sealed class FloatFloatMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(float key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(float key);
+    method public final int getCapacity();
+    method public final float getOrDefault(float key, float defaultValue);
+    method public final inline float getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal float[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal float[] values;
+  }
+
+  public final class FloatFloatMapKt {
+    method public static androidx.collection.FloatFloatMap emptyFloatFloatMap();
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf();
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf(float key1, float value1);
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf(float key1, float value1, float key2, float value2);
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf(float key1, float value1, float key2, float value2, float key3, float value3);
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf(float key1, float value1, float key2, float value2, float key3, float value3, float key4, float value4);
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf(float key1, float value1, float key2, float value2, float key3, float value3, float key4, float value4, float key5, float value5);
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf();
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(float key1, float value1);
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(float key1, float value1, float key2, float value2);
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(float key1, float value1, float key2, float value2, float key3, float value3);
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(float key1, float value1, float key2, float value2, float key3, float value3, float key4, float value4);
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(float key1, float value1, float key2, float value2, float key3, float value3, float key4, float value4, float key5, float value5);
+  }
+
+  @kotlin.jvm.JvmInline public final value class FloatFloatPair {
+    ctor public FloatFloatPair(float first, float second);
+    method public inline operator float component1();
+    method public inline operator float component2();
+    method public inline float getFirst();
+    method public inline float getSecond();
+    property public final inline float first;
+    property public final inline float second;
+  }
+
+  public abstract sealed class FloatIntMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(float key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(float key);
+    method public final int getCapacity();
+    method public final int getOrDefault(float key, int defaultValue);
+    method public final inline int getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal float[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal int[] values;
+  }
+
+  public final class FloatIntMapKt {
+    method public static androidx.collection.FloatIntMap emptyFloatIntMap();
+    method public static androidx.collection.FloatIntMap floatIntMapOf();
+    method public static androidx.collection.FloatIntMap floatIntMapOf(float key1, int value1);
+    method public static androidx.collection.FloatIntMap floatIntMapOf(float key1, int value1, float key2, int value2);
+    method public static androidx.collection.FloatIntMap floatIntMapOf(float key1, int value1, float key2, int value2, float key3, int value3);
+    method public static androidx.collection.FloatIntMap floatIntMapOf(float key1, int value1, float key2, int value2, float key3, int value3, float key4, int value4);
+    method public static androidx.collection.FloatIntMap floatIntMapOf(float key1, int value1, float key2, int value2, float key3, int value3, float key4, int value4, float key5, int value5);
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf();
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(float key1, int value1);
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(float key1, int value1, float key2, int value2);
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(float key1, int value1, float key2, int value2, float key3, int value3);
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(float key1, int value1, float key2, int value2, float key3, int value3, float key4, int value4);
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(float key1, int value1, float key2, int value2, float key3, int value3, float key4, int value4, float key5, int value5);
+  }
+
   public abstract sealed class FloatList {
     method public final boolean any();
     method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
@@ -122,6 +246,18 @@
     method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
     method public final float last();
     method public final inline float last(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
     method public final int lastIndexOf(float element);
@@ -148,6 +284,119 @@
     method public static inline androidx.collection.MutableFloatList mutableFloatListOf(float... elements);
   }
 
+  public abstract sealed class FloatLongMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(float key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(float key);
+    method public final int getCapacity();
+    method public final long getOrDefault(float key, long defaultValue);
+    method public final inline long getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal float[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal long[] values;
+  }
+
+  public final class FloatLongMapKt {
+    method public static androidx.collection.FloatLongMap emptyFloatLongMap();
+    method public static androidx.collection.FloatLongMap floatLongMapOf();
+    method public static androidx.collection.FloatLongMap floatLongMapOf(float key1, long value1);
+    method public static androidx.collection.FloatLongMap floatLongMapOf(float key1, long value1, float key2, long value2);
+    method public static androidx.collection.FloatLongMap floatLongMapOf(float key1, long value1, float key2, long value2, float key3, long value3);
+    method public static androidx.collection.FloatLongMap floatLongMapOf(float key1, long value1, float key2, long value2, float key3, long value3, float key4, long value4);
+    method public static androidx.collection.FloatLongMap floatLongMapOf(float key1, long value1, float key2, long value2, float key3, long value3, float key4, long value4, float key5, long value5);
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf();
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(float key1, long value1);
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(float key1, long value1, float key2, long value2);
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(float key1, long value1, float key2, long value2, float key3, long value3);
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(float key1, long value1, float key2, long value2, float key3, long value3, float key4, long value4);
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(float key1, long value1, float key2, long value2, float key3, long value3, float key4, long value4, float key5, long value5);
+  }
+
+  public abstract sealed class FloatObjectMap<V> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(V value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+    method public final operator V? get(float key);
+    method public final int getCapacity();
+    method public final V getOrDefault(float key, V defaultValue);
+    method public final inline V getOrElse(float key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal float[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal Object![] values;
+  }
+
+  public final class FloatObjectMapKt {
+    method public static <V> androidx.collection.FloatObjectMap<V> emptyFloatObjectMap();
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf();
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(float key1, V value1);
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(float key1, V value1, float key2, V value2);
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(float key1, V value1, float key2, V value2, float key3, V value3);
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(float key1, V value1, float key2, V value2, float key3, V value3, float key4, V value4);
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(float key1, V value1, float key2, V value2, float key3, V value3, float key4, V value4, float key5, V value5);
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf();
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(float key1, V value1);
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(float key1, V value1, float key2, V value2);
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(float key1, V value1, float key2, V value2, float key3, V value3);
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(float key1, V value1, float key2, V value2, float key3, V value3, float key4, V value4);
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(float key1, V value1, float key2, V value2, float key3, V value3, float key4, V value4, float key5, V value5);
+  }
+
   public abstract sealed class FloatSet {
     method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -163,6 +412,18 @@
     method @IntRange(from=0L) public final int getSize();
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Float,? extends java.lang.CharSequence> transform);
     method public final boolean none();
     property @IntRange(from=0L) public final int capacity;
     property @IntRange(from=0L) public final int size;
@@ -184,6 +445,130 @@
     method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float... elements);
   }
 
+  public abstract sealed class IntFloatMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(int key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(int key);
+    method public final int getCapacity();
+    method public final float getOrDefault(int key, float defaultValue);
+    method public final inline float getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal int[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal float[] values;
+  }
+
+  public final class IntFloatMapKt {
+    method public static androidx.collection.IntFloatMap emptyIntFloatMap();
+    method public static androidx.collection.IntFloatMap intFloatMapOf();
+    method public static androidx.collection.IntFloatMap intFloatMapOf(int key1, float value1);
+    method public static androidx.collection.IntFloatMap intFloatMapOf(int key1, float value1, int key2, float value2);
+    method public static androidx.collection.IntFloatMap intFloatMapOf(int key1, float value1, int key2, float value2, int key3, float value3);
+    method public static androidx.collection.IntFloatMap intFloatMapOf(int key1, float value1, int key2, float value2, int key3, float value3, int key4, float value4);
+    method public static androidx.collection.IntFloatMap intFloatMapOf(int key1, float value1, int key2, float value2, int key3, float value3, int key4, float value4, int key5, float value5);
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf();
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(int key1, float value1);
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(int key1, float value1, int key2, float value2);
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(int key1, float value1, int key2, float value2, int key3, float value3);
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(int key1, float value1, int key2, float value2, int key3, float value3, int key4, float value4);
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(int key1, float value1, int key2, float value2, int key3, float value3, int key4, float value4, int key5, float value5);
+  }
+
+  public abstract sealed class IntIntMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(int key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(int key);
+    method public final int getCapacity();
+    method public final int getOrDefault(int key, int defaultValue);
+    method public final inline int getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal int[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal int[] values;
+  }
+
+  public final class IntIntMapKt {
+    method public static androidx.collection.IntIntMap emptyIntIntMap();
+    method public static androidx.collection.IntIntMap intIntMapOf();
+    method public static androidx.collection.IntIntMap intIntMapOf(int key1, int value1);
+    method public static androidx.collection.IntIntMap intIntMapOf(int key1, int value1, int key2, int value2);
+    method public static androidx.collection.IntIntMap intIntMapOf(int key1, int value1, int key2, int value2, int key3, int value3);
+    method public static androidx.collection.IntIntMap intIntMapOf(int key1, int value1, int key2, int value2, int key3, int value3, int key4, int value4);
+    method public static androidx.collection.IntIntMap intIntMapOf(int key1, int value1, int key2, int value2, int key3, int value3, int key4, int value4, int key5, int value5);
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf();
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(int key1, int value1);
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(int key1, int value1, int key2, int value2);
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(int key1, int value1, int key2, int value2, int key3, int value3);
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(int key1, int value1, int key2, int value2, int key3, int value3, int key4, int value4);
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(int key1, int value1, int key2, int value2, int key3, int value3, int key4, int value4, int key5, int value5);
+  }
+
+  @kotlin.jvm.JvmInline public final value class IntIntPair {
+    ctor public IntIntPair(int first, int second);
+    method public inline operator int component1();
+    method public inline operator int component2();
+    method public int getFirst();
+    method public int getSecond();
+    property public final int first;
+    property public final int second;
+  }
+
   public abstract sealed class IntList {
     method public final boolean any();
     method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
@@ -212,6 +597,18 @@
     method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
     method public final int last();
     method public final inline int last(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
     method public final int lastIndexOf(int element);
@@ -238,6 +635,119 @@
     method public static inline androidx.collection.MutableIntList mutableIntListOf(int... elements);
   }
 
+  public abstract sealed class IntLongMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(int key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(int key);
+    method public final int getCapacity();
+    method public final long getOrDefault(int key, long defaultValue);
+    method public final inline long getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal int[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal long[] values;
+  }
+
+  public final class IntLongMapKt {
+    method public static androidx.collection.IntLongMap emptyIntLongMap();
+    method public static androidx.collection.IntLongMap intLongMapOf();
+    method public static androidx.collection.IntLongMap intLongMapOf(int key1, long value1);
+    method public static androidx.collection.IntLongMap intLongMapOf(int key1, long value1, int key2, long value2);
+    method public static androidx.collection.IntLongMap intLongMapOf(int key1, long value1, int key2, long value2, int key3, long value3);
+    method public static androidx.collection.IntLongMap intLongMapOf(int key1, long value1, int key2, long value2, int key3, long value3, int key4, long value4);
+    method public static androidx.collection.IntLongMap intLongMapOf(int key1, long value1, int key2, long value2, int key3, long value3, int key4, long value4, int key5, long value5);
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf();
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(int key1, long value1);
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(int key1, long value1, int key2, long value2);
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(int key1, long value1, int key2, long value2, int key3, long value3);
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(int key1, long value1, int key2, long value2, int key3, long value3, int key4, long value4);
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(int key1, long value1, int key2, long value2, int key3, long value3, int key4, long value4, int key5, long value5);
+  }
+
+  public abstract sealed class IntObjectMap<V> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(V value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+    method public final operator V? get(int key);
+    method public final int getCapacity();
+    method public final V getOrDefault(int key, V defaultValue);
+    method public final inline V getOrElse(int key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal int[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal Object![] values;
+  }
+
+  public final class IntObjectMapKt {
+    method public static <V> androidx.collection.IntObjectMap<V> emptyIntObjectMap();
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf();
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(int key1, V value1);
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(int key1, V value1, int key2, V value2);
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(int key1, V value1, int key2, V value2, int key3, V value3);
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(int key1, V value1, int key2, V value2, int key3, V value3, int key4, V value4);
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(int key1, V value1, int key2, V value2, int key3, V value3, int key4, V value4, int key5, V value5);
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf();
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(int key1, V value1);
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(int key1, V value1, int key2, V value2);
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(int key1, V value1, int key2, V value2, int key3, V value3);
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(int key1, V value1, int key2, V value2, int key3, V value3, int key4, V value4);
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(int key1, V value1, int key2, V value2, int key3, V value3, int key4, V value4, int key5, V value5);
+  }
+
   public abstract sealed class IntSet {
     method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -253,6 +763,18 @@
     method @IntRange(from=0L) public final int getSize();
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.CharSequence> transform);
     method public final boolean none();
     property @IntRange(from=0L) public final int capacity;
     property @IntRange(from=0L) public final int size;
@@ -274,6 +796,120 @@
     method public static androidx.collection.MutableIntSet mutableIntSetOf(int... elements);
   }
 
+  public abstract sealed class LongFloatMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(long key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(long key);
+    method public final int getCapacity();
+    method public final float getOrDefault(long key, float defaultValue);
+    method public final inline float getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal long[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal float[] values;
+  }
+
+  public final class LongFloatMapKt {
+    method public static androidx.collection.LongFloatMap emptyLongFloatMap();
+    method public static androidx.collection.LongFloatMap longFloatMapOf();
+    method public static androidx.collection.LongFloatMap longFloatMapOf(long key1, float value1);
+    method public static androidx.collection.LongFloatMap longFloatMapOf(long key1, float value1, long key2, float value2);
+    method public static androidx.collection.LongFloatMap longFloatMapOf(long key1, float value1, long key2, float value2, long key3, float value3);
+    method public static androidx.collection.LongFloatMap longFloatMapOf(long key1, float value1, long key2, float value2, long key3, float value3, long key4, float value4);
+    method public static androidx.collection.LongFloatMap longFloatMapOf(long key1, float value1, long key2, float value2, long key3, float value3, long key4, float value4, long key5, float value5);
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf();
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(long key1, float value1);
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(long key1, float value1, long key2, float value2);
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(long key1, float value1, long key2, float value2, long key3, float value3);
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(long key1, float value1, long key2, float value2, long key3, float value3, long key4, float value4);
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(long key1, float value1, long key2, float value2, long key3, float value3, long key4, float value4, long key5, float value5);
+  }
+
+  public abstract sealed class LongIntMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(long key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(long key);
+    method public final int getCapacity();
+    method public final int getOrDefault(long key, int defaultValue);
+    method public final inline int getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal long[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal int[] values;
+  }
+
+  public final class LongIntMapKt {
+    method public static androidx.collection.LongIntMap emptyLongIntMap();
+    method public static androidx.collection.LongIntMap longIntMapOf();
+    method public static androidx.collection.LongIntMap longIntMapOf(long key1, int value1);
+    method public static androidx.collection.LongIntMap longIntMapOf(long key1, int value1, long key2, int value2);
+    method public static androidx.collection.LongIntMap longIntMapOf(long key1, int value1, long key2, int value2, long key3, int value3);
+    method public static androidx.collection.LongIntMap longIntMapOf(long key1, int value1, long key2, int value2, long key3, int value3, long key4, int value4);
+    method public static androidx.collection.LongIntMap longIntMapOf(long key1, int value1, long key2, int value2, long key3, int value3, long key4, int value4, long key5, int value5);
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf();
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(long key1, int value1);
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(long key1, int value1, long key2, int value2);
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(long key1, int value1, long key2, int value2, long key3, int value3);
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(long key1, int value1, long key2, int value2, long key3, int value3, long key4, int value4);
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(long key1, int value1, long key2, int value2, long key3, int value3, long key4, int value4, long key5, int value5);
+  }
+
   public abstract sealed class LongList {
     method public final boolean any();
     method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
@@ -302,6 +938,18 @@
     method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
     method public final long last();
     method public final inline long last(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
     method public final int lastIndexOf(long element);
@@ -328,6 +976,129 @@
     method public static inline androidx.collection.MutableLongList mutableLongListOf(long... elements);
   }
 
+  public abstract sealed class LongLongMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(long key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(long key);
+    method public final int getCapacity();
+    method public final long getOrDefault(long key, long defaultValue);
+    method public final inline long getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal long[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal long[] values;
+  }
+
+  public final class LongLongMapKt {
+    method public static androidx.collection.LongLongMap emptyLongLongMap();
+    method public static androidx.collection.LongLongMap longLongMapOf();
+    method public static androidx.collection.LongLongMap longLongMapOf(long key1, long value1);
+    method public static androidx.collection.LongLongMap longLongMapOf(long key1, long value1, long key2, long value2);
+    method public static androidx.collection.LongLongMap longLongMapOf(long key1, long value1, long key2, long value2, long key3, long value3);
+    method public static androidx.collection.LongLongMap longLongMapOf(long key1, long value1, long key2, long value2, long key3, long value3, long key4, long value4);
+    method public static androidx.collection.LongLongMap longLongMapOf(long key1, long value1, long key2, long value2, long key3, long value3, long key4, long value4, long key5, long value5);
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf();
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(long key1, long value1);
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(long key1, long value1, long key2, long value2);
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(long key1, long value1, long key2, long value2, long key3, long value3);
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(long key1, long value1, long key2, long value2, long key3, long value3, long key4, long value4);
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(long key1, long value1, long key2, long value2, long key3, long value3, long key4, long value4, long key5, long value5);
+  }
+
+  public final class LongLongPair {
+    ctor public LongLongPair(long first, long second);
+    method public inline operator long component1();
+    method public inline operator long component2();
+    method public long getFirst();
+    method public long getSecond();
+    property public final long first;
+    property public final long second;
+  }
+
+  public abstract sealed class LongObjectMap<V> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(V value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+    method public final operator V? get(long key);
+    method public final int getCapacity();
+    method public final V getOrDefault(long key, V defaultValue);
+    method public final inline V getOrElse(long key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal long[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal Object![] values;
+  }
+
+  public final class LongObjectMapKt {
+    method public static <V> androidx.collection.LongObjectMap<V> emptyLongObjectMap();
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf();
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(long key1, V value1);
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(long key1, V value1, long key2, V value2);
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(long key1, V value1, long key2, V value2, long key3, V value3);
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(long key1, V value1, long key2, V value2, long key3, V value3, long key4, V value4);
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(long key1, V value1, long key2, V value2, long key3, V value3, long key4, V value4, long key5, V value5);
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf();
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(long key1, V value1);
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(long key1, V value1, long key2, V value2);
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(long key1, V value1, long key2, V value2, long key3, V value3);
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(long key1, V value1, long key2, V value2, long key3, V value3, long key4, V value4);
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(long key1, V value1, long key2, V value2, long key3, V value3, long key4, V value4, long key5, V value5);
+  }
+
   public abstract sealed class LongSet {
     method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -343,6 +1114,18 @@
     method @IntRange(from=0L) public final int getSize();
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Long,? extends java.lang.CharSequence> transform);
     method public final boolean none();
     property @IntRange(from=0L) public final int capacity;
     property @IntRange(from=0L) public final int size;
@@ -431,6 +1214,42 @@
     method public static inline <K, V> androidx.collection.LruCache<K,V> lruCache(int maxSize, optional kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf, optional kotlin.jvm.functions.Function1<? super K,? extends V> create, optional kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved);
   }
 
+  public final class MutableFloatFloatMap extends androidx.collection.FloatFloatMap {
+    ctor public MutableFloatFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatFloatMap from);
+    method public void put(float key, float value);
+    method public void putAll(androidx.collection.FloatFloatMap from);
+    method public void remove(float key);
+    method public boolean remove(float key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(float key, float value);
+    method public int trim();
+  }
+
+  public final class MutableFloatIntMap extends androidx.collection.FloatIntMap {
+    ctor public MutableFloatIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatIntMap from);
+    method public void put(float key, int value);
+    method public void putAll(androidx.collection.FloatIntMap from);
+    method public void remove(float key);
+    method public boolean remove(float key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(float key, int value);
+    method public int trim();
+  }
+
   public final class MutableFloatList extends androidx.collection.FloatList {
     ctor public MutableFloatList(optional int initialCapacity);
     method public boolean add(float element);
@@ -462,6 +1281,42 @@
     property public final inline int capacity;
   }
 
+  public final class MutableFloatLongMap extends androidx.collection.FloatLongMap {
+    ctor public MutableFloatLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatLongMap from);
+    method public void put(float key, long value);
+    method public void putAll(androidx.collection.FloatLongMap from);
+    method public void remove(float key);
+    method public boolean remove(float key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(float key, long value);
+    method public int trim();
+  }
+
+  public final class MutableFloatObjectMap<V> extends androidx.collection.FloatObjectMap<V> {
+    ctor public MutableFloatObjectMap(optional int initialCapacity);
+    method public void clear();
+    method public inline V getOrPut(float key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatObjectMap<V> from);
+    method public V? put(float key, V value);
+    method public void putAll(androidx.collection.FloatObjectMap<V> from);
+    method public V? remove(float key);
+    method public boolean remove(float key, V value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public operator void set(float key, V value);
+    method public int trim();
+  }
+
   public final class MutableFloatSet extends androidx.collection.FloatSet {
     ctor public MutableFloatSet(optional int initialCapacity);
     method public boolean add(float element);
@@ -480,6 +1335,42 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public final class MutableIntFloatMap extends androidx.collection.IntFloatMap {
+    ctor public MutableIntFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntFloatMap from);
+    method public void put(int key, float value);
+    method public void putAll(androidx.collection.IntFloatMap from);
+    method public void remove(int key);
+    method public boolean remove(int key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(int key, float value);
+    method public int trim();
+  }
+
+  public final class MutableIntIntMap extends androidx.collection.IntIntMap {
+    ctor public MutableIntIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntIntMap from);
+    method public void put(int key, int value);
+    method public void putAll(androidx.collection.IntIntMap from);
+    method public void remove(int key);
+    method public boolean remove(int key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(int key, int value);
+    method public int trim();
+  }
+
   public final class MutableIntList extends androidx.collection.IntList {
     ctor public MutableIntList(optional int initialCapacity);
     method public boolean add(int element);
@@ -511,6 +1402,42 @@
     property public final inline int capacity;
   }
 
+  public final class MutableIntLongMap extends androidx.collection.IntLongMap {
+    ctor public MutableIntLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntLongMap from);
+    method public void put(int key, long value);
+    method public void putAll(androidx.collection.IntLongMap from);
+    method public void remove(int key);
+    method public boolean remove(int key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(int key, long value);
+    method public int trim();
+  }
+
+  public final class MutableIntObjectMap<V> extends androidx.collection.IntObjectMap<V> {
+    ctor public MutableIntObjectMap(optional int initialCapacity);
+    method public void clear();
+    method public inline V getOrPut(int key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntObjectMap<V> from);
+    method public V? put(int key, V value);
+    method public void putAll(androidx.collection.IntObjectMap<V> from);
+    method public V? remove(int key);
+    method public boolean remove(int key, V value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public operator void set(int key, V value);
+    method public int trim();
+  }
+
   public final class MutableIntSet extends androidx.collection.IntSet {
     ctor public MutableIntSet(optional int initialCapacity);
     method public boolean add(int element);
@@ -529,6 +1456,42 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public final class MutableLongFloatMap extends androidx.collection.LongFloatMap {
+    ctor public MutableLongFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongFloatMap from);
+    method public void put(long key, float value);
+    method public void putAll(androidx.collection.LongFloatMap from);
+    method public void remove(long key);
+    method public boolean remove(long key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(long key, float value);
+    method public int trim();
+  }
+
+  public final class MutableLongIntMap extends androidx.collection.LongIntMap {
+    ctor public MutableLongIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongIntMap from);
+    method public void put(long key, int value);
+    method public void putAll(androidx.collection.LongIntMap from);
+    method public void remove(long key);
+    method public boolean remove(long key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(long key, int value);
+    method public int trim();
+  }
+
   public final class MutableLongList extends androidx.collection.LongList {
     ctor public MutableLongList(optional int initialCapacity);
     method public void add(@IntRange(from=0L) int index, long element);
@@ -560,6 +1523,42 @@
     property public final inline int capacity;
   }
 
+  public final class MutableLongLongMap extends androidx.collection.LongLongMap {
+    ctor public MutableLongLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongLongMap from);
+    method public void put(long key, long value);
+    method public void putAll(androidx.collection.LongLongMap from);
+    method public void remove(long key);
+    method public boolean remove(long key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(long key, long value);
+    method public int trim();
+  }
+
+  public final class MutableLongObjectMap<V> extends androidx.collection.LongObjectMap<V> {
+    ctor public MutableLongObjectMap(optional int initialCapacity);
+    method public void clear();
+    method public inline V getOrPut(long key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongObjectMap<V> from);
+    method public V? put(long key, V value);
+    method public void putAll(androidx.collection.LongObjectMap<V> from);
+    method public V? remove(long key);
+    method public boolean remove(long key, V value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public operator void set(long key, V value);
+    method public int trim();
+  }
+
   public final class MutableLongSet extends androidx.collection.LongSet {
     ctor public MutableLongSet(optional int initialCapacity);
     method public boolean add(long element);
@@ -578,11 +1577,122 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public final class MutableObjectFloatMap<K> extends androidx.collection.ObjectFloatMap<K> {
+    ctor public MutableObjectFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+    method public inline operator void minusAssign(Iterable<? extends K> keys);
+    method public inline operator void minusAssign(K key);
+    method public inline operator void minusAssign(K![] keys);
+    method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+    method public inline operator void plusAssign(androidx.collection.ObjectFloatMap<K> from);
+    method public void put(K key, float value);
+    method public void putAll(androidx.collection.ObjectFloatMap<K> from);
+    method public void remove(K key);
+    method public boolean remove(K key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(K key, float value);
+    method public int trim();
+  }
+
+  public final class MutableObjectIntMap<K> extends androidx.collection.ObjectIntMap<K> {
+    ctor public MutableObjectIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+    method public inline operator void minusAssign(Iterable<? extends K> keys);
+    method public inline operator void minusAssign(K key);
+    method public inline operator void minusAssign(K![] keys);
+    method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+    method public inline operator void plusAssign(androidx.collection.ObjectIntMap<K> from);
+    method public void put(K key, int value);
+    method public void putAll(androidx.collection.ObjectIntMap<K> from);
+    method public void remove(K key);
+    method public boolean remove(K key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(K key, int value);
+    method public int trim();
+  }
+
+  public final class MutableObjectList<E> extends androidx.collection.ObjectList<E> {
+    ctor public MutableObjectList(optional int initialCapacity);
+    method public boolean add(E element);
+    method public void add(@IntRange(from=0L) int index, E element);
+    method public boolean addAll(androidx.collection.ObjectList<E> elements);
+    method public boolean addAll(androidx.collection.ScatterSet<E> elements);
+    method public boolean addAll(E![] elements);
+    method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.ObjectList<E> elements);
+    method public boolean addAll(@IntRange(from=0L) int index, E![] elements);
+    method public boolean addAll(@IntRange(from=0L) int index, java.util.Collection<? extends E> elements);
+    method public boolean addAll(Iterable<? extends E> elements);
+    method public boolean addAll(java.util.List<? extends E> elements);
+    method public boolean addAll(kotlin.sequences.Sequence<? extends E> elements);
+    method public java.util.List<E> asList();
+    method public java.util.List<E> asMutableList();
+    method public void clear();
+    method public void ensureCapacity(int capacity);
+    method public inline int getCapacity();
+    method public operator void minusAssign(androidx.collection.ObjectList<E> elements);
+    method public operator void minusAssign(androidx.collection.ScatterSet<E> elements);
+    method public inline operator void minusAssign(E element);
+    method public operator void minusAssign(E![] elements);
+    method public operator void minusAssign(Iterable<? extends E> elements);
+    method public operator void minusAssign(java.util.List<? extends E> elements);
+    method public operator void minusAssign(kotlin.sequences.Sequence<? extends E> elements);
+    method public operator void plusAssign(androidx.collection.ObjectList<E> elements);
+    method public operator void plusAssign(androidx.collection.ScatterSet<E> elements);
+    method public inline operator void plusAssign(E element);
+    method public operator void plusAssign(E![] elements);
+    method public operator void plusAssign(Iterable<? extends E> elements);
+    method public operator void plusAssign(java.util.List<? extends E> elements);
+    method public operator void plusAssign(kotlin.sequences.Sequence<? extends E> elements);
+    method public boolean remove(E element);
+    method public boolean removeAll(androidx.collection.ObjectList<E> elements);
+    method public boolean removeAll(androidx.collection.ScatterSet<E> elements);
+    method public boolean removeAll(E![] elements);
+    method public boolean removeAll(Iterable<? extends E> elements);
+    method public boolean removeAll(java.util.List<? extends E> elements);
+    method public boolean removeAll(kotlin.sequences.Sequence<? extends E> elements);
+    method public E removeAt(@IntRange(from=0L) int index);
+    method public inline void removeIf(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public void removeRange(@IntRange(from=0L) int start, @IntRange(from=0L) int end);
+    method public boolean retainAll(androidx.collection.ObjectList<E> elements);
+    method public boolean retainAll(E![] elements);
+    method public boolean retainAll(Iterable<? extends E> elements);
+    method public boolean retainAll(java.util.Collection<? extends E> elements);
+    method public boolean retainAll(kotlin.sequences.Sequence<? extends E> elements);
+    method public operator E set(@IntRange(from=0L) int index, E element);
+    method public void trim(optional int minCapacity);
+    property public final inline int capacity;
+  }
+
+  public final class MutableObjectLongMap<K> extends androidx.collection.ObjectLongMap<K> {
+    ctor public MutableObjectLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+    method public inline operator void minusAssign(Iterable<? extends K> keys);
+    method public inline operator void minusAssign(K key);
+    method public inline operator void minusAssign(K![] keys);
+    method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+    method public inline operator void plusAssign(androidx.collection.ObjectLongMap<K> from);
+    method public void put(K key, long value);
+    method public void putAll(androidx.collection.ObjectLongMap<K> from);
+    method public void remove(K key);
+    method public boolean remove(K key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(K key, long value);
+    method public int trim();
+  }
+
   public final class MutableScatterMap<K, V> extends androidx.collection.ScatterMap<K,V> {
     ctor public MutableScatterMap(optional int initialCapacity);
     method public java.util.Map<K,V> asMutableMap();
     method public void clear();
     method public inline V getOrPut(K key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ObjectList<K> keys);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
     method public inline operator void minusAssign(Iterable<? extends K> keys);
     method public inline operator void minusAssign(K key);
     method public inline operator void minusAssign(K![] keys);
@@ -609,23 +1719,27 @@
   public final class MutableScatterSet<E> extends androidx.collection.ScatterSet<E> {
     ctor public MutableScatterSet(optional int initialCapacity);
     method public boolean add(E element);
+    method public boolean addAll(androidx.collection.ObjectList<E> elements);
     method public boolean addAll(androidx.collection.ScatterSet<E> elements);
     method public boolean addAll(E![] elements);
     method public boolean addAll(Iterable<? extends E> elements);
     method public boolean addAll(kotlin.sequences.Sequence<? extends E> elements);
     method public java.util.Set<E> asMutableSet();
     method public void clear();
+    method public operator void minusAssign(androidx.collection.ObjectList<E> elements);
     method public operator void minusAssign(androidx.collection.ScatterSet<E> elements);
     method public operator void minusAssign(E element);
     method public operator void minusAssign(E![] elements);
     method public operator void minusAssign(Iterable<? extends E> elements);
     method public operator void minusAssign(kotlin.sequences.Sequence<? extends E> elements);
+    method public operator void plusAssign(androidx.collection.ObjectList<E> elements);
     method public operator void plusAssign(androidx.collection.ScatterSet<E> elements);
     method public operator void plusAssign(E element);
     method public operator void plusAssign(E![] elements);
     method public operator void plusAssign(Iterable<? extends E> elements);
     method public operator void plusAssign(kotlin.sequences.Sequence<? extends E> elements);
     method public boolean remove(E element);
+    method public boolean removeAll(androidx.collection.ObjectList<E> elements);
     method public boolean removeAll(androidx.collection.ScatterSet<E> elements);
     method public boolean removeAll(E![] elements);
     method public boolean removeAll(Iterable<? extends E> elements);
@@ -635,34 +1749,244 @@
     method @IntRange(from=0L) public int trim();
   }
 
-  @kotlin.jvm.JvmInline public final value class PairFloatFloat {
-    ctor public PairFloatFloat(float first, float second);
-    method public inline operator float component1();
-    method public inline operator float component2();
-    method public inline float getFirst();
-    method public inline float getSecond();
-    property public final inline float first;
-    property public final inline float second;
+  public abstract sealed class ObjectFloatMap<K> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(K key);
+    method public final boolean containsKey(K key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(K key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(K key);
+    method public final int getCapacity();
+    method public final float getOrDefault(K key, float defaultValue);
+    method public final inline float getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal Object![] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal float[] values;
   }
 
-  @kotlin.jvm.JvmInline public final value class PairIntInt {
-    ctor public PairIntInt(int first, int second);
-    method public inline operator int component1();
-    method public inline operator int component2();
-    method public int getFirst();
-    method public int getSecond();
-    property public final int first;
-    property public final int second;
+  public final class ObjectFloatMapKt {
+    method public static <K> androidx.collection.ObjectFloatMap<K> emptyObjectFloatMap();
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf();
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(K key1, float value1);
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(K key1, float value1, K key2, float value2);
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(K key1, float value1, K key2, float value2, K key3, float value3);
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(K key1, float value1, K key2, float value2, K key3, float value3, K key4, float value4);
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(K key1, float value1, K key2, float value2, K key3, float value3, K key4, float value4, K key5, float value5);
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMap();
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(K key1, float value1);
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(K key1, float value1, K key2, float value2);
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(K key1, float value1, K key2, float value2, K key3, float value3);
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(K key1, float value1, K key2, float value2, K key3, float value3, K key4, float value4);
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(K key1, float value1, K key2, float value2, K key3, float value3, K key4, float value4, K key5, float value5);
   }
 
-  public final class PairLongLong {
-    ctor public PairLongLong(long first, long second);
-    method public inline operator long component1();
-    method public inline operator long component2();
-    method public long getFirst();
-    method public long getSecond();
-    property public final long first;
-    property public final long second;
+  public abstract sealed class ObjectIntMap<K> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(K key);
+    method public final boolean containsKey(K key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(K key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(K key);
+    method public final int getCapacity();
+    method public final int getOrDefault(K key, int defaultValue);
+    method public final inline int getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal Object![] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal int[] values;
+  }
+
+  public final class ObjectIntMapKt {
+    method public static <K> androidx.collection.ObjectIntMap<K> emptyObjectIntMap();
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf();
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(K key1, int value1);
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(K key1, int value1, K key2, int value2);
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(K key1, int value1, K key2, int value2, K key3, int value3);
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(K key1, int value1, K key2, int value2, K key3, int value3, K key4, int value4);
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(K key1, int value1, K key2, int value2, K key3, int value3, K key4, int value4, K key5, int value5);
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMap();
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(K key1, int value1);
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(K key1, int value1, K key2, int value2);
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(K key1, int value1, K key2, int value2, K key3, int value3);
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(K key1, int value1, K key2, int value2, K key3, int value3, K key4, int value4);
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(K key1, int value1, K key2, int value2, K key3, int value3, K key4, int value4, K key5, int value5);
+  }
+
+  public abstract sealed class ObjectList<E> {
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public abstract java.util.List<E> asList();
+    method public final operator boolean contains(E element);
+    method public final boolean containsAll(androidx.collection.ObjectList<E> elements);
+    method public final boolean containsAll(E![] elements);
+    method public final boolean containsAll(Iterable<? extends E> elements);
+    method public final boolean containsAll(java.util.List<? extends E> elements);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final E elementAt(@IntRange(from=0L) int index);
+    method public final inline E elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends E> defaultValue);
+    method public final E first();
+    method public final inline E first(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final inline E? firstOrNull();
+    method public final inline E? firstOrNull(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final inline <R> R fold(R initial, kotlin.jvm.functions.Function2<? super R,? super E,? extends R> operation);
+    method public final inline <R> R foldIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super R,? super E,? extends R> operation);
+    method public final inline <R> R foldRight(R initial, kotlin.jvm.functions.Function2<? super E,? super R,? extends R> operation);
+    method public final inline <R> R foldRightIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super E,? super R,? extends R> operation);
+    method public final inline void forEach(kotlin.jvm.functions.Function1<? super E,kotlin.Unit> block);
+    method public final inline void forEachIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super E,kotlin.Unit> block);
+    method public final inline void forEachReversed(kotlin.jvm.functions.Function1<? super E,kotlin.Unit> block);
+    method public final inline void forEachReversedIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super E,kotlin.Unit> block);
+    method public final operator E get(@IntRange(from=0L) int index);
+    method public final inline kotlin.ranges.IntRange getIndices();
+    method @IntRange(from=-1L) public final inline int getLastIndex();
+    method @IntRange(from=0L) public final int getSize();
+    method public final int indexOf(E element);
+    method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, optional kotlin.jvm.functions.Function1<? super E,? extends java.lang.CharSequence>? transform);
+    method public final E last();
+    method public final inline E last(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final int lastIndexOf(E element);
+    method public final inline E? lastOrNull();
+    method public final inline E? lastOrNull(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final boolean none();
+    method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    property public final inline kotlin.ranges.IntRange indices;
+    property @IntRange(from=-1L) public final inline int lastIndex;
+    property @IntRange(from=0L) public final int size;
+    field @kotlin.PublishedApi internal int _size;
+    field @kotlin.PublishedApi internal Object![] content;
+  }
+
+  public final class ObjectListKt {
+    method public static <E> androidx.collection.ObjectList<E> emptyObjectList();
+    method public static inline <E> androidx.collection.MutableObjectList<E> mutableObjectListOf();
+    method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1);
+    method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1, E element2);
+    method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1, E element2, E element3);
+    method public static inline <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E?... elements);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf();
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1, E element2);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1, E element2, E element3);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E?... elements);
+  }
+
+  public abstract sealed class ObjectLongMap<K> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(K key);
+    method public final boolean containsKey(K key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(K key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(K key);
+    method public final int getCapacity();
+    method public final long getOrDefault(K key, long defaultValue);
+    method public final inline long getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,? extends java.lang.CharSequence> transform);
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal Object![] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal long[] values;
+  }
+
+  public final class ObjectLongMapKt {
+    method public static <K> androidx.collection.ObjectLongMap<K> emptyObjectLongMap();
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf();
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(K key1, long value1);
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(K key1, long value1, K key2, long value2);
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(K key1, long value1, K key2, long value2, K key3, long value3);
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(K key1, long value1, K key2, long value2, K key3, long value3, K key4, long value4);
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(K key1, long value1, K key2, long value2, K key3, long value3, K key4, long value4, K key5, long value5);
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMap();
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(K key1, long value1);
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(K key1, long value1, K key2, long value2);
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(K key1, long value1, K key2, long value2, K key3, long value3);
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(K key1, long value1, K key2, long value2, K key3, long value3, K key4, long value4);
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(K key1, long value1, K key2, long value2, K key3, long value3, K key4, long value4, K key5, long value5);
   }
 
   public abstract sealed class ScatterMap<K, V> {
@@ -686,6 +2010,13 @@
     method public final int getSize();
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, optional kotlin.jvm.functions.Function2<? super K,? super V,? extends java.lang.CharSequence>? transform);
     method public final boolean none();
     property public final int capacity;
     property public final int size;
@@ -725,6 +2056,13 @@
     method @IntRange(from=0L) public final int getSize();
     method public final boolean isEmpty();
     method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, optional kotlin.jvm.functions.Function1<? super E,? extends java.lang.CharSequence>? transform);
     method public final boolean none();
     property @IntRange(from=0L) public final int capacity;
     property @IntRange(from=0L) public final int size;
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
new file mode 100644
index 0000000..5fd9383
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
@@ -0,0 +1,1005 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyFloatFloatMap = MutableFloatFloatMap(0)
+
+/**
+ * Returns an empty, read-only [FloatFloatMap].
+ */
+public fun emptyFloatFloatMap(): FloatFloatMap = EmptyFloatFloatMap
+
+/**
+ * Returns a new [MutableFloatFloatMap].
+ */
+public fun floatFloatMapOf(): FloatFloatMap = EmptyFloatFloatMap
+
+/**
+ * Returns a new [FloatFloatMap] with [key1] associated with [value1].
+ */
+public fun floatFloatMapOf(
+    key1: Float,
+    value1: Float
+): FloatFloatMap = MutableFloatFloatMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [FloatFloatMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun floatFloatMapOf(
+    key1: Float,
+    value1: Float,
+    key2: Float,
+    value2: Float,
+): FloatFloatMap = MutableFloatFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [FloatFloatMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun floatFloatMapOf(
+    key1: Float,
+    value1: Float,
+    key2: Float,
+    value2: Float,
+    key3: Float,
+    value3: Float,
+): FloatFloatMap = MutableFloatFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [FloatFloatMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun floatFloatMapOf(
+    key1: Float,
+    value1: Float,
+    key2: Float,
+    value2: Float,
+    key3: Float,
+    value3: Float,
+    key4: Float,
+    value4: Float,
+): FloatFloatMap = MutableFloatFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [FloatFloatMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun floatFloatMapOf(
+    key1: Float,
+    value1: Float,
+    key2: Float,
+    value2: Float,
+    key3: Float,
+    value3: Float,
+    key4: Float,
+    value4: Float,
+    key5: Float,
+    value5: Float,
+): FloatFloatMap = MutableFloatFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutableFloatFloatMap].
+ */
+public fun mutableFloatFloatMapOf(): MutableFloatFloatMap = MutableFloatFloatMap()
+
+/**
+ * Returns a new [MutableFloatFloatMap] with [key1] associated with [value1].
+ */
+public fun mutableFloatFloatMapOf(
+    key1: Float,
+    value1: Float
+): MutableFloatFloatMap = MutableFloatFloatMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableFloatFloatMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun mutableFloatFloatMapOf(
+    key1: Float,
+    value1: Float,
+    key2: Float,
+    value2: Float,
+): MutableFloatFloatMap = MutableFloatFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableFloatFloatMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun mutableFloatFloatMapOf(
+    key1: Float,
+    value1: Float,
+    key2: Float,
+    value2: Float,
+    key3: Float,
+    value3: Float,
+): MutableFloatFloatMap = MutableFloatFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableFloatFloatMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun mutableFloatFloatMapOf(
+    key1: Float,
+    value1: Float,
+    key2: Float,
+    value2: Float,
+    key3: Float,
+    value3: Float,
+    key4: Float,
+    value4: Float,
+): MutableFloatFloatMap = MutableFloatFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableFloatFloatMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun mutableFloatFloatMapOf(
+    key1: Float,
+    value1: Float,
+    key2: Float,
+    value2: Float,
+    key3: Float,
+    value3: Float,
+    key4: Float,
+    value4: Float,
+    key5: Float,
+    value5: Float,
+): MutableFloatFloatMap = MutableFloatFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [FloatFloatMap] is a container with a [Map]-like interface for
+ * [Float] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableFloatFloatMap].
+ *
+ * @see [MutableFloatFloatMap]
+ * @see [ScatterMap]
+ */
+public sealed class FloatFloatMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: FloatArray = EmptyFloatArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: FloatArray = EmptyFloatArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Float): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Float, defaultValue: Float): Float {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Float, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Float, value: Float) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Float) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Float) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Float, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Float, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Float, Float) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Float): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@FloatFloatMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: Float, value: Float) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@FloatFloatMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [FloatFloatMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is FloatFloatMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableFloatFloatMap] is a container with a [MutableMap]-like interface for
+ * [Float] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableFloatFloatMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableFloatFloatMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : FloatFloatMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = FloatArray(newCapacity)
+        values = FloatArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Float, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Float, value: Float) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Float, value: Float) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: FloatFloatMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: FloatFloatMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Float) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Float, value: Float): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Float, Float) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Float) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: FloatArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableFloatFloatMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatPair.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatPair.kt
new file mode 100644
index 0000000..602efb3
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatPair.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmInline
+
+/**
+ * Container to ease passing around a tuple of two [Float] values.
+ *
+ * *Note*: This class is optimized by using a value class, a Kotlin language featured
+ * not available from Java code. Java developers can get the same functionality by
+ * using [Pair] or by constructing a custom implementation using Float parameters
+ * directly (see [LongLongPair] for an example).
+ */
+@JvmInline
+public value class FloatFloatPair internal constructor(
+    @PublishedApi @JvmField internal val packedValue: Long
+) {
+    /**
+     * Constructs a [FloatFloatPair] with two [Float] values.
+     *
+     * @param first the first value in the pair
+     * @param second the second value in the pair
+     */
+    public constructor(first: Float, second: Float) : this(packFloats(first, second))
+
+    /**
+     * The first value in the pair.
+     */
+    public inline val first: Float
+        get() = Float.fromBits((packedValue shr 32).toInt())
+
+    /**
+     * The second value in the pair.
+     */
+    public inline val second: Float
+        get() = Float.fromBits((packedValue and 0xFFFFFFFF).toInt())
+
+    /**
+     * Returns the [first] component of the pair. For instance, the first component
+     * of `PairFloatFloat(3f, 4f)` is `3f`.
+     *
+     * This method allows to use destructuring declarations when working with pairs,
+     * for example:
+     * ```
+     * val (first, second) = myPair
+     * ```
+     */
+    // NOTE: Unpack the value directly because using `first` forces an invokestatic
+    public inline operator fun component1(): Float = Float.fromBits((packedValue shr 32).toInt())
+
+    /**
+     * Returns the [second] component of the pair. For instance, the second component
+     * of `PairFloatFloat(3f, 4f)` is `4f`.
+     *
+     * This method allows to use destructuring declarations when working with pairs,
+     * for example:
+     * ```
+     * val (first, second) = myPair
+     * ```
+     */
+    // NOTE: Unpack the value directly because using `second` forces an invokestatic
+    public inline operator fun component2(): Float =
+        Float.fromBits((packedValue and 0xFFFFFFFF).toInt())
+
+    override fun toString(): String = "($first, $second)"
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
new file mode 100644
index 0000000..3980c9c
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
@@ -0,0 +1,1005 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyFloatIntMap = MutableFloatIntMap(0)
+
+/**
+ * Returns an empty, read-only [FloatIntMap].
+ */
+public fun emptyFloatIntMap(): FloatIntMap = EmptyFloatIntMap
+
+/**
+ * Returns a new [MutableFloatIntMap].
+ */
+public fun floatIntMapOf(): FloatIntMap = EmptyFloatIntMap
+
+/**
+ * Returns a new [FloatIntMap] with [key1] associated with [value1].
+ */
+public fun floatIntMapOf(
+    key1: Float,
+    value1: Int
+): FloatIntMap = MutableFloatIntMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [FloatIntMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun floatIntMapOf(
+    key1: Float,
+    value1: Int,
+    key2: Float,
+    value2: Int,
+): FloatIntMap = MutableFloatIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [FloatIntMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun floatIntMapOf(
+    key1: Float,
+    value1: Int,
+    key2: Float,
+    value2: Int,
+    key3: Float,
+    value3: Int,
+): FloatIntMap = MutableFloatIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [FloatIntMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun floatIntMapOf(
+    key1: Float,
+    value1: Int,
+    key2: Float,
+    value2: Int,
+    key3: Float,
+    value3: Int,
+    key4: Float,
+    value4: Int,
+): FloatIntMap = MutableFloatIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [FloatIntMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun floatIntMapOf(
+    key1: Float,
+    value1: Int,
+    key2: Float,
+    value2: Int,
+    key3: Float,
+    value3: Int,
+    key4: Float,
+    value4: Int,
+    key5: Float,
+    value5: Int,
+): FloatIntMap = MutableFloatIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutableFloatIntMap].
+ */
+public fun mutableFloatIntMapOf(): MutableFloatIntMap = MutableFloatIntMap()
+
+/**
+ * Returns a new [MutableFloatIntMap] with [key1] associated with [value1].
+ */
+public fun mutableFloatIntMapOf(
+    key1: Float,
+    value1: Int
+): MutableFloatIntMap = MutableFloatIntMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableFloatIntMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun mutableFloatIntMapOf(
+    key1: Float,
+    value1: Int,
+    key2: Float,
+    value2: Int,
+): MutableFloatIntMap = MutableFloatIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableFloatIntMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun mutableFloatIntMapOf(
+    key1: Float,
+    value1: Int,
+    key2: Float,
+    value2: Int,
+    key3: Float,
+    value3: Int,
+): MutableFloatIntMap = MutableFloatIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableFloatIntMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun mutableFloatIntMapOf(
+    key1: Float,
+    value1: Int,
+    key2: Float,
+    value2: Int,
+    key3: Float,
+    value3: Int,
+    key4: Float,
+    value4: Int,
+): MutableFloatIntMap = MutableFloatIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableFloatIntMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun mutableFloatIntMapOf(
+    key1: Float,
+    value1: Int,
+    key2: Float,
+    value2: Int,
+    key3: Float,
+    value3: Int,
+    key4: Float,
+    value4: Int,
+    key5: Float,
+    value5: Int,
+): MutableFloatIntMap = MutableFloatIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [FloatIntMap] is a container with a [Map]-like interface for
+ * [Float] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableFloatIntMap].
+ *
+ * @see [MutableFloatIntMap]
+ * @see [ScatterMap]
+ */
+public sealed class FloatIntMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: FloatArray = EmptyFloatArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: IntArray = EmptyIntArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Float): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Float, defaultValue: Int): Int {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Float, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Float, value: Int) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Float) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Int) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Float, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Float, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Float, Int) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Int): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@FloatIntMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: Float, value: Int) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@FloatIntMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [FloatIntMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is FloatIntMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableFloatIntMap] is a container with a [MutableMap]-like interface for
+ * [Float] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableFloatIntMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableFloatIntMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : FloatIntMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = FloatArray(newCapacity)
+        values = IntArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Float, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Float, value: Int) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Float, value: Int) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: FloatIntMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: FloatIntMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Float) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Float, value: Int): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Float, Int) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Float) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: FloatArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableFloatIntMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
index cf0c648..391aa06 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
@@ -21,6 +21,15 @@
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
 
 /**
  * [FloatList] is a [List]-like collection for [Float] values. It allows retrieving
@@ -417,6 +426,67 @@
     }
 
     /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        this@FloatList.forEachIndexed { index, element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(element)
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. [transform] dictates how each element will be represented.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (Float) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        this@FloatList.forEachIndexed { index, element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(element))
+        }
+        append(postfix)
+    }
+
+    /**
      * Returns a hash code based on the contents of the [FloatList].
      */
     override fun hashCode(): Int {
@@ -449,23 +519,7 @@
      * Returns a String representation of the list, surrounded by "[]" and each element
      * separated by ", ".
      */
-    override fun toString(): String {
-        if (isEmpty()) {
-            return "[]"
-        }
-        val last = lastIndex
-        return buildString {
-            append('[')
-            val content = content
-            for (i in 0 until last) {
-                append(content[i])
-                append(',')
-                append(' ')
-            }
-            append(content[last])
-            append(']')
-        }
-    }
+    override fun toString(): String = joinToString(prefix = "[", postfix = "]")
 }
 
 /**
@@ -835,10 +889,6 @@
     }
 }
 
-// Empty array used when nothing is allocated
-@Suppress("PrivatePropertyName")
-private val EmptyFloatArray = FloatArray(0)
-
 private val EmptyFloatList: FloatList = MutableFloatList(0)
 
 /**
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
new file mode 100644
index 0000000..b48b09d
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
@@ -0,0 +1,1005 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyFloatLongMap = MutableFloatLongMap(0)
+
+/**
+ * Returns an empty, read-only [FloatLongMap].
+ */
+public fun emptyFloatLongMap(): FloatLongMap = EmptyFloatLongMap
+
+/**
+ * Returns a new [MutableFloatLongMap].
+ */
+public fun floatLongMapOf(): FloatLongMap = EmptyFloatLongMap
+
+/**
+ * Returns a new [FloatLongMap] with [key1] associated with [value1].
+ */
+public fun floatLongMapOf(
+    key1: Float,
+    value1: Long
+): FloatLongMap = MutableFloatLongMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [FloatLongMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun floatLongMapOf(
+    key1: Float,
+    value1: Long,
+    key2: Float,
+    value2: Long,
+): FloatLongMap = MutableFloatLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [FloatLongMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun floatLongMapOf(
+    key1: Float,
+    value1: Long,
+    key2: Float,
+    value2: Long,
+    key3: Float,
+    value3: Long,
+): FloatLongMap = MutableFloatLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [FloatLongMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun floatLongMapOf(
+    key1: Float,
+    value1: Long,
+    key2: Float,
+    value2: Long,
+    key3: Float,
+    value3: Long,
+    key4: Float,
+    value4: Long,
+): FloatLongMap = MutableFloatLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [FloatLongMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun floatLongMapOf(
+    key1: Float,
+    value1: Long,
+    key2: Float,
+    value2: Long,
+    key3: Float,
+    value3: Long,
+    key4: Float,
+    value4: Long,
+    key5: Float,
+    value5: Long,
+): FloatLongMap = MutableFloatLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutableFloatLongMap].
+ */
+public fun mutableFloatLongMapOf(): MutableFloatLongMap = MutableFloatLongMap()
+
+/**
+ * Returns a new [MutableFloatLongMap] with [key1] associated with [value1].
+ */
+public fun mutableFloatLongMapOf(
+    key1: Float,
+    value1: Long
+): MutableFloatLongMap = MutableFloatLongMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableFloatLongMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun mutableFloatLongMapOf(
+    key1: Float,
+    value1: Long,
+    key2: Float,
+    value2: Long,
+): MutableFloatLongMap = MutableFloatLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableFloatLongMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun mutableFloatLongMapOf(
+    key1: Float,
+    value1: Long,
+    key2: Float,
+    value2: Long,
+    key3: Float,
+    value3: Long,
+): MutableFloatLongMap = MutableFloatLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableFloatLongMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun mutableFloatLongMapOf(
+    key1: Float,
+    value1: Long,
+    key2: Float,
+    value2: Long,
+    key3: Float,
+    value3: Long,
+    key4: Float,
+    value4: Long,
+): MutableFloatLongMap = MutableFloatLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableFloatLongMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun mutableFloatLongMapOf(
+    key1: Float,
+    value1: Long,
+    key2: Float,
+    value2: Long,
+    key3: Float,
+    value3: Long,
+    key4: Float,
+    value4: Long,
+    key5: Float,
+    value5: Long,
+): MutableFloatLongMap = MutableFloatLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [FloatLongMap] is a container with a [Map]-like interface for
+ * [Float] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableFloatLongMap].
+ *
+ * @see [MutableFloatLongMap]
+ * @see [ScatterMap]
+ */
+public sealed class FloatLongMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: FloatArray = EmptyFloatArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: LongArray = EmptyLongArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Float): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Float, defaultValue: Long): Long {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Float, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Float, value: Long) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Float) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Long) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Float, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Float, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Float, Long) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Long): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@FloatLongMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: Float, value: Long) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@FloatLongMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [FloatLongMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is FloatLongMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableFloatLongMap] is a container with a [MutableMap]-like interface for
+ * [Float] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableFloatLongMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableFloatLongMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : FloatLongMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = FloatArray(newCapacity)
+        values = LongArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Float, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Float, value: Long) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Float, value: Long) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: FloatLongMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: FloatLongMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Float) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Float, value: Long): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Float, Long) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Float) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: FloatArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableFloatLongMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
new file mode 100644
index 0000000..e990f27
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
@@ -0,0 +1,1016 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyFloatObjectMap = MutableFloatObjectMap<Nothing>(0)
+
+/**
+ * Returns an empty, read-only [FloatObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> emptyFloatObjectMap(): FloatObjectMap<V> = EmptyFloatObjectMap as FloatObjectMap<V>
+
+/**
+ * Returns an empty, read-only [FloatObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> floatObjectMapOf(): FloatObjectMap<V> = EmptyFloatObjectMap as FloatObjectMap<V>
+
+/**
+ * Returns a new [FloatObjectMap] with [key1] associated with [value1].
+ */
+public fun <V> floatObjectMapOf(
+    key1: Float,
+    value1: V
+): FloatObjectMap<V> = MutableFloatObjectMap<V>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [FloatObjectMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun <V> floatObjectMapOf(
+    key1: Float,
+    value1: V,
+    key2: Float,
+    value2: V,
+): FloatObjectMap<V> = MutableFloatObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [FloatObjectMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun <V> floatObjectMapOf(
+    key1: Float,
+    value1: V,
+    key2: Float,
+    value2: V,
+    key3: Float,
+    value3: V,
+): FloatObjectMap<V> = MutableFloatObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [FloatObjectMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <V> floatObjectMapOf(
+    key1: Float,
+    value1: V,
+    key2: Float,
+    value2: V,
+    key3: Float,
+    value3: V,
+    key4: Float,
+    value4: V,
+): FloatObjectMap<V> = MutableFloatObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [FloatObjectMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <V> floatObjectMapOf(
+    key1: Float,
+    value1: V,
+    key2: Float,
+    value2: V,
+    key3: Float,
+    value3: V,
+    key4: Float,
+    value4: V,
+    key5: Float,
+    value5: V,
+): FloatObjectMap<V> = MutableFloatObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutableFloatObjectMap].
+ */
+public fun <V> mutableFloatObjectMapOf(): MutableFloatObjectMap<V> = MutableFloatObjectMap()
+
+/**
+ * Returns a new [MutableFloatObjectMap] with [key1] associated with [value1].
+ */
+public fun <V> mutableFloatObjectMapOf(
+    key1: Float,
+    value1: V
+): MutableFloatObjectMap<V> = MutableFloatObjectMap<V>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableFloatObjectMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun <V> mutableFloatObjectMapOf(
+    key1: Float,
+    value1: V,
+    key2: Float,
+    value2: V,
+): MutableFloatObjectMap<V> = MutableFloatObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableFloatObjectMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun <V> mutableFloatObjectMapOf(
+    key1: Float,
+    value1: V,
+    key2: Float,
+    value2: V,
+    key3: Float,
+    value3: V,
+): MutableFloatObjectMap<V> = MutableFloatObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableFloatObjectMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <V> mutableFloatObjectMapOf(
+    key1: Float,
+    value1: V,
+    key2: Float,
+    value2: V,
+    key3: Float,
+    value3: V,
+    key4: Float,
+    value4: V,
+): MutableFloatObjectMap<V> = MutableFloatObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableFloatObjectMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <V> mutableFloatObjectMapOf(
+    key1: Float,
+    value1: V,
+    key2: Float,
+    value2: V,
+    key3: Float,
+    value3: V,
+    key4: Float,
+    value4: V,
+    key5: Float,
+    value5: V,
+): MutableFloatObjectMap<V> = MutableFloatObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [FloatObjectMap] is a container with a [Map]-like interface for keys with
+ * [Float] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableFloatObjectMap].
+ *
+ * @see [MutableFloatObjectMap]
+ */
+public sealed class FloatObjectMap<V> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: FloatArray = EmptyFloatArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: Array<Any?> = EMPTY_OBJECTS
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     */
+    public operator fun get(key: Float): V? {
+        val index = findKeyIndex(key)
+        @Suppress("UNCHECKED_CAST")
+        return if (index >= 0) values[index] as V? else null
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Float, defaultValue: V): V {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            @Suppress("UNCHECKED_CAST")
+            return values[index] as V
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Float, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Float, value: V) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index], v[index] as V)
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Float) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: V) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(v[index] as V)
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Float, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Float, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Float, V) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: V): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@FloatObjectMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: Float, value: V) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@FloatObjectMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [FloatObjectMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is FloatObjectMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value == null) {
+                if (other[key] != null || !other.containsKey(key)) {
+                    return false
+                }
+            } else if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(if (value === this) "(this)" else value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    internal inline fun findKeyIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableFloatObjectMap] is a container with a [MutableMap]-like interface for keys with
+ * [Float] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableFloatObjectMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see ScatterMap
+ */
+public class MutableFloatObjectMap<V>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : FloatObjectMap<V>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = FloatArray(newCapacity)
+        values = arrayOfNulls(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Float, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue().also { set(key, it) }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Float, value: V) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Float, value: V): V? {
+        val index = findAbsoluteInsertIndex(key)
+        val oldValue = values[index]
+        keys[index] = key
+        values[index] = value
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: FloatObjectMap<V>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: FloatObjectMap<V>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map. If the
+     * [key] was present in the map, this function returns the value that was
+     * present before removal.
+     */
+    public fun remove(key: Float): V? {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return removeValueAt(index)
+        }
+        return null
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Float, value: V): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Float, V) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index], values[index] as V)) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Float) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: FloatArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatSet) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatList) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int): V? {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        val oldValue = values[index]
+        values[index] = null
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        values.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableFloatObjectMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
index 278902c4..321d0d2 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
@@ -28,14 +28,23 @@
 
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
 
-// This is a copy of ScatterSet, but with Float elements
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// This is a copy of ScatterSet, but with primitive elements
 
 // Default empty set to avoid allocations
 private val EmptyFloatSet = MutableFloatSet(0)
 
 // An empty array of floats
-private val EmptyFloatArray = FloatArray(0)
+internal val EmptyFloatArray = FloatArray(0)
 
 /**
  * Returns an empty, read-only [FloatSet].
@@ -313,6 +322,71 @@
     public operator fun contains(element: Float): Boolean = findElementIndex(element) >= 0
 
     /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@FloatSet.forEach { element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(element)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. [transform] dictates how each element will be represented.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (Float) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@FloatSet.forEach { element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(element))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
      * Returns the hash code value for this set. The hash code of a set is defined to be the
      * sum of the hash codes of the elements in the set.
      */
@@ -358,23 +432,7 @@
      * Returns a string representation of this set. The set is denoted in the
      * string by the `{}`. Each element is separated by `, `.
      */
-    public override fun toString(): String {
-        if (isEmpty()) {
-            return "[]"
-        }
-
-        val s = StringBuilder().append('[')
-        val last = _size - 1
-        var index = 0
-        forEach { element ->
-            s.append(element)
-            if (index++ < last) {
-                s.append(',').append(' ')
-            }
-        }
-
-        return s.append(']').toString()
-    }
+    override fun toString(): String = joinToString(prefix = "[", postfix = "]")
 
     /**
      * Scans the set to find the index in the backing arrays of the
@@ -770,7 +828,7 @@
  * Returns the hash code of [k]. This follows the [HashSet] default behavior on Android
  * of returning [Object.hashcode()] with the higher bits of hash spread to the lower bits.
  */
-private inline fun hash(k: Float): Int {
+internal inline fun hash(k: Float): Int {
     val hash = k.hashCode()
     return hash xor (hash ushr 16)
 }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
new file mode 100644
index 0000000..8d14611
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
@@ -0,0 +1,1005 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyIntFloatMap = MutableIntFloatMap(0)
+
+/**
+ * Returns an empty, read-only [IntFloatMap].
+ */
+public fun emptyIntFloatMap(): IntFloatMap = EmptyIntFloatMap
+
+/**
+ * Returns a new [MutableIntFloatMap].
+ */
+public fun intFloatMapOf(): IntFloatMap = EmptyIntFloatMap
+
+/**
+ * Returns a new [IntFloatMap] with [key1] associated with [value1].
+ */
+public fun intFloatMapOf(
+    key1: Int,
+    value1: Float
+): IntFloatMap = MutableIntFloatMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [IntFloatMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun intFloatMapOf(
+    key1: Int,
+    value1: Float,
+    key2: Int,
+    value2: Float,
+): IntFloatMap = MutableIntFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [IntFloatMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun intFloatMapOf(
+    key1: Int,
+    value1: Float,
+    key2: Int,
+    value2: Float,
+    key3: Int,
+    value3: Float,
+): IntFloatMap = MutableIntFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [IntFloatMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun intFloatMapOf(
+    key1: Int,
+    value1: Float,
+    key2: Int,
+    value2: Float,
+    key3: Int,
+    value3: Float,
+    key4: Int,
+    value4: Float,
+): IntFloatMap = MutableIntFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [IntFloatMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun intFloatMapOf(
+    key1: Int,
+    value1: Float,
+    key2: Int,
+    value2: Float,
+    key3: Int,
+    value3: Float,
+    key4: Int,
+    value4: Float,
+    key5: Int,
+    value5: Float,
+): IntFloatMap = MutableIntFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutableIntFloatMap].
+ */
+public fun mutableIntFloatMapOf(): MutableIntFloatMap = MutableIntFloatMap()
+
+/**
+ * Returns a new [MutableIntFloatMap] with [key1] associated with [value1].
+ */
+public fun mutableIntFloatMapOf(
+    key1: Int,
+    value1: Float
+): MutableIntFloatMap = MutableIntFloatMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableIntFloatMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun mutableIntFloatMapOf(
+    key1: Int,
+    value1: Float,
+    key2: Int,
+    value2: Float,
+): MutableIntFloatMap = MutableIntFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableIntFloatMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun mutableIntFloatMapOf(
+    key1: Int,
+    value1: Float,
+    key2: Int,
+    value2: Float,
+    key3: Int,
+    value3: Float,
+): MutableIntFloatMap = MutableIntFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableIntFloatMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun mutableIntFloatMapOf(
+    key1: Int,
+    value1: Float,
+    key2: Int,
+    value2: Float,
+    key3: Int,
+    value3: Float,
+    key4: Int,
+    value4: Float,
+): MutableIntFloatMap = MutableIntFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableIntFloatMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun mutableIntFloatMapOf(
+    key1: Int,
+    value1: Float,
+    key2: Int,
+    value2: Float,
+    key3: Int,
+    value3: Float,
+    key4: Int,
+    value4: Float,
+    key5: Int,
+    value5: Float,
+): MutableIntFloatMap = MutableIntFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [IntFloatMap] is a container with a [Map]-like interface for
+ * [Int] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableIntFloatMap].
+ *
+ * @see [MutableIntFloatMap]
+ * @see [ScatterMap]
+ */
+public sealed class IntFloatMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: IntArray = EmptyIntArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: FloatArray = EmptyFloatArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Int): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Int, defaultValue: Float): Float {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Int, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Int, value: Float) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Int) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Float) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Int, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Int, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Int, Float) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Float): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@IntFloatMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: Int, value: Float) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@IntFloatMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [IntFloatMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is IntFloatMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableIntFloatMap] is a container with a [MutableMap]-like interface for
+ * [Int] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableIntFloatMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableIntFloatMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : IntFloatMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = IntArray(newCapacity)
+        values = FloatArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Int, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Int, value: Float) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Int, value: Float) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: IntFloatMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: IntFloatMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Int) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Int, value: Float): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Int, Float) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Int) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: IntArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableIntFloatMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
new file mode 100644
index 0000000..618fc54
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
@@ -0,0 +1,1005 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyIntIntMap = MutableIntIntMap(0)
+
+/**
+ * Returns an empty, read-only [IntIntMap].
+ */
+public fun emptyIntIntMap(): IntIntMap = EmptyIntIntMap
+
+/**
+ * Returns a new [MutableIntIntMap].
+ */
+public fun intIntMapOf(): IntIntMap = EmptyIntIntMap
+
+/**
+ * Returns a new [IntIntMap] with [key1] associated with [value1].
+ */
+public fun intIntMapOf(
+    key1: Int,
+    value1: Int
+): IntIntMap = MutableIntIntMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [IntIntMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun intIntMapOf(
+    key1: Int,
+    value1: Int,
+    key2: Int,
+    value2: Int,
+): IntIntMap = MutableIntIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [IntIntMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun intIntMapOf(
+    key1: Int,
+    value1: Int,
+    key2: Int,
+    value2: Int,
+    key3: Int,
+    value3: Int,
+): IntIntMap = MutableIntIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [IntIntMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun intIntMapOf(
+    key1: Int,
+    value1: Int,
+    key2: Int,
+    value2: Int,
+    key3: Int,
+    value3: Int,
+    key4: Int,
+    value4: Int,
+): IntIntMap = MutableIntIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [IntIntMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun intIntMapOf(
+    key1: Int,
+    value1: Int,
+    key2: Int,
+    value2: Int,
+    key3: Int,
+    value3: Int,
+    key4: Int,
+    value4: Int,
+    key5: Int,
+    value5: Int,
+): IntIntMap = MutableIntIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutableIntIntMap].
+ */
+public fun mutableIntIntMapOf(): MutableIntIntMap = MutableIntIntMap()
+
+/**
+ * Returns a new [MutableIntIntMap] with [key1] associated with [value1].
+ */
+public fun mutableIntIntMapOf(
+    key1: Int,
+    value1: Int
+): MutableIntIntMap = MutableIntIntMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableIntIntMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun mutableIntIntMapOf(
+    key1: Int,
+    value1: Int,
+    key2: Int,
+    value2: Int,
+): MutableIntIntMap = MutableIntIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableIntIntMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun mutableIntIntMapOf(
+    key1: Int,
+    value1: Int,
+    key2: Int,
+    value2: Int,
+    key3: Int,
+    value3: Int,
+): MutableIntIntMap = MutableIntIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableIntIntMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun mutableIntIntMapOf(
+    key1: Int,
+    value1: Int,
+    key2: Int,
+    value2: Int,
+    key3: Int,
+    value3: Int,
+    key4: Int,
+    value4: Int,
+): MutableIntIntMap = MutableIntIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableIntIntMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun mutableIntIntMapOf(
+    key1: Int,
+    value1: Int,
+    key2: Int,
+    value2: Int,
+    key3: Int,
+    value3: Int,
+    key4: Int,
+    value4: Int,
+    key5: Int,
+    value5: Int,
+): MutableIntIntMap = MutableIntIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [IntIntMap] is a container with a [Map]-like interface for
+ * [Int] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableIntIntMap].
+ *
+ * @see [MutableIntIntMap]
+ * @see [ScatterMap]
+ */
+public sealed class IntIntMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: IntArray = EmptyIntArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: IntArray = EmptyIntArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Int): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Int, defaultValue: Int): Int {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Int, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Int, value: Int) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Int) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Int) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Int, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Int, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Int, Int) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Int): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@IntIntMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: Int, value: Int) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@IntIntMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [IntIntMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is IntIntMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableIntIntMap] is a container with a [MutableMap]-like interface for
+ * [Int] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableIntIntMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableIntIntMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : IntIntMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = IntArray(newCapacity)
+        values = IntArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Int, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Int, value: Int) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Int, value: Int) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: IntIntMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: IntIntMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Int) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Int, value: Int): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Int, Int) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Int) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: IntArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableIntIntMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntPair.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntPair.kt
new file mode 100644
index 0000000..f192d1e
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntPair.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmInline
+
+/**
+ * Container to ease passing around a tuple of two [Int] values.
+ *
+ * *Note*: This class is optimized by using a value class, a Kotlin language featured
+ * not available from Java code. Java developers can get the same functionality by
+ * using [Pair] or by constructing a custom implementation using Int parameters
+ * directly (see [LongLongPair] for an example).
+ */
+@JvmInline
+public value class IntIntPair internal constructor(
+    @PublishedApi @JvmField internal val packedValue: Long
+) {
+    /**
+     * Constructs a [IntIntPair] with two [Int] values.
+     *
+     * @param first the first value in the pair
+     * @param second the second value in the pair
+     */
+    public constructor(first: Int, second: Int) : this(packInts(first, second))
+
+    /**
+     * The first value in the pair.
+     */
+    public val first: Int
+        get() = (packedValue shr 32).toInt()
+
+    /**
+     * The second value in the pair.
+     */
+    public val second: Int
+        get() = (packedValue and 0xFFFFFFFF).toInt()
+
+    /**
+     * Returns the [first] component of the pair. For instance, the first component
+     * of `PairIntInt(3, 4)` is `3`.
+     *
+     * This method allows to use destructuring declarations when working with pairs,
+     * for example:
+     * ```
+     * val (first, second) = myPair
+     * ```
+     */
+    // NOTE: Unpack the value directly because using `first` forces an invokestatic
+    public inline operator fun component1(): Int = (packedValue shr 32).toInt()
+
+    /**
+     * Returns the [second] component of the pair. For instance, the second component
+     * of `PairIntInt(3, 4)` is `4`.
+     *
+     * This method allows to use destructuring declarations when working with pairs,
+     * for example:
+     * ```
+     * val (first, second) = myPair
+     * ```
+     */
+    // NOTE: Unpack the value directly because using `second` forces an invokestatic
+    public inline operator fun component2(): Int = (packedValue and 0xFFFFFFFF).toInt()
+
+    override fun toString(): String = "($first, $second)"
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
index 8c6122db..0709d1c 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
@@ -21,6 +21,15 @@
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
 
 /**
  * [IntList] is a [List]-like collection for [Int] values. It allows retrieving
@@ -417,6 +426,67 @@
     }
 
     /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        this@IntList.forEachIndexed { index, element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(element)
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. [transform] dictates how each element will be represented.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (Int) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        this@IntList.forEachIndexed { index, element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(element))
+        }
+        append(postfix)
+    }
+
+    /**
      * Returns a hash code based on the contents of the [IntList].
      */
     override fun hashCode(): Int {
@@ -449,23 +519,7 @@
      * Returns a String representation of the list, surrounded by "[]" and each element
      * separated by ", ".
      */
-    override fun toString(): String {
-        if (isEmpty()) {
-            return "[]"
-        }
-        val last = lastIndex
-        return buildString {
-            append('[')
-            val content = content
-            for (i in 0 until last) {
-                append(content[i])
-                append(',')
-                append(' ')
-            }
-            append(content[last])
-            append(']')
-        }
-    }
+    override fun toString(): String = joinToString(prefix = "[", postfix = "]")
 }
 
 /**
@@ -835,10 +889,6 @@
     }
 }
 
-// Empty array used when nothing is allocated
-@Suppress("PrivatePropertyName")
-private val EmptyIntArray = IntArray(0)
-
 private val EmptyIntList: IntList = MutableIntList(0)
 
 /**
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
new file mode 100644
index 0000000..ccf0d9e
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
@@ -0,0 +1,1005 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyIntLongMap = MutableIntLongMap(0)
+
+/**
+ * Returns an empty, read-only [IntLongMap].
+ */
+public fun emptyIntLongMap(): IntLongMap = EmptyIntLongMap
+
+/**
+ * Returns a new [MutableIntLongMap].
+ */
+public fun intLongMapOf(): IntLongMap = EmptyIntLongMap
+
+/**
+ * Returns a new [IntLongMap] with [key1] associated with [value1].
+ */
+public fun intLongMapOf(
+    key1: Int,
+    value1: Long
+): IntLongMap = MutableIntLongMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [IntLongMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun intLongMapOf(
+    key1: Int,
+    value1: Long,
+    key2: Int,
+    value2: Long,
+): IntLongMap = MutableIntLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [IntLongMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun intLongMapOf(
+    key1: Int,
+    value1: Long,
+    key2: Int,
+    value2: Long,
+    key3: Int,
+    value3: Long,
+): IntLongMap = MutableIntLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [IntLongMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun intLongMapOf(
+    key1: Int,
+    value1: Long,
+    key2: Int,
+    value2: Long,
+    key3: Int,
+    value3: Long,
+    key4: Int,
+    value4: Long,
+): IntLongMap = MutableIntLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [IntLongMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun intLongMapOf(
+    key1: Int,
+    value1: Long,
+    key2: Int,
+    value2: Long,
+    key3: Int,
+    value3: Long,
+    key4: Int,
+    value4: Long,
+    key5: Int,
+    value5: Long,
+): IntLongMap = MutableIntLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutableIntLongMap].
+ */
+public fun mutableIntLongMapOf(): MutableIntLongMap = MutableIntLongMap()
+
+/**
+ * Returns a new [MutableIntLongMap] with [key1] associated with [value1].
+ */
+public fun mutableIntLongMapOf(
+    key1: Int,
+    value1: Long
+): MutableIntLongMap = MutableIntLongMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableIntLongMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun mutableIntLongMapOf(
+    key1: Int,
+    value1: Long,
+    key2: Int,
+    value2: Long,
+): MutableIntLongMap = MutableIntLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableIntLongMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun mutableIntLongMapOf(
+    key1: Int,
+    value1: Long,
+    key2: Int,
+    value2: Long,
+    key3: Int,
+    value3: Long,
+): MutableIntLongMap = MutableIntLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableIntLongMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun mutableIntLongMapOf(
+    key1: Int,
+    value1: Long,
+    key2: Int,
+    value2: Long,
+    key3: Int,
+    value3: Long,
+    key4: Int,
+    value4: Long,
+): MutableIntLongMap = MutableIntLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableIntLongMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun mutableIntLongMapOf(
+    key1: Int,
+    value1: Long,
+    key2: Int,
+    value2: Long,
+    key3: Int,
+    value3: Long,
+    key4: Int,
+    value4: Long,
+    key5: Int,
+    value5: Long,
+): MutableIntLongMap = MutableIntLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [IntLongMap] is a container with a [Map]-like interface for
+ * [Int] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableIntLongMap].
+ *
+ * @see [MutableIntLongMap]
+ * @see [ScatterMap]
+ */
+public sealed class IntLongMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: IntArray = EmptyIntArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: LongArray = EmptyLongArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Int): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Int, defaultValue: Long): Long {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Int, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Int, value: Long) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Int) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Long) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Int, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Int, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Int, Long) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Long): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@IntLongMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: Int, value: Long) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@IntLongMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [IntLongMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is IntLongMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableIntLongMap] is a container with a [MutableMap]-like interface for
+ * [Int] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableIntLongMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableIntLongMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : IntLongMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = IntArray(newCapacity)
+        values = LongArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Int, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Int, value: Long) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Int, value: Long) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: IntLongMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: IntLongMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Int) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Int, value: Long): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Int, Long) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Int) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: IntArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableIntLongMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
new file mode 100644
index 0000000..0f101b1
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
@@ -0,0 +1,1016 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyIntObjectMap = MutableIntObjectMap<Nothing>(0)
+
+/**
+ * Returns an empty, read-only [IntObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> emptyIntObjectMap(): IntObjectMap<V> = EmptyIntObjectMap as IntObjectMap<V>
+
+/**
+ * Returns an empty, read-only [IntObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> intObjectMapOf(): IntObjectMap<V> = EmptyIntObjectMap as IntObjectMap<V>
+
+/**
+ * Returns a new [IntObjectMap] with [key1] associated with [value1].
+ */
+public fun <V> intObjectMapOf(
+    key1: Int,
+    value1: V
+): IntObjectMap<V> = MutableIntObjectMap<V>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [IntObjectMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun <V> intObjectMapOf(
+    key1: Int,
+    value1: V,
+    key2: Int,
+    value2: V,
+): IntObjectMap<V> = MutableIntObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [IntObjectMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun <V> intObjectMapOf(
+    key1: Int,
+    value1: V,
+    key2: Int,
+    value2: V,
+    key3: Int,
+    value3: V,
+): IntObjectMap<V> = MutableIntObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [IntObjectMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <V> intObjectMapOf(
+    key1: Int,
+    value1: V,
+    key2: Int,
+    value2: V,
+    key3: Int,
+    value3: V,
+    key4: Int,
+    value4: V,
+): IntObjectMap<V> = MutableIntObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [IntObjectMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <V> intObjectMapOf(
+    key1: Int,
+    value1: V,
+    key2: Int,
+    value2: V,
+    key3: Int,
+    value3: V,
+    key4: Int,
+    value4: V,
+    key5: Int,
+    value5: V,
+): IntObjectMap<V> = MutableIntObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutableIntObjectMap].
+ */
+public fun <V> mutableIntObjectMapOf(): MutableIntObjectMap<V> = MutableIntObjectMap()
+
+/**
+ * Returns a new [MutableIntObjectMap] with [key1] associated with [value1].
+ */
+public fun <V> mutableIntObjectMapOf(
+    key1: Int,
+    value1: V
+): MutableIntObjectMap<V> = MutableIntObjectMap<V>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableIntObjectMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun <V> mutableIntObjectMapOf(
+    key1: Int,
+    value1: V,
+    key2: Int,
+    value2: V,
+): MutableIntObjectMap<V> = MutableIntObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableIntObjectMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun <V> mutableIntObjectMapOf(
+    key1: Int,
+    value1: V,
+    key2: Int,
+    value2: V,
+    key3: Int,
+    value3: V,
+): MutableIntObjectMap<V> = MutableIntObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableIntObjectMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <V> mutableIntObjectMapOf(
+    key1: Int,
+    value1: V,
+    key2: Int,
+    value2: V,
+    key3: Int,
+    value3: V,
+    key4: Int,
+    value4: V,
+): MutableIntObjectMap<V> = MutableIntObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableIntObjectMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <V> mutableIntObjectMapOf(
+    key1: Int,
+    value1: V,
+    key2: Int,
+    value2: V,
+    key3: Int,
+    value3: V,
+    key4: Int,
+    value4: V,
+    key5: Int,
+    value5: V,
+): MutableIntObjectMap<V> = MutableIntObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [IntObjectMap] is a container with a [Map]-like interface for keys with
+ * [Int] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableIntObjectMap].
+ *
+ * @see [MutableIntObjectMap]
+ */
+public sealed class IntObjectMap<V> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: IntArray = EmptyIntArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: Array<Any?> = EMPTY_OBJECTS
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     */
+    public operator fun get(key: Int): V? {
+        val index = findKeyIndex(key)
+        @Suppress("UNCHECKED_CAST")
+        return if (index >= 0) values[index] as V? else null
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Int, defaultValue: V): V {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            @Suppress("UNCHECKED_CAST")
+            return values[index] as V
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Int, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Int, value: V) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index], v[index] as V)
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Int) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: V) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(v[index] as V)
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Int, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Int, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Int, V) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: V): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@IntObjectMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: Int, value: V) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@IntObjectMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [IntObjectMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is IntObjectMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value == null) {
+                if (other[key] != null || !other.containsKey(key)) {
+                    return false
+                }
+            } else if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(if (value === this) "(this)" else value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    internal inline fun findKeyIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableIntObjectMap] is a container with a [MutableMap]-like interface for keys with
+ * [Int] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableIntObjectMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see ScatterMap
+ */
+public class MutableIntObjectMap<V>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : IntObjectMap<V>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = IntArray(newCapacity)
+        values = arrayOfNulls(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Int, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue().also { set(key, it) }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Int, value: V) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Int, value: V): V? {
+        val index = findAbsoluteInsertIndex(key)
+        val oldValue = values[index]
+        keys[index] = key
+        values[index] = value
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: IntObjectMap<V>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: IntObjectMap<V>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map. If the
+     * [key] was present in the map, this function returns the value that was
+     * present before removal.
+     */
+    public fun remove(key: Int): V? {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return removeValueAt(index)
+        }
+        return null
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Int, value: V): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Int, V) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index], values[index] as V)) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Int) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: IntArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntSet) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntList) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int): V? {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        val oldValue = values[index]
+        values[index] = null
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        values.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableIntObjectMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
index 2db0f7f..1713598 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
@@ -28,14 +28,23 @@
 
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
 
-// This is a copy of ScatterSet, but with Int elements
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// This is a copy of ScatterSet, but with primitive elements
 
 // Default empty set to avoid allocations
 private val EmptyIntSet = MutableIntSet(0)
 
 // An empty array of ints
-private val EmptyIntArray = IntArray(0)
+internal val EmptyIntArray = IntArray(0)
 
 /**
  * Returns an empty, read-only [IntSet].
@@ -313,6 +322,71 @@
     public operator fun contains(element: Int): Boolean = findElementIndex(element) >= 0
 
     /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@IntSet.forEach { element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(element)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. [transform] dictates how each element will be represented.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (Int) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@IntSet.forEach { element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(element))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
      * Returns the hash code value for this set. The hash code of a set is defined to be the
      * sum of the hash codes of the elements in the set.
      */
@@ -358,23 +432,7 @@
      * Returns a string representation of this set. The set is denoted in the
      * string by the `{}`. Each element is separated by `, `.
      */
-    public override fun toString(): String {
-        if (isEmpty()) {
-            return "[]"
-        }
-
-        val s = StringBuilder().append('[')
-        val last = _size - 1
-        var index = 0
-        forEach { element ->
-            s.append(element)
-            if (index++ < last) {
-                s.append(',').append(' ')
-            }
-        }
-
-        return s.append(']').toString()
-    }
+    override fun toString(): String = joinToString(prefix = "[", postfix = "]")
 
     /**
      * Scans the set to find the index in the backing arrays of the
@@ -770,7 +828,7 @@
  * Returns the hash code of [k]. This follows the [HashSet] default behavior on Android
  * of returning [Object.hashcode()] with the higher bits of hash spread to the lower bits.
  */
-private inline fun hash(k: Int): Int {
+internal inline fun hash(k: Int): Int {
     val hash = k.hashCode()
     return hash xor (hash ushr 16)
 }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
new file mode 100644
index 0000000..f82d6ca
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
@@ -0,0 +1,1005 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyLongFloatMap = MutableLongFloatMap(0)
+
+/**
+ * Returns an empty, read-only [LongFloatMap].
+ */
+public fun emptyLongFloatMap(): LongFloatMap = EmptyLongFloatMap
+
+/**
+ * Returns a new [MutableLongFloatMap].
+ */
+public fun longFloatMapOf(): LongFloatMap = EmptyLongFloatMap
+
+/**
+ * Returns a new [LongFloatMap] with [key1] associated with [value1].
+ */
+public fun longFloatMapOf(
+    key1: Long,
+    value1: Float
+): LongFloatMap = MutableLongFloatMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [LongFloatMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun longFloatMapOf(
+    key1: Long,
+    value1: Float,
+    key2: Long,
+    value2: Float,
+): LongFloatMap = MutableLongFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [LongFloatMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun longFloatMapOf(
+    key1: Long,
+    value1: Float,
+    key2: Long,
+    value2: Float,
+    key3: Long,
+    value3: Float,
+): LongFloatMap = MutableLongFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [LongFloatMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun longFloatMapOf(
+    key1: Long,
+    value1: Float,
+    key2: Long,
+    value2: Float,
+    key3: Long,
+    value3: Float,
+    key4: Long,
+    value4: Float,
+): LongFloatMap = MutableLongFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [LongFloatMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun longFloatMapOf(
+    key1: Long,
+    value1: Float,
+    key2: Long,
+    value2: Float,
+    key3: Long,
+    value3: Float,
+    key4: Long,
+    value4: Float,
+    key5: Long,
+    value5: Float,
+): LongFloatMap = MutableLongFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutableLongFloatMap].
+ */
+public fun mutableLongFloatMapOf(): MutableLongFloatMap = MutableLongFloatMap()
+
+/**
+ * Returns a new [MutableLongFloatMap] with [key1] associated with [value1].
+ */
+public fun mutableLongFloatMapOf(
+    key1: Long,
+    value1: Float
+): MutableLongFloatMap = MutableLongFloatMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableLongFloatMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun mutableLongFloatMapOf(
+    key1: Long,
+    value1: Float,
+    key2: Long,
+    value2: Float,
+): MutableLongFloatMap = MutableLongFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableLongFloatMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun mutableLongFloatMapOf(
+    key1: Long,
+    value1: Float,
+    key2: Long,
+    value2: Float,
+    key3: Long,
+    value3: Float,
+): MutableLongFloatMap = MutableLongFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableLongFloatMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun mutableLongFloatMapOf(
+    key1: Long,
+    value1: Float,
+    key2: Long,
+    value2: Float,
+    key3: Long,
+    value3: Float,
+    key4: Long,
+    value4: Float,
+): MutableLongFloatMap = MutableLongFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableLongFloatMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun mutableLongFloatMapOf(
+    key1: Long,
+    value1: Float,
+    key2: Long,
+    value2: Float,
+    key3: Long,
+    value3: Float,
+    key4: Long,
+    value4: Float,
+    key5: Long,
+    value5: Float,
+): MutableLongFloatMap = MutableLongFloatMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [LongFloatMap] is a container with a [Map]-like interface for
+ * [Long] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableLongFloatMap].
+ *
+ * @see [MutableLongFloatMap]
+ * @see [ScatterMap]
+ */
+public sealed class LongFloatMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: LongArray = EmptyLongArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: FloatArray = EmptyFloatArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Long): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Long, defaultValue: Float): Float {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Long, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Long, value: Float) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Long) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Float) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Long, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Long, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Long, Float) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Float): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@LongFloatMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: Long, value: Float) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@LongFloatMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [LongFloatMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is LongFloatMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableLongFloatMap] is a container with a [MutableMap]-like interface for
+ * [Long] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableLongFloatMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableLongFloatMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : LongFloatMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = LongArray(newCapacity)
+        values = FloatArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Long, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Long, value: Float) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Long, value: Float) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: LongFloatMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: LongFloatMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Long) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Long, value: Float): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Long, Float) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Long) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: LongArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableLongFloatMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
new file mode 100644
index 0000000..358ec9a
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
@@ -0,0 +1,1005 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyLongIntMap = MutableLongIntMap(0)
+
+/**
+ * Returns an empty, read-only [LongIntMap].
+ */
+public fun emptyLongIntMap(): LongIntMap = EmptyLongIntMap
+
+/**
+ * Returns a new [MutableLongIntMap].
+ */
+public fun longIntMapOf(): LongIntMap = EmptyLongIntMap
+
+/**
+ * Returns a new [LongIntMap] with [key1] associated with [value1].
+ */
+public fun longIntMapOf(
+    key1: Long,
+    value1: Int
+): LongIntMap = MutableLongIntMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [LongIntMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun longIntMapOf(
+    key1: Long,
+    value1: Int,
+    key2: Long,
+    value2: Int,
+): LongIntMap = MutableLongIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [LongIntMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun longIntMapOf(
+    key1: Long,
+    value1: Int,
+    key2: Long,
+    value2: Int,
+    key3: Long,
+    value3: Int,
+): LongIntMap = MutableLongIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [LongIntMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun longIntMapOf(
+    key1: Long,
+    value1: Int,
+    key2: Long,
+    value2: Int,
+    key3: Long,
+    value3: Int,
+    key4: Long,
+    value4: Int,
+): LongIntMap = MutableLongIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [LongIntMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun longIntMapOf(
+    key1: Long,
+    value1: Int,
+    key2: Long,
+    value2: Int,
+    key3: Long,
+    value3: Int,
+    key4: Long,
+    value4: Int,
+    key5: Long,
+    value5: Int,
+): LongIntMap = MutableLongIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutableLongIntMap].
+ */
+public fun mutableLongIntMapOf(): MutableLongIntMap = MutableLongIntMap()
+
+/**
+ * Returns a new [MutableLongIntMap] with [key1] associated with [value1].
+ */
+public fun mutableLongIntMapOf(
+    key1: Long,
+    value1: Int
+): MutableLongIntMap = MutableLongIntMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableLongIntMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun mutableLongIntMapOf(
+    key1: Long,
+    value1: Int,
+    key2: Long,
+    value2: Int,
+): MutableLongIntMap = MutableLongIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableLongIntMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun mutableLongIntMapOf(
+    key1: Long,
+    value1: Int,
+    key2: Long,
+    value2: Int,
+    key3: Long,
+    value3: Int,
+): MutableLongIntMap = MutableLongIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableLongIntMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun mutableLongIntMapOf(
+    key1: Long,
+    value1: Int,
+    key2: Long,
+    value2: Int,
+    key3: Long,
+    value3: Int,
+    key4: Long,
+    value4: Int,
+): MutableLongIntMap = MutableLongIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableLongIntMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun mutableLongIntMapOf(
+    key1: Long,
+    value1: Int,
+    key2: Long,
+    value2: Int,
+    key3: Long,
+    value3: Int,
+    key4: Long,
+    value4: Int,
+    key5: Long,
+    value5: Int,
+): MutableLongIntMap = MutableLongIntMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [LongIntMap] is a container with a [Map]-like interface for
+ * [Long] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableLongIntMap].
+ *
+ * @see [MutableLongIntMap]
+ * @see [ScatterMap]
+ */
+public sealed class LongIntMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: LongArray = EmptyLongArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: IntArray = EmptyIntArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Long): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Long, defaultValue: Int): Int {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Long, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Long, value: Int) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Long) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Int) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Long, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Long, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Long, Int) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Int): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@LongIntMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: Long, value: Int) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@LongIntMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [LongIntMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is LongIntMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableLongIntMap] is a container with a [MutableMap]-like interface for
+ * [Long] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableLongIntMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableLongIntMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : LongIntMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = LongArray(newCapacity)
+        values = IntArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Long, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Long, value: Int) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Long, value: Int) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: LongIntMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: LongIntMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Long) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Long, value: Int): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Long, Int) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Long) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: LongArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableLongIntMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
index 94dfd82..56e15d1 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
@@ -21,6 +21,15 @@
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
 
 /**
  * [LongList] is a [List]-like collection for [Long] values. It allows retrieving
@@ -417,6 +426,67 @@
     }
 
     /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        this@LongList.forEachIndexed { index, element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(element)
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. [transform] dictates how each element will be represented.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (Long) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        this@LongList.forEachIndexed { index, element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(element))
+        }
+        append(postfix)
+    }
+
+    /**
      * Returns a hash code based on the contents of the [LongList].
      */
     override fun hashCode(): Int {
@@ -449,23 +519,7 @@
      * Returns a String representation of the list, surrounded by "[]" and each element
      * separated by ", ".
      */
-    override fun toString(): String {
-        if (isEmpty()) {
-            return "[]"
-        }
-        val last = lastIndex
-        return buildString {
-            append('[')
-            val content = content
-            for (i in 0 until last) {
-                append(content[i])
-                append(',')
-                append(' ')
-            }
-            append(content[last])
-            append(']')
-        }
-    }
+    override fun toString(): String = joinToString(prefix = "[", postfix = "]")
 }
 
 /**
@@ -835,10 +889,6 @@
     }
 }
 
-// Empty array used when nothing is allocated
-@Suppress("PrivatePropertyName")
-private val EmptyLongArray = LongArray(0)
-
 private val EmptyLongList: LongList = MutableLongList(0)
 
 /**
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
new file mode 100644
index 0000000..59b4f85
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
@@ -0,0 +1,1005 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyLongLongMap = MutableLongLongMap(0)
+
+/**
+ * Returns an empty, read-only [LongLongMap].
+ */
+public fun emptyLongLongMap(): LongLongMap = EmptyLongLongMap
+
+/**
+ * Returns a new [MutableLongLongMap].
+ */
+public fun longLongMapOf(): LongLongMap = EmptyLongLongMap
+
+/**
+ * Returns a new [LongLongMap] with [key1] associated with [value1].
+ */
+public fun longLongMapOf(
+    key1: Long,
+    value1: Long
+): LongLongMap = MutableLongLongMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [LongLongMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun longLongMapOf(
+    key1: Long,
+    value1: Long,
+    key2: Long,
+    value2: Long,
+): LongLongMap = MutableLongLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [LongLongMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun longLongMapOf(
+    key1: Long,
+    value1: Long,
+    key2: Long,
+    value2: Long,
+    key3: Long,
+    value3: Long,
+): LongLongMap = MutableLongLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [LongLongMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun longLongMapOf(
+    key1: Long,
+    value1: Long,
+    key2: Long,
+    value2: Long,
+    key3: Long,
+    value3: Long,
+    key4: Long,
+    value4: Long,
+): LongLongMap = MutableLongLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [LongLongMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun longLongMapOf(
+    key1: Long,
+    value1: Long,
+    key2: Long,
+    value2: Long,
+    key3: Long,
+    value3: Long,
+    key4: Long,
+    value4: Long,
+    key5: Long,
+    value5: Long,
+): LongLongMap = MutableLongLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutableLongLongMap].
+ */
+public fun mutableLongLongMapOf(): MutableLongLongMap = MutableLongLongMap()
+
+/**
+ * Returns a new [MutableLongLongMap] with [key1] associated with [value1].
+ */
+public fun mutableLongLongMapOf(
+    key1: Long,
+    value1: Long
+): MutableLongLongMap = MutableLongLongMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableLongLongMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun mutableLongLongMapOf(
+    key1: Long,
+    value1: Long,
+    key2: Long,
+    value2: Long,
+): MutableLongLongMap = MutableLongLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableLongLongMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun mutableLongLongMapOf(
+    key1: Long,
+    value1: Long,
+    key2: Long,
+    value2: Long,
+    key3: Long,
+    value3: Long,
+): MutableLongLongMap = MutableLongLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableLongLongMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun mutableLongLongMapOf(
+    key1: Long,
+    value1: Long,
+    key2: Long,
+    value2: Long,
+    key3: Long,
+    value3: Long,
+    key4: Long,
+    value4: Long,
+): MutableLongLongMap = MutableLongLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableLongLongMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun mutableLongLongMapOf(
+    key1: Long,
+    value1: Long,
+    key2: Long,
+    value2: Long,
+    key3: Long,
+    value3: Long,
+    key4: Long,
+    value4: Long,
+    key5: Long,
+    value5: Long,
+): MutableLongLongMap = MutableLongLongMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [LongLongMap] is a container with a [Map]-like interface for
+ * [Long] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableLongLongMap].
+ *
+ * @see [MutableLongLongMap]
+ * @see [ScatterMap]
+ */
+public sealed class LongLongMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: LongArray = EmptyLongArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: LongArray = EmptyLongArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Long): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Long, defaultValue: Long): Long {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Long, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Long, value: Long) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Long) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Long) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Long, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Long, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Long, Long) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Long): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@LongLongMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: Long, value: Long) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@LongLongMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [LongLongMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is LongLongMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableLongLongMap] is a container with a [MutableMap]-like interface for
+ * [Long] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableLongLongMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableLongLongMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : LongLongMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = LongArray(newCapacity)
+        values = LongArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Long, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Long, value: Long) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Long, value: Long) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: LongLongMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: LongLongMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Long) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Long, value: Long): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Long, Long) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Long) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: LongArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableLongLongMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongPair.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongPair.kt
new file mode 100644
index 0000000..ce84157
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongPair.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
+package androidx.collection
+
+/**
+ * Container to ease passing around a tuple of two [Long] values.
+ *
+ * @param first the first value in the pair
+ * @param second the second value in the pair
+ */
+public class LongLongPair public constructor(public val first: Long, public val second: Long) {
+    /**
+     * Returns the [first] component of the pair. For instance, the first component
+     * of `PairLongLong(3, 4)` is `3`.
+     *
+     * This method allows to use destructuring declarations when working with pairs,
+     * for example:
+     * ```
+     * val (first, second) = myPair
+     * ```
+     */
+    public inline operator fun component1(): Long = first
+
+    /**
+     * Returns the [second] component of the pair. For instance, the second component
+     * of `PairLongLong(3, 4)` is `4`.
+     *
+     * This method allows to use destructuring declarations when working with pairs,
+     * for example:
+     * ```
+     * val (first, second) = myPair
+     * ```
+     */
+    public inline operator fun component2(): Long = second
+
+    /**
+     * Checks the two values for equality.
+     *
+     * @param other the [LongLongPair] to which this one is to be checked for equality
+     * @return true if the underlying values of the [LongLongPair] are both considered equal
+     */
+    override fun equals(other: Any?): Boolean {
+        if (!(other is LongLongPair)) {
+            return false
+        }
+        return other.first == first && other.second == second
+    }
+
+    /**
+     * Compute a hash code using the hash codes of the underlying values
+     *
+     * @return a hashcode of the [LongLongPair]
+     */
+    override fun hashCode(): Int {
+        return first.hashCode() xor second.hashCode()
+    }
+
+    override fun toString(): String {
+        return "($first, $second)";
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
new file mode 100644
index 0000000..64f367e
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
@@ -0,0 +1,1016 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyLongObjectMap = MutableLongObjectMap<Nothing>(0)
+
+/**
+ * Returns an empty, read-only [LongObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> emptyLongObjectMap(): LongObjectMap<V> = EmptyLongObjectMap as LongObjectMap<V>
+
+/**
+ * Returns an empty, read-only [LongObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> longObjectMapOf(): LongObjectMap<V> = EmptyLongObjectMap as LongObjectMap<V>
+
+/**
+ * Returns a new [LongObjectMap] with [key1] associated with [value1].
+ */
+public fun <V> longObjectMapOf(
+    key1: Long,
+    value1: V
+): LongObjectMap<V> = MutableLongObjectMap<V>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [LongObjectMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun <V> longObjectMapOf(
+    key1: Long,
+    value1: V,
+    key2: Long,
+    value2: V,
+): LongObjectMap<V> = MutableLongObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [LongObjectMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun <V> longObjectMapOf(
+    key1: Long,
+    value1: V,
+    key2: Long,
+    value2: V,
+    key3: Long,
+    value3: V,
+): LongObjectMap<V> = MutableLongObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [LongObjectMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <V> longObjectMapOf(
+    key1: Long,
+    value1: V,
+    key2: Long,
+    value2: V,
+    key3: Long,
+    value3: V,
+    key4: Long,
+    value4: V,
+): LongObjectMap<V> = MutableLongObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [LongObjectMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <V> longObjectMapOf(
+    key1: Long,
+    value1: V,
+    key2: Long,
+    value2: V,
+    key3: Long,
+    value3: V,
+    key4: Long,
+    value4: V,
+    key5: Long,
+    value5: V,
+): LongObjectMap<V> = MutableLongObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutableLongObjectMap].
+ */
+public fun <V> mutableLongObjectMapOf(): MutableLongObjectMap<V> = MutableLongObjectMap()
+
+/**
+ * Returns a new [MutableLongObjectMap] with [key1] associated with [value1].
+ */
+public fun <V> mutableLongObjectMapOf(
+    key1: Long,
+    value1: V
+): MutableLongObjectMap<V> = MutableLongObjectMap<V>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableLongObjectMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun <V> mutableLongObjectMapOf(
+    key1: Long,
+    value1: V,
+    key2: Long,
+    value2: V,
+): MutableLongObjectMap<V> = MutableLongObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableLongObjectMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun <V> mutableLongObjectMapOf(
+    key1: Long,
+    value1: V,
+    key2: Long,
+    value2: V,
+    key3: Long,
+    value3: V,
+): MutableLongObjectMap<V> = MutableLongObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableLongObjectMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <V> mutableLongObjectMapOf(
+    key1: Long,
+    value1: V,
+    key2: Long,
+    value2: V,
+    key3: Long,
+    value3: V,
+    key4: Long,
+    value4: V,
+): MutableLongObjectMap<V> = MutableLongObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableLongObjectMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <V> mutableLongObjectMapOf(
+    key1: Long,
+    value1: V,
+    key2: Long,
+    value2: V,
+    key3: Long,
+    value3: V,
+    key4: Long,
+    value4: V,
+    key5: Long,
+    value5: V,
+): MutableLongObjectMap<V> = MutableLongObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [LongObjectMap] is a container with a [Map]-like interface for keys with
+ * [Long] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableLongObjectMap].
+ *
+ * @see [MutableLongObjectMap]
+ */
+public sealed class LongObjectMap<V> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: LongArray = EmptyLongArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: Array<Any?> = EMPTY_OBJECTS
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     */
+    public operator fun get(key: Long): V? {
+        val index = findKeyIndex(key)
+        @Suppress("UNCHECKED_CAST")
+        return if (index >= 0) values[index] as V? else null
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Long, defaultValue: V): V {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            @Suppress("UNCHECKED_CAST")
+            return values[index] as V
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Long, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Long, value: V) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index], v[index] as V)
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Long) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: V) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(v[index] as V)
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Long, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Long, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Long, V) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: V): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@LongObjectMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: Long, value: V) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@LongObjectMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [LongObjectMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is LongObjectMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value == null) {
+                if (other[key] != null || !other.containsKey(key)) {
+                    return false
+                }
+            } else if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(if (value === this) "(this)" else value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    internal inline fun findKeyIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableLongObjectMap] is a container with a [MutableMap]-like interface for keys with
+ * [Long] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableLongObjectMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see ScatterMap
+ */
+public class MutableLongObjectMap<V>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : LongObjectMap<V>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = LongArray(newCapacity)
+        values = arrayOfNulls(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Long, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue().also { set(key, it) }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Long, value: V) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Long, value: V): V? {
+        val index = findAbsoluteInsertIndex(key)
+        val oldValue = values[index]
+        keys[index] = key
+        values[index] = value
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: LongObjectMap<V>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: LongObjectMap<V>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map. If the
+     * [key] was present in the map, this function returns the value that was
+     * present before removal.
+     */
+    public fun remove(key: Long): V? {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return removeValueAt(index)
+        }
+        return null
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Long, value: V): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Long, V) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index], values[index] as V)) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Long) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: LongArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongSet) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongList) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int): V? {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        val oldValue = values[index]
+        values[index] = null
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        values.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableLongObjectMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
index f292716..3216858 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
@@ -28,14 +28,23 @@
 
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
 
-// This is a copy of ScatterSet, but with Long elements
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// This is a copy of ScatterSet, but with primitive elements
 
 // Default empty set to avoid allocations
 private val EmptyLongSet = MutableLongSet(0)
 
 // An empty array of longs
-private val EmptyLongArray = LongArray(0)
+internal val EmptyLongArray = LongArray(0)
 
 /**
  * Returns an empty, read-only [LongSet].
@@ -313,6 +322,71 @@
     public operator fun contains(element: Long): Boolean = findElementIndex(element) >= 0
 
     /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@LongSet.forEach { element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(element)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. [transform] dictates how each element will be represented.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (Long) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@LongSet.forEach { element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(element))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
      * Returns the hash code value for this set. The hash code of a set is defined to be the
      * sum of the hash codes of the elements in the set.
      */
@@ -358,23 +432,7 @@
      * Returns a string representation of this set. The set is denoted in the
      * string by the `{}`. Each element is separated by `, `.
      */
-    public override fun toString(): String {
-        if (isEmpty()) {
-            return "[]"
-        }
-
-        val s = StringBuilder().append('[')
-        val last = _size - 1
-        var index = 0
-        forEach { element ->
-            s.append(element)
-            if (index++ < last) {
-                s.append(',').append(' ')
-            }
-        }
-
-        return s.append(']').toString()
-    }
+    override fun toString(): String = joinToString(prefix = "[", postfix = "]")
 
     /**
      * Scans the set to find the index in the backing arrays of the
@@ -770,7 +828,7 @@
  * Returns the hash code of [k]. This follows the [HashSet] default behavior on Android
  * of returning [Object.hashcode()] with the higher bits of hash spread to the lower bits.
  */
-private inline fun hash(k: Long): Int {
+internal inline fun hash(k: Long): Int {
     val hash = k.hashCode()
     return hash xor (hash ushr 16)
 }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
new file mode 100644
index 0000000..4b0163a
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
@@ -0,0 +1,1034 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyObjectFloatMap = MutableObjectFloatMap<Any?>(0)
+
+/**
+ * Returns an empty, read-only [ObjectFloatMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> emptyObjectFloatMap(): ObjectFloatMap<K> =
+    EmptyObjectFloatMap as ObjectFloatMap<K>
+
+/**
+ * Returns an empty, read-only [ObjectFloatMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> objectFloatMap(): ObjectFloatMap<K> =
+    EmptyObjectFloatMap as ObjectFloatMap<K>
+
+/**
+ * Returns a new [ObjectFloatMap] with only [key1] associated with [value1].
+ */
+public fun <K> objectFloatMapOf(
+    key1: K,
+    value1: Float
+): ObjectFloatMap<K> =
+    MutableObjectFloatMap<K>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [ObjectFloatMap] with only [key1] and [key2] associated with
+ * [value1] and [value2], respectively.
+ */
+public fun <K> objectFloatMapOf(
+    key1: K,
+    value1: Float,
+    key2: K,
+    value2: Float,
+): ObjectFloatMap<K> =
+    MutableObjectFloatMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [ObjectFloatMap] with only [key1], [key2], and [key3] associated with
+ * [value1], [value2], and [value3], respectively.
+ */
+public fun <K> objectFloatMapOf(
+    key1: K,
+    value1: Float,
+    key2: K,
+    value2: Float,
+    key3: K,
+    value3: Float,
+): ObjectFloatMap<K> =
+    MutableObjectFloatMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [ObjectFloatMap] with only [key1], [key2], [key3], and [key4] associated with
+ * [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <K> objectFloatMapOf(
+    key1: K,
+    value1: Float,
+    key2: K,
+    value2: Float,
+    key3: K,
+    value3: Float,
+    key4: K,
+    value4: Float,
+): ObjectFloatMap<K> =
+    MutableObjectFloatMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [ObjectFloatMap] with only [key1], [key2], [key3], [key4], and [key5] associated
+ * with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <K> objectFloatMapOf(
+    key1: K,
+    value1: Float,
+    key2: K,
+    value2: Float,
+    key3: K,
+    value3: Float,
+    key4: K,
+    value4: Float,
+    key5: K,
+    value5: Float,
+): ObjectFloatMap<K> =
+    MutableObjectFloatMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new empty [MutableObjectFloatMap].
+ */
+public fun <K> mutableObjectFloatMapOf(): MutableObjectFloatMap<K> = MutableObjectFloatMap()
+
+/**
+ * Returns a new [MutableObjectFloatMap] with only [key1] associated with [value1].
+ */
+public fun <K> mutableObjectFloatMapOf(
+    key1: K,
+    value1: Float,
+): MutableObjectFloatMap<K> =
+    MutableObjectFloatMap<K>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableObjectFloatMap] with only [key1] and [key2] associated with
+ * [value1] and [value2], respectively.
+ */
+public fun <K> mutableObjectFloatMapOf(
+    key1: K,
+    value1: Float,
+    key2: K,
+    value2: Float,
+): MutableObjectFloatMap<K> =
+    MutableObjectFloatMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableObjectFloatMap] with only [key1], [key2], and [key3] associated with
+ * [value1], [value2], and [value3], respectively.
+ */
+public fun <K> mutableObjectFloatMapOf(
+    key1: K,
+    value1: Float,
+    key2: K,
+    value2: Float,
+    key3: K,
+    value3: Float,
+): MutableObjectFloatMap<K> =
+    MutableObjectFloatMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableObjectFloatMap] with only [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <K> mutableObjectFloatMapOf(
+    key1: K,
+    value1: Float,
+    key2: K,
+    value2: Float,
+    key3: K,
+    value3: Float,
+    key4: K,
+    value4: Float,
+): MutableObjectFloatMap<K> =
+    MutableObjectFloatMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableObjectFloatMap] with only [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <K> mutableObjectFloatMapOf(
+    key1: K,
+    value1: Float,
+    key2: K,
+    value2: Float,
+    key3: K,
+    value3: Float,
+    key4: K,
+    value4: Float,
+    key5: K,
+    value5: Float,
+): MutableObjectFloatMap<K> =
+    MutableObjectFloatMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [ObjectFloatMap] is a container with a [Map]-like interface for keys with
+ * reference types and [Float] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableObjectFloatMap].
+ *
+ * @see [MutableObjectFloatMap]
+ * @see ScatterMap
+ */
+public sealed class ObjectFloatMap<K> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: Array<Any?> = EMPTY_OBJECTS
+
+    @PublishedApi
+    @JvmField
+    internal var values: FloatArray = EmptyFloatArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     * @throws NoSuchElementException when [key] is not found
+     */
+    public operator fun get(key: K): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("There is no key $key in the map")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: K, defaultValue: Float): Float {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: K, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: K, value: Float) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K, v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: K) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K)
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Float) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (K, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (K, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (K, Float) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Float): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@ObjectFloatMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: K, value: Float) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@ObjectFloatMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [ObjectFloatMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is ObjectFloatMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        val o = other as ObjectFloatMap<Any?>
+
+        forEach { key, value ->
+            if (value != o[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(if (key === this) "(this)" else key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: K): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableObjectFloatMap] is a container with a [MutableMap]-like interface for keys with
+ * reference types and [Float] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableObjectFloatMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableObjectFloatMap<K>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : ObjectFloatMap<K>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = arrayOfNulls(newCapacity)
+        values = FloatArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: K, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        val value = defaultValue()
+        set(key, value)
+        return value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: K, value: Float) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public fun put(key: K, value: Float) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: ObjectFloatMap<K>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: ObjectFloatMap<K>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: K) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: K, value: Float): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (K, Float) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index] as K, values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: K) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array<out K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Iterable<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Sequence<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: ScatterSet<K>) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        keys[index] = null
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        keys.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: K): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableObjectFloatMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
new file mode 100644
index 0000000..91b2ee3
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
@@ -0,0 +1,1034 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyObjectIntMap = MutableObjectIntMap<Any?>(0)
+
+/**
+ * Returns an empty, read-only [ObjectIntMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> emptyObjectIntMap(): ObjectIntMap<K> =
+    EmptyObjectIntMap as ObjectIntMap<K>
+
+/**
+ * Returns an empty, read-only [ObjectIntMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> objectIntMap(): ObjectIntMap<K> =
+    EmptyObjectIntMap as ObjectIntMap<K>
+
+/**
+ * Returns a new [ObjectIntMap] with only [key1] associated with [value1].
+ */
+public fun <K> objectIntMapOf(
+    key1: K,
+    value1: Int
+): ObjectIntMap<K> =
+    MutableObjectIntMap<K>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [ObjectIntMap] with only [key1] and [key2] associated with
+ * [value1] and [value2], respectively.
+ */
+public fun <K> objectIntMapOf(
+    key1: K,
+    value1: Int,
+    key2: K,
+    value2: Int,
+): ObjectIntMap<K> =
+    MutableObjectIntMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [ObjectIntMap] with only [key1], [key2], and [key3] associated with
+ * [value1], [value2], and [value3], respectively.
+ */
+public fun <K> objectIntMapOf(
+    key1: K,
+    value1: Int,
+    key2: K,
+    value2: Int,
+    key3: K,
+    value3: Int,
+): ObjectIntMap<K> =
+    MutableObjectIntMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [ObjectIntMap] with only [key1], [key2], [key3], and [key4] associated with
+ * [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <K> objectIntMapOf(
+    key1: K,
+    value1: Int,
+    key2: K,
+    value2: Int,
+    key3: K,
+    value3: Int,
+    key4: K,
+    value4: Int,
+): ObjectIntMap<K> =
+    MutableObjectIntMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [ObjectIntMap] with only [key1], [key2], [key3], [key4], and [key5] associated
+ * with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <K> objectIntMapOf(
+    key1: K,
+    value1: Int,
+    key2: K,
+    value2: Int,
+    key3: K,
+    value3: Int,
+    key4: K,
+    value4: Int,
+    key5: K,
+    value5: Int,
+): ObjectIntMap<K> =
+    MutableObjectIntMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new empty [MutableObjectIntMap].
+ */
+public fun <K> mutableObjectIntMapOf(): MutableObjectIntMap<K> = MutableObjectIntMap()
+
+/**
+ * Returns a new [MutableObjectIntMap] with only [key1] associated with [value1].
+ */
+public fun <K> mutableObjectIntMapOf(
+    key1: K,
+    value1: Int,
+): MutableObjectIntMap<K> =
+    MutableObjectIntMap<K>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableObjectIntMap] with only [key1] and [key2] associated with
+ * [value1] and [value2], respectively.
+ */
+public fun <K> mutableObjectIntMapOf(
+    key1: K,
+    value1: Int,
+    key2: K,
+    value2: Int,
+): MutableObjectIntMap<K> =
+    MutableObjectIntMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableObjectIntMap] with only [key1], [key2], and [key3] associated with
+ * [value1], [value2], and [value3], respectively.
+ */
+public fun <K> mutableObjectIntMapOf(
+    key1: K,
+    value1: Int,
+    key2: K,
+    value2: Int,
+    key3: K,
+    value3: Int,
+): MutableObjectIntMap<K> =
+    MutableObjectIntMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableObjectIntMap] with only [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <K> mutableObjectIntMapOf(
+    key1: K,
+    value1: Int,
+    key2: K,
+    value2: Int,
+    key3: K,
+    value3: Int,
+    key4: K,
+    value4: Int,
+): MutableObjectIntMap<K> =
+    MutableObjectIntMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableObjectIntMap] with only [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <K> mutableObjectIntMapOf(
+    key1: K,
+    value1: Int,
+    key2: K,
+    value2: Int,
+    key3: K,
+    value3: Int,
+    key4: K,
+    value4: Int,
+    key5: K,
+    value5: Int,
+): MutableObjectIntMap<K> =
+    MutableObjectIntMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [ObjectIntMap] is a container with a [Map]-like interface for keys with
+ * reference types and [Int] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableObjectIntMap].
+ *
+ * @see [MutableObjectIntMap]
+ * @see ScatterMap
+ */
+public sealed class ObjectIntMap<K> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: Array<Any?> = EMPTY_OBJECTS
+
+    @PublishedApi
+    @JvmField
+    internal var values: IntArray = EmptyIntArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     * @throws NoSuchElementException when [key] is not found
+     */
+    public operator fun get(key: K): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("There is no key $key in the map")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: K, defaultValue: Int): Int {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: K, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: K, value: Int) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K, v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: K) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K)
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Int) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (K, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (K, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (K, Int) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Int): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@ObjectIntMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: K, value: Int) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@ObjectIntMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [ObjectIntMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is ObjectIntMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        val o = other as ObjectIntMap<Any?>
+
+        forEach { key, value ->
+            if (value != o[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(if (key === this) "(this)" else key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: K): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableObjectIntMap] is a container with a [MutableMap]-like interface for keys with
+ * reference types and [Int] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableObjectIntMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableObjectIntMap<K>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : ObjectIntMap<K>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = arrayOfNulls(newCapacity)
+        values = IntArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: K, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        val value = defaultValue()
+        set(key, value)
+        return value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: K, value: Int) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public fun put(key: K, value: Int) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: ObjectIntMap<K>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: ObjectIntMap<K>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: K) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: K, value: Int): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (K, Int) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index] as K, values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: K) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array<out K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Iterable<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Sequence<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: ScatterSet<K>) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        keys[index] = null
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        keys.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: K): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableObjectIntMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectList.kt
new file mode 100644
index 0000000..89002a95
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectList.kt
@@ -0,0 +1,1617 @@
+/*
+ * 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.
+ */
+@file:Suppress("NOTHING_TO_INLINE", "RedundantVisibilityModifier", "UNCHECKED_CAST")
+@file:OptIn(ExperimentalContracts::class)
+
+package androidx.collection
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+/**
+ * [ObjectList] is a [List]-like collection for reference types. It is optimized for fast
+ * access, avoiding virtual and interface method access. Methods avoid allocation whenever
+ * possible. For example [forEach] does not need allocate an [Iterator].
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ *
+ * **Note** [List] access is available through [asList] when developers need access to the
+ * common API.
+ *
+ * It is best to use this for all internal implementations where a list of reference types
+ * is needed. Use [List] in public API to take advantage of the commonly-used interface.
+ * It is common to use [ObjectList] internally and use [asList] to get a [List] interface
+ * for interacting with public APIs.
+ *
+ * @see MutableObjectList
+ * @see FloatList
+ * @see IntList
+ * @eee LongList
+ */
+public sealed class ObjectList<E>(initialCapacity: Int) {
+    @JvmField
+    @PublishedApi
+    internal var content: Array<Any?> = if (initialCapacity == 0) {
+        EmptyArray
+    } else {
+        arrayOfNulls(initialCapacity)
+    }
+
+    @Suppress("PropertyName")
+    @JvmField
+    @PublishedApi
+    internal var _size: Int = 0
+
+    /**
+     * The number of elements in the [ObjectList].
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns the last valid index in the [ObjectList]. This can be `-1` when the list is empty.
+     */
+    @get:androidx.annotation.IntRange(from = -1)
+    public inline val lastIndex: Int get() = _size - 1
+
+    /**
+     * Returns an [IntRange] of the valid indices for this [ObjectList].
+     */
+    public inline val indices: IntRange get() = 0 until _size
+
+    /**
+     * Returns `true` if the collection has no elements in it.
+     */
+    public fun none(): Boolean {
+        return isEmpty()
+    }
+
+    /**
+     * Returns `true` if there's at least one element in the collection.
+     */
+    public fun any(): Boolean {
+        return isNotEmpty()
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate].
+     */
+    public inline fun any(predicate: (element: E) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEach {
+            if (predicate(it)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate] while
+     * iterating in the reverse order.
+     */
+    public inline fun reversedAny(predicate: (element: E) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEachReversed {
+            if (predicate(it)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if the [ObjectList] contains [element] or `false` otherwise.
+     */
+    public operator fun contains(element: E): Boolean {
+        return indexOf(element) >= 0
+    }
+
+    /**
+     * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public fun containsAll(@Suppress("ArrayReturn") elements: Array<E>): Boolean {
+        for (i in elements.indices) {
+            if (!contains(elements[i])) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public fun containsAll(elements: List<E>): Boolean {
+        for (i in elements.indices) {
+            if (!contains(elements[i])) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public fun containsAll(elements: Iterable<E>): Boolean {
+        elements.forEach { element ->
+            if (!contains(element)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public fun containsAll(elements: ObjectList<E>): Boolean {
+        elements.forEach { element ->
+            if (!contains(element)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns the number of elements in this list.
+     */
+    public fun count(): Int = _size
+
+    /**
+     * Counts the number of elements matching [predicate].
+     * @return The number of elements in this list for which [predicate] returns true.
+     */
+    public inline fun count(predicate: (element: E) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        var count = 0
+        forEach { if (predicate(it)) count++ }
+        return count
+    }
+
+    /**
+     * Returns the first element in the [ObjectList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public fun first(): E {
+        if (isEmpty()) {
+            throw NoSuchElementException("ObjectList is empty.")
+        }
+        return content[0] as E
+    }
+
+    /**
+     * Returns the first element in the [ObjectList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfFirst
+     * @see firstOrNull
+     */
+    public inline fun first(predicate: (element: E) -> Boolean): E {
+        contract { callsInPlace(predicate) }
+        forEach { element ->
+            if (predicate(element)) return element
+        }
+        throw NoSuchElementException("ObjectList contains no element matching the predicate.")
+    }
+
+    /**
+     * Returns the first element in the [ObjectList] or `null` if it [isEmpty].
+     */
+    public inline fun firstOrNull(): E? = if (isEmpty()) null else get(0)
+
+    /**
+     * Returns the first element in the [ObjectList] for which [predicate] returns `true` or
+     * `null` if nothing matches.
+     * @see indexOfFirst
+     */
+    public inline fun firstOrNull(predicate: (element: E) -> Boolean): E? {
+        contract { callsInPlace(predicate) }
+        forEach { element ->
+            if (predicate(element)) return element
+        }
+        return null
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [ObjectList] in order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes current accumulator value and an element, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> fold(initial: R, operation: (acc: R, element: E) -> R): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEach { element ->
+            acc = operation(acc, element)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [ObjectList] in order.
+     */
+    public inline fun <R> foldIndexed(
+        initial: R,
+        operation: (index: Int, acc: R, element: E) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachIndexed { i, element ->
+            acc = operation(i, acc, element)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [ObjectList] in reverse order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes an element and the current accumulator value, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> foldRight(initial: R, operation: (element: E, acc: R) -> R): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachReversed { element ->
+            acc = operation(element, acc)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [ObjectList] in reverse order.
+     */
+    public inline fun <R> foldRightIndexed(
+        initial: R,
+        operation: (index: Int, element: E, acc: R) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachReversedIndexed { i, element ->
+            acc = operation(i, element, acc)
+        }
+        return acc
+    }
+
+    /**
+     * Calls [block] for each element in the [ObjectList], in order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEach(block: (element: E) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in 0 until _size) {
+            block(content[i] as E)
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [ObjectList] along with its index, in order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachIndexed(block: (index: Int, element: E) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in 0 until _size) {
+            block(i, content[i] as E)
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [ObjectList] in reverse order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEachReversed(block: (element: E) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in _size - 1 downTo 0) {
+            block(content[i] as E)
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [ObjectList] along with its index, in reverse
+     * order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachReversedIndexed(block: (index: Int, element: E) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in _size - 1 downTo 0) {
+            block(i, content[i] as E)
+        }
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public operator fun get(@androidx.annotation.IntRange(from = 0) index: Int): E {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        return content[index] as E
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): E {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        return content[index] as E
+    }
+
+    /**
+     * Returns the element at the given [index] or [defaultValue] if [index] is out of bounds
+     * of the collection.
+     * @param index The index of the element whose value should be returned
+     * @param defaultValue A lambda to call with [index] as a parameter to return a value at
+     * an index not in the list.
+     */
+    public inline fun elementAtOrElse(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        defaultValue: (index: Int) -> E
+    ): E {
+        if (index !in 0 until _size) {
+            return defaultValue(index)
+        }
+        return content[index] as E
+    }
+
+    /**
+     * Returns the index of [element] in the [ObjectList] or `-1` if [element] is not there.
+     */
+    public fun indexOf(element: E): Int {
+        // Comparing with == for each element is slower than comparing with .equals().
+        // We split the iteration for null and for non-null to speed it up.
+        // See ObjectListBenchmarkTest.contains()
+        if (element == null) {
+            forEachIndexed { i, item ->
+                if (item == null) {
+                    return i
+                }
+            }
+        } else {
+            forEachIndexed { i, item ->
+                @Suppress("ReplaceCallWithBinaryOperator")
+                if (element.equals(item)) {
+                    return i
+                }
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns the index if the first element in the [ObjectList] for which [predicate]
+     * returns `true` or -1 if there was no element for which predicate returned `true`.
+     */
+    public inline fun indexOfFirst(predicate: (element: E) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        forEachIndexed { i, element ->
+            if (predicate(element)) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns the index if the last element in the [ObjectList] for which [predicate]
+     * returns `true` or -1 if there was no element for which predicate returned `true`.
+     */
+    public inline fun indexOfLast(predicate: (element: E) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        forEachReversedIndexed { i, element ->
+            if (predicate(element)) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns `true` if the [ObjectList] has no elements in it or `false` otherwise.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if there are elements in the [ObjectList] or `false` if it is empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the last element in the [ObjectList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public fun last(): E {
+        if (isEmpty()) {
+            throw NoSuchElementException("ObjectList is empty.")
+        }
+        return content[lastIndex] as E
+    }
+
+    /**
+     * Returns the last element in the [ObjectList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfLast
+     * @see lastOrNull
+     */
+    public inline fun last(predicate: (element: E) -> Boolean): E {
+        contract { callsInPlace(predicate) }
+        forEachReversed { element ->
+            if (predicate(element)) {
+                return element
+            }
+        }
+        throw NoSuchElementException("ObjectList contains no element matching the predicate.")
+    }
+
+    /**
+     * Returns the last element in the [ObjectList] or `null` if it [isEmpty].
+     */
+    public inline fun lastOrNull(): E? = if (isEmpty()) null else content[lastIndex] as E
+
+    /**
+     * Returns the last element in the [ObjectList] for which [predicate] returns `true` or
+     * `null` if nothing matches.
+     * @see indexOfLast
+     */
+    public inline fun lastOrNull(predicate: (element: E) -> Boolean): E? {
+        contract { callsInPlace(predicate) }
+        forEachReversed { element ->
+            if (predicate(element)) {
+                return element
+            }
+        }
+        return null
+    }
+
+    /**
+     * Returns the index of the last element in the [ObjectList] that is the same as
+     * [element] or `-1` if no elements match.
+     */
+    public fun lastIndexOf(element: E): Int {
+        // Comparing with == for each element is slower than comparing with .equals().
+        // We split the iteration for null and for non-null to speed it up.
+        // See ObjectListBenchmarkTest.contains()
+        if (element == null) {
+            forEachReversedIndexed { i, item ->
+                if (item == null) {
+                    return i
+                }
+            }
+        } else {
+            forEachReversedIndexed { i, item ->
+                @Suppress("ReplaceCallWithBinaryOperator")
+                if (element.equals(item)) {
+                    return i
+                }
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     *
+     * [transform] may be supplied to convert each element to a custom String.
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        transform: ((E) -> CharSequence)? = null
+    ): String = buildString {
+        append(prefix)
+        this@ObjectList.forEachIndexed { index, element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            if (transform == null) {
+                append(element)
+            } else {
+                append(transform(element))
+            }
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns a [List] view into the [ObjectList]. All access to the collection will be
+     * less efficient and abides by the allocation requirements of the [List]. For example,
+     * [List.forEach] will allocate an iterator. All access will go through the more expensive
+     * interface calls. Critical performance areas should use the [ObjectList] API rather than
+     * [List] API, when possible.
+     */
+    public abstract fun asList(): List<E>
+
+    /**
+     * Returns a hash code based on the contents of the [ObjectList].
+     */
+    override fun hashCode(): Int {
+        var hashCode = 0
+        forEach { element ->
+            hashCode += 31 * element.hashCode()
+        }
+        return hashCode
+    }
+
+    /**
+     * Returns `true` if [other] is a [ObjectList] and the contents of this and [other] are the
+     * same.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (other !is ObjectList<*> || other._size != _size) {
+            return false
+        }
+        val content = content
+        val otherContent = other.content
+        for (i in indices) {
+            if (content[i] != otherContent[i]) {
+                return false
+            }
+        }
+        return true
+    }
+
+    /**
+     * Returns a String representation of the list, surrounded by "[]" and each element
+     * separated by ", ".
+     */
+    override fun toString(): String = joinToString(prefix = "[", postfix = "]") { element ->
+        if (element === this) {
+            "(this)"
+        } else {
+            element.toString()
+        }
+    }
+}
+
+/**
+ * [MutableObjectList] is a [MutableList]-like collection for reference types. It is optimized
+ * for fast access, avoiding virtual and interface method access. Methods avoid allocation
+ * whenever possible. For example [forEach] does not need allocate an [Iterator].
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ *
+ * **Note** [List] access is available through [asList] when developers need access to the
+ * common API.
+
+ * **Note** [MutableList] access is available through [asMutableList] when developers need
+ * access to the common API.
+ *
+ * It is best to use this for all internal implementations where a list of reference types
+ * is needed. Use [MutableList] in public API to take advantage of the commonly-used interface.
+ * It is common to use [MutableObjectList] internally and use [asMutableList] or [asList]
+ * to get a [MutableList] or [List] interface for interacting with public APIs.
+ *
+ * @see ObjectList
+ * @see MutableFloatList
+ * @see MutableIntList
+ * @eee MutableLongList
+ */
+public class MutableObjectList<E>(
+    initialCapacity: Int = 16
+) : ObjectList<E>(initialCapacity) {
+    private var list: ObjectListMutableList<E>? = null
+
+    /**
+     * Returns the total number of elements that can be held before the [MutableObjectList] must
+     * grow.
+     *
+     * @see ensureCapacity
+     */
+    public inline val capacity: Int
+        get() = content.size
+
+    /**
+     * Adds [element] to the [MutableObjectList] and returns `true`.
+     */
+    public fun add(element: E): Boolean {
+        ensureCapacity(_size + 1)
+        content[_size] = element
+        _size++
+        return true
+    }
+
+    /**
+     * Adds [element] to the [MutableObjectList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public fun add(@androidx.annotation.IntRange(from = 0) index: Int, element: E) {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        ensureCapacity(_size + 1)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + 1,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        content[index] = element
+        _size++
+    }
+
+    /**
+     * Adds all [elements] to the [MutableObjectList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutableObjectList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive.
+     */
+    public fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        @Suppress("ArrayReturn") elements: Array<E>
+    ): Boolean {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        if (elements.isEmpty()) return false
+        ensureCapacity(_size + elements.size)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + elements.size,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        elements.copyInto(content, index)
+        _size += elements.size
+        return true
+    }
+
+    /**
+     * Adds all [elements] to the [MutableObjectList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutableObjectList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive.
+     */
+    public fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: Collection<E>
+    ): Boolean {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        if (elements.isEmpty()) return false
+        ensureCapacity(_size + elements.size)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + elements.size,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        elements.forEachIndexed { i, element ->
+            content[index + i] = element
+        }
+        _size += elements.size
+        return true
+    }
+
+    /**
+     * Adds all [elements] to the [MutableObjectList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutableObjectList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: ObjectList<E>
+    ): Boolean {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        if (elements.isEmpty()) return false
+        ensureCapacity(_size + elements._size)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + elements._size,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        elements.content.copyInto(
+            destination = content,
+            destinationOffset = index,
+            startIndex = 0,
+            endIndex = elements._size
+        )
+        _size += elements._size
+        return true
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+     * [MutableObjectList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: ObjectList<E>): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+     * [MutableObjectList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: ScatterSet<E>): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+     * [MutableObjectList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(@Suppress("ArrayReturn") elements: Array<E>): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+     * [MutableObjectList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: List<E>): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+     * [MutableObjectList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: Iterable<E>): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+     * [MutableObjectList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: Sequence<E>): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList].
+     */
+    public operator fun plusAssign(elements: ObjectList<E>) {
+        if (elements.isEmpty()) return
+        ensureCapacity(_size + elements._size)
+        val content = content
+        elements.content.copyInto(
+            destination = content,
+            destinationOffset = _size,
+            startIndex = 0,
+            endIndex = elements._size
+        )
+        _size += elements._size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList].
+     */
+    public operator fun plusAssign(elements: ScatterSet<E>) {
+        if (elements.isEmpty()) return
+        ensureCapacity(_size + elements.size)
+        elements.forEach { element ->
+            plusAssign(element)
+        }
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList].
+     */
+    public operator fun plusAssign(@Suppress("ArrayReturn") elements: Array<E>) {
+        if (elements.isEmpty()) return
+        ensureCapacity(_size + elements.size)
+        val content = content
+        elements.copyInto(content, _size)
+        _size += elements.size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList].
+     */
+    public operator fun plusAssign(elements: List<E>) {
+        if (elements.isEmpty()) return
+        val size = _size
+        ensureCapacity(size + elements.size)
+        val content = content
+        for (i in elements.indices) {
+            content[i + size] = elements[i]
+        }
+        _size += elements.size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList].
+     */
+    public operator fun plusAssign(elements: Iterable<E>) {
+        elements.forEach { element ->
+            plusAssign(element)
+        }
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList].
+     */
+    public operator fun plusAssign(elements: Sequence<E>) {
+        elements.forEach { element ->
+            plusAssign(element)
+        }
+    }
+
+    /**
+     * Removes all elements in the [MutableObjectList]. The storage isn't released.
+     * @see trim
+     */
+    public fun clear() {
+        content.fill(null, fromIndex = 0, toIndex = _size)
+        _size = 0
+    }
+
+    /**
+     * Reduces the internal storage. If [capacity] is greater than [minCapacity] and [size], the
+     * internal storage is reduced to the maximum of [size] and [minCapacity].
+     * @see ensureCapacity
+     */
+    public fun trim(minCapacity: Int = _size) {
+        val minSize = maxOf(minCapacity, _size)
+        if (capacity > minSize) {
+            content = content.copyOf(minSize)
+        }
+    }
+
+    /**
+     * Ensures that there is enough space to store [capacity] elements in the [MutableObjectList].
+     * @see trim
+     */
+    public fun ensureCapacity(capacity: Int) {
+        val oldContent = content
+        if (oldContent.size < capacity) {
+            val newSize = maxOf(capacity, oldContent.size * 3 / 2)
+            content = oldContent.copyOf(newSize)
+        }
+    }
+
+    /**
+     * [add] [element] to the [MutableObjectList].
+     */
+    public inline operator fun plusAssign(element: E) {
+        add(element)
+    }
+
+    /**
+     * [remove] [element] from the [MutableObjectList]
+     */
+    public inline operator fun minusAssign(element: E) {
+        remove(element)
+    }
+
+    /**
+     * Removes [element] from the [MutableObjectList]. If [element] was in the [MutableObjectList]
+     * and was removed, `true` will be returned, or `false` will be returned if the element
+     * was not found.
+     */
+    public fun remove(element: E): Boolean {
+        val index = indexOf(element)
+        if (index >= 0) {
+            removeAt(index)
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Removes all elements in this list for which [predicate] returns `true`.
+     */
+    public inline fun removeIf(predicate: (element: E) -> Boolean) {
+        var gap = 0
+        val size = _size
+        val content = content
+        for (i in indices) {
+            content[i - gap] = content[i]
+            if (predicate(content[i] as E)) {
+                gap++
+            }
+        }
+        content.fill(null, fromIndex = size - gap, toIndex = size)
+        _size -= gap
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(@Suppress("ArrayReturn") elements: Array<E>): Boolean {
+        val initialSize = _size
+        for (i in elements.indices) {
+            remove(elements[i])
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: ObjectList<E>): Boolean {
+        val initialSize = _size
+        minusAssign(elements)
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: ScatterSet<E>): Boolean {
+        val initialSize = _size
+        minusAssign(elements)
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: List<E>): Boolean {
+        val initialSize = _size
+        minusAssign(elements)
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: Iterable<E>): Boolean {
+        val initialSize = _size
+        minusAssign(elements)
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: Sequence<E>): Boolean {
+        val initialSize = _size
+        minusAssign(elements)
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList].
+     */
+    public operator fun minusAssign(@Suppress("ArrayReturn") elements: Array<E>) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList].
+     */
+    public operator fun minusAssign(elements: ObjectList<E>) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList].
+     */
+    public operator fun minusAssign(elements: ScatterSet<E>) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList].
+     */
+    public operator fun minusAssign(elements: List<E>) {
+        for (i in elements.indices) {
+            minusAssign(elements[i])
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList].
+     */
+    public operator fun minusAssign(elements: Iterable<E>) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList].
+     */
+    public operator fun minusAssign(elements: Sequence<E>) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
+     * Removes the element at the given [index] and returns it.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public fun removeAt(@androidx.annotation.IntRange(from = 0) index: Int): E {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        val content = content
+        val element = content[index]
+        if (index != lastIndex) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index,
+                startIndex = index + 1,
+                endIndex = _size
+            )
+        }
+        _size--
+        content[_size] = null
+        return element as E
+    }
+
+    /**
+     * Removes elements from index [start] (inclusive) to [end] (exclusive).
+     * @throws IndexOutOfBoundsException if [start] or [end] isn't between 0 and [size], inclusive
+     * @throws IllegalArgumentException if [start] is greater than [end]
+     */
+    public fun removeRange(
+        @androidx.annotation.IntRange(from = 0) start: Int,
+        @androidx.annotation.IntRange(from = 0) end: Int
+    ) {
+        if (start !in 0.._size || end !in 0.._size) {
+            throw IndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size")
+        }
+        if (end < start) {
+            throw IllegalArgumentException("Start ($start) is more than end ($end)")
+        }
+        if (end != start) {
+            if (end < _size) {
+                content.copyInto(
+                    destination = content,
+                    destinationOffset = start,
+                    startIndex = end,
+                    endIndex = _size
+                )
+            }
+            val newSize = _size - (end - start)
+            content.fill(null, fromIndex = newSize, toIndex = _size)
+            _size = newSize
+        }
+    }
+
+    /**
+     * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+     * @return `true` if the [MutableObjectList] has changed.
+     */
+    public fun retainAll(@Suppress("ArrayReturn") elements: Array<E>): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val element = content[i]
+            if (elements.indexOf(element) < 0) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+     * @return `true` if the [MutableObjectList] has changed.
+     */
+    public fun retainAll(elements: ObjectList<E>): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val element = content[i] as E
+            if (element !in elements) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+     * @return `true` if the [MutableObjectList] has changed.
+     */
+    public fun retainAll(elements: Collection<E>): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val element = content[i] as E
+            if (element !in elements) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+     * @return `true` if the [MutableObjectList] has changed.
+     */
+    public fun retainAll(elements: Iterable<E>): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val element = content[i] as E
+            if (element !in elements) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+     * @return `true` if the [MutableObjectList] has changed.
+     */
+    public fun retainAll(elements: Sequence<E>): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val element = content[i] as E
+            if (element !in elements) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Sets the value at [index] to [element].
+     * @return the previous value set at [index]
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public operator fun set(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        element: E
+    ): E {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex")
+        }
+        val content = content
+        val old = content[index]
+        content[index] = element
+        return old as E
+    }
+
+    override fun asList(): List<E> = asMutableList()
+
+    /**
+     * Returns a [MutableList] view into the [MutableObjectList]. All access to the collection
+     * will be less efficient and abides by the allocation requirements of the
+     * [MutableList]. For example, [MutableList.forEach] will allocate an iterator.
+     * All access will go through the more expensive interface calls. Critical performance
+     * areas should use the [MutableObjectList] API rather than [MutableList] API, when possible.
+     */
+    public fun asMutableList(): MutableList<E> = list ?: ObjectListMutableList(this).also {
+        list = it
+    }
+
+    private class MutableObjectListIterator<T>(
+        private val list: MutableList<T>,
+        private var index: Int
+    ) : MutableListIterator<T> {
+        override fun hasNext(): Boolean {
+            return index < list.size
+        }
+
+        override fun next(): T {
+            return list[index++]
+        }
+
+        override fun remove() {
+            index--
+            list.removeAt(index)
+        }
+
+        override fun hasPrevious(): Boolean {
+            return index > 0
+        }
+
+        override fun nextIndex(): Int {
+            return index
+        }
+
+        override fun previous(): T {
+            index--
+            return list[index]
+        }
+
+        override fun previousIndex(): Int {
+            return index - 1
+        }
+
+        override fun add(element: T) {
+            list.add(index, element)
+            index++
+        }
+
+        override fun set(element: T) {
+            list[index] = element
+        }
+    }
+
+    /**
+     * [MutableList] implementation for a [MutableObjectList], used in [asMutableList].
+     */
+    private class ObjectListMutableList<T>(
+        private val objectList: MutableObjectList<T>
+    ) : MutableList<T> {
+        override val size: Int
+            get() = objectList.size
+
+        override fun contains(element: T): Boolean = objectList.contains(element)
+
+        override fun containsAll(elements: Collection<T>): Boolean =
+            objectList.containsAll(elements)
+
+        override fun get(index: Int): T {
+            checkIndex(index)
+            return objectList[index]
+        }
+
+        override fun indexOf(element: T): Int = objectList.indexOf(element)
+
+        override fun isEmpty(): Boolean = objectList.isEmpty()
+
+        override fun iterator(): MutableIterator<T> = MutableObjectListIterator(this, 0)
+
+        override fun lastIndexOf(element: T): Int = objectList.lastIndexOf(element)
+
+        override fun add(element: T): Boolean = objectList.add(element)
+
+        override fun add(index: Int, element: T) = objectList.add(index, element)
+
+        override fun addAll(index: Int, elements: Collection<T>): Boolean =
+            objectList.addAll(index, elements)
+
+        override fun addAll(elements: Collection<T>): Boolean = objectList.addAll(elements)
+
+        override fun clear() = objectList.clear()
+
+        override fun listIterator(): MutableListIterator<T> = MutableObjectListIterator(this, 0)
+
+        override fun listIterator(index: Int): MutableListIterator<T> =
+            MutableObjectListIterator(this, index)
+
+        override fun remove(element: T): Boolean = objectList.remove(element)
+
+        override fun removeAll(elements: Collection<T>): Boolean = objectList.removeAll(elements)
+
+        override fun removeAt(index: Int): T {
+            checkIndex(index)
+            return objectList.removeAt(index)
+        }
+
+        override fun retainAll(elements: Collection<T>): Boolean = objectList.retainAll(elements)
+
+        override fun set(index: Int, element: T): T {
+            checkIndex(index)
+            return objectList.set(index, element)
+        }
+
+        override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
+            checkSubIndex(fromIndex, toIndex)
+            return SubList(this, fromIndex, toIndex)
+        }
+    }
+
+    /**
+     * A view into an underlying [MutableList] that directly accesses the underlying [MutableList].
+     * This is important for the implementation of [List.subList]. A change to the [SubList]
+     * also changes the referenced [MutableList].
+     */
+    private class SubList<T>(
+        private val list: MutableList<T>,
+        private val start: Int,
+        private var end: Int
+    ) : MutableList<T> {
+        override val size: Int
+            get() = end - start
+
+        override fun contains(element: T): Boolean {
+            for (i in start until end) {
+                if (list[i] == element) {
+                    return true
+                }
+            }
+            return false
+        }
+
+        override fun containsAll(elements: Collection<T>): Boolean {
+            elements.forEach {
+                if (!contains(it)) {
+                    return false
+                }
+            }
+            return true
+        }
+
+        override fun get(index: Int): T {
+            checkIndex(index)
+            return list[index + start]
+        }
+
+        override fun indexOf(element: T): Int {
+            for (i in start until end) {
+                if (list[i] == element) {
+                    return i - start
+                }
+            }
+            return -1
+        }
+
+        override fun isEmpty(): Boolean = end == start
+
+        override fun iterator(): MutableIterator<T> = MutableObjectListIterator(this, 0)
+
+        override fun lastIndexOf(element: T): Int {
+            for (i in end - 1 downTo start) {
+                if (list[i] == element) {
+                    return i - start
+                }
+            }
+            return -1
+        }
+
+        override fun add(element: T): Boolean {
+            list.add(end++, element)
+            return true
+        }
+
+        override fun add(index: Int, element: T) {
+            list.add(index + start, element)
+            end++
+        }
+
+        override fun addAll(index: Int, elements: Collection<T>): Boolean {
+            list.addAll(index + start, elements)
+            end += elements.size
+            return elements.size > 0
+        }
+
+        override fun addAll(elements: Collection<T>): Boolean {
+            list.addAll(end, elements)
+            end += elements.size
+            return elements.size > 0
+        }
+
+        override fun clear() {
+            for (i in end - 1 downTo start) {
+                list.removeAt(i)
+            }
+            end = start
+        }
+
+        override fun listIterator(): MutableListIterator<T> = MutableObjectListIterator(this, 0)
+
+        override fun listIterator(index: Int): MutableListIterator<T> =
+            MutableObjectListIterator(this, index)
+
+        override fun remove(element: T): Boolean {
+            for (i in start until end) {
+                if (list[i] == element) {
+                    list.removeAt(i)
+                    end--
+                    return true
+                }
+            }
+            return false
+        }
+
+        override fun removeAll(elements: Collection<T>): Boolean {
+            val originalEnd = end
+            elements.forEach {
+                remove(it)
+            }
+            return originalEnd != end
+        }
+
+        override fun removeAt(index: Int): T {
+            checkIndex(index)
+            val element = list.removeAt(index + start)
+            end--
+            return element
+        }
+
+        override fun retainAll(elements: Collection<T>): Boolean {
+            val originalEnd = end
+            for (i in end - 1 downTo start) {
+                val element = list[i]
+                if (element !in elements) {
+                    list.removeAt(i)
+                    end--
+                }
+            }
+            return originalEnd != end
+        }
+
+        override fun set(index: Int, element: T): T {
+            checkIndex(index)
+            return list.set(index + start, element)
+        }
+
+        override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
+            checkSubIndex(fromIndex, toIndex)
+            return SubList(this, fromIndex, toIndex)
+        }
+    }
+}
+
+private fun List<*>.checkIndex(index: Int) {
+    val size = size
+    if (index < 0 || index >= size) {
+        throw IndexOutOfBoundsException("Index $index is out of bounds. " +
+            "The list has $size elements.")
+    }
+}
+
+private fun List<*>.checkSubIndex(fromIndex: Int, toIndex: Int) {
+    val size = size
+    if (fromIndex > toIndex) {
+        throw IllegalArgumentException("Indices are out of order. fromIndex ($fromIndex) is " +
+            "greater than toIndex ($toIndex).")
+    }
+    if (fromIndex < 0) {
+        throw IndexOutOfBoundsException("fromIndex ($fromIndex) is less than 0.")
+    }
+    if (toIndex > size) {
+        throw IndexOutOfBoundsException(
+            "toIndex ($toIndex) is more than than the list size ($size)"
+        )
+    }
+}
+
+// Empty array used when nothing is allocated
+private val EmptyArray = arrayOfNulls<Any>(0)
+
+private val EmptyObjectList: ObjectList<Any?> = MutableObjectList(0)
+
+/**
+ * @return a read-only [ObjectList] with nothing in it.
+ */
+public fun <E> emptyObjectList(): ObjectList<E> = EmptyObjectList as ObjectList<E>
+
+/**
+ * @return a read-only [ObjectList] with nothing in it.
+ */
+public fun <E> objectListOf(): ObjectList<E> = EmptyObjectList as ObjectList<E>
+
+/**
+ * @return a new read-only [ObjectList] with [element1] as the only element in the list.
+ */
+public fun <E> objectListOf(element1: E): ObjectList<E> = mutableObjectListOf(element1)
+
+/**
+ * @return a new read-only [ObjectList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun <E> objectListOf(element1: E, element2: E): ObjectList<E> =
+    mutableObjectListOf(element1, element2)
+
+/**
+ * @return a new read-only [ObjectList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun <E> objectListOf(element1: E, element2: E, element3: E): ObjectList<E> =
+    mutableObjectListOf(element1, element2, element3)
+
+/**
+ * @return a new read-only [ObjectList] with [elements] in order.
+ */
+public fun <E> objectListOf(vararg elements: E): ObjectList<E> =
+    MutableObjectList<E>(elements.size).apply { plusAssign(elements as Array<E>) }
+
+/**
+ * @return a new empty [MutableObjectList] with the default capacity.
+ */
+public inline fun <E> mutableObjectListOf(): MutableObjectList<E> = MutableObjectList()
+
+/**
+ * @return a new [MutableObjectList] with [element1] as the only element in the list.
+ */
+public fun <E> mutableObjectListOf(element1: E): MutableObjectList<E> {
+    val list = MutableObjectList<E>(1)
+    list += element1
+    return list
+}
+
+/**
+ * @return a new [MutableObjectList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun <E> mutableObjectListOf(element1: E, element2: E): MutableObjectList<E> {
+    val list = MutableObjectList<E>(2)
+    list += element1
+    list += element2
+    return list
+}
+
+/**
+ * @return a new [MutableObjectList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun <E> mutableObjectListOf(element1: E, element2: E, element3: E): MutableObjectList<E> {
+    val list = MutableObjectList<E>(3)
+    list += element1
+    list += element2
+    list += element3
+    return list
+}
+
+/**
+ * @return a new [MutableObjectList] with the given elements, in order.
+ */
+public inline fun <E> mutableObjectListOf(vararg elements: E): MutableObjectList<E> =
+    MutableObjectList<E>(elements.size).apply { plusAssign(elements as Array<E>) }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
new file mode 100644
index 0000000..57f6d29
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
@@ -0,0 +1,1034 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyObjectLongMap = MutableObjectLongMap<Any?>(0)
+
+/**
+ * Returns an empty, read-only [ObjectLongMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> emptyObjectLongMap(): ObjectLongMap<K> =
+    EmptyObjectLongMap as ObjectLongMap<K>
+
+/**
+ * Returns an empty, read-only [ObjectLongMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> objectLongMap(): ObjectLongMap<K> =
+    EmptyObjectLongMap as ObjectLongMap<K>
+
+/**
+ * Returns a new [ObjectLongMap] with only [key1] associated with [value1].
+ */
+public fun <K> objectLongMapOf(
+    key1: K,
+    value1: Long
+): ObjectLongMap<K> =
+    MutableObjectLongMap<K>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [ObjectLongMap] with only [key1] and [key2] associated with
+ * [value1] and [value2], respectively.
+ */
+public fun <K> objectLongMapOf(
+    key1: K,
+    value1: Long,
+    key2: K,
+    value2: Long,
+): ObjectLongMap<K> =
+    MutableObjectLongMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [ObjectLongMap] with only [key1], [key2], and [key3] associated with
+ * [value1], [value2], and [value3], respectively.
+ */
+public fun <K> objectLongMapOf(
+    key1: K,
+    value1: Long,
+    key2: K,
+    value2: Long,
+    key3: K,
+    value3: Long,
+): ObjectLongMap<K> =
+    MutableObjectLongMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [ObjectLongMap] with only [key1], [key2], [key3], and [key4] associated with
+ * [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <K> objectLongMapOf(
+    key1: K,
+    value1: Long,
+    key2: K,
+    value2: Long,
+    key3: K,
+    value3: Long,
+    key4: K,
+    value4: Long,
+): ObjectLongMap<K> =
+    MutableObjectLongMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [ObjectLongMap] with only [key1], [key2], [key3], [key4], and [key5] associated
+ * with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <K> objectLongMapOf(
+    key1: K,
+    value1: Long,
+    key2: K,
+    value2: Long,
+    key3: K,
+    value3: Long,
+    key4: K,
+    value4: Long,
+    key5: K,
+    value5: Long,
+): ObjectLongMap<K> =
+    MutableObjectLongMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new empty [MutableObjectLongMap].
+ */
+public fun <K> mutableObjectLongMapOf(): MutableObjectLongMap<K> = MutableObjectLongMap()
+
+/**
+ * Returns a new [MutableObjectLongMap] with only [key1] associated with [value1].
+ */
+public fun <K> mutableObjectLongMapOf(
+    key1: K,
+    value1: Long,
+): MutableObjectLongMap<K> =
+    MutableObjectLongMap<K>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableObjectLongMap] with only [key1] and [key2] associated with
+ * [value1] and [value2], respectively.
+ */
+public fun <K> mutableObjectLongMapOf(
+    key1: K,
+    value1: Long,
+    key2: K,
+    value2: Long,
+): MutableObjectLongMap<K> =
+    MutableObjectLongMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableObjectLongMap] with only [key1], [key2], and [key3] associated with
+ * [value1], [value2], and [value3], respectively.
+ */
+public fun <K> mutableObjectLongMapOf(
+    key1: K,
+    value1: Long,
+    key2: K,
+    value2: Long,
+    key3: K,
+    value3: Long,
+): MutableObjectLongMap<K> =
+    MutableObjectLongMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableObjectLongMap] with only [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <K> mutableObjectLongMapOf(
+    key1: K,
+    value1: Long,
+    key2: K,
+    value2: Long,
+    key3: K,
+    value3: Long,
+    key4: K,
+    value4: Long,
+): MutableObjectLongMap<K> =
+    MutableObjectLongMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableObjectLongMap] with only [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <K> mutableObjectLongMapOf(
+    key1: K,
+    value1: Long,
+    key2: K,
+    value2: Long,
+    key3: K,
+    value3: Long,
+    key4: K,
+    value4: Long,
+    key5: K,
+    value5: Long,
+): MutableObjectLongMap<K> =
+    MutableObjectLongMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [ObjectLongMap] is a container with a [Map]-like interface for keys with
+ * reference types and [Long] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableObjectLongMap].
+ *
+ * @see [MutableObjectLongMap]
+ * @see ScatterMap
+ */
+public sealed class ObjectLongMap<K> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: Array<Any?> = EMPTY_OBJECTS
+
+    @PublishedApi
+    @JvmField
+    internal var values: LongArray = EmptyLongArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     * @throws NoSuchElementException when [key] is not found
+     */
+    public operator fun get(key: K): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("There is no key $key in the map")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: K, defaultValue: Long): Long {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: K, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: K, value: Long) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K, v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: K) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K)
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Long) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (K, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (K, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (K, Long) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Long): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@ObjectLongMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: K, value: Long) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@ObjectLongMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [ObjectLongMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is ObjectLongMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        val o = other as ObjectLongMap<Any?>
+
+        forEach { key, value ->
+            if (value != o[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(if (key === this) "(this)" else key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: K): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableObjectLongMap] is a container with a [MutableMap]-like interface for keys with
+ * reference types and [Long] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableObjectLongMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableObjectLongMap<K>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : ObjectLongMap<K>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = arrayOfNulls(newCapacity)
+        values = LongArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: K, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        val value = defaultValue()
+        set(key, value)
+        return value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: K, value: Long) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public fun put(key: K, value: Long) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: ObjectLongMap<K>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: ObjectLongMap<K>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: K) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: K, value: Long): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (K, Long) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index] as K, values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: K) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array<out K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Iterable<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Sequence<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: ScatterSet<K>) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        keys[index] = null
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        keys.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: K): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableObjectLongMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/PairFloatFloat.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/PairFloatFloat.kt
deleted file mode 100644
index aac435a..0000000
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/PairFloatFloat.kt
+++ /dev/null
@@ -1,83 +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.
- */
-@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
-
-package androidx.collection
-
-import kotlin.jvm.JvmField
-import kotlin.jvm.JvmInline
-
-/**
- * Container to ease passing around a tuple of two [Float] values.
- *
- * *Note*: This class is optimized by using a value class, a Kotlin language featured
- * not available from Java code. Java developers can get the same functionality by
- * using [Pair] or by constructing a custom implementation using Float parameters
- * directly (see [PairLongLong] for an example).
- */
-@JvmInline
-public value class PairFloatFloat internal constructor(
-    @PublishedApi @JvmField internal val packedValue: Long
-) {
-    /**
-     * Constructs a [PairFloatFloat] with two [Float] values.
-     *
-     * @param first the first value in the pair
-     * @param second the second value in the pair
-     */
-    public constructor(first: Float, second: Float) : this(packFloats(first, second))
-
-    /**
-     * The first value in the pair.
-     */
-    public inline val first: Float
-        get() = Float.fromBits((packedValue shr 32).toInt())
-
-    /**
-     * The second value in the pair.
-     */
-    public inline val second: Float
-        get() = Float.fromBits((packedValue and 0xFFFFFFFF).toInt())
-
-    /**
-     * Returns the [first] component of the pair. For instance, the first component
-     * of `PairFloatFloat(3f, 4f)` is `3f`.
-     *
-     * This method allows to use destructuring declarations when working with pairs,
-     * for example:
-     * ```
-     * val (first, second) = myPair
-     * ```
-     */
-    // NOTE: Unpack the value directly because using `first` forces an invokestatic
-    public inline operator fun component1(): Float = Float.fromBits((packedValue shr 32).toInt())
-
-    /**
-     * Returns the [second] component of the pair. For instance, the second component
-     * of `PairFloatFloat(3f, 4f)` is `4f`.
-     *
-     * This method allows to use destructuring declarations when working with pairs,
-     * for example:
-     * ```
-     * val (first, second) = myPair
-     * ```
-     */
-    // NOTE: Unpack the value directly because using `second` forces an invokestatic
-    public inline operator fun component2(): Float =
-        Float.fromBits((packedValue and 0xFFFFFFFF).toInt())
-
-    override fun toString(): String = "($first, $second)"
-}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/PairIntInt.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/PairIntInt.kt
deleted file mode 100644
index 0c7df8f..0000000
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/PairIntInt.kt
+++ /dev/null
@@ -1,82 +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.
- */
-@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
-
-package androidx.collection
-
-import kotlin.jvm.JvmField
-import kotlin.jvm.JvmInline
-
-/**
- * Container to ease passing around a tuple of two [Int] values.
- *
- * *Note*: This class is optimized by using a value class, a Kotlin language featured
- * not available from Java code. Java developers can get the same functionality by
- * using [Pair] or by constructing a custom implementation using Int parameters
- * directly (see [PairLongLong] for an example).
- */
-@JvmInline
-public value class PairIntInt internal constructor(
-    @PublishedApi @JvmField internal val packedValue: Long
-) {
-    /**
-     * Constructs a [PairIntInt] with two [Int] values.
-     *
-     * @param first the first value in the pair
-     * @param second the second value in the pair
-     */
-    public constructor(first: Int, second: Int) : this(packInts(first, second))
-
-    /**
-     * The first value in the pair.
-     */
-    public val first: Int
-        get() = (packedValue shr 32).toInt()
-
-    /**
-     * The second value in the pair.
-     */
-    public val second: Int
-        get() = (packedValue and 0xFFFFFFFF).toInt()
-
-    /**
-     * Returns the [first] component of the pair. For instance, the first component
-     * of `PairIntInt(3, 4)` is `3`.
-     *
-     * This method allows to use destructuring declarations when working with pairs,
-     * for example:
-     * ```
-     * val (first, second) = myPair
-     * ```
-     */
-    // NOTE: Unpack the value directly because using `first` forces an invokestatic
-    public inline operator fun component1(): Int = (packedValue shr 32).toInt()
-
-    /**
-     * Returns the [second] component of the pair. For instance, the second component
-     * of `PairIntInt(3, 4)` is `4`.
-     *
-     * This method allows to use destructuring declarations when working with pairs,
-     * for example:
-     * ```
-     * val (first, second) = myPair
-     * ```
-     */
-    // NOTE: Unpack the value directly because using `second` forces an invokestatic
-    public inline operator fun component2(): Int = (packedValue and 0xFFFFFFFF).toInt()
-
-    override fun toString(): String = "($first, $second)"
-}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/PairLongLong.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/PairLongLong.kt
deleted file mode 100644
index 7184f9a..0000000
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/PairLongLong.kt
+++ /dev/null
@@ -1,76 +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.
- */
-@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
-
-package androidx.collection
-
-/**
- * Container to ease passing around a tuple of two [Long] values.
- *
- * @param first the first value in the pair
- * @param second the second value in the pair
- */
-public class PairLongLong public constructor(public val first: Long, public val second: Long) {
-    /**
-     * Returns the [first] component of the pair. For instance, the first component
-     * of `PairLongLong(3, 4)` is `3`.
-     *
-     * This method allows to use destructuring declarations when working with pairs,
-     * for example:
-     * ```
-     * val (first, second) = myPair
-     * ```
-     */
-    public inline operator fun component1(): Long = first
-
-    /**
-     * Returns the [second] component of the pair. For instance, the second component
-     * of `PairLongLong(3, 4)` is `4`.
-     *
-     * This method allows to use destructuring declarations when working with pairs,
-     * for example:
-     * ```
-     * val (first, second) = myPair
-     * ```
-     */
-    public inline operator fun component2(): Long = second
-
-    /**
-     * Checks the two values for equality.
-     *
-     * @param other the [PairLongLong] to which this one is to be checked for equality
-     * @return true if the underlying values of the [PairLongLong] are both considered equal
-     */
-    override fun equals(other: Any?): Boolean {
-        if (!(other is PairLongLong)) {
-            return false
-        }
-        return other.first == first && other.second == second
-    }
-
-    /**
-     * Compute a hash code using the hash codes of the underlying values
-     *
-     * @return a hashcode of the [PairLongLong]
-     */
-    override fun hashCode(): Int {
-        return first.hashCode() xor second.hashCode()
-    }
-
-    override fun toString(): String {
-        return "($first, $second)";
-    }
-}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterMap.kt
index 1de7322..547f6a3 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterMap.kt
@@ -28,6 +28,7 @@
 
 import androidx.collection.internal.EMPTY_OBJECTS
 import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
 import kotlin.math.max
 
 // A "flat" hash map based on abseil's flat_hash_map
@@ -482,6 +483,47 @@
     }
 
     /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     *
+     * [transform] may be supplied to convert each element to a custom String.
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        transform: ((key: K, value: V) -> CharSequence)? = null
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@ScatterMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            if (transform == null) {
+                append(key)
+                append('=')
+                append(value)
+            } else {
+                append(transform(key, value))
+            }
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
      * Returns the hash code value for this map. The hash code the sum of the hash
      * codes of each key/value pair.
      */
@@ -819,16 +861,9 @@
      * or `null` if the key was not present in the map.
      */
     public fun put(key: K, value: V): V? {
-        var index = findInsertIndex(key)
-        val oldValue = if (index < 0) {
-            index = -index
-            // New entry, we must add the key
-            keys[index] = key
-            null
-        } else {
-            // Existing entry, we can keep the key
-            values[index]
-        }
+        val index = findAbsoluteInsertIndex(key)
+        val oldValue = values[index]
+        keys[index] = key
         values[index] = value
 
         @Suppress("UNCHECKED_CAST")
@@ -995,6 +1030,24 @@
         }
     }
 
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: ScatterSet<K>) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: ObjectList<K>) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
     private fun removeValueAt(index: Int): V? {
         _size -= 1
 
@@ -1072,52 +1125,6 @@
     }
 
     /**
-     * Equivalent of [findInsertIndex] but the returned index is *negative*
-     * if insertion requires a new mapping, and positive if the value takes
-     * place of an existing mapping.
-     */
-    private fun findInsertIndex(key: K): Int {
-        val hash = hash(key)
-        val hash1 = h1(hash)
-        val hash2 = h2(hash)
-
-        val probeMask = _capacity
-        var probeOffset = hash1 and probeMask
-        var probeIndex = 0
-
-        while (true) {
-            val g = group(metadata, probeOffset)
-            var m = g.match(hash2)
-            while (m.hasNext()) {
-                val index = (probeOffset + m.get()) and probeMask
-                if (keys[index] == key) {
-                    return index
-                }
-                m = m.next()
-            }
-
-            if (g.maskEmpty() != 0L) {
-                break
-            }
-
-            probeIndex += GroupWidth
-            probeOffset = (probeOffset + probeIndex) and probeMask
-        }
-
-        var index = findFirstAvailableSlot(hash1)
-        if (growthLimit == 0 && !isDeleted(metadata, index)) {
-            adjustStorage()
-            index = findFirstAvailableSlot(hash1)
-        }
-
-        _size += 1
-        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
-        writeMetadata(index, hash2.toLong())
-
-        return -index
-    }
-
-    /**
      * Finds the first empty or deleted slot in the table in which we can
      * store a value without resizing the internal storage.
      */
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt
index fe0cfd4..93587cf 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt
@@ -29,6 +29,7 @@
 import androidx.collection.internal.EMPTY_OBJECTS
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
 
 // This is a copy of ScatterMap, but without values
 
@@ -328,6 +329,45 @@
     public operator fun contains(element: E): Boolean = findElementIndex(element) >= 0
 
     /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     *
+     * [transform] may be supplied to convert each element to a custom String.
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        transform: ((E) -> CharSequence)? = null
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@ScatterSet.forEach { element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            if (transform == null) {
+                append(element)
+            } else {
+                append(transform(element))
+            }
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
      * Returns the hash code value for this set. The hash code of a set is defined to
      * be the sum of the hash codes of the elements in the set, where the hash code
      * of a null element is defined to be zero
@@ -377,22 +417,12 @@
      * Returns a string representation of this set. The set is denoted in the
      * string by the `[]`. Each element is separated by `, `.
      */
-    public override fun toString(): String {
-        if (isEmpty()) {
-            return "[]"
+    override fun toString(): String = joinToString(prefix = "[", postfix = "]") { element ->
+        if (element === this) {
+            "(this)"
+        } else {
+            element.toString()
         }
-
-        val s = StringBuilder().append('[')
-        val last = _size - 1
-        var index = 0
-        forEach { element ->
-            s.append(if (element === this) "(this)" else element)
-            if (index++ < last) {
-                s.append(',').append(' ')
-            }
-        }
-
-        return s.append(']').toString()
     }
 
     /**
@@ -613,6 +643,18 @@
     }
 
     /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements An [ObjectList] whose elements are to be added to the set
+     * @return `true` if any of the specified elements were added to the collection,
+     * `false` if the collection was not modified.
+     */
+    public fun addAll(elements: ObjectList<E>): Boolean {
+        val oldSize = size
+        plusAssign(elements)
+        return oldSize != size
+    }
+
+    /**
      * Adds all the [elements] into this set.
      * @param elements An array of elements to add to the set.
      */
@@ -653,6 +695,16 @@
     }
 
     /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements An [ObjectList] whose elements are to be added to the set
+     */
+    public operator fun plusAssign(elements: ObjectList<E>) {
+        elements.forEach { element ->
+            plusAssign(element)
+        }
+    }
+
+    /**
      * Removes the specified [element] from the set.
      * @param element The element to be removed from the set.
      * @return `true` if the [element] was present in the set, or `false` if it wasn't
@@ -724,6 +776,17 @@
 
     /**
      * Removes the specified [elements] from the set, if present.
+     * @param elements An [ObjectList] whose elements should be removed from the set.
+     * @return `true` if the set was changed or `false` if none of the elements were present.
+     */
+    public fun removeAll(elements: ObjectList<E>): Boolean {
+        val oldSize = size
+        minusAssign(elements)
+        return oldSize != size
+    }
+
+    /**
+     * Removes the specified [elements] from the set, if present.
      * @param elements An array of elements to be removed from the set.
      */
     public operator fun minusAssign(@Suppress("ArrayReturn") elements: Array<out E>) {
@@ -763,6 +826,16 @@
     }
 
     /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [ObjectList] whose elements should be removed from the set.
+     */
+    public operator fun minusAssign(elements: ObjectList<E>) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
      * Removes any values for which the specified [predicate] returns true.
      */
     public inline fun removeIf(predicate: (E) -> Boolean) {
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt
new file mode 100644
index 0000000..2e9661e
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt
@@ -0,0 +1,706 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class FloatFloatMapTest {
+    @Test
+    fun floatFloatMap() {
+        val map = MutableFloatFloatMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyFloatFloatMap() {
+        val map = emptyFloatFloatMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyFloatFloatMap(), map)
+    }
+
+    @Test
+    fun floatFloatMapFunction() {
+        val map = mutableFloatFloatMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableFloatFloatMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatFloatMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableFloatFloatMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatFloatMapInitFunction() {
+        val map1 = floatFloatMapOf(
+            1f, 1f,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1f, map1[1f])
+
+        val map2 = floatFloatMapOf(
+            1f, 1f,
+            2f, 2f,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1f, map2[1f])
+        assertEquals(2f, map2[2f])
+
+        val map3 = floatFloatMapOf(
+            1f, 1f,
+            2f, 2f,
+            3f, 3f,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1f, map3[1f])
+        assertEquals(2f, map3[2f])
+        assertEquals(3f, map3[3f])
+
+        val map4 = floatFloatMapOf(
+            1f, 1f,
+            2f, 2f,
+            3f, 3f,
+            4f, 4f,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1f, map4[1f])
+        assertEquals(2f, map4[2f])
+        assertEquals(3f, map4[3f])
+        assertEquals(4f, map4[4f])
+
+        val map5 = floatFloatMapOf(
+            1f, 1f,
+            2f, 2f,
+            3f, 3f,
+            4f, 4f,
+            5f, 5f,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1f, map5[1f])
+        assertEquals(2f, map5[2f])
+        assertEquals(3f, map5[3f])
+        assertEquals(4f, map5[4f])
+        assertEquals(5f, map5[5f])
+    }
+
+    @Test
+    fun mutableFloatFloatMapInitFunction() {
+        val map1 = mutableFloatFloatMapOf(
+            1f, 1f,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1f, map1[1f])
+
+        val map2 = mutableFloatFloatMapOf(
+            1f, 1f,
+            2f, 2f,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1f, map2[1f])
+        assertEquals(2f, map2[2f])
+
+        val map3 = mutableFloatFloatMapOf(
+            1f, 1f,
+            2f, 2f,
+            3f, 3f,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1f, map3[1f])
+        assertEquals(2f, map3[2f])
+        assertEquals(3f, map3[3f])
+
+        val map4 = mutableFloatFloatMapOf(
+            1f, 1f,
+            2f, 2f,
+            3f, 3f,
+            4f, 4f,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1f, map4[1f])
+        assertEquals(2f, map4[2f])
+        assertEquals(3f, map4[3f])
+        assertEquals(4f, map4[4f])
+
+        val map5 = mutableFloatFloatMapOf(
+            1f, 1f,
+            2f, 2f,
+            3f, 3f,
+            4f, 4f,
+            5f, 5f,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1f, map5[1f])
+        assertEquals(2f, map5[2f])
+        assertEquals(3f, map5[3f])
+        assertEquals(4f, map5[4f])
+        assertEquals(5f, map5[5f])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1f])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableFloatFloatMap(12)
+        map[1f] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1f])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableFloatFloatMap(2)
+        map[1f] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1f, map[1f])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableFloatFloatMap(0)
+        map[1f] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1f])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[1f] = 2f
+
+        assertEquals(1, map.size)
+        assertEquals(2f, map[1f])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableFloatFloatMap()
+
+        map.put(1f, 1f)
+        assertEquals(1f, map[1f])
+        map.put(1f, 2f)
+        assertEquals(2f, map[1f])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertFailsWith<NoSuchElementException> {
+            map[2f]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertEquals(2f, map.getOrDefault(2f, 2f))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertEquals(3f, map.getOrElse(3f) { 3f })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        var counter = 0
+        map.getOrPut(1f) {
+            counter++
+            2f
+        }
+        assertEquals(1f, map[1f])
+        assertEquals(0, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            2f
+        }
+        assertEquals(2f, map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            3f
+        }
+        assertEquals(2f, map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(3f) {
+            counter++
+            3f
+        }
+        assertEquals(3f, map[3f])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableFloatFloatMap()
+        map.remove(1f)
+
+        map[1f] = 1f
+        map.remove(1f)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableFloatFloatMap(6)
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+        map[4f] = 4f
+        map[5f] = 5f
+        map[6f] = 6f
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1f)
+        map.remove(2f)
+        map.remove(3f)
+        map.remove(4f)
+        map.remove(5f)
+        map.remove(6f)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7f] = 7f
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+        map[4f] = 4f
+        map[5f] = 5f
+        map[6f] = 6f
+
+        map.removeIf { key, _ -> key == 1f || key == 3f }
+
+        assertEquals(4, map.size)
+        assertEquals(2f, map[2f])
+        assertEquals(4f, map[4f])
+        assertEquals(5f, map[5f])
+        assertEquals(6f, map[6f])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+
+        map -= 1f
+
+        assertEquals(2, map.size)
+        assertFalse(1f in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+
+        map -= floatArrayOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+
+        map -= floatSetOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+
+        map -= floatListOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableFloatFloatMap()
+        assertFalse(map.remove(1f, 1f))
+
+        map[1f] = 1f
+        assertTrue(map.remove(1f, 1f))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableFloatFloatMap()
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toFloat()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableFloatFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toFloat()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toFloat())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableFloatFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toFloat()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableFloatFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toFloat()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableFloatFloatMap()
+
+        for (i in 0 until 32) {
+            map[i.toFloat()] = i.toFloat()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableFloatFloatMap()
+        assertEquals("{}", map.toString())
+
+        map[1f] = 1f
+        map[2f] = 2f
+        val oneValueString = 1f.toString()
+        val twoValueString = 2f.toString()
+        val oneKeyString = 1f.toString()
+        val twoKeyString = 2f.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableFloatFloatMap()
+        repeat(5) {
+            map[it.toFloat()] = it.toFloat()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toFloat()}=${order[0].toFloat()}, ${order[1].toFloat()}=" +
+            "${order[1].toFloat()}, ${order[2].toFloat()}=${order[2].toFloat()}," +
+            " ${order[3].toFloat()}=${order[3].toFloat()}, ${order[4].toFloat()}=" +
+            "${order[4].toFloat()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toFloat()}=${order[0].toFloat()}, ${order[1].toFloat()}=" +
+            "${order[1].toFloat()}, ${order[2].toFloat()}=${order[2].toFloat()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toFloat()}=${order[0].toFloat()}-${order[1].toFloat()}=" +
+            "${order[1].toFloat()}-${order[2].toFloat()}=${order[2].toFloat()}-" +
+            "${order[3].toFloat()}=${order[3].toFloat()}-${order[4].toFloat()}=" +
+            "${order[4].toFloat()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableFloatFloatMap()
+        assertNotEquals(map, map2)
+
+        map2[1f] = 1f
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertTrue(map.containsKey(1f))
+        assertFalse(map.containsKey(2f))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertTrue(1f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertTrue(map.containsValue(1f))
+        assertFalse(map.containsValue(3f))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableFloatFloatMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1f] = 1f
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableFloatFloatMap()
+        assertEquals(0, map.count())
+
+        map[1f] = 1f
+        assertEquals(1, map.count())
+
+        map[2f] = 2f
+        map[3f] = 3f
+        map[4f] = 4f
+        map[5f] = 5f
+        map[6f] = 6f
+
+        assertEquals(2, map.count { key, _ -> key <= 2f })
+        assertEquals(0, map.count { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+        map[4f] = 4f
+        map[5f] = 5f
+        map[6f] = 6f
+
+        assertTrue(map.any { key, _ -> key == 4f })
+        assertFalse(map.any { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+        map[4f] = 4f
+        map[5f] = 5f
+        map[6f] = 6f
+
+        assertTrue(map.all { key, value -> key > 0f && value >= 1f })
+        assertFalse(map.all { key, _ -> key < 6f })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableFloatFloatMap()
+        assertEquals(7, map.trim())
+
+        map[1f] = 1f
+        map[3f] = 3f
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toFloat()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toFloat()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt
new file mode 100644
index 0000000..f895a0c
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt
@@ -0,0 +1,706 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class FloatIntMapTest {
+    @Test
+    fun floatIntMap() {
+        val map = MutableFloatIntMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyFloatIntMap() {
+        val map = emptyFloatIntMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyFloatIntMap(), map)
+    }
+
+    @Test
+    fun floatIntMapFunction() {
+        val map = mutableFloatIntMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableFloatIntMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatIntMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableFloatIntMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatIntMapInitFunction() {
+        val map1 = floatIntMapOf(
+            1f, 1,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1, map1[1f])
+
+        val map2 = floatIntMapOf(
+            1f, 1,
+            2f, 2,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1, map2[1f])
+        assertEquals(2, map2[2f])
+
+        val map3 = floatIntMapOf(
+            1f, 1,
+            2f, 2,
+            3f, 3,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1, map3[1f])
+        assertEquals(2, map3[2f])
+        assertEquals(3, map3[3f])
+
+        val map4 = floatIntMapOf(
+            1f, 1,
+            2f, 2,
+            3f, 3,
+            4f, 4,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1, map4[1f])
+        assertEquals(2, map4[2f])
+        assertEquals(3, map4[3f])
+        assertEquals(4, map4[4f])
+
+        val map5 = floatIntMapOf(
+            1f, 1,
+            2f, 2,
+            3f, 3,
+            4f, 4,
+            5f, 5,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1, map5[1f])
+        assertEquals(2, map5[2f])
+        assertEquals(3, map5[3f])
+        assertEquals(4, map5[4f])
+        assertEquals(5, map5[5f])
+    }
+
+    @Test
+    fun mutableFloatIntMapInitFunction() {
+        val map1 = mutableFloatIntMapOf(
+            1f, 1,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1, map1[1f])
+
+        val map2 = mutableFloatIntMapOf(
+            1f, 1,
+            2f, 2,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1, map2[1f])
+        assertEquals(2, map2[2f])
+
+        val map3 = mutableFloatIntMapOf(
+            1f, 1,
+            2f, 2,
+            3f, 3,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1, map3[1f])
+        assertEquals(2, map3[2f])
+        assertEquals(3, map3[3f])
+
+        val map4 = mutableFloatIntMapOf(
+            1f, 1,
+            2f, 2,
+            3f, 3,
+            4f, 4,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1, map4[1f])
+        assertEquals(2, map4[2f])
+        assertEquals(3, map4[3f])
+        assertEquals(4, map4[4f])
+
+        val map5 = mutableFloatIntMapOf(
+            1f, 1,
+            2f, 2,
+            3f, 3,
+            4f, 4,
+            5f, 5,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1, map5[1f])
+        assertEquals(2, map5[2f])
+        assertEquals(3, map5[3f])
+        assertEquals(4, map5[4f])
+        assertEquals(5, map5[5f])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1f])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableFloatIntMap(12)
+        map[1f] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1f])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableFloatIntMap(2)
+        map[1f] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1, map[1f])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableFloatIntMap(0)
+        map[1f] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1f])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[1f] = 2
+
+        assertEquals(1, map.size)
+        assertEquals(2, map[1f])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableFloatIntMap()
+
+        map.put(1f, 1)
+        assertEquals(1, map[1f])
+        map.put(1f, 2)
+        assertEquals(2, map[1f])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertFailsWith<NoSuchElementException> {
+            map[2f]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertEquals(2, map.getOrDefault(2f, 2))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertEquals(3, map.getOrElse(3f) { 3 })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        var counter = 0
+        map.getOrPut(1f) {
+            counter++
+            2
+        }
+        assertEquals(1, map[1f])
+        assertEquals(0, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            2
+        }
+        assertEquals(2, map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            3
+        }
+        assertEquals(2, map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(3f) {
+            counter++
+            3
+        }
+        assertEquals(3, map[3f])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableFloatIntMap()
+        map.remove(1f)
+
+        map[1f] = 1
+        map.remove(1f)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableFloatIntMap(6)
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+        map[4f] = 4
+        map[5f] = 5
+        map[6f] = 6
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1f)
+        map.remove(2f)
+        map.remove(3f)
+        map.remove(4f)
+        map.remove(5f)
+        map.remove(6f)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7f] = 7
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+        map[4f] = 4
+        map[5f] = 5
+        map[6f] = 6
+
+        map.removeIf { key, _ -> key == 1f || key == 3f }
+
+        assertEquals(4, map.size)
+        assertEquals(2, map[2f])
+        assertEquals(4, map[4f])
+        assertEquals(5, map[5f])
+        assertEquals(6, map[6f])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+
+        map -= 1f
+
+        assertEquals(2, map.size)
+        assertFalse(1f in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+
+        map -= floatArrayOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+
+        map -= floatSetOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+
+        map -= floatListOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableFloatIntMap()
+        assertFalse(map.remove(1f, 1))
+
+        map[1f] = 1
+        assertTrue(map.remove(1f, 1))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableFloatIntMap()
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toInt()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableFloatIntMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toInt()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toFloat())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableFloatIntMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toInt()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableFloatIntMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toInt()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableFloatIntMap()
+
+        for (i in 0 until 32) {
+            map[i.toFloat()] = i.toInt()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableFloatIntMap()
+        assertEquals("{}", map.toString())
+
+        map[1f] = 1
+        map[2f] = 2
+        val oneValueString = 1.toString()
+        val twoValueString = 2.toString()
+        val oneKeyString = 1f.toString()
+        val twoKeyString = 2f.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableFloatIntMap()
+        repeat(5) {
+            map[it.toFloat()] = it.toInt()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toFloat()}=${order[0].toInt()}, ${order[1].toFloat()}=" +
+            "${order[1].toInt()}, ${order[2].toFloat()}=${order[2].toInt()}," +
+            " ${order[3].toFloat()}=${order[3].toInt()}, ${order[4].toFloat()}=" +
+            "${order[4].toInt()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toFloat()}=${order[0].toInt()}, ${order[1].toFloat()}=" +
+            "${order[1].toInt()}, ${order[2].toFloat()}=${order[2].toInt()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toFloat()}=${order[0].toInt()}-${order[1].toFloat()}=" +
+            "${order[1].toInt()}-${order[2].toFloat()}=${order[2].toInt()}-" +
+            "${order[3].toFloat()}=${order[3].toInt()}-${order[4].toFloat()}=" +
+            "${order[4].toInt()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableFloatIntMap()
+        assertNotEquals(map, map2)
+
+        map2[1f] = 1
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertTrue(map.containsKey(1f))
+        assertFalse(map.containsKey(2f))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertTrue(1f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertTrue(map.containsValue(1))
+        assertFalse(map.containsValue(3))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableFloatIntMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1f] = 1
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableFloatIntMap()
+        assertEquals(0, map.count())
+
+        map[1f] = 1
+        assertEquals(1, map.count())
+
+        map[2f] = 2
+        map[3f] = 3
+        map[4f] = 4
+        map[5f] = 5
+        map[6f] = 6
+
+        assertEquals(2, map.count { key, _ -> key <= 2f })
+        assertEquals(0, map.count { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+        map[4f] = 4
+        map[5f] = 5
+        map[6f] = 6
+
+        assertTrue(map.any { key, _ -> key == 4f })
+        assertFalse(map.any { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+        map[4f] = 4
+        map[5f] = 5
+        map[6f] = 6
+
+        assertTrue(map.all { key, value -> key > 0f && value >= 1 })
+        assertFalse(map.all { key, _ -> key < 6f })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableFloatIntMap()
+        assertEquals(7, map.trim())
+
+        map[1f] = 1
+        map[3f] = 3
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toInt()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toFloat()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
index b4e501b..f0f7cb1 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
@@ -15,7 +15,6 @@
  */
 package androidx.collection
 
-import kotlin.math.roundToInt
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -23,6 +22,14 @@
 import kotlin.test.assertNotEquals
 import kotlin.test.assertTrue
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 class FloatListTest {
     private val list: MutableFloatList = mutableFloatListOf(1f, 2f, 3f, 4f, 5f)
 
@@ -81,11 +88,32 @@
 
     @Test
     fun string() {
-        assertEquals("[1.0, 2.0, 3.0, 4.0, 5.0]", list.toString())
+        assertEquals("[${1f}, ${2f}, ${3f}, ${4f}, ${5f}]", list.toString())
         assertEquals("[]", mutableFloatListOf().toString())
     }
 
     @Test
+    fun joinToString() {
+        assertEquals("${1f}, ${2f}, ${3f}, ${4f}, ${5f}", list.joinToString())
+        assertEquals(
+            "x${1f}, ${2f}, ${3f}...",
+            list.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${1f}-${2f}-${3f}-${4f}-${5f}<",
+            list.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        assertEquals("one, two, three...", list.joinToString(limit = 3) {
+            when (it.toInt()) {
+                1 -> "one"
+                2 -> "two"
+                3 -> "three"
+                else -> "whoops"
+            }
+        })
+    }
+
+    @Test
     fun size() {
         assertEquals(5, list.size)
         assertEquals(5, list.count())
@@ -335,7 +363,7 @@
 
     @Test
     fun fold() {
-        assertEquals("12345", list.fold("") { acc, i -> acc + i.roundToInt().toString() })
+        assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
     }
 
     @Test
@@ -343,14 +371,14 @@
         assertEquals(
             "01-12-23-34-45-",
             list.foldIndexed("") { index, acc, i ->
-                "$acc$index${i.roundToInt()}-"
+                "$acc$index${i.toInt()}-"
             }
         )
     }
 
     @Test
     fun foldRight() {
-        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.roundToInt().toString() })
+        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
     }
 
     @Test
@@ -358,7 +386,7 @@
         assertEquals(
             "45-34-23-12-01-",
             list.foldRightIndexed("") { index, i, acc ->
-                "$acc$index${i.roundToInt()}-"
+                "$acc$index${i.toInt()}-"
             }
         )
     }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt
new file mode 100644
index 0000000..4436132
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt
@@ -0,0 +1,706 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class FloatLongMapTest {
+    @Test
+    fun floatLongMap() {
+        val map = MutableFloatLongMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyFloatLongMap() {
+        val map = emptyFloatLongMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyFloatLongMap(), map)
+    }
+
+    @Test
+    fun floatLongMapFunction() {
+        val map = mutableFloatLongMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableFloatLongMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatLongMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableFloatLongMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatLongMapInitFunction() {
+        val map1 = floatLongMapOf(
+            1f, 1L,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1L, map1[1f])
+
+        val map2 = floatLongMapOf(
+            1f, 1L,
+            2f, 2L,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1L, map2[1f])
+        assertEquals(2L, map2[2f])
+
+        val map3 = floatLongMapOf(
+            1f, 1L,
+            2f, 2L,
+            3f, 3L,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1L, map3[1f])
+        assertEquals(2L, map3[2f])
+        assertEquals(3L, map3[3f])
+
+        val map4 = floatLongMapOf(
+            1f, 1L,
+            2f, 2L,
+            3f, 3L,
+            4f, 4L,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1L, map4[1f])
+        assertEquals(2L, map4[2f])
+        assertEquals(3L, map4[3f])
+        assertEquals(4L, map4[4f])
+
+        val map5 = floatLongMapOf(
+            1f, 1L,
+            2f, 2L,
+            3f, 3L,
+            4f, 4L,
+            5f, 5L,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1L, map5[1f])
+        assertEquals(2L, map5[2f])
+        assertEquals(3L, map5[3f])
+        assertEquals(4L, map5[4f])
+        assertEquals(5L, map5[5f])
+    }
+
+    @Test
+    fun mutableFloatLongMapInitFunction() {
+        val map1 = mutableFloatLongMapOf(
+            1f, 1L,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1L, map1[1f])
+
+        val map2 = mutableFloatLongMapOf(
+            1f, 1L,
+            2f, 2L,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1L, map2[1f])
+        assertEquals(2L, map2[2f])
+
+        val map3 = mutableFloatLongMapOf(
+            1f, 1L,
+            2f, 2L,
+            3f, 3L,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1L, map3[1f])
+        assertEquals(2L, map3[2f])
+        assertEquals(3L, map3[3f])
+
+        val map4 = mutableFloatLongMapOf(
+            1f, 1L,
+            2f, 2L,
+            3f, 3L,
+            4f, 4L,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1L, map4[1f])
+        assertEquals(2L, map4[2f])
+        assertEquals(3L, map4[3f])
+        assertEquals(4L, map4[4f])
+
+        val map5 = mutableFloatLongMapOf(
+            1f, 1L,
+            2f, 2L,
+            3f, 3L,
+            4f, 4L,
+            5f, 5L,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1L, map5[1f])
+        assertEquals(2L, map5[2f])
+        assertEquals(3L, map5[3f])
+        assertEquals(4L, map5[4f])
+        assertEquals(5L, map5[5f])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1f])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableFloatLongMap(12)
+        map[1f] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1f])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableFloatLongMap(2)
+        map[1f] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1L, map[1f])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableFloatLongMap(0)
+        map[1f] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1f])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[1f] = 2L
+
+        assertEquals(1, map.size)
+        assertEquals(2L, map[1f])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableFloatLongMap()
+
+        map.put(1f, 1L)
+        assertEquals(1L, map[1f])
+        map.put(1f, 2L)
+        assertEquals(2L, map[1f])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertFailsWith<NoSuchElementException> {
+            map[2f]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertEquals(2L, map.getOrDefault(2f, 2L))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertEquals(3L, map.getOrElse(3f) { 3L })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        var counter = 0
+        map.getOrPut(1f) {
+            counter++
+            2L
+        }
+        assertEquals(1L, map[1f])
+        assertEquals(0, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            2L
+        }
+        assertEquals(2L, map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            3L
+        }
+        assertEquals(2L, map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(3f) {
+            counter++
+            3L
+        }
+        assertEquals(3L, map[3f])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableFloatLongMap()
+        map.remove(1f)
+
+        map[1f] = 1L
+        map.remove(1f)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableFloatLongMap(6)
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+        map[4f] = 4L
+        map[5f] = 5L
+        map[6f] = 6L
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1f)
+        map.remove(2f)
+        map.remove(3f)
+        map.remove(4f)
+        map.remove(5f)
+        map.remove(6f)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7f] = 7L
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+        map[4f] = 4L
+        map[5f] = 5L
+        map[6f] = 6L
+
+        map.removeIf { key, _ -> key == 1f || key == 3f }
+
+        assertEquals(4, map.size)
+        assertEquals(2L, map[2f])
+        assertEquals(4L, map[4f])
+        assertEquals(5L, map[5f])
+        assertEquals(6L, map[6f])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+
+        map -= 1f
+
+        assertEquals(2, map.size)
+        assertFalse(1f in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+
+        map -= floatArrayOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+
+        map -= floatSetOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+
+        map -= floatListOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableFloatLongMap()
+        assertFalse(map.remove(1f, 1L))
+
+        map[1f] = 1L
+        assertTrue(map.remove(1f, 1L))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableFloatLongMap()
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toLong()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableFloatLongMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toLong()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toFloat())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableFloatLongMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toLong()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableFloatLongMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toLong()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableFloatLongMap()
+
+        for (i in 0 until 32) {
+            map[i.toFloat()] = i.toLong()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableFloatLongMap()
+        assertEquals("{}", map.toString())
+
+        map[1f] = 1L
+        map[2f] = 2L
+        val oneValueString = 1L.toString()
+        val twoValueString = 2L.toString()
+        val oneKeyString = 1f.toString()
+        val twoKeyString = 2f.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableFloatLongMap()
+        repeat(5) {
+            map[it.toFloat()] = it.toLong()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toFloat()}=${order[0].toLong()}, ${order[1].toFloat()}=" +
+            "${order[1].toLong()}, ${order[2].toFloat()}=${order[2].toLong()}," +
+            " ${order[3].toFloat()}=${order[3].toLong()}, ${order[4].toFloat()}=" +
+            "${order[4].toLong()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toFloat()}=${order[0].toLong()}, ${order[1].toFloat()}=" +
+            "${order[1].toLong()}, ${order[2].toFloat()}=${order[2].toLong()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toFloat()}=${order[0].toLong()}-${order[1].toFloat()}=" +
+            "${order[1].toLong()}-${order[2].toFloat()}=${order[2].toLong()}-" +
+            "${order[3].toFloat()}=${order[3].toLong()}-${order[4].toFloat()}=" +
+            "${order[4].toLong()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableFloatLongMap()
+        assertNotEquals(map, map2)
+
+        map2[1f] = 1L
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertTrue(map.containsKey(1f))
+        assertFalse(map.containsKey(2f))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertTrue(1f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertTrue(map.containsValue(1L))
+        assertFalse(map.containsValue(3L))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableFloatLongMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1f] = 1L
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableFloatLongMap()
+        assertEquals(0, map.count())
+
+        map[1f] = 1L
+        assertEquals(1, map.count())
+
+        map[2f] = 2L
+        map[3f] = 3L
+        map[4f] = 4L
+        map[5f] = 5L
+        map[6f] = 6L
+
+        assertEquals(2, map.count { key, _ -> key <= 2f })
+        assertEquals(0, map.count { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+        map[4f] = 4L
+        map[5f] = 5L
+        map[6f] = 6L
+
+        assertTrue(map.any { key, _ -> key == 4f })
+        assertFalse(map.any { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+        map[4f] = 4L
+        map[5f] = 5L
+        map[6f] = 6L
+
+        assertTrue(map.all { key, value -> key > 0f && value >= 1L })
+        assertFalse(map.all { key, _ -> key < 6f })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableFloatLongMap()
+        assertEquals(7, map.trim())
+
+        map[1f] = 1L
+        map[3f] = 3L
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toLong()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toFloat()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt
new file mode 100644
index 0000000..44fe178
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt
@@ -0,0 +1,734 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class FloatObjectMapTest {
+    @Test
+    fun floatObjectMap() {
+        val map = MutableFloatObjectMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyFloatObjectMap() {
+        val map = emptyFloatObjectMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyFloatObjectMap<String>(), map)
+    }
+
+    @Test
+    fun floatObjectMapFunction() {
+        val map = mutableFloatObjectMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableFloatObjectMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatObjectMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableFloatObjectMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatObjectMapInitFunction() {
+        val map1 = floatObjectMapOf(
+            1f, "World",
+        )
+        assertEquals(1, map1.size)
+        assertEquals("World", map1[1f])
+
+        val map2 = floatObjectMapOf(
+            1f, "World",
+            2f, "Monde",
+        )
+        assertEquals(2, map2.size)
+        assertEquals("World", map2[1f])
+        assertEquals("Monde", map2[2f])
+
+        val map3 = floatObjectMapOf(
+            1f, "World",
+            2f, "Monde",
+            3f, "Welt",
+        )
+        assertEquals(3, map3.size)
+        assertEquals("World", map3[1f])
+        assertEquals("Monde", map3[2f])
+        assertEquals("Welt", map3[3f])
+
+        val map4 = floatObjectMapOf(
+            1f, "World",
+            2f, "Monde",
+            3f, "Welt",
+            4f, "Sekai",
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals("World", map4[1f])
+        assertEquals("Monde", map4[2f])
+        assertEquals("Welt", map4[3f])
+        assertEquals("Sekai", map4[4f])
+
+        val map5 = floatObjectMapOf(
+            1f, "World",
+            2f, "Monde",
+            3f, "Welt",
+            4f, "Sekai",
+            5f, "Mondo",
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals("World", map5[1f])
+        assertEquals("Monde", map5[2f])
+        assertEquals("Welt", map5[3f])
+        assertEquals("Sekai", map5[4f])
+        assertEquals("Mondo", map5[5f])
+    }
+
+    @Test
+    fun mutableFloatObjectMapInitFunction() {
+        val map1 = mutableFloatObjectMapOf(
+            1f, "World",
+        )
+        assertEquals(1, map1.size)
+        assertEquals("World", map1[1f])
+
+        val map2 = mutableFloatObjectMapOf(
+            1f, "World",
+            2f, "Monde",
+        )
+        assertEquals(2, map2.size)
+        assertEquals("World", map2[1f])
+        assertEquals("Monde", map2[2f])
+
+        val map3 = mutableFloatObjectMapOf(
+            1f, "World",
+            2f, "Monde",
+            3f, "Welt",
+        )
+        assertEquals(3, map3.size)
+        assertEquals("World", map3[1f])
+        assertEquals("Monde", map3[2f])
+        assertEquals("Welt", map3[3f])
+
+        val map4 = mutableFloatObjectMapOf(
+            1f, "World",
+            2f, "Monde",
+            3f, "Welt",
+            4f, "Sekai",
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals("World", map4[1f])
+        assertEquals("Monde", map4[2f])
+        assertEquals("Welt", map4[3f])
+        assertEquals("Sekai", map4[4f])
+
+        val map5 = mutableFloatObjectMapOf(
+            1f, "World",
+            2f, "Monde",
+            3f, "Welt",
+            4f, "Sekai",
+            5f, "Mondo",
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals("World", map5[1f])
+        assertEquals("Monde", map5[2f])
+        assertEquals("Welt", map5[3f])
+        assertEquals("Sekai", map5[4f])
+        assertEquals("Mondo", map5[5f])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1f])
+    }
+
+    @Test
+    fun insertIndex0() {
+        val map = MutableFloatObjectMap<String>()
+        map.put(1f, "World")
+        assertEquals("World", map[1f])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableFloatObjectMap<String>(12)
+        map[1f] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1f])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableFloatObjectMap<String>(2)
+        map[1f] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals("World", map[1f])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableFloatObjectMap<String>(0)
+        map[1f] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1f])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[1f] = "Monde"
+
+        assertEquals(1, map.size)
+        assertEquals("Monde", map[1f])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableFloatObjectMap<String?>()
+
+        assertNull(map.put(1f, "World"))
+        assertEquals("World", map.put(1f, "Monde"))
+        assertNull(map.put(2f, null))
+        assertNull(map.put(2f, "Monde"))
+    }
+
+    @Test
+    fun putAllMap() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        map.putAll(mutableFloatObjectMapOf(3f, "Welt", 7f, "Mundo"))
+
+        assertEquals(4, map.size)
+        assertEquals("Welt", map[3f])
+        assertEquals("Mundo", map[7f])
+    }
+
+    @Test
+    fun plusMap() {
+        val map = MutableFloatObjectMap<String>()
+        map += floatObjectMapOf(3f, "Welt", 7f, "Mundo")
+
+        assertEquals(2, map.size)
+        assertEquals("Welt", map[3f])
+        assertEquals("Mundo", map[7f])
+    }
+
+    @Test
+    fun nullValue() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = null
+
+        assertEquals(1, map.size)
+        assertNull(map[1f])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+
+        assertNull(map[2f])
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+
+        assertEquals("Monde", map.getOrDefault(2f, "Monde"))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        assertEquals("Monde", map.getOrElse(2f) { "Monde" })
+        assertEquals("Welt", map.getOrElse(3f) { "Welt" })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+
+        var counter = 0
+        map.getOrPut(1f) {
+            counter++
+            "Monde"
+        }
+        assertEquals("World", map[1f])
+        assertEquals(0, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            "Monde"
+        }
+        assertEquals("Monde", map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Monde", map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(3f) {
+            counter++
+            null
+        }
+        assertNull(map[3f])
+        assertEquals(2, counter)
+
+        map.getOrPut(3f) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Welt", map[3f])
+        assertEquals(3, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableFloatObjectMap<String?>()
+        assertNull(map.remove(1f))
+
+        map[1f] = "World"
+        assertEquals("World", map.remove(1f))
+        assertEquals(0, map.size)
+
+        map[1f] = null
+        assertNull(map.remove(1f))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableFloatObjectMap<String>(6)
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+        map[4f] = "Sekai"
+        map[5f] = "Mondo"
+        map[6f] = "Sesang"
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1f)
+        map.remove(2f)
+        map.remove(3f)
+        map.remove(4f)
+        map.remove(5f)
+        map.remove(6f)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7f] = "Mundo"
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+        map[4f] = "Sekai"
+        map[5f] = "Mondo"
+        map[6f] = "Sesang"
+
+        map.removeIf { key, value ->
+            key == 1f || key == 3f || value.startsWith('S')
+        }
+
+        assertEquals(2, map.size)
+        assertEquals("Monde", map[2f])
+        assertEquals("Mondo", map[5f])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+
+        map -= 1f
+
+        assertEquals(2, map.size)
+        assertNull(map[1f])
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+
+        map -= floatArrayOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertNull(map[3f])
+        assertNull(map[2f])
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+
+        map -= floatSetOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertNull(map[3f])
+        assertNull(map[2f])
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+
+        map -= floatListOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertNull(map[3f])
+        assertNull(map[2f])
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableFloatObjectMap<String?>()
+        assertFalse(map.remove(1f, "World"))
+
+        map[1f] = "World"
+        assertTrue(map.remove(1f, "World"))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableFloatObjectMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toString()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableFloatObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key.toInt().toString(), value)
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableFloatObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertEquals(key.toInt().toString(), map[key])
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableFloatObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachValue { value ->
+                assertNotNull(value.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableFloatObjectMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toFloat()] = i.toString()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableFloatObjectMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map[1f] = "World"
+        map[2f] = "Monde"
+        val oneKey = 1f.toString()
+        val twoKey = 2f.toString()
+        assertTrue(
+            "{$oneKey=World, $twoKey=Monde}" == map.toString() ||
+                "{$twoKey=Monde, $oneKey=World}" == map.toString()
+        )
+
+        map.clear()
+        map[1f] = null
+        assertEquals("{$oneKey=null}", map.toString())
+
+        val selfAsValueMap = MutableFloatObjectMap<Any>()
+        selfAsValueMap[1f] = selfAsValueMap
+        assertEquals("{$oneKey=(this)}", selfAsValueMap.toString())
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableFloatObjectMap<String>()
+        repeat(5) {
+            map[it.toFloat()] = it.toString()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toFloat()}=${order[0]}, ${order[1].toFloat()}=${order[1]}, " +
+            "${order[2].toFloat()}=${order[2]}, ${order[3].toFloat()}=${order[3]}, " +
+            "${order[4].toFloat()}=${order[4]}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toFloat()}=${order[0]}, ${order[1].toFloat()}=${order[1]}, " +
+            "${order[2].toFloat()}=${order[2]}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toFloat()}=${order[0]}-${order[1].toFloat()}=${order[1]}-" +
+            "${order[2].toFloat()}=${order[2]}-${order[3].toFloat()}=${order[3]}-" +
+            "${order[4].toFloat()}=${order[4]}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableFloatObjectMap<String?>()
+        map2[2f] = null
+
+        assertNotEquals(map, map2)
+
+        map2[1f] = "World"
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        assertTrue(map.containsKey(1f))
+        assertFalse(map.containsKey(3f))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        assertTrue(1f in map)
+        assertFalse(3f in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        assertTrue(map.containsValue("World"))
+        assertTrue(map.containsValue(null))
+        assertFalse(map.containsValue("Monde"))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableFloatObjectMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1f] = "World"
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableFloatObjectMap<String>()
+        assertEquals(0, map.count())
+
+        map[1f] = "World"
+        assertEquals(1, map.count())
+
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+        map[4f] = "Sekai"
+        map[5f] = "Mondo"
+        map[6f] = "Sesang"
+
+        assertEquals(2, map.count { key, _ -> key < 3f })
+        assertEquals(0, map.count { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+        map[4f] = "Sekai"
+        map[5f] = "Mondo"
+        map[6f] = "Sesang"
+
+        assertTrue(map.any { key, _ -> key > 5f })
+        assertFalse(map.any { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+        map[4f] = "Sekai"
+        map[5f] = "Mondo"
+        map[6f] = "Sesang"
+
+        assertTrue(map.all { key, value -> key < 7f && value.isNotEmpty() })
+        assertFalse(map.all { key, _ -> key < 6f })
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
index 98f7b59..767b2f0 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
@@ -23,6 +23,14 @@
 import kotlin.test.assertSame
 import kotlin.test.assertTrue
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 class FloatSetTest {
     @Test
     fun emptyFloatSetConstructor() {
@@ -147,9 +155,9 @@
         val set = MutableFloatSet()
         set += 1f
         set += 2f
-        var element: Float = Float.NaN
-        var otherElement: Float = Float.NaN
-        set.forEach { if (element.isNaN()) element = it else otherElement = it }
+        var element: Float = -1f
+        var otherElement: Float = -1f
+        set.forEach { if (element == -1f) element = it else otherElement = it }
         assertEquals(element, set.first())
         set -= element
         assertEquals(otherElement, set.first())
@@ -333,8 +341,37 @@
         set += 1f
         set += 5f
         assertTrue(
-            "[1.0, 5.0]" == set.toString() ||
-                "[5.0, 1.0]" == set.toString()
+            "[${1f}, ${5f}]" == set.toString() ||
+                "[${5f}, ${1f}]" == set.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val set = floatSetOf(1f, 2f, 3f, 4f, 5f)
+        val order = IntArray(5)
+        var index = 0
+        set.forEach { element ->
+            order[index++] = element.toInt()
+        }
+        assertEquals(
+            "${order[0].toFloat()}, ${order[1].toFloat()}, ${order[2].toFloat()}, " +
+            "${order[3].toFloat()}, ${order[4].toFloat()}",
+            set.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toFloat()}, ${order[1].toFloat()}, ${order[2].toFloat()}...",
+            set.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toFloat()}-${order[1].toFloat()}-${order[2].toFloat()}-" +
+            "${order[3].toFloat()}-${order[4].toFloat()}<",
+            set.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            set.joinToString(limit = 3) { names[it.toInt()] }
         )
     }
 
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt
new file mode 100644
index 0000000..0b675f6
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt
@@ -0,0 +1,706 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class IntFloatMapTest {
+    @Test
+    fun intFloatMap() {
+        val map = MutableIntFloatMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyIntFloatMap() {
+        val map = emptyIntFloatMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyIntFloatMap(), map)
+    }
+
+    @Test
+    fun intFloatMapFunction() {
+        val map = mutableIntFloatMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableIntFloatMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intFloatMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableIntFloatMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intFloatMapInitFunction() {
+        val map1 = intFloatMapOf(
+            1, 1f,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1f, map1[1])
+
+        val map2 = intFloatMapOf(
+            1, 1f,
+            2, 2f,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1f, map2[1])
+        assertEquals(2f, map2[2])
+
+        val map3 = intFloatMapOf(
+            1, 1f,
+            2, 2f,
+            3, 3f,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1f, map3[1])
+        assertEquals(2f, map3[2])
+        assertEquals(3f, map3[3])
+
+        val map4 = intFloatMapOf(
+            1, 1f,
+            2, 2f,
+            3, 3f,
+            4, 4f,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1f, map4[1])
+        assertEquals(2f, map4[2])
+        assertEquals(3f, map4[3])
+        assertEquals(4f, map4[4])
+
+        val map5 = intFloatMapOf(
+            1, 1f,
+            2, 2f,
+            3, 3f,
+            4, 4f,
+            5, 5f,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1f, map5[1])
+        assertEquals(2f, map5[2])
+        assertEquals(3f, map5[3])
+        assertEquals(4f, map5[4])
+        assertEquals(5f, map5[5])
+    }
+
+    @Test
+    fun mutableIntFloatMapInitFunction() {
+        val map1 = mutableIntFloatMapOf(
+            1, 1f,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1f, map1[1])
+
+        val map2 = mutableIntFloatMapOf(
+            1, 1f,
+            2, 2f,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1f, map2[1])
+        assertEquals(2f, map2[2])
+
+        val map3 = mutableIntFloatMapOf(
+            1, 1f,
+            2, 2f,
+            3, 3f,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1f, map3[1])
+        assertEquals(2f, map3[2])
+        assertEquals(3f, map3[3])
+
+        val map4 = mutableIntFloatMapOf(
+            1, 1f,
+            2, 2f,
+            3, 3f,
+            4, 4f,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1f, map4[1])
+        assertEquals(2f, map4[2])
+        assertEquals(3f, map4[3])
+        assertEquals(4f, map4[4])
+
+        val map5 = mutableIntFloatMapOf(
+            1, 1f,
+            2, 2f,
+            3, 3f,
+            4, 4f,
+            5, 5f,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1f, map5[1])
+        assertEquals(2f, map5[2])
+        assertEquals(3f, map5[3])
+        assertEquals(4f, map5[4])
+        assertEquals(5f, map5[5])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableIntFloatMap(12)
+        map[1] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableIntFloatMap(2)
+        map[1] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1f, map[1])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableIntFloatMap(0)
+        map[1] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[1] = 2f
+
+        assertEquals(1, map.size)
+        assertEquals(2f, map[1])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableIntFloatMap()
+
+        map.put(1, 1f)
+        assertEquals(1f, map[1])
+        map.put(1, 2f)
+        assertEquals(2f, map[1])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertFailsWith<NoSuchElementException> {
+            map[2]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertEquals(2f, map.getOrDefault(2, 2f))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertEquals(3f, map.getOrElse(3) { 3f })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        var counter = 0
+        map.getOrPut(1) {
+            counter++
+            2f
+        }
+        assertEquals(1f, map[1])
+        assertEquals(0, counter)
+
+        map.getOrPut(2) {
+            counter++
+            2f
+        }
+        assertEquals(2f, map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(2) {
+            counter++
+            3f
+        }
+        assertEquals(2f, map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(3) {
+            counter++
+            3f
+        }
+        assertEquals(3f, map[3])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableIntFloatMap()
+        map.remove(1)
+
+        map[1] = 1f
+        map.remove(1)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableIntFloatMap(6)
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+        map[4] = 4f
+        map[5] = 5f
+        map[6] = 6f
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1)
+        map.remove(2)
+        map.remove(3)
+        map.remove(4)
+        map.remove(5)
+        map.remove(6)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7] = 7f
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+        map[4] = 4f
+        map[5] = 5f
+        map[6] = 6f
+
+        map.removeIf { key, _ -> key == 1 || key == 3 }
+
+        assertEquals(4, map.size)
+        assertEquals(2f, map[2])
+        assertEquals(4f, map[4])
+        assertEquals(5f, map[5])
+        assertEquals(6f, map[6])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+
+        map -= 1
+
+        assertEquals(2, map.size)
+        assertFalse(1 in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+
+        map -= intArrayOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+
+        map -= intSetOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+
+        map -= intListOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableIntFloatMap()
+        assertFalse(map.remove(1, 1f))
+
+        map[1] = 1f
+        assertTrue(map.remove(1, 1f))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableIntFloatMap()
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toFloat()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableIntFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toFloat()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableIntFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toFloat()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableIntFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toFloat()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableIntFloatMap()
+
+        for (i in 0 until 32) {
+            map[i.toInt()] = i.toFloat()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableIntFloatMap()
+        assertEquals("{}", map.toString())
+
+        map[1] = 1f
+        map[2] = 2f
+        val oneValueString = 1f.toString()
+        val twoValueString = 2f.toString()
+        val oneKeyString = 1.toString()
+        val twoKeyString = 2.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableIntFloatMap()
+        repeat(5) {
+            map[it.toInt()] = it.toFloat()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toInt()}=${order[0].toFloat()}, ${order[1].toInt()}=" +
+            "${order[1].toFloat()}, ${order[2].toInt()}=${order[2].toFloat()}," +
+            " ${order[3].toInt()}=${order[3].toFloat()}, ${order[4].toInt()}=" +
+            "${order[4].toFloat()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toInt()}=${order[0].toFloat()}, ${order[1].toInt()}=" +
+            "${order[1].toFloat()}, ${order[2].toInt()}=${order[2].toFloat()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toInt()}=${order[0].toFloat()}-${order[1].toInt()}=" +
+            "${order[1].toFloat()}-${order[2].toInt()}=${order[2].toFloat()}-" +
+            "${order[3].toInt()}=${order[3].toFloat()}-${order[4].toInt()}=" +
+            "${order[4].toFloat()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableIntFloatMap()
+        assertNotEquals(map, map2)
+
+        map2[1] = 1f
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertTrue(map.containsKey(1))
+        assertFalse(map.containsKey(2))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertTrue(1 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertTrue(map.containsValue(1f))
+        assertFalse(map.containsValue(3f))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableIntFloatMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1] = 1f
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableIntFloatMap()
+        assertEquals(0, map.count())
+
+        map[1] = 1f
+        assertEquals(1, map.count())
+
+        map[2] = 2f
+        map[3] = 3f
+        map[4] = 4f
+        map[5] = 5f
+        map[6] = 6f
+
+        assertEquals(2, map.count { key, _ -> key <= 2 })
+        assertEquals(0, map.count { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+        map[4] = 4f
+        map[5] = 5f
+        map[6] = 6f
+
+        assertTrue(map.any { key, _ -> key == 4 })
+        assertFalse(map.any { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+        map[4] = 4f
+        map[5] = 5f
+        map[6] = 6f
+
+        assertTrue(map.all { key, value -> key > 0 && value >= 1f })
+        assertFalse(map.all { key, _ -> key < 6 })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableIntFloatMap()
+        assertEquals(7, map.trim())
+
+        map[1] = 1f
+        map[3] = 3f
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toFloat()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toInt()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt
new file mode 100644
index 0000000..37ffbfe
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt
@@ -0,0 +1,706 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class IntIntMapTest {
+    @Test
+    fun intIntMap() {
+        val map = MutableIntIntMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyIntIntMap() {
+        val map = emptyIntIntMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyIntIntMap(), map)
+    }
+
+    @Test
+    fun intIntMapFunction() {
+        val map = mutableIntIntMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableIntIntMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intIntMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableIntIntMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intIntMapInitFunction() {
+        val map1 = intIntMapOf(
+            1, 1,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1, map1[1])
+
+        val map2 = intIntMapOf(
+            1, 1,
+            2, 2,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1, map2[1])
+        assertEquals(2, map2[2])
+
+        val map3 = intIntMapOf(
+            1, 1,
+            2, 2,
+            3, 3,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1, map3[1])
+        assertEquals(2, map3[2])
+        assertEquals(3, map3[3])
+
+        val map4 = intIntMapOf(
+            1, 1,
+            2, 2,
+            3, 3,
+            4, 4,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1, map4[1])
+        assertEquals(2, map4[2])
+        assertEquals(3, map4[3])
+        assertEquals(4, map4[4])
+
+        val map5 = intIntMapOf(
+            1, 1,
+            2, 2,
+            3, 3,
+            4, 4,
+            5, 5,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1, map5[1])
+        assertEquals(2, map5[2])
+        assertEquals(3, map5[3])
+        assertEquals(4, map5[4])
+        assertEquals(5, map5[5])
+    }
+
+    @Test
+    fun mutableIntIntMapInitFunction() {
+        val map1 = mutableIntIntMapOf(
+            1, 1,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1, map1[1])
+
+        val map2 = mutableIntIntMapOf(
+            1, 1,
+            2, 2,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1, map2[1])
+        assertEquals(2, map2[2])
+
+        val map3 = mutableIntIntMapOf(
+            1, 1,
+            2, 2,
+            3, 3,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1, map3[1])
+        assertEquals(2, map3[2])
+        assertEquals(3, map3[3])
+
+        val map4 = mutableIntIntMapOf(
+            1, 1,
+            2, 2,
+            3, 3,
+            4, 4,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1, map4[1])
+        assertEquals(2, map4[2])
+        assertEquals(3, map4[3])
+        assertEquals(4, map4[4])
+
+        val map5 = mutableIntIntMapOf(
+            1, 1,
+            2, 2,
+            3, 3,
+            4, 4,
+            5, 5,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1, map5[1])
+        assertEquals(2, map5[2])
+        assertEquals(3, map5[3])
+        assertEquals(4, map5[4])
+        assertEquals(5, map5[5])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableIntIntMap(12)
+        map[1] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableIntIntMap(2)
+        map[1] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1, map[1])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableIntIntMap(0)
+        map[1] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[1] = 2
+
+        assertEquals(1, map.size)
+        assertEquals(2, map[1])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableIntIntMap()
+
+        map.put(1, 1)
+        assertEquals(1, map[1])
+        map.put(1, 2)
+        assertEquals(2, map[1])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertFailsWith<NoSuchElementException> {
+            map[2]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertEquals(2, map.getOrDefault(2, 2))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertEquals(3, map.getOrElse(3) { 3 })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        var counter = 0
+        map.getOrPut(1) {
+            counter++
+            2
+        }
+        assertEquals(1, map[1])
+        assertEquals(0, counter)
+
+        map.getOrPut(2) {
+            counter++
+            2
+        }
+        assertEquals(2, map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(2) {
+            counter++
+            3
+        }
+        assertEquals(2, map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(3) {
+            counter++
+            3
+        }
+        assertEquals(3, map[3])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableIntIntMap()
+        map.remove(1)
+
+        map[1] = 1
+        map.remove(1)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableIntIntMap(6)
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+        map[4] = 4
+        map[5] = 5
+        map[6] = 6
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1)
+        map.remove(2)
+        map.remove(3)
+        map.remove(4)
+        map.remove(5)
+        map.remove(6)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7] = 7
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+        map[4] = 4
+        map[5] = 5
+        map[6] = 6
+
+        map.removeIf { key, _ -> key == 1 || key == 3 }
+
+        assertEquals(4, map.size)
+        assertEquals(2, map[2])
+        assertEquals(4, map[4])
+        assertEquals(5, map[5])
+        assertEquals(6, map[6])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+
+        map -= 1
+
+        assertEquals(2, map.size)
+        assertFalse(1 in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+
+        map -= intArrayOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+
+        map -= intSetOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+
+        map -= intListOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableIntIntMap()
+        assertFalse(map.remove(1, 1))
+
+        map[1] = 1
+        assertTrue(map.remove(1, 1))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableIntIntMap()
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toInt()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableIntIntMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toInt()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableIntIntMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toInt()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableIntIntMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toInt()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableIntIntMap()
+
+        for (i in 0 until 32) {
+            map[i.toInt()] = i.toInt()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableIntIntMap()
+        assertEquals("{}", map.toString())
+
+        map[1] = 1
+        map[2] = 2
+        val oneValueString = 1.toString()
+        val twoValueString = 2.toString()
+        val oneKeyString = 1.toString()
+        val twoKeyString = 2.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableIntIntMap()
+        repeat(5) {
+            map[it.toInt()] = it.toInt()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toInt()}=${order[0].toInt()}, ${order[1].toInt()}=" +
+            "${order[1].toInt()}, ${order[2].toInt()}=${order[2].toInt()}," +
+            " ${order[3].toInt()}=${order[3].toInt()}, ${order[4].toInt()}=" +
+            "${order[4].toInt()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toInt()}=${order[0].toInt()}, ${order[1].toInt()}=" +
+            "${order[1].toInt()}, ${order[2].toInt()}=${order[2].toInt()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toInt()}=${order[0].toInt()}-${order[1].toInt()}=" +
+            "${order[1].toInt()}-${order[2].toInt()}=${order[2].toInt()}-" +
+            "${order[3].toInt()}=${order[3].toInt()}-${order[4].toInt()}=" +
+            "${order[4].toInt()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableIntIntMap()
+        assertNotEquals(map, map2)
+
+        map2[1] = 1
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertTrue(map.containsKey(1))
+        assertFalse(map.containsKey(2))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertTrue(1 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertTrue(map.containsValue(1))
+        assertFalse(map.containsValue(3))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableIntIntMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1] = 1
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableIntIntMap()
+        assertEquals(0, map.count())
+
+        map[1] = 1
+        assertEquals(1, map.count())
+
+        map[2] = 2
+        map[3] = 3
+        map[4] = 4
+        map[5] = 5
+        map[6] = 6
+
+        assertEquals(2, map.count { key, _ -> key <= 2 })
+        assertEquals(0, map.count { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+        map[4] = 4
+        map[5] = 5
+        map[6] = 6
+
+        assertTrue(map.any { key, _ -> key == 4 })
+        assertFalse(map.any { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+        map[4] = 4
+        map[5] = 5
+        map[6] = 6
+
+        assertTrue(map.all { key, value -> key > 0 && value >= 1 })
+        assertFalse(map.all { key, _ -> key < 6 })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableIntIntMap()
+        assertEquals(7, map.trim())
+
+        map[1] = 1
+        map[3] = 3
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toInt()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toInt()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
index 66d89af..f1688d0 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
@@ -22,6 +22,14 @@
 import kotlin.test.assertNotEquals
 import kotlin.test.assertTrue
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 class IntListTest {
     private val list: MutableIntList = mutableIntListOf(1, 2, 3, 4, 5)
 
@@ -80,11 +88,32 @@
 
     @Test
     fun string() {
-        assertEquals("[1, 2, 3, 4, 5]", list.toString())
+        assertEquals("[${1}, ${2}, ${3}, ${4}, ${5}]", list.toString())
         assertEquals("[]", mutableIntListOf().toString())
     }
 
     @Test
+    fun joinToString() {
+        assertEquals("${1}, ${2}, ${3}, ${4}, ${5}", list.joinToString())
+        assertEquals(
+            "x${1}, ${2}, ${3}...",
+            list.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${1}-${2}-${3}-${4}-${5}<",
+            list.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        assertEquals("one, two, three...", list.joinToString(limit = 3) {
+            when (it.toInt()) {
+                1 -> "one"
+                2 -> "two"
+                3 -> "three"
+                else -> "whoops"
+            }
+        })
+    }
+
+    @Test
     fun size() {
         assertEquals(5, list.size)
         assertEquals(5, list.count())
@@ -334,7 +363,7 @@
 
     @Test
     fun fold() {
-        assertEquals("12345", list.fold("") { acc, i -> acc + i.toString() })
+        assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
     }
 
     @Test
@@ -342,14 +371,14 @@
         assertEquals(
             "01-12-23-34-45-",
             list.foldIndexed("") { index, acc, i ->
-                "$acc$index$i-"
+                "$acc$index${i.toInt()}-"
             }
         )
     }
 
     @Test
     fun foldRight() {
-        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toString() })
+        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
     }
 
     @Test
@@ -357,7 +386,7 @@
         assertEquals(
             "45-34-23-12-01-",
             list.foldRightIndexed("") { index, i, acc ->
-                "$acc$index$i-"
+                "$acc$index${i.toInt()}-"
             }
         )
     }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt
new file mode 100644
index 0000000..32add2e
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt
@@ -0,0 +1,706 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class IntLongMapTest {
+    @Test
+    fun intLongMap() {
+        val map = MutableIntLongMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyIntLongMap() {
+        val map = emptyIntLongMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyIntLongMap(), map)
+    }
+
+    @Test
+    fun intLongMapFunction() {
+        val map = mutableIntLongMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableIntLongMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intLongMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableIntLongMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intLongMapInitFunction() {
+        val map1 = intLongMapOf(
+            1, 1L,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1L, map1[1])
+
+        val map2 = intLongMapOf(
+            1, 1L,
+            2, 2L,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1L, map2[1])
+        assertEquals(2L, map2[2])
+
+        val map3 = intLongMapOf(
+            1, 1L,
+            2, 2L,
+            3, 3L,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1L, map3[1])
+        assertEquals(2L, map3[2])
+        assertEquals(3L, map3[3])
+
+        val map4 = intLongMapOf(
+            1, 1L,
+            2, 2L,
+            3, 3L,
+            4, 4L,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1L, map4[1])
+        assertEquals(2L, map4[2])
+        assertEquals(3L, map4[3])
+        assertEquals(4L, map4[4])
+
+        val map5 = intLongMapOf(
+            1, 1L,
+            2, 2L,
+            3, 3L,
+            4, 4L,
+            5, 5L,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1L, map5[1])
+        assertEquals(2L, map5[2])
+        assertEquals(3L, map5[3])
+        assertEquals(4L, map5[4])
+        assertEquals(5L, map5[5])
+    }
+
+    @Test
+    fun mutableIntLongMapInitFunction() {
+        val map1 = mutableIntLongMapOf(
+            1, 1L,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1L, map1[1])
+
+        val map2 = mutableIntLongMapOf(
+            1, 1L,
+            2, 2L,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1L, map2[1])
+        assertEquals(2L, map2[2])
+
+        val map3 = mutableIntLongMapOf(
+            1, 1L,
+            2, 2L,
+            3, 3L,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1L, map3[1])
+        assertEquals(2L, map3[2])
+        assertEquals(3L, map3[3])
+
+        val map4 = mutableIntLongMapOf(
+            1, 1L,
+            2, 2L,
+            3, 3L,
+            4, 4L,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1L, map4[1])
+        assertEquals(2L, map4[2])
+        assertEquals(3L, map4[3])
+        assertEquals(4L, map4[4])
+
+        val map5 = mutableIntLongMapOf(
+            1, 1L,
+            2, 2L,
+            3, 3L,
+            4, 4L,
+            5, 5L,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1L, map5[1])
+        assertEquals(2L, map5[2])
+        assertEquals(3L, map5[3])
+        assertEquals(4L, map5[4])
+        assertEquals(5L, map5[5])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableIntLongMap(12)
+        map[1] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableIntLongMap(2)
+        map[1] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1L, map[1])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableIntLongMap(0)
+        map[1] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[1] = 2L
+
+        assertEquals(1, map.size)
+        assertEquals(2L, map[1])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableIntLongMap()
+
+        map.put(1, 1L)
+        assertEquals(1L, map[1])
+        map.put(1, 2L)
+        assertEquals(2L, map[1])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertFailsWith<NoSuchElementException> {
+            map[2]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertEquals(2L, map.getOrDefault(2, 2L))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertEquals(3L, map.getOrElse(3) { 3L })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        var counter = 0
+        map.getOrPut(1) {
+            counter++
+            2L
+        }
+        assertEquals(1L, map[1])
+        assertEquals(0, counter)
+
+        map.getOrPut(2) {
+            counter++
+            2L
+        }
+        assertEquals(2L, map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(2) {
+            counter++
+            3L
+        }
+        assertEquals(2L, map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(3) {
+            counter++
+            3L
+        }
+        assertEquals(3L, map[3])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableIntLongMap()
+        map.remove(1)
+
+        map[1] = 1L
+        map.remove(1)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableIntLongMap(6)
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+        map[4] = 4L
+        map[5] = 5L
+        map[6] = 6L
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1)
+        map.remove(2)
+        map.remove(3)
+        map.remove(4)
+        map.remove(5)
+        map.remove(6)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7] = 7L
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+        map[4] = 4L
+        map[5] = 5L
+        map[6] = 6L
+
+        map.removeIf { key, _ -> key == 1 || key == 3 }
+
+        assertEquals(4, map.size)
+        assertEquals(2L, map[2])
+        assertEquals(4L, map[4])
+        assertEquals(5L, map[5])
+        assertEquals(6L, map[6])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+
+        map -= 1
+
+        assertEquals(2, map.size)
+        assertFalse(1 in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+
+        map -= intArrayOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+
+        map -= intSetOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+
+        map -= intListOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableIntLongMap()
+        assertFalse(map.remove(1, 1L))
+
+        map[1] = 1L
+        assertTrue(map.remove(1, 1L))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableIntLongMap()
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toLong()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableIntLongMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toLong()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableIntLongMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toLong()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableIntLongMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toLong()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableIntLongMap()
+
+        for (i in 0 until 32) {
+            map[i.toInt()] = i.toLong()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableIntLongMap()
+        assertEquals("{}", map.toString())
+
+        map[1] = 1L
+        map[2] = 2L
+        val oneValueString = 1L.toString()
+        val twoValueString = 2L.toString()
+        val oneKeyString = 1.toString()
+        val twoKeyString = 2.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableIntLongMap()
+        repeat(5) {
+            map[it.toInt()] = it.toLong()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toInt()}=${order[0].toLong()}, ${order[1].toInt()}=" +
+            "${order[1].toLong()}, ${order[2].toInt()}=${order[2].toLong()}," +
+            " ${order[3].toInt()}=${order[3].toLong()}, ${order[4].toInt()}=" +
+            "${order[4].toLong()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toInt()}=${order[0].toLong()}, ${order[1].toInt()}=" +
+            "${order[1].toLong()}, ${order[2].toInt()}=${order[2].toLong()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toInt()}=${order[0].toLong()}-${order[1].toInt()}=" +
+            "${order[1].toLong()}-${order[2].toInt()}=${order[2].toLong()}-" +
+            "${order[3].toInt()}=${order[3].toLong()}-${order[4].toInt()}=" +
+            "${order[4].toLong()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableIntLongMap()
+        assertNotEquals(map, map2)
+
+        map2[1] = 1L
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertTrue(map.containsKey(1))
+        assertFalse(map.containsKey(2))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertTrue(1 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertTrue(map.containsValue(1L))
+        assertFalse(map.containsValue(3L))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableIntLongMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1] = 1L
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableIntLongMap()
+        assertEquals(0, map.count())
+
+        map[1] = 1L
+        assertEquals(1, map.count())
+
+        map[2] = 2L
+        map[3] = 3L
+        map[4] = 4L
+        map[5] = 5L
+        map[6] = 6L
+
+        assertEquals(2, map.count { key, _ -> key <= 2 })
+        assertEquals(0, map.count { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+        map[4] = 4L
+        map[5] = 5L
+        map[6] = 6L
+
+        assertTrue(map.any { key, _ -> key == 4 })
+        assertFalse(map.any { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+        map[4] = 4L
+        map[5] = 5L
+        map[6] = 6L
+
+        assertTrue(map.all { key, value -> key > 0 && value >= 1L })
+        assertFalse(map.all { key, _ -> key < 6 })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableIntLongMap()
+        assertEquals(7, map.trim())
+
+        map[1] = 1L
+        map[3] = 3L
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toLong()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toInt()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt
new file mode 100644
index 0000000..d5eeee1
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt
@@ -0,0 +1,734 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class IntObjectMapTest {
+    @Test
+    fun intObjectMap() {
+        val map = MutableIntObjectMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyIntObjectMap() {
+        val map = emptyIntObjectMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyIntObjectMap<String>(), map)
+    }
+
+    @Test
+    fun intObjectMapFunction() {
+        val map = mutableIntObjectMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableIntObjectMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intObjectMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableIntObjectMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intObjectMapInitFunction() {
+        val map1 = intObjectMapOf(
+            1, "World",
+        )
+        assertEquals(1, map1.size)
+        assertEquals("World", map1[1])
+
+        val map2 = intObjectMapOf(
+            1, "World",
+            2, "Monde",
+        )
+        assertEquals(2, map2.size)
+        assertEquals("World", map2[1])
+        assertEquals("Monde", map2[2])
+
+        val map3 = intObjectMapOf(
+            1, "World",
+            2, "Monde",
+            3, "Welt",
+        )
+        assertEquals(3, map3.size)
+        assertEquals("World", map3[1])
+        assertEquals("Monde", map3[2])
+        assertEquals("Welt", map3[3])
+
+        val map4 = intObjectMapOf(
+            1, "World",
+            2, "Monde",
+            3, "Welt",
+            4, "Sekai",
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals("World", map4[1])
+        assertEquals("Monde", map4[2])
+        assertEquals("Welt", map4[3])
+        assertEquals("Sekai", map4[4])
+
+        val map5 = intObjectMapOf(
+            1, "World",
+            2, "Monde",
+            3, "Welt",
+            4, "Sekai",
+            5, "Mondo",
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals("World", map5[1])
+        assertEquals("Monde", map5[2])
+        assertEquals("Welt", map5[3])
+        assertEquals("Sekai", map5[4])
+        assertEquals("Mondo", map5[5])
+    }
+
+    @Test
+    fun mutableIntObjectMapInitFunction() {
+        val map1 = mutableIntObjectMapOf(
+            1, "World",
+        )
+        assertEquals(1, map1.size)
+        assertEquals("World", map1[1])
+
+        val map2 = mutableIntObjectMapOf(
+            1, "World",
+            2, "Monde",
+        )
+        assertEquals(2, map2.size)
+        assertEquals("World", map2[1])
+        assertEquals("Monde", map2[2])
+
+        val map3 = mutableIntObjectMapOf(
+            1, "World",
+            2, "Monde",
+            3, "Welt",
+        )
+        assertEquals(3, map3.size)
+        assertEquals("World", map3[1])
+        assertEquals("Monde", map3[2])
+        assertEquals("Welt", map3[3])
+
+        val map4 = mutableIntObjectMapOf(
+            1, "World",
+            2, "Monde",
+            3, "Welt",
+            4, "Sekai",
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals("World", map4[1])
+        assertEquals("Monde", map4[2])
+        assertEquals("Welt", map4[3])
+        assertEquals("Sekai", map4[4])
+
+        val map5 = mutableIntObjectMapOf(
+            1, "World",
+            2, "Monde",
+            3, "Welt",
+            4, "Sekai",
+            5, "Mondo",
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals("World", map5[1])
+        assertEquals("Monde", map5[2])
+        assertEquals("Welt", map5[3])
+        assertEquals("Sekai", map5[4])
+        assertEquals("Mondo", map5[5])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1])
+    }
+
+    @Test
+    fun insertIndex0() {
+        val map = MutableIntObjectMap<String>()
+        map.put(1, "World")
+        assertEquals("World", map[1])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableIntObjectMap<String>(12)
+        map[1] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableIntObjectMap<String>(2)
+        map[1] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals("World", map[1])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableIntObjectMap<String>(0)
+        map[1] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[1] = "Monde"
+
+        assertEquals(1, map.size)
+        assertEquals("Monde", map[1])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableIntObjectMap<String?>()
+
+        assertNull(map.put(1, "World"))
+        assertEquals("World", map.put(1, "Monde"))
+        assertNull(map.put(2, null))
+        assertNull(map.put(2, "Monde"))
+    }
+
+    @Test
+    fun putAllMap() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        map.putAll(mutableIntObjectMapOf(3, "Welt", 7, "Mundo"))
+
+        assertEquals(4, map.size)
+        assertEquals("Welt", map[3])
+        assertEquals("Mundo", map[7])
+    }
+
+    @Test
+    fun plusMap() {
+        val map = MutableIntObjectMap<String>()
+        map += intObjectMapOf(3, "Welt", 7, "Mundo")
+
+        assertEquals(2, map.size)
+        assertEquals("Welt", map[3])
+        assertEquals("Mundo", map[7])
+    }
+
+    @Test
+    fun nullValue() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = null
+
+        assertEquals(1, map.size)
+        assertNull(map[1])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+
+        assertNull(map[2])
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+
+        assertEquals("Monde", map.getOrDefault(2, "Monde"))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        assertEquals("Monde", map.getOrElse(2) { "Monde" })
+        assertEquals("Welt", map.getOrElse(3) { "Welt" })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+
+        var counter = 0
+        map.getOrPut(1) {
+            counter++
+            "Monde"
+        }
+        assertEquals("World", map[1])
+        assertEquals(0, counter)
+
+        map.getOrPut(2) {
+            counter++
+            "Monde"
+        }
+        assertEquals("Monde", map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(2) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Monde", map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(3) {
+            counter++
+            null
+        }
+        assertNull(map[3])
+        assertEquals(2, counter)
+
+        map.getOrPut(3) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Welt", map[3])
+        assertEquals(3, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableIntObjectMap<String?>()
+        assertNull(map.remove(1))
+
+        map[1] = "World"
+        assertEquals("World", map.remove(1))
+        assertEquals(0, map.size)
+
+        map[1] = null
+        assertNull(map.remove(1))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableIntObjectMap<String>(6)
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+        map[4] = "Sekai"
+        map[5] = "Mondo"
+        map[6] = "Sesang"
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1)
+        map.remove(2)
+        map.remove(3)
+        map.remove(4)
+        map.remove(5)
+        map.remove(6)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7] = "Mundo"
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+        map[4] = "Sekai"
+        map[5] = "Mondo"
+        map[6] = "Sesang"
+
+        map.removeIf { key, value ->
+            key == 1 || key == 3 || value.startsWith('S')
+        }
+
+        assertEquals(2, map.size)
+        assertEquals("Monde", map[2])
+        assertEquals("Mondo", map[5])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+
+        map -= 1
+
+        assertEquals(2, map.size)
+        assertNull(map[1])
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+
+        map -= intArrayOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertNull(map[3])
+        assertNull(map[2])
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+
+        map -= intSetOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertNull(map[3])
+        assertNull(map[2])
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+
+        map -= intListOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertNull(map[3])
+        assertNull(map[2])
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableIntObjectMap<String?>()
+        assertFalse(map.remove(1, "World"))
+
+        map[1] = "World"
+        assertTrue(map.remove(1, "World"))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableIntObjectMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toString()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableIntObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key.toInt().toString(), value)
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableIntObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertEquals(key.toInt().toString(), map[key])
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableIntObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachValue { value ->
+                assertNotNull(value.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableIntObjectMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toInt()] = i.toString()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableIntObjectMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map[1] = "World"
+        map[2] = "Monde"
+        val oneKey = 1.toString()
+        val twoKey = 2.toString()
+        assertTrue(
+            "{$oneKey=World, $twoKey=Monde}" == map.toString() ||
+                "{$twoKey=Monde, $oneKey=World}" == map.toString()
+        )
+
+        map.clear()
+        map[1] = null
+        assertEquals("{$oneKey=null}", map.toString())
+
+        val selfAsValueMap = MutableIntObjectMap<Any>()
+        selfAsValueMap[1] = selfAsValueMap
+        assertEquals("{$oneKey=(this)}", selfAsValueMap.toString())
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableIntObjectMap<String>()
+        repeat(5) {
+            map[it.toInt()] = it.toString()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toInt()}=${order[0]}, ${order[1].toInt()}=${order[1]}, " +
+            "${order[2].toInt()}=${order[2]}, ${order[3].toInt()}=${order[3]}, " +
+            "${order[4].toInt()}=${order[4]}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toInt()}=${order[0]}, ${order[1].toInt()}=${order[1]}, " +
+            "${order[2].toInt()}=${order[2]}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toInt()}=${order[0]}-${order[1].toInt()}=${order[1]}-" +
+            "${order[2].toInt()}=${order[2]}-${order[3].toInt()}=${order[3]}-" +
+            "${order[4].toInt()}=${order[4]}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableIntObjectMap<String?>()
+        map2[2] = null
+
+        assertNotEquals(map, map2)
+
+        map2[1] = "World"
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        assertTrue(map.containsKey(1))
+        assertFalse(map.containsKey(3))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        assertTrue(1 in map)
+        assertFalse(3 in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        assertTrue(map.containsValue("World"))
+        assertTrue(map.containsValue(null))
+        assertFalse(map.containsValue("Monde"))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableIntObjectMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1] = "World"
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableIntObjectMap<String>()
+        assertEquals(0, map.count())
+
+        map[1] = "World"
+        assertEquals(1, map.count())
+
+        map[2] = "Monde"
+        map[3] = "Welt"
+        map[4] = "Sekai"
+        map[5] = "Mondo"
+        map[6] = "Sesang"
+
+        assertEquals(2, map.count { key, _ -> key < 3 })
+        assertEquals(0, map.count { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+        map[4] = "Sekai"
+        map[5] = "Mondo"
+        map[6] = "Sesang"
+
+        assertTrue(map.any { key, _ -> key > 5 })
+        assertFalse(map.any { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+        map[4] = "Sekai"
+        map[5] = "Mondo"
+        map[6] = "Sesang"
+
+        assertTrue(map.all { key, value -> key < 7 && value.isNotEmpty() })
+        assertFalse(map.all { key, _ -> key < 6 })
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
index 9d326e1..36a2692 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
@@ -23,6 +23,14 @@
 import kotlin.test.assertSame
 import kotlin.test.assertTrue
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 class IntSetTest {
     @Test
     fun emptyIntSetConstructor() {
@@ -147,9 +155,9 @@
         val set = MutableIntSet()
         set += 1
         set += 2
-        var element: Int = Int.MIN_VALUE
-        var otherElement: Int = Int.MIN_VALUE
-        set.forEach { if (element == Int.MIN_VALUE) element = it else otherElement = it }
+        var element: Int = -1
+        var otherElement: Int = -1
+        set.forEach { if (element == -1) element = it else otherElement = it }
         assertEquals(element, set.first())
         set -= element
         assertEquals(otherElement, set.first())
@@ -333,8 +341,37 @@
         set += 1
         set += 5
         assertTrue(
-            "[1, 5]" == set.toString() ||
-                "[5, 1]" == set.toString()
+            "[${1}, ${5}]" == set.toString() ||
+                "[${5}, ${1}]" == set.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val set = intSetOf(1, 2, 3, 4, 5)
+        val order = IntArray(5)
+        var index = 0
+        set.forEach { element ->
+            order[index++] = element.toInt()
+        }
+        assertEquals(
+            "${order[0].toInt()}, ${order[1].toInt()}, ${order[2].toInt()}, " +
+            "${order[3].toInt()}, ${order[4].toInt()}",
+            set.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toInt()}, ${order[1].toInt()}, ${order[2].toInt()}...",
+            set.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toInt()}-${order[1].toInt()}-${order[2].toInt()}-" +
+            "${order[3].toInt()}-${order[4].toInt()}<",
+            set.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            set.joinToString(limit = 3) { names[it.toInt()] }
         )
     }
 
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt
new file mode 100644
index 0000000..da332c3
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt
@@ -0,0 +1,706 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class LongFloatMapTest {
+    @Test
+    fun longFloatMap() {
+        val map = MutableLongFloatMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyLongFloatMap() {
+        val map = emptyLongFloatMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyLongFloatMap(), map)
+    }
+
+    @Test
+    fun longFloatMapFunction() {
+        val map = mutableLongFloatMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableLongFloatMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longFloatMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableLongFloatMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longFloatMapInitFunction() {
+        val map1 = longFloatMapOf(
+            1L, 1f,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1f, map1[1L])
+
+        val map2 = longFloatMapOf(
+            1L, 1f,
+            2L, 2f,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1f, map2[1L])
+        assertEquals(2f, map2[2L])
+
+        val map3 = longFloatMapOf(
+            1L, 1f,
+            2L, 2f,
+            3L, 3f,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1f, map3[1L])
+        assertEquals(2f, map3[2L])
+        assertEquals(3f, map3[3L])
+
+        val map4 = longFloatMapOf(
+            1L, 1f,
+            2L, 2f,
+            3L, 3f,
+            4L, 4f,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1f, map4[1L])
+        assertEquals(2f, map4[2L])
+        assertEquals(3f, map4[3L])
+        assertEquals(4f, map4[4L])
+
+        val map5 = longFloatMapOf(
+            1L, 1f,
+            2L, 2f,
+            3L, 3f,
+            4L, 4f,
+            5L, 5f,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1f, map5[1L])
+        assertEquals(2f, map5[2L])
+        assertEquals(3f, map5[3L])
+        assertEquals(4f, map5[4L])
+        assertEquals(5f, map5[5L])
+    }
+
+    @Test
+    fun mutableLongFloatMapInitFunction() {
+        val map1 = mutableLongFloatMapOf(
+            1L, 1f,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1f, map1[1L])
+
+        val map2 = mutableLongFloatMapOf(
+            1L, 1f,
+            2L, 2f,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1f, map2[1L])
+        assertEquals(2f, map2[2L])
+
+        val map3 = mutableLongFloatMapOf(
+            1L, 1f,
+            2L, 2f,
+            3L, 3f,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1f, map3[1L])
+        assertEquals(2f, map3[2L])
+        assertEquals(3f, map3[3L])
+
+        val map4 = mutableLongFloatMapOf(
+            1L, 1f,
+            2L, 2f,
+            3L, 3f,
+            4L, 4f,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1f, map4[1L])
+        assertEquals(2f, map4[2L])
+        assertEquals(3f, map4[3L])
+        assertEquals(4f, map4[4L])
+
+        val map5 = mutableLongFloatMapOf(
+            1L, 1f,
+            2L, 2f,
+            3L, 3f,
+            4L, 4f,
+            5L, 5f,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1f, map5[1L])
+        assertEquals(2f, map5[2L])
+        assertEquals(3f, map5[3L])
+        assertEquals(4f, map5[4L])
+        assertEquals(5f, map5[5L])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1L])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableLongFloatMap(12)
+        map[1L] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1L])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableLongFloatMap(2)
+        map[1L] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1f, map[1L])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableLongFloatMap(0)
+        map[1L] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1L])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[1L] = 2f
+
+        assertEquals(1, map.size)
+        assertEquals(2f, map[1L])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableLongFloatMap()
+
+        map.put(1L, 1f)
+        assertEquals(1f, map[1L])
+        map.put(1L, 2f)
+        assertEquals(2f, map[1L])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertFailsWith<NoSuchElementException> {
+            map[2L]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertEquals(2f, map.getOrDefault(2L, 2f))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertEquals(3f, map.getOrElse(3L) { 3f })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        var counter = 0
+        map.getOrPut(1L) {
+            counter++
+            2f
+        }
+        assertEquals(1f, map[1L])
+        assertEquals(0, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            2f
+        }
+        assertEquals(2f, map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            3f
+        }
+        assertEquals(2f, map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(3L) {
+            counter++
+            3f
+        }
+        assertEquals(3f, map[3L])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableLongFloatMap()
+        map.remove(1L)
+
+        map[1L] = 1f
+        map.remove(1L)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableLongFloatMap(6)
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+        map[4L] = 4f
+        map[5L] = 5f
+        map[6L] = 6f
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1L)
+        map.remove(2L)
+        map.remove(3L)
+        map.remove(4L)
+        map.remove(5L)
+        map.remove(6L)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7L] = 7f
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+        map[4L] = 4f
+        map[5L] = 5f
+        map[6L] = 6f
+
+        map.removeIf { key, _ -> key == 1L || key == 3L }
+
+        assertEquals(4, map.size)
+        assertEquals(2f, map[2L])
+        assertEquals(4f, map[4L])
+        assertEquals(5f, map[5L])
+        assertEquals(6f, map[6L])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+
+        map -= 1L
+
+        assertEquals(2, map.size)
+        assertFalse(1L in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+
+        map -= longArrayOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+
+        map -= longSetOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+
+        map -= longListOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableLongFloatMap()
+        assertFalse(map.remove(1L, 1f))
+
+        map[1L] = 1f
+        assertTrue(map.remove(1L, 1f))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableLongFloatMap()
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toFloat()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableLongFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toFloat()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toLong())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableLongFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toFloat()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableLongFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toFloat()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableLongFloatMap()
+
+        for (i in 0 until 32) {
+            map[i.toLong()] = i.toFloat()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableLongFloatMap()
+        assertEquals("{}", map.toString())
+
+        map[1L] = 1f
+        map[2L] = 2f
+        val oneValueString = 1f.toString()
+        val twoValueString = 2f.toString()
+        val oneKeyString = 1L.toString()
+        val twoKeyString = 2L.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableLongFloatMap()
+        repeat(5) {
+            map[it.toLong()] = it.toFloat()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toLong()}=${order[0].toFloat()}, ${order[1].toLong()}=" +
+            "${order[1].toFloat()}, ${order[2].toLong()}=${order[2].toFloat()}," +
+            " ${order[3].toLong()}=${order[3].toFloat()}, ${order[4].toLong()}=" +
+            "${order[4].toFloat()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toLong()}=${order[0].toFloat()}, ${order[1].toLong()}=" +
+            "${order[1].toFloat()}, ${order[2].toLong()}=${order[2].toFloat()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toLong()}=${order[0].toFloat()}-${order[1].toLong()}=" +
+            "${order[1].toFloat()}-${order[2].toLong()}=${order[2].toFloat()}-" +
+            "${order[3].toLong()}=${order[3].toFloat()}-${order[4].toLong()}=" +
+            "${order[4].toFloat()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableLongFloatMap()
+        assertNotEquals(map, map2)
+
+        map2[1L] = 1f
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertTrue(map.containsKey(1L))
+        assertFalse(map.containsKey(2L))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertTrue(1L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertTrue(map.containsValue(1f))
+        assertFalse(map.containsValue(3f))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableLongFloatMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1L] = 1f
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableLongFloatMap()
+        assertEquals(0, map.count())
+
+        map[1L] = 1f
+        assertEquals(1, map.count())
+
+        map[2L] = 2f
+        map[3L] = 3f
+        map[4L] = 4f
+        map[5L] = 5f
+        map[6L] = 6f
+
+        assertEquals(2, map.count { key, _ -> key <= 2L })
+        assertEquals(0, map.count { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+        map[4L] = 4f
+        map[5L] = 5f
+        map[6L] = 6f
+
+        assertTrue(map.any { key, _ -> key == 4L })
+        assertFalse(map.any { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+        map[4L] = 4f
+        map[5L] = 5f
+        map[6L] = 6f
+
+        assertTrue(map.all { key, value -> key > 0L && value >= 1f })
+        assertFalse(map.all { key, _ -> key < 6L })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableLongFloatMap()
+        assertEquals(7, map.trim())
+
+        map[1L] = 1f
+        map[3L] = 3f
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toFloat()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toLong()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt
new file mode 100644
index 0000000..ec13454
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt
@@ -0,0 +1,706 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class LongIntMapTest {
+    @Test
+    fun longIntMap() {
+        val map = MutableLongIntMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyLongIntMap() {
+        val map = emptyLongIntMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyLongIntMap(), map)
+    }
+
+    @Test
+    fun longIntMapFunction() {
+        val map = mutableLongIntMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableLongIntMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longIntMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableLongIntMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longIntMapInitFunction() {
+        val map1 = longIntMapOf(
+            1L, 1,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1, map1[1L])
+
+        val map2 = longIntMapOf(
+            1L, 1,
+            2L, 2,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1, map2[1L])
+        assertEquals(2, map2[2L])
+
+        val map3 = longIntMapOf(
+            1L, 1,
+            2L, 2,
+            3L, 3,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1, map3[1L])
+        assertEquals(2, map3[2L])
+        assertEquals(3, map3[3L])
+
+        val map4 = longIntMapOf(
+            1L, 1,
+            2L, 2,
+            3L, 3,
+            4L, 4,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1, map4[1L])
+        assertEquals(2, map4[2L])
+        assertEquals(3, map4[3L])
+        assertEquals(4, map4[4L])
+
+        val map5 = longIntMapOf(
+            1L, 1,
+            2L, 2,
+            3L, 3,
+            4L, 4,
+            5L, 5,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1, map5[1L])
+        assertEquals(2, map5[2L])
+        assertEquals(3, map5[3L])
+        assertEquals(4, map5[4L])
+        assertEquals(5, map5[5L])
+    }
+
+    @Test
+    fun mutableLongIntMapInitFunction() {
+        val map1 = mutableLongIntMapOf(
+            1L, 1,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1, map1[1L])
+
+        val map2 = mutableLongIntMapOf(
+            1L, 1,
+            2L, 2,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1, map2[1L])
+        assertEquals(2, map2[2L])
+
+        val map3 = mutableLongIntMapOf(
+            1L, 1,
+            2L, 2,
+            3L, 3,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1, map3[1L])
+        assertEquals(2, map3[2L])
+        assertEquals(3, map3[3L])
+
+        val map4 = mutableLongIntMapOf(
+            1L, 1,
+            2L, 2,
+            3L, 3,
+            4L, 4,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1, map4[1L])
+        assertEquals(2, map4[2L])
+        assertEquals(3, map4[3L])
+        assertEquals(4, map4[4L])
+
+        val map5 = mutableLongIntMapOf(
+            1L, 1,
+            2L, 2,
+            3L, 3,
+            4L, 4,
+            5L, 5,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1, map5[1L])
+        assertEquals(2, map5[2L])
+        assertEquals(3, map5[3L])
+        assertEquals(4, map5[4L])
+        assertEquals(5, map5[5L])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1L])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableLongIntMap(12)
+        map[1L] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1L])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableLongIntMap(2)
+        map[1L] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1, map[1L])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableLongIntMap(0)
+        map[1L] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1L])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[1L] = 2
+
+        assertEquals(1, map.size)
+        assertEquals(2, map[1L])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableLongIntMap()
+
+        map.put(1L, 1)
+        assertEquals(1, map[1L])
+        map.put(1L, 2)
+        assertEquals(2, map[1L])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertFailsWith<NoSuchElementException> {
+            map[2L]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertEquals(2, map.getOrDefault(2L, 2))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertEquals(3, map.getOrElse(3L) { 3 })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        var counter = 0
+        map.getOrPut(1L) {
+            counter++
+            2
+        }
+        assertEquals(1, map[1L])
+        assertEquals(0, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            2
+        }
+        assertEquals(2, map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            3
+        }
+        assertEquals(2, map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(3L) {
+            counter++
+            3
+        }
+        assertEquals(3, map[3L])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableLongIntMap()
+        map.remove(1L)
+
+        map[1L] = 1
+        map.remove(1L)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableLongIntMap(6)
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+        map[4L] = 4
+        map[5L] = 5
+        map[6L] = 6
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1L)
+        map.remove(2L)
+        map.remove(3L)
+        map.remove(4L)
+        map.remove(5L)
+        map.remove(6L)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7L] = 7
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+        map[4L] = 4
+        map[5L] = 5
+        map[6L] = 6
+
+        map.removeIf { key, _ -> key == 1L || key == 3L }
+
+        assertEquals(4, map.size)
+        assertEquals(2, map[2L])
+        assertEquals(4, map[4L])
+        assertEquals(5, map[5L])
+        assertEquals(6, map[6L])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+
+        map -= 1L
+
+        assertEquals(2, map.size)
+        assertFalse(1L in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+
+        map -= longArrayOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+
+        map -= longSetOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+
+        map -= longListOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableLongIntMap()
+        assertFalse(map.remove(1L, 1))
+
+        map[1L] = 1
+        assertTrue(map.remove(1L, 1))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableLongIntMap()
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toInt()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableLongIntMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toInt()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toLong())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableLongIntMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toInt()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableLongIntMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toInt()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableLongIntMap()
+
+        for (i in 0 until 32) {
+            map[i.toLong()] = i.toInt()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableLongIntMap()
+        assertEquals("{}", map.toString())
+
+        map[1L] = 1
+        map[2L] = 2
+        val oneValueString = 1.toString()
+        val twoValueString = 2.toString()
+        val oneKeyString = 1L.toString()
+        val twoKeyString = 2L.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableLongIntMap()
+        repeat(5) {
+            map[it.toLong()] = it.toInt()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toLong()}=${order[0].toInt()}, ${order[1].toLong()}=" +
+            "${order[1].toInt()}, ${order[2].toLong()}=${order[2].toInt()}," +
+            " ${order[3].toLong()}=${order[3].toInt()}, ${order[4].toLong()}=" +
+            "${order[4].toInt()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toLong()}=${order[0].toInt()}, ${order[1].toLong()}=" +
+            "${order[1].toInt()}, ${order[2].toLong()}=${order[2].toInt()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toLong()}=${order[0].toInt()}-${order[1].toLong()}=" +
+            "${order[1].toInt()}-${order[2].toLong()}=${order[2].toInt()}-" +
+            "${order[3].toLong()}=${order[3].toInt()}-${order[4].toLong()}=" +
+            "${order[4].toInt()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableLongIntMap()
+        assertNotEquals(map, map2)
+
+        map2[1L] = 1
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertTrue(map.containsKey(1L))
+        assertFalse(map.containsKey(2L))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertTrue(1L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertTrue(map.containsValue(1))
+        assertFalse(map.containsValue(3))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableLongIntMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1L] = 1
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableLongIntMap()
+        assertEquals(0, map.count())
+
+        map[1L] = 1
+        assertEquals(1, map.count())
+
+        map[2L] = 2
+        map[3L] = 3
+        map[4L] = 4
+        map[5L] = 5
+        map[6L] = 6
+
+        assertEquals(2, map.count { key, _ -> key <= 2L })
+        assertEquals(0, map.count { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+        map[4L] = 4
+        map[5L] = 5
+        map[6L] = 6
+
+        assertTrue(map.any { key, _ -> key == 4L })
+        assertFalse(map.any { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+        map[4L] = 4
+        map[5L] = 5
+        map[6L] = 6
+
+        assertTrue(map.all { key, value -> key > 0L && value >= 1 })
+        assertFalse(map.all { key, _ -> key < 6L })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableLongIntMap()
+        assertEquals(7, map.trim())
+
+        map[1L] = 1
+        map[3L] = 3
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toInt()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toLong()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
index 45aa039..00178592 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
@@ -22,6 +22,14 @@
 import kotlin.test.assertNotEquals
 import kotlin.test.assertTrue
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 class LongListTest {
     private val list: MutableLongList = mutableLongListOf(1L, 2L, 3L, 4L, 5L)
 
@@ -80,11 +88,32 @@
 
     @Test
     fun string() {
-        assertEquals("[1, 2, 3, 4, 5]", list.toString())
+        assertEquals("[${1L}, ${2L}, ${3L}, ${4L}, ${5L}]", list.toString())
         assertEquals("[]", mutableLongListOf().toString())
     }
 
     @Test
+    fun joinToString() {
+        assertEquals("${1L}, ${2L}, ${3L}, ${4L}, ${5L}", list.joinToString())
+        assertEquals(
+            "x${1L}, ${2L}, ${3L}...",
+            list.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${1L}-${2L}-${3L}-${4L}-${5L}<",
+            list.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        assertEquals("one, two, three...", list.joinToString(limit = 3) {
+            when (it.toInt()) {
+                1 -> "one"
+                2 -> "two"
+                3 -> "three"
+                else -> "whoops"
+            }
+        })
+    }
+
+    @Test
     fun size() {
         assertEquals(5, list.size)
         assertEquals(5, list.count())
@@ -334,7 +363,7 @@
 
     @Test
     fun fold() {
-        assertEquals("12345", list.fold("") { acc, i -> acc + i.toString() })
+        assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
     }
 
     @Test
@@ -342,14 +371,14 @@
         assertEquals(
             "01-12-23-34-45-",
             list.foldIndexed("") { index, acc, i ->
-                "$acc$index$i-"
+                "$acc$index${i.toInt()}-"
             }
         )
     }
 
     @Test
     fun foldRight() {
-        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toString() })
+        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
     }
 
     @Test
@@ -357,7 +386,7 @@
         assertEquals(
             "45-34-23-12-01-",
             list.foldRightIndexed("") { index, i, acc ->
-                "$acc$index$i-"
+                "$acc$index${i.toInt()}-"
             }
         )
     }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt
new file mode 100644
index 0000000..571c266
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt
@@ -0,0 +1,706 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class LongLongMapTest {
+    @Test
+    fun longLongMap() {
+        val map = MutableLongLongMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyLongLongMap() {
+        val map = emptyLongLongMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyLongLongMap(), map)
+    }
+
+    @Test
+    fun longLongMapFunction() {
+        val map = mutableLongLongMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableLongLongMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longLongMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableLongLongMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longLongMapInitFunction() {
+        val map1 = longLongMapOf(
+            1L, 1L,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1L, map1[1L])
+
+        val map2 = longLongMapOf(
+            1L, 1L,
+            2L, 2L,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1L, map2[1L])
+        assertEquals(2L, map2[2L])
+
+        val map3 = longLongMapOf(
+            1L, 1L,
+            2L, 2L,
+            3L, 3L,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1L, map3[1L])
+        assertEquals(2L, map3[2L])
+        assertEquals(3L, map3[3L])
+
+        val map4 = longLongMapOf(
+            1L, 1L,
+            2L, 2L,
+            3L, 3L,
+            4L, 4L,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1L, map4[1L])
+        assertEquals(2L, map4[2L])
+        assertEquals(3L, map4[3L])
+        assertEquals(4L, map4[4L])
+
+        val map5 = longLongMapOf(
+            1L, 1L,
+            2L, 2L,
+            3L, 3L,
+            4L, 4L,
+            5L, 5L,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1L, map5[1L])
+        assertEquals(2L, map5[2L])
+        assertEquals(3L, map5[3L])
+        assertEquals(4L, map5[4L])
+        assertEquals(5L, map5[5L])
+    }
+
+    @Test
+    fun mutableLongLongMapInitFunction() {
+        val map1 = mutableLongLongMapOf(
+            1L, 1L,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1L, map1[1L])
+
+        val map2 = mutableLongLongMapOf(
+            1L, 1L,
+            2L, 2L,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1L, map2[1L])
+        assertEquals(2L, map2[2L])
+
+        val map3 = mutableLongLongMapOf(
+            1L, 1L,
+            2L, 2L,
+            3L, 3L,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1L, map3[1L])
+        assertEquals(2L, map3[2L])
+        assertEquals(3L, map3[3L])
+
+        val map4 = mutableLongLongMapOf(
+            1L, 1L,
+            2L, 2L,
+            3L, 3L,
+            4L, 4L,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1L, map4[1L])
+        assertEquals(2L, map4[2L])
+        assertEquals(3L, map4[3L])
+        assertEquals(4L, map4[4L])
+
+        val map5 = mutableLongLongMapOf(
+            1L, 1L,
+            2L, 2L,
+            3L, 3L,
+            4L, 4L,
+            5L, 5L,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1L, map5[1L])
+        assertEquals(2L, map5[2L])
+        assertEquals(3L, map5[3L])
+        assertEquals(4L, map5[4L])
+        assertEquals(5L, map5[5L])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1L])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableLongLongMap(12)
+        map[1L] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1L])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableLongLongMap(2)
+        map[1L] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1L, map[1L])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableLongLongMap(0)
+        map[1L] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1L])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[1L] = 2L
+
+        assertEquals(1, map.size)
+        assertEquals(2L, map[1L])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableLongLongMap()
+
+        map.put(1L, 1L)
+        assertEquals(1L, map[1L])
+        map.put(1L, 2L)
+        assertEquals(2L, map[1L])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertFailsWith<NoSuchElementException> {
+            map[2L]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertEquals(2L, map.getOrDefault(2L, 2L))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertEquals(3L, map.getOrElse(3L) { 3L })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        var counter = 0
+        map.getOrPut(1L) {
+            counter++
+            2L
+        }
+        assertEquals(1L, map[1L])
+        assertEquals(0, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            2L
+        }
+        assertEquals(2L, map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            3L
+        }
+        assertEquals(2L, map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(3L) {
+            counter++
+            3L
+        }
+        assertEquals(3L, map[3L])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableLongLongMap()
+        map.remove(1L)
+
+        map[1L] = 1L
+        map.remove(1L)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableLongLongMap(6)
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+        map[4L] = 4L
+        map[5L] = 5L
+        map[6L] = 6L
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1L)
+        map.remove(2L)
+        map.remove(3L)
+        map.remove(4L)
+        map.remove(5L)
+        map.remove(6L)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7L] = 7L
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+        map[4L] = 4L
+        map[5L] = 5L
+        map[6L] = 6L
+
+        map.removeIf { key, _ -> key == 1L || key == 3L }
+
+        assertEquals(4, map.size)
+        assertEquals(2L, map[2L])
+        assertEquals(4L, map[4L])
+        assertEquals(5L, map[5L])
+        assertEquals(6L, map[6L])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+
+        map -= 1L
+
+        assertEquals(2, map.size)
+        assertFalse(1L in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+
+        map -= longArrayOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+
+        map -= longSetOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+
+        map -= longListOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableLongLongMap()
+        assertFalse(map.remove(1L, 1L))
+
+        map[1L] = 1L
+        assertTrue(map.remove(1L, 1L))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableLongLongMap()
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toLong()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableLongLongMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toLong()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toLong())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableLongLongMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toLong()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableLongLongMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toLong()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableLongLongMap()
+
+        for (i in 0 until 32) {
+            map[i.toLong()] = i.toLong()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableLongLongMap()
+        assertEquals("{}", map.toString())
+
+        map[1L] = 1L
+        map[2L] = 2L
+        val oneValueString = 1L.toString()
+        val twoValueString = 2L.toString()
+        val oneKeyString = 1L.toString()
+        val twoKeyString = 2L.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableLongLongMap()
+        repeat(5) {
+            map[it.toLong()] = it.toLong()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toLong()}=${order[0].toLong()}, ${order[1].toLong()}=" +
+            "${order[1].toLong()}, ${order[2].toLong()}=${order[2].toLong()}," +
+            " ${order[3].toLong()}=${order[3].toLong()}, ${order[4].toLong()}=" +
+            "${order[4].toLong()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toLong()}=${order[0].toLong()}, ${order[1].toLong()}=" +
+            "${order[1].toLong()}, ${order[2].toLong()}=${order[2].toLong()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toLong()}=${order[0].toLong()}-${order[1].toLong()}=" +
+            "${order[1].toLong()}-${order[2].toLong()}=${order[2].toLong()}-" +
+            "${order[3].toLong()}=${order[3].toLong()}-${order[4].toLong()}=" +
+            "${order[4].toLong()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableLongLongMap()
+        assertNotEquals(map, map2)
+
+        map2[1L] = 1L
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertTrue(map.containsKey(1L))
+        assertFalse(map.containsKey(2L))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertTrue(1L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertTrue(map.containsValue(1L))
+        assertFalse(map.containsValue(3L))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableLongLongMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1L] = 1L
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableLongLongMap()
+        assertEquals(0, map.count())
+
+        map[1L] = 1L
+        assertEquals(1, map.count())
+
+        map[2L] = 2L
+        map[3L] = 3L
+        map[4L] = 4L
+        map[5L] = 5L
+        map[6L] = 6L
+
+        assertEquals(2, map.count { key, _ -> key <= 2L })
+        assertEquals(0, map.count { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+        map[4L] = 4L
+        map[5L] = 5L
+        map[6L] = 6L
+
+        assertTrue(map.any { key, _ -> key == 4L })
+        assertFalse(map.any { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+        map[4L] = 4L
+        map[5L] = 5L
+        map[6L] = 6L
+
+        assertTrue(map.all { key, value -> key > 0L && value >= 1L })
+        assertFalse(map.all { key, _ -> key < 6L })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableLongLongMap()
+        assertEquals(7, map.trim())
+
+        map[1L] = 1L
+        map[3L] = 3L
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toLong()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toLong()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt
new file mode 100644
index 0000000..67fbc5a
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt
@@ -0,0 +1,734 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class LongObjectMapTest {
+    @Test
+    fun longObjectMap() {
+        val map = MutableLongObjectMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyLongObjectMap() {
+        val map = emptyLongObjectMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyLongObjectMap<String>(), map)
+    }
+
+    @Test
+    fun longObjectMapFunction() {
+        val map = mutableLongObjectMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableLongObjectMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longObjectMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableLongObjectMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longObjectMapInitFunction() {
+        val map1 = longObjectMapOf(
+            1L, "World",
+        )
+        assertEquals(1, map1.size)
+        assertEquals("World", map1[1L])
+
+        val map2 = longObjectMapOf(
+            1L, "World",
+            2L, "Monde",
+        )
+        assertEquals(2, map2.size)
+        assertEquals("World", map2[1L])
+        assertEquals("Monde", map2[2L])
+
+        val map3 = longObjectMapOf(
+            1L, "World",
+            2L, "Monde",
+            3L, "Welt",
+        )
+        assertEquals(3, map3.size)
+        assertEquals("World", map3[1L])
+        assertEquals("Monde", map3[2L])
+        assertEquals("Welt", map3[3L])
+
+        val map4 = longObjectMapOf(
+            1L, "World",
+            2L, "Monde",
+            3L, "Welt",
+            4L, "Sekai",
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals("World", map4[1L])
+        assertEquals("Monde", map4[2L])
+        assertEquals("Welt", map4[3L])
+        assertEquals("Sekai", map4[4L])
+
+        val map5 = longObjectMapOf(
+            1L, "World",
+            2L, "Monde",
+            3L, "Welt",
+            4L, "Sekai",
+            5L, "Mondo",
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals("World", map5[1L])
+        assertEquals("Monde", map5[2L])
+        assertEquals("Welt", map5[3L])
+        assertEquals("Sekai", map5[4L])
+        assertEquals("Mondo", map5[5L])
+    }
+
+    @Test
+    fun mutableLongObjectMapInitFunction() {
+        val map1 = mutableLongObjectMapOf(
+            1L, "World",
+        )
+        assertEquals(1, map1.size)
+        assertEquals("World", map1[1L])
+
+        val map2 = mutableLongObjectMapOf(
+            1L, "World",
+            2L, "Monde",
+        )
+        assertEquals(2, map2.size)
+        assertEquals("World", map2[1L])
+        assertEquals("Monde", map2[2L])
+
+        val map3 = mutableLongObjectMapOf(
+            1L, "World",
+            2L, "Monde",
+            3L, "Welt",
+        )
+        assertEquals(3, map3.size)
+        assertEquals("World", map3[1L])
+        assertEquals("Monde", map3[2L])
+        assertEquals("Welt", map3[3L])
+
+        val map4 = mutableLongObjectMapOf(
+            1L, "World",
+            2L, "Monde",
+            3L, "Welt",
+            4L, "Sekai",
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals("World", map4[1L])
+        assertEquals("Monde", map4[2L])
+        assertEquals("Welt", map4[3L])
+        assertEquals("Sekai", map4[4L])
+
+        val map5 = mutableLongObjectMapOf(
+            1L, "World",
+            2L, "Monde",
+            3L, "Welt",
+            4L, "Sekai",
+            5L, "Mondo",
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals("World", map5[1L])
+        assertEquals("Monde", map5[2L])
+        assertEquals("Welt", map5[3L])
+        assertEquals("Sekai", map5[4L])
+        assertEquals("Mondo", map5[5L])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1L])
+    }
+
+    @Test
+    fun insertIndex0() {
+        val map = MutableLongObjectMap<String>()
+        map.put(1L, "World")
+        assertEquals("World", map[1L])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableLongObjectMap<String>(12)
+        map[1L] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1L])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableLongObjectMap<String>(2)
+        map[1L] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals("World", map[1L])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableLongObjectMap<String>(0)
+        map[1L] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1L])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[1L] = "Monde"
+
+        assertEquals(1, map.size)
+        assertEquals("Monde", map[1L])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableLongObjectMap<String?>()
+
+        assertNull(map.put(1L, "World"))
+        assertEquals("World", map.put(1L, "Monde"))
+        assertNull(map.put(2L, null))
+        assertNull(map.put(2L, "Monde"))
+    }
+
+    @Test
+    fun putAllMap() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        map.putAll(mutableLongObjectMapOf(3L, "Welt", 7L, "Mundo"))
+
+        assertEquals(4, map.size)
+        assertEquals("Welt", map[3L])
+        assertEquals("Mundo", map[7L])
+    }
+
+    @Test
+    fun plusMap() {
+        val map = MutableLongObjectMap<String>()
+        map += longObjectMapOf(3L, "Welt", 7L, "Mundo")
+
+        assertEquals(2, map.size)
+        assertEquals("Welt", map[3L])
+        assertEquals("Mundo", map[7L])
+    }
+
+    @Test
+    fun nullValue() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = null
+
+        assertEquals(1, map.size)
+        assertNull(map[1L])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+
+        assertNull(map[2L])
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+
+        assertEquals("Monde", map.getOrDefault(2L, "Monde"))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        assertEquals("Monde", map.getOrElse(2L) { "Monde" })
+        assertEquals("Welt", map.getOrElse(3L) { "Welt" })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+
+        var counter = 0
+        map.getOrPut(1L) {
+            counter++
+            "Monde"
+        }
+        assertEquals("World", map[1L])
+        assertEquals(0, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            "Monde"
+        }
+        assertEquals("Monde", map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Monde", map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(3L) {
+            counter++
+            null
+        }
+        assertNull(map[3L])
+        assertEquals(2, counter)
+
+        map.getOrPut(3L) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Welt", map[3L])
+        assertEquals(3, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableLongObjectMap<String?>()
+        assertNull(map.remove(1L))
+
+        map[1L] = "World"
+        assertEquals("World", map.remove(1L))
+        assertEquals(0, map.size)
+
+        map[1L] = null
+        assertNull(map.remove(1L))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableLongObjectMap<String>(6)
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+        map[4L] = "Sekai"
+        map[5L] = "Mondo"
+        map[6L] = "Sesang"
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1L)
+        map.remove(2L)
+        map.remove(3L)
+        map.remove(4L)
+        map.remove(5L)
+        map.remove(6L)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7L] = "Mundo"
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+        map[4L] = "Sekai"
+        map[5L] = "Mondo"
+        map[6L] = "Sesang"
+
+        map.removeIf { key, value ->
+            key == 1L || key == 3L || value.startsWith('S')
+        }
+
+        assertEquals(2, map.size)
+        assertEquals("Monde", map[2L])
+        assertEquals("Mondo", map[5L])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+
+        map -= 1L
+
+        assertEquals(2, map.size)
+        assertNull(map[1L])
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+
+        map -= longArrayOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertNull(map[3L])
+        assertNull(map[2L])
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+
+        map -= longSetOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertNull(map[3L])
+        assertNull(map[2L])
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+
+        map -= longListOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertNull(map[3L])
+        assertNull(map[2L])
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableLongObjectMap<String?>()
+        assertFalse(map.remove(1L, "World"))
+
+        map[1L] = "World"
+        assertTrue(map.remove(1L, "World"))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableLongObjectMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toString()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableLongObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key.toInt().toString(), value)
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableLongObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertEquals(key.toInt().toString(), map[key])
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableLongObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachValue { value ->
+                assertNotNull(value.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableLongObjectMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toLong()] = i.toString()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableLongObjectMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map[1L] = "World"
+        map[2L] = "Monde"
+        val oneKey = 1L.toString()
+        val twoKey = 2L.toString()
+        assertTrue(
+            "{$oneKey=World, $twoKey=Monde}" == map.toString() ||
+                "{$twoKey=Monde, $oneKey=World}" == map.toString()
+        )
+
+        map.clear()
+        map[1L] = null
+        assertEquals("{$oneKey=null}", map.toString())
+
+        val selfAsValueMap = MutableLongObjectMap<Any>()
+        selfAsValueMap[1L] = selfAsValueMap
+        assertEquals("{$oneKey=(this)}", selfAsValueMap.toString())
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableLongObjectMap<String>()
+        repeat(5) {
+            map[it.toLong()] = it.toString()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toLong()}=${order[0]}, ${order[1].toLong()}=${order[1]}, " +
+            "${order[2].toLong()}=${order[2]}, ${order[3].toLong()}=${order[3]}, " +
+            "${order[4].toLong()}=${order[4]}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toLong()}=${order[0]}, ${order[1].toLong()}=${order[1]}, " +
+            "${order[2].toLong()}=${order[2]}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toLong()}=${order[0]}-${order[1].toLong()}=${order[1]}-" +
+            "${order[2].toLong()}=${order[2]}-${order[3].toLong()}=${order[3]}-" +
+            "${order[4].toLong()}=${order[4]}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableLongObjectMap<String?>()
+        map2[2L] = null
+
+        assertNotEquals(map, map2)
+
+        map2[1L] = "World"
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        assertTrue(map.containsKey(1L))
+        assertFalse(map.containsKey(3L))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        assertTrue(1L in map)
+        assertFalse(3L in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        assertTrue(map.containsValue("World"))
+        assertTrue(map.containsValue(null))
+        assertFalse(map.containsValue("Monde"))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableLongObjectMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1L] = "World"
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableLongObjectMap<String>()
+        assertEquals(0, map.count())
+
+        map[1L] = "World"
+        assertEquals(1, map.count())
+
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+        map[4L] = "Sekai"
+        map[5L] = "Mondo"
+        map[6L] = "Sesang"
+
+        assertEquals(2, map.count { key, _ -> key < 3L })
+        assertEquals(0, map.count { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+        map[4L] = "Sekai"
+        map[5L] = "Mondo"
+        map[6L] = "Sesang"
+
+        assertTrue(map.any { key, _ -> key > 5L })
+        assertFalse(map.any { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+        map[4L] = "Sekai"
+        map[5L] = "Mondo"
+        map[6L] = "Sesang"
+
+        assertTrue(map.all { key, value -> key < 7L && value.isNotEmpty() })
+        assertFalse(map.all { key, _ -> key < 6L })
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
index 1278fcf..951539e 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
@@ -23,6 +23,14 @@
 import kotlin.test.assertSame
 import kotlin.test.assertTrue
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 class LongSetTest {
     @Test
     fun emptyLongSetConstructor() {
@@ -147,9 +155,9 @@
         val set = MutableLongSet()
         set += 1L
         set += 2L
-        var element: Long = Long.MIN_VALUE
-        var otherElement: Long = Long.MIN_VALUE
-        set.forEach { if (element == Long.MIN_VALUE) element = it else otherElement = it }
+        var element: Long = -1L
+        var otherElement: Long = -1L
+        set.forEach { if (element == -1L) element = it else otherElement = it }
         assertEquals(element, set.first())
         set -= element
         assertEquals(otherElement, set.first())
@@ -333,8 +341,37 @@
         set += 1L
         set += 5L
         assertTrue(
-            "[1, 5]" == set.toString() ||
-                "[5, 1]" == set.toString()
+            "[${1L}, ${5L}]" == set.toString() ||
+                "[${5L}, ${1L}]" == set.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val set = longSetOf(1L, 2L, 3L, 4L, 5L)
+        val order = IntArray(5)
+        var index = 0
+        set.forEach { element ->
+            order[index++] = element.toInt()
+        }
+        assertEquals(
+            "${order[0].toLong()}, ${order[1].toLong()}, ${order[2].toLong()}, " +
+            "${order[3].toLong()}, ${order[4].toLong()}",
+            set.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toLong()}, ${order[1].toLong()}, ${order[2].toLong()}...",
+            set.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toLong()}-${order[1].toLong()}-${order[2].toLong()}-" +
+            "${order[3].toLong()}-${order[4].toLong()}<",
+            set.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            set.joinToString(limit = 3) { names[it.toInt()] }
         )
     }
 
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt
new file mode 100644
index 0000000..98ff5de
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt
@@ -0,0 +1,731 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class ObjectFloatTest {
+    @Test
+    fun objectFloatMap() {
+        val map = MutableObjectFloatMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun emptyObjectFloatMap() {
+        val map = emptyObjectFloatMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyObjectFloatMap<String>(), map)
+    }
+
+    @Test
+    fun objectFloatMapFunction() {
+        val map = mutableObjectFloatMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableObjectFloatMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectFloatMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableObjectFloatMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectFloatMapInitFunction() {
+        val map1 = objectFloatMapOf(
+            "Hello", 1f,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1f, map1["Hello"])
+
+        val map2 = objectFloatMapOf(
+            "Hello", 1f,
+            "Bonjour", 2f,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1f, map2["Hello"])
+        assertEquals(2f, map2["Bonjour"])
+
+        val map3 = objectFloatMapOf(
+            "Hello", 1f,
+            "Bonjour", 2f,
+            "Hallo", 3f,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1f, map3["Hello"])
+        assertEquals(2f, map3["Bonjour"])
+        assertEquals(3f, map3["Hallo"])
+
+        val map4 = objectFloatMapOf(
+            "Hello", 1f,
+            "Bonjour", 2f,
+            "Hallo", 3f,
+            "Konnichiwa", 4f,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1f, map4["Hello"])
+        assertEquals(2f, map4["Bonjour"])
+        assertEquals(3f, map4["Hallo"])
+        assertEquals(4f, map4["Konnichiwa"])
+
+        val map5 = objectFloatMapOf(
+            "Hello", 1f,
+            "Bonjour", 2f,
+            "Hallo", 3f,
+            "Konnichiwa", 4f,
+            "Ciao", 5f,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1f, map5["Hello"])
+        assertEquals(2f, map5["Bonjour"])
+        assertEquals(3f, map5["Hallo"])
+        assertEquals(4f, map5["Konnichiwa"])
+        assertEquals(5f, map5["Ciao"])
+    }
+
+    @Test
+    fun mutableObjectFloatMapInitFunction() {
+        val map1 = mutableObjectFloatMapOf(
+            "Hello", 1f,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1f, map1["Hello"])
+
+        val map2 = mutableObjectFloatMapOf(
+            "Hello", 1f,
+            "Bonjour", 2f,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1f, map2["Hello"])
+        assertEquals(2f, map2["Bonjour"])
+
+        val map3 = mutableObjectFloatMapOf(
+            "Hello", 1f,
+            "Bonjour", 2f,
+            "Hallo", 3f,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1f, map3["Hello"])
+        assertEquals(2f, map3["Bonjour"])
+        assertEquals(3f, map3["Hallo"])
+
+        val map4 = mutableObjectFloatMapOf(
+            "Hello", 1f,
+            "Bonjour", 2f,
+            "Hallo", 3f,
+            "Konnichiwa", 4f,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1f, map4["Hello"])
+        assertEquals(2f, map4["Bonjour"])
+        assertEquals(3f, map4["Hallo"])
+        assertEquals(4f, map4["Konnichiwa"])
+
+        val map5 = mutableObjectFloatMapOf(
+            "Hello", 1f,
+            "Bonjour", 2f,
+            "Hallo", 3f,
+            "Konnichiwa", 4f,
+            "Ciao", 5f,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1f, map5["Hello"])
+        assertEquals(2f, map5["Bonjour"])
+        assertEquals(3f, map5["Hallo"])
+        assertEquals(4f, map5["Konnichiwa"])
+        assertEquals(5f, map5["Ciao"])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map["Hello"])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableObjectFloatMap<String>(12)
+        map["Hello"] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map["Hello"])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableObjectFloatMap<String>(2)
+        map["Hello"] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1f, map["Hello"])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableObjectFloatMap<String>(0)
+        map["Hello"] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map["Hello"])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Hello"] = 2f
+
+        assertEquals(1, map.size)
+        assertEquals(2f, map["Hello"])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableObjectFloatMap<String>()
+
+        map.put("Hello", 1f)
+        assertEquals(1f, map["Hello"])
+        map.put("Hello", 2f)
+        assertEquals(2f, map["Hello"])
+    }
+
+    @Test
+    fun nullKey() {
+        val map = MutableObjectFloatMap<String?>()
+        map[null] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[null])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+
+        assertFailsWith<NoSuchElementException> {
+            map["Bonjour"]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+
+        assertEquals(2f, map.getOrDefault("Bonjour", 2f))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+
+        assertEquals(3f, map.getOrElse("Hallo") { 3f })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+
+        var counter = 0
+        map.getOrPut("Hello") {
+            counter++
+            2f
+        }
+        assertEquals(1f, map["Hello"])
+        assertEquals(0, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            2f
+        }
+        assertEquals(2f, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            3f
+        }
+        assertEquals(2f, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Hallo") {
+            counter++
+            3f
+        }
+        assertEquals(3f, map["Hallo"])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableObjectFloatMap<String?>()
+        map.remove("Hello")
+
+        map["Hello"] = 1f
+        map.remove("Hello")
+        assertEquals(0, map.size)
+
+        map[null] = 1f
+        map.remove(null)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableObjectFloatMap<String>(6)
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+        map["Konnichiwa"] = 4f
+        map["Ciao"] = 5f
+        map["Annyeong"] = 6f
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove("Hello")
+        map.remove("Bonjour")
+        map.remove("Hallo")
+        map.remove("Konnichiwa")
+        map.remove("Ciao")
+        map.remove("Annyeong")
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map["Hola"] = 7f
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+        map["Konnichiwa"] = 4f
+        map["Ciao"] = 5f
+        map["Annyeong"] = 6f
+
+        map.removeIf { key, _ -> key.startsWith('H') }
+
+        assertEquals(4, map.size)
+        assertEquals(2f, map["Bonjour"])
+        assertEquals(4f, map["Konnichiwa"])
+        assertEquals(5f, map["Ciao"])
+        assertEquals(6f, map["Annyeong"])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+
+        map -= "Hello"
+
+        assertEquals(2, map.size)
+        assertFalse("Hello" in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+
+        map -= arrayOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusIterable() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+
+        map -= listOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusSequence() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+
+        map -= listOf("Hallo", "Bonjour").asSequence()
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableObjectFloatMap<String?>()
+        assertFalse(map.remove("Hello", 1f))
+
+        map["Hello"] = 1f
+        assertTrue(map.remove("Hello", 1f))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableObjectFloatMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toFloat()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableObjectFloatMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toFloat()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt().toString())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableObjectFloatMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toFloat()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertNotNull(key.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableObjectFloatMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toFloat()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableObjectFloatMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toString()] = i.toFloat()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableObjectFloatMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        val oneString = 1f.toString()
+        val twoString = 2f.toString()
+        assertTrue(
+            "{Hello=$oneString, Bonjour=$twoString}" == map.toString() ||
+                "{Bonjour=$twoString, Hello=$oneString}" == map.toString()
+        )
+
+        map.clear()
+        map[null] = 2f
+        assertEquals("{null=$twoString}", map.toString())
+
+        val selfAsKeyMap = MutableObjectFloatMap<Any>()
+        selfAsKeyMap[selfAsKeyMap] = 1f
+        assertEquals("{(this)=$oneString}", selfAsKeyMap.toString())
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableObjectFloatMap<String?>()
+        repeat(5) {
+            map[it.toString()] = it.toFloat()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { _, value ->
+            order[index++] = value.toInt()
+        }
+        assertEquals(
+            "${order[0]}=${order[0].toFloat()}, ${order[1]}=${order[1].toFloat()}, " +
+            "${order[2]}=${order[2].toFloat()}, ${order[3]}=${order[3].toFloat()}, " +
+            "${order[4]}=${order[4].toFloat()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0]}=${order[0].toFloat()}, ${order[1]}=${order[1].toFloat()}, " +
+            "${order[2]}=${order[2].toFloat()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0]}=${order[0].toFloat()}-${order[1]}=${order[1].toFloat()}-" +
+            "${order[2]}=${order[2].toFloat()}-${order[3]}=${order[3].toFloat()}-" +
+            "${order[4]}=${order[4].toFloat()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { _, value -> names[value.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableObjectFloatMap<String?>()
+        map["Hello"] = 1f
+        map[null] = 2f
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableObjectFloatMap<String?>()
+        map2[null] = 2f
+
+        assertNotEquals(map, map2)
+
+        map2["Hello"] = 1f
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableObjectFloatMap<String?>()
+        map["Hello"] = 1f
+        map[null] = 2f
+
+        assertTrue(map.containsKey("Hello"))
+        assertTrue(map.containsKey(null))
+        assertFalse(map.containsKey("Bonjour"))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableObjectFloatMap<String?>()
+        map["Hello"] = 1f
+        map[null] = 2f
+
+        assertTrue("Hello" in map)
+        assertTrue(null in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableObjectFloatMap<String?>()
+        map["Hello"] = 1f
+        map[null] = 2f
+
+        assertTrue(map.containsValue(1f))
+        assertTrue(map.containsValue(2f))
+        assertFalse(map.containsValue(3f))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableObjectFloatMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map["Hello"] = 1f
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableObjectFloatMap<String>()
+        assertEquals(0, map.count())
+
+        map["Hello"] = 1f
+        assertEquals(1, map.count())
+
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+        map["Konnichiwa"] = 4f
+        map["Ciao"] = 5f
+        map["Annyeong"] = 6f
+
+        assertEquals(2, map.count { key, _ -> key.startsWith("H") })
+        assertEquals(0, map.count { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+        map["Konnichiwa"] = 4f
+        map["Ciao"] = 5f
+        map["Annyeong"] = 6f
+
+        assertTrue(map.any { key, _ -> key.startsWith("K") })
+        assertFalse(map.any { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+        map["Konnichiwa"] = 4f
+        map["Ciao"] = 5f
+        map["Annyeong"] = 6f
+
+        assertTrue(map.all { key, value -> key.length >= 4 && value.toInt() >= 1 })
+        assertFalse(map.all { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableObjectFloatMap<String>()
+        assertEquals(7, map.trim())
+
+        map["Hello"] = 1f
+        map["Hallo"] = 3f
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toFloat()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toString()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt
new file mode 100644
index 0000000..73b00eb
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt
@@ -0,0 +1,731 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class ObjectIntTest {
+    @Test
+    fun objectIntMap() {
+        val map = MutableObjectIntMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun emptyObjectIntMap() {
+        val map = emptyObjectIntMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyObjectIntMap<String>(), map)
+    }
+
+    @Test
+    fun objectIntMapFunction() {
+        val map = mutableObjectIntMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableObjectIntMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectIntMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableObjectIntMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectIntMapInitFunction() {
+        val map1 = objectIntMapOf(
+            "Hello", 1,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1, map1["Hello"])
+
+        val map2 = objectIntMapOf(
+            "Hello", 1,
+            "Bonjour", 2,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1, map2["Hello"])
+        assertEquals(2, map2["Bonjour"])
+
+        val map3 = objectIntMapOf(
+            "Hello", 1,
+            "Bonjour", 2,
+            "Hallo", 3,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1, map3["Hello"])
+        assertEquals(2, map3["Bonjour"])
+        assertEquals(3, map3["Hallo"])
+
+        val map4 = objectIntMapOf(
+            "Hello", 1,
+            "Bonjour", 2,
+            "Hallo", 3,
+            "Konnichiwa", 4,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1, map4["Hello"])
+        assertEquals(2, map4["Bonjour"])
+        assertEquals(3, map4["Hallo"])
+        assertEquals(4, map4["Konnichiwa"])
+
+        val map5 = objectIntMapOf(
+            "Hello", 1,
+            "Bonjour", 2,
+            "Hallo", 3,
+            "Konnichiwa", 4,
+            "Ciao", 5,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1, map5["Hello"])
+        assertEquals(2, map5["Bonjour"])
+        assertEquals(3, map5["Hallo"])
+        assertEquals(4, map5["Konnichiwa"])
+        assertEquals(5, map5["Ciao"])
+    }
+
+    @Test
+    fun mutableObjectIntMapInitFunction() {
+        val map1 = mutableObjectIntMapOf(
+            "Hello", 1,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1, map1["Hello"])
+
+        val map2 = mutableObjectIntMapOf(
+            "Hello", 1,
+            "Bonjour", 2,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1, map2["Hello"])
+        assertEquals(2, map2["Bonjour"])
+
+        val map3 = mutableObjectIntMapOf(
+            "Hello", 1,
+            "Bonjour", 2,
+            "Hallo", 3,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1, map3["Hello"])
+        assertEquals(2, map3["Bonjour"])
+        assertEquals(3, map3["Hallo"])
+
+        val map4 = mutableObjectIntMapOf(
+            "Hello", 1,
+            "Bonjour", 2,
+            "Hallo", 3,
+            "Konnichiwa", 4,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1, map4["Hello"])
+        assertEquals(2, map4["Bonjour"])
+        assertEquals(3, map4["Hallo"])
+        assertEquals(4, map4["Konnichiwa"])
+
+        val map5 = mutableObjectIntMapOf(
+            "Hello", 1,
+            "Bonjour", 2,
+            "Hallo", 3,
+            "Konnichiwa", 4,
+            "Ciao", 5,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1, map5["Hello"])
+        assertEquals(2, map5["Bonjour"])
+        assertEquals(3, map5["Hallo"])
+        assertEquals(4, map5["Konnichiwa"])
+        assertEquals(5, map5["Ciao"])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map["Hello"])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableObjectIntMap<String>(12)
+        map["Hello"] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map["Hello"])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableObjectIntMap<String>(2)
+        map["Hello"] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1, map["Hello"])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableObjectIntMap<String>(0)
+        map["Hello"] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map["Hello"])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Hello"] = 2
+
+        assertEquals(1, map.size)
+        assertEquals(2, map["Hello"])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableObjectIntMap<String>()
+
+        map.put("Hello", 1)
+        assertEquals(1, map["Hello"])
+        map.put("Hello", 2)
+        assertEquals(2, map["Hello"])
+    }
+
+    @Test
+    fun nullKey() {
+        val map = MutableObjectIntMap<String?>()
+        map[null] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[null])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+
+        assertFailsWith<NoSuchElementException> {
+            map["Bonjour"]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+
+        assertEquals(2, map.getOrDefault("Bonjour", 2))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+
+        assertEquals(3, map.getOrElse("Hallo") { 3 })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+
+        var counter = 0
+        map.getOrPut("Hello") {
+            counter++
+            2
+        }
+        assertEquals(1, map["Hello"])
+        assertEquals(0, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            2
+        }
+        assertEquals(2, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            3
+        }
+        assertEquals(2, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Hallo") {
+            counter++
+            3
+        }
+        assertEquals(3, map["Hallo"])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableObjectIntMap<String?>()
+        map.remove("Hello")
+
+        map["Hello"] = 1
+        map.remove("Hello")
+        assertEquals(0, map.size)
+
+        map[null] = 1
+        map.remove(null)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableObjectIntMap<String>(6)
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+        map["Konnichiwa"] = 4
+        map["Ciao"] = 5
+        map["Annyeong"] = 6
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove("Hello")
+        map.remove("Bonjour")
+        map.remove("Hallo")
+        map.remove("Konnichiwa")
+        map.remove("Ciao")
+        map.remove("Annyeong")
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map["Hola"] = 7
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+        map["Konnichiwa"] = 4
+        map["Ciao"] = 5
+        map["Annyeong"] = 6
+
+        map.removeIf { key, _ -> key.startsWith('H') }
+
+        assertEquals(4, map.size)
+        assertEquals(2, map["Bonjour"])
+        assertEquals(4, map["Konnichiwa"])
+        assertEquals(5, map["Ciao"])
+        assertEquals(6, map["Annyeong"])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+
+        map -= "Hello"
+
+        assertEquals(2, map.size)
+        assertFalse("Hello" in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+
+        map -= arrayOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusIterable() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+
+        map -= listOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusSequence() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+
+        map -= listOf("Hallo", "Bonjour").asSequence()
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableObjectIntMap<String?>()
+        assertFalse(map.remove("Hello", 1))
+
+        map["Hello"] = 1
+        assertTrue(map.remove("Hello", 1))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableObjectIntMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toInt()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableObjectIntMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toInt()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt().toString())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableObjectIntMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toInt()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertNotNull(key.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableObjectIntMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toInt()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableObjectIntMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toString()] = i.toInt()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableObjectIntMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        val oneString = 1.toString()
+        val twoString = 2.toString()
+        assertTrue(
+            "{Hello=$oneString, Bonjour=$twoString}" == map.toString() ||
+                "{Bonjour=$twoString, Hello=$oneString}" == map.toString()
+        )
+
+        map.clear()
+        map[null] = 2
+        assertEquals("{null=$twoString}", map.toString())
+
+        val selfAsKeyMap = MutableObjectIntMap<Any>()
+        selfAsKeyMap[selfAsKeyMap] = 1
+        assertEquals("{(this)=$oneString}", selfAsKeyMap.toString())
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableObjectIntMap<String?>()
+        repeat(5) {
+            map[it.toString()] = it.toInt()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { _, value ->
+            order[index++] = value.toInt()
+        }
+        assertEquals(
+            "${order[0]}=${order[0].toInt()}, ${order[1]}=${order[1].toInt()}, " +
+            "${order[2]}=${order[2].toInt()}, ${order[3]}=${order[3].toInt()}, " +
+            "${order[4]}=${order[4].toInt()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0]}=${order[0].toInt()}, ${order[1]}=${order[1].toInt()}, " +
+            "${order[2]}=${order[2].toInt()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0]}=${order[0].toInt()}-${order[1]}=${order[1].toInt()}-" +
+            "${order[2]}=${order[2].toInt()}-${order[3]}=${order[3].toInt()}-" +
+            "${order[4]}=${order[4].toInt()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { _, value -> names[value.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableObjectIntMap<String?>()
+        map["Hello"] = 1
+        map[null] = 2
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableObjectIntMap<String?>()
+        map2[null] = 2
+
+        assertNotEquals(map, map2)
+
+        map2["Hello"] = 1
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableObjectIntMap<String?>()
+        map["Hello"] = 1
+        map[null] = 2
+
+        assertTrue(map.containsKey("Hello"))
+        assertTrue(map.containsKey(null))
+        assertFalse(map.containsKey("Bonjour"))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableObjectIntMap<String?>()
+        map["Hello"] = 1
+        map[null] = 2
+
+        assertTrue("Hello" in map)
+        assertTrue(null in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableObjectIntMap<String?>()
+        map["Hello"] = 1
+        map[null] = 2
+
+        assertTrue(map.containsValue(1))
+        assertTrue(map.containsValue(2))
+        assertFalse(map.containsValue(3))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableObjectIntMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map["Hello"] = 1
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableObjectIntMap<String>()
+        assertEquals(0, map.count())
+
+        map["Hello"] = 1
+        assertEquals(1, map.count())
+
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+        map["Konnichiwa"] = 4
+        map["Ciao"] = 5
+        map["Annyeong"] = 6
+
+        assertEquals(2, map.count { key, _ -> key.startsWith("H") })
+        assertEquals(0, map.count { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+        map["Konnichiwa"] = 4
+        map["Ciao"] = 5
+        map["Annyeong"] = 6
+
+        assertTrue(map.any { key, _ -> key.startsWith("K") })
+        assertFalse(map.any { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+        map["Konnichiwa"] = 4
+        map["Ciao"] = 5
+        map["Annyeong"] = 6
+
+        assertTrue(map.all { key, value -> key.length >= 4 && value.toInt() >= 1 })
+        assertFalse(map.all { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableObjectIntMap<String>()
+        assertEquals(7, map.trim())
+
+        map["Hello"] = 1
+        map["Hallo"] = 3
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toInt()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toString()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectListTest.kt
new file mode 100644
index 0000000..37752f6
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectListTest.kt
@@ -0,0 +1,1336 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class ObjectListTest {
+    private val list: MutableObjectList<Int> = mutableObjectListOf(1, 2, 3, 4, 5)
+
+    @Test
+    fun emptyConstruction() {
+        val l = mutableObjectListOf<Int>()
+        assertEquals(0, l.size)
+        assertEquals(16, l.capacity)
+    }
+
+    @Test
+    fun sizeConstruction() {
+        val l = MutableObjectList<Int>(4)
+        assertEquals(4, l.capacity)
+    }
+
+    @Test
+    fun contentConstruction() {
+        val l = mutableObjectListOf(1, 2, 3)
+        assertEquals(3, l.size)
+        assertEquals(1, l[0])
+        assertEquals(2, l[1])
+        assertEquals(3, l[2])
+        assertEquals(3, l.capacity)
+        repeat(2) {
+            val l2 = mutableObjectListOf(1, 2, 3, 4, 5)
+            assertEquals(list, l2)
+            l2.removeAt(0)
+        }
+    }
+
+    @Test
+    fun hashCodeTest() {
+        val l2 = mutableObjectListOf(1, 2, 3, 4, 5)
+        assertEquals(list.hashCode(), l2.hashCode())
+        l2.removeAt(4)
+        assertNotEquals(list.hashCode(), l2.hashCode())
+        l2.add(5)
+        assertEquals(list.hashCode(), l2.hashCode())
+        l2.clear()
+        assertNotEquals(list.hashCode(), l2.hashCode())
+    }
+
+    @Test
+    fun equalsTest() {
+        val l2 = mutableObjectListOf(1, 2, 3, 4, 5)
+        assertEquals(list, l2)
+        assertNotEquals(list, mutableObjectListOf())
+        l2.removeAt(4)
+        assertNotEquals(list, l2)
+        l2.add(5)
+        assertEquals(list, l2)
+        l2.clear()
+        assertNotEquals(list, l2)
+    }
+
+    @Test
+    fun string() {
+        assertEquals("[1, 2, 3, 4, 5]", list.toString())
+        assertEquals("[]", mutableObjectListOf<Int>().toString())
+        val weirdList = MutableObjectList<Any>()
+        weirdList.add(weirdList)
+        assertEquals("[(this)]", weirdList.toString())
+    }
+
+    @Test
+    fun joinToString() {
+        assertEquals("1, 2, 3, 4, 5", list.joinToString())
+        assertEquals(
+            "x1, 2, 3...",
+            list.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">1-2-3-4-5<",
+            list.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        assertEquals("one, two, three...", list.joinToString(limit = 3) {
+            when (it) {
+                1 -> "one"
+                2 -> "two"
+                3 -> "three"
+                else -> "whoops"
+            }
+        })
+    }
+
+    @Test
+    fun size() {
+        assertEquals(5, list.size)
+        assertEquals(5, list.count())
+        val l2 = mutableObjectListOf<Int>()
+        assertEquals(0, l2.size)
+        assertEquals(0, l2.count())
+        l2 += 1
+        assertEquals(1, l2.size)
+        assertEquals(1, l2.count())
+    }
+
+    @Test
+    fun get() {
+        assertEquals(1, list[0])
+        assertEquals(5, list[4])
+        assertEquals(1, list.elementAt(0))
+        assertEquals(5, list.elementAt(4))
+    }
+
+    @Test
+    fun getOutOfBounds() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list[5]
+        }
+    }
+
+    @Test
+    fun getOutOfBoundsNegative() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list[-1]
+        }
+    }
+
+    @Test
+    fun elementAtOfBounds() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list.elementAt(5)
+        }
+    }
+
+    @Test
+    fun elementAtOfBoundsNegative() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list.elementAt(-1)
+        }
+    }
+
+    @Test
+    fun elementAtOrElse() {
+        assertEquals(1, list.elementAtOrElse(0) {
+            assertEquals(0, it)
+            0
+        })
+        assertEquals(0, list.elementAtOrElse(-1) {
+            assertEquals(-1, it)
+            0
+        })
+        assertEquals(0, list.elementAtOrElse(5) {
+            assertEquals(5, it)
+            0
+        })
+    }
+
+    @Test
+    fun count() {
+        assertEquals(1, list.count { it < 2 })
+        assertEquals(0, list.count { it < 0 })
+        assertEquals(5, list.count { it < 10 })
+    }
+
+    @Test
+    fun isEmpty() {
+        assertFalse(list.isEmpty())
+        assertFalse(list.none())
+        assertTrue(mutableObjectListOf<Int>().isEmpty())
+        assertTrue(mutableObjectListOf<Int>().none())
+    }
+
+    @Test
+    fun isNotEmpty() {
+        assertTrue(list.isNotEmpty())
+        assertTrue(list.any())
+        assertFalse(mutableObjectListOf<Int>().isNotEmpty())
+    }
+
+    @Test
+    fun indices() {
+        assertEquals(IntRange(0, 4), list.indices)
+        assertEquals(IntRange(0, -1), mutableObjectListOf<Int>().indices)
+    }
+
+    @Test
+    fun any() {
+        assertTrue(list.any { it == 5 })
+        assertTrue(list.any { it == 1 })
+        assertFalse(list.any { it == 0 })
+    }
+
+    @Test
+    fun reversedAny() {
+        val reversedList = mutableObjectListOf<Int>()
+        assertFalse(
+            list.reversedAny {
+                reversedList.add(it)
+                false
+            }
+        )
+        val reversedContent = mutableObjectListOf(5, 4, 3, 2, 1)
+        assertEquals(reversedContent, reversedList)
+
+        val reversedSublist = mutableObjectListOf<Int>()
+        assertTrue(
+            list.reversedAny {
+                reversedSublist.add(it)
+                reversedSublist.size == 2
+            }
+        )
+        assertEquals(reversedSublist, mutableObjectListOf(5, 4))
+    }
+
+    @Test
+    fun forEach() {
+        val copy = mutableObjectListOf<Int>()
+        list.forEach { copy += it }
+        assertEquals(list, copy)
+    }
+
+    @Test
+    fun forEachReversed() {
+        val copy = mutableObjectListOf<Int>()
+        list.forEachReversed { copy += it }
+        assertEquals(copy, mutableObjectListOf(5, 4, 3, 2, 1))
+    }
+
+    @Test
+    fun forEachIndexed() {
+        val copy = mutableObjectListOf<Int>()
+        val indices = mutableObjectListOf<Int>()
+        list.forEachIndexed { index, open ->
+            copy += open
+            indices += index
+        }
+        assertEquals(list, copy)
+        assertEquals(indices, mutableObjectListOf(0, 1, 2, 3, 4))
+    }
+
+    @Test
+    fun forEachReversedIndexed() {
+        val copy = mutableObjectListOf<Int>()
+        val indices = mutableObjectListOf<Int>()
+        list.forEachReversedIndexed { index, open ->
+            copy += open
+            indices += index
+        }
+        assertEquals(copy, mutableObjectListOf(5, 4, 3, 2, 1))
+        assertEquals(indices, mutableObjectListOf(4, 3, 2, 1, 0))
+    }
+
+    @Test
+    fun indexOfFirst() {
+        assertEquals(0, list.indexOfFirst { it < 2 })
+        assertEquals(4, list.indexOfFirst { it > 4 })
+        assertEquals(-1, list.indexOfFirst { it < 0 })
+        assertEquals(0, mutableObjectListOf(8, 8).indexOfFirst { it > 7 })
+    }
+
+    @Test
+    fun firstOrNullNoParam() {
+        assertEquals(1, list.firstOrNull())
+        assertNull(emptyObjectList<Int>().firstOrNull())
+    }
+
+    @Test
+    fun firstOrNull() {
+        assertEquals(1, list.firstOrNull { it < 5 })
+        assertEquals(3, list.firstOrNull { it > 2 })
+        assertEquals(5, list.firstOrNull { it > 4 })
+        assertNull(list.firstOrNull { it > 5 })
+    }
+
+    @Test
+    fun lastOrNullNoParam() {
+        assertEquals(5, list.lastOrNull())
+        assertNull(emptyObjectList<Int>().lastOrNull())
+    }
+
+    @Test
+    fun lastOrNull() {
+        assertEquals(4, list.lastOrNull { it < 5 })
+        assertEquals(5, list.lastOrNull { it > 2 })
+        assertEquals(1, list.lastOrNull { it < 2 })
+        assertNull(list.firstOrNull { it > 5 })
+    }
+
+    @Test
+    fun indexOfLast() {
+        assertEquals(0, list.indexOfLast { it < 2 })
+        assertEquals(4, list.indexOfLast { it > 4 })
+        assertEquals(-1, list.indexOfLast { it < 0 })
+        assertEquals(1, objectListOf(8, 8).indexOfLast { it > 7 })
+    }
+
+    @Test
+    fun contains() {
+        assertTrue(list.contains(5))
+        assertTrue(list.contains(1))
+        assertFalse(list.contains(0))
+    }
+
+    @Test
+    fun containsAllList() {
+        assertTrue(list.containsAll(mutableObjectListOf(2, 3, 1)))
+        assertFalse(list.containsAll(mutableObjectListOf(2, 3, 6)))
+    }
+
+    @Test
+    fun lastIndexOf() {
+        assertEquals(4, list.lastIndexOf(5))
+        assertEquals(1, list.lastIndexOf(2))
+        val copy = mutableObjectListOf<Int>()
+        copy.addAll(list)
+        copy.addAll(list)
+        assertEquals(5, copy.lastIndexOf(1))
+    }
+
+    @Test
+    fun first() {
+        assertEquals(1, list.first())
+    }
+
+    @Test
+    fun firstException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableObjectListOf<Int>().first()
+        }
+    }
+
+    @Test
+    fun firstWithPredicate() {
+        assertEquals(5, list.first { it > 4 })
+        assertEquals(1, mutableObjectListOf(1, 5).first { it > 0 })
+    }
+
+    @Test
+    fun firstWithPredicateException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableObjectListOf<Int>().first { it > 8 }
+        }
+    }
+
+    @Test
+    fun last() {
+        assertEquals(5, list.last())
+    }
+
+    @Test
+    fun lastException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableObjectListOf<Int>().last()
+        }
+    }
+
+    @Test
+    fun lastWithPredicate() {
+        assertEquals(1, list.last { it < 2 })
+        assertEquals(5, objectListOf(1, 5).last { it > 0 })
+    }
+
+    @Test
+    fun lastWithPredicateException() {
+        assertFailsWith<NoSuchElementException> {
+            objectListOf(2).last { it > 2 }
+        }
+    }
+
+    @Test
+    fun fold() {
+        assertEquals("12345", list.fold("") { acc, i -> acc + i.toString() })
+    }
+
+    @Test
+    fun foldIndexed() {
+        assertEquals(
+            "01-12-23-34-45-",
+            list.foldIndexed("") { index, acc, i ->
+                "$acc$index$i-"
+            }
+        )
+    }
+
+    @Test
+    fun foldRight() {
+        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toString() })
+    }
+
+    @Test
+    fun foldRightIndexed() {
+        assertEquals(
+            "45-34-23-12-01-",
+            list.foldRightIndexed("") { index, i, acc ->
+                "$acc$index$i-"
+            }
+        )
+    }
+
+    @Test
+    fun add() {
+        val l = mutableObjectListOf(1, 2, 3)
+        l += 4
+        l.add(5)
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun addAtIndex() {
+        val l = mutableObjectListOf(2, 4)
+        l.add(2, 5)
+        l.add(0, 1)
+        l.add(2, 3)
+        assertEquals(list, l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.add(-1, 2)
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.add(6, 2)
+        }
+    }
+
+    @Test
+    fun addAllListAtIndex() {
+        val l = mutableObjectListOf(4)
+        val l2 = mutableObjectListOf(1, 2)
+        val l3 = mutableObjectListOf(5)
+        val l4 = mutableObjectListOf(3)
+        assertTrue(l4.addAll(1, l3))
+        assertTrue(l4.addAll(0, l2))
+        assertTrue(l4.addAll(3, l))
+        assertFalse(l4.addAll(0, mutableObjectListOf()))
+        assertEquals(list, l4)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l4.addAll(6, mutableObjectListOf())
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l4.addAll(-1, mutableObjectListOf())
+        }
+    }
+
+    @Test
+    fun addAllObjectList() {
+        val l = MutableObjectList<Int>()
+        l.add(3)
+        l.add(4)
+        l.add(5)
+        val l2 = mutableObjectListOf(1, 2)
+        assertTrue(l2.addAll(l))
+        assertEquals(list, l2)
+        assertFalse(l2.addAll(mutableObjectListOf()))
+    }
+
+    @Test
+    fun addAllList() {
+        val l = listOf(3, 4, 5)
+        val l2 = mutableObjectListOf(1, 2)
+        assertTrue(l2.addAll(l))
+        assertEquals(list, l2)
+        assertFalse(l2.addAll(mutableObjectListOf()))
+    }
+
+    @Test
+    fun addAllIterable() {
+        val l = listOf(3, 4, 5) as Iterable<Int>
+        val l2 = mutableObjectListOf(1, 2)
+        assertTrue(l2.addAll(l))
+        assertEquals(list, l2)
+        assertFalse(l2.addAll(mutableObjectListOf()))
+    }
+
+    @Test
+    fun addAllSequence() {
+        val l = listOf(3, 4, 5).asSequence()
+        val l2 = mutableObjectListOf(1, 2)
+        assertTrue(l2.addAll(l))
+        assertEquals(list, l2)
+        assertFalse(l2.addAll(mutableObjectListOf()))
+    }
+
+    @Test
+    fun plusAssignObjectList() {
+        val l = objectListOf(3, 4, 5)
+        val l2 = mutableObjectListOf(1, 2)
+        l2 += l
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun plusAssignIterable() {
+        val l = listOf(3, 4, 5) as Iterable<Int>
+        val l2 = mutableObjectListOf(1, 2)
+        l2 += l
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun plusAssignSequence() {
+        val l = arrayOf(3, 4, 5).asSequence()
+        val l2 = mutableObjectListOf(1, 2)
+        l2 += l
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun addAllArrayAtIndex() {
+        val a1 = arrayOf(4)
+        val a2 = arrayOf(1, 2)
+        val a3 = arrayOf(5)
+        val l = mutableObjectListOf(3)
+        assertTrue(l.addAll(1, a3))
+        assertTrue(l.addAll(0, a2))
+        assertTrue(l.addAll(3, a1))
+        assertFalse(l.addAll(0, arrayOf()))
+        assertEquals(list, l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.addAll(6, arrayOf())
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.addAll(-1, arrayOf())
+        }
+    }
+
+    @Test
+    fun addAllArray() {
+        val a = arrayOf(3, 4, 5)
+        val v = mutableObjectListOf(1, 2)
+        v.addAll(a)
+        assertEquals(5, v.size)
+        assertEquals(3, v[2])
+        assertEquals(4, v[3])
+        assertEquals(5, v[4])
+    }
+
+    @Test
+    fun plusAssignArray() {
+        val a = arrayOf(3, 4, 5)
+        val v = mutableObjectListOf(1, 2)
+        v += a
+        assertEquals(list, v)
+    }
+
+    @Test
+    fun clear() {
+        val l = mutableObjectListOf<Int>()
+        l.addAll(list)
+        assertTrue(l.isNotEmpty())
+        l.clear()
+        assertTrue(l.isEmpty())
+        repeat(5) { index ->
+            assertNull(l.content[index])
+        }
+    }
+
+    @Test
+    fun trim() {
+        val l = mutableObjectListOf(1)
+        l.trim()
+        assertEquals(1, l.capacity)
+        l += arrayOf(1, 2, 3, 4, 5)
+        l.trim()
+        assertEquals(6, l.capacity)
+        assertEquals(6, l.size)
+        l.clear()
+        l.trim()
+        assertEquals(0, l.capacity)
+        l.trim(100)
+        assertEquals(0, l.capacity)
+        l += arrayOf(1, 2, 3, 4, 5)
+        l -= 5
+        l.trim(5)
+        assertEquals(5, l.capacity)
+        l.trim(4)
+        assertEquals(4, l.capacity)
+        l.trim(3)
+        assertEquals(4, l.capacity)
+    }
+
+    @Test
+    fun remove() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        l.remove(3)
+        assertEquals(mutableObjectListOf(1, 2, 4, 5), l)
+    }
+
+    @Test
+    fun removeIf() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5, 6)
+        l.removeIf { it == 100 }
+        assertEquals(objectListOf(1, 2, 3, 4, 5, 6), l)
+        l.removeIf { it % 2 == 0 }
+        assertEquals(objectListOf(1, 3, 5), l)
+        repeat(3) {
+            assertNull(l.content[3 + it])
+        }
+        l.removeIf { it != 3 }
+        assertEquals(objectListOf(3), l)
+    }
+
+    @Test
+    fun removeAt() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        l.removeAt(2)
+        assertNull(l.content[4])
+        assertEquals(mutableObjectListOf(1, 2, 4, 5), l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.removeAt(6)
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.removeAt(-1)
+        }
+    }
+
+    @Test
+    fun set() {
+        val l = mutableObjectListOf(0, 0, 0, 0, 0)
+        l[0] = 1
+        l[4] = 5
+        l[2] = 3
+        l[1] = 2
+        l[3] = 4
+        assertEquals(list, l)
+        assertFailsWith<IndexOutOfBoundsException> {
+            l[-1] = 1
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l[6] = 1
+        }
+        assertEquals(4, l.set(3, 1));
+    }
+
+    @Test
+    fun ensureCapacity() {
+        val l = mutableObjectListOf(1)
+        assertEquals(1, l.capacity)
+        l.ensureCapacity(5)
+        assertEquals(5, l.capacity)
+    }
+
+    @Test
+    fun removeAllObjectList() {
+        assertFalse(list.removeAll(mutableObjectListOf(0, 10, 15)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        assertTrue(l.removeAll(mutableObjectListOf(20, 0, 15, 10, 5)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllScatterSet() {
+        assertFalse(list.removeAll(scatterSetOf(0, 10, 15)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        assertTrue(l.removeAll(scatterSetOf(20, 0, 15, 10, 5)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllArray() {
+        assertFalse(list.removeAll(arrayOf(0, 10, 15)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        assertTrue(l.removeAll(arrayOf(20, 0, 15, 10, 5)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllList() {
+        assertFalse(list.removeAll(listOf(0, 10, 15)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        assertTrue(l.removeAll(listOf(20, 0, 15, 10, 5)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllIterable() {
+        assertFalse(list.removeAll(listOf(0, 10, 15) as Iterable<Int>))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        assertTrue(l.removeAll(listOf(20, 0, 15, 10, 5) as Iterable<Int>))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllSequence() {
+        assertFalse(list.removeAll(listOf(0, 10, 15).asSequence()))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        assertTrue(l.removeAll(listOf(20, 0, 15, 10, 5).asSequence()))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun minusAssignObjectList() {
+        val l = mutableObjectListOf<Int>().also { it += list }
+        l -= mutableObjectListOf(0, 10, 15)
+        assertEquals(list, l)
+        val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        l2 -= mutableObjectListOf(20, 0, 15, 10, 5)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignScatterSet() {
+        val l = mutableObjectListOf<Int>().also { it += list }
+        l -= scatterSetOf(0, 10, 15)
+        assertEquals(list, l)
+        val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        l2 -= scatterSetOf(20, 0, 15, 10, 5)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignArray() {
+        val l = mutableObjectListOf<Int>().also { it += list }
+        l -= arrayOf(0, 10, 15)
+        assertEquals(list, l)
+        val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        l2 -= arrayOf(20, 0, 15, 10, 5)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignList() {
+        val l = mutableObjectListOf<Int>().also { it += list }
+        l -= listOf(0, 10, 15)
+        assertEquals(list, l)
+        val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        l2 -= listOf(20, 0, 15, 10, 5)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignIterable() {
+        val l = mutableObjectListOf<Int>().also { it += list }
+        l -= listOf(0, 10, 15) as Iterable<Int>
+        assertEquals(list, l)
+        val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        l2 -= listOf(20, 0, 15, 10, 5) as Iterable<Int>
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignSequence() {
+        val l = mutableObjectListOf<Int>().also { it += list }
+        l -= listOf(0, 10, 15).asSequence()
+        assertEquals(list, l)
+        val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        l2 -= listOf(20, 0, 15, 10, 5).asSequence()
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun retainAll() {
+        assertFalse(list.retainAll(mutableObjectListOf(1, 2, 3, 4, 5, 6)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+        assertTrue(l.retainAll(mutableObjectListOf(1, 2, 3, 4, 5, 6)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun retainAllArray() {
+        assertFalse(list.retainAll(arrayOf(1, 2, 3, 4, 5, 6)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+        assertTrue(l.retainAll(arrayOf(1, 2, 3, 4, 5, 6)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun retainAllCollection() {
+        assertFalse(list.retainAll(listOf(1, 2, 3, 4, 5, 6)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+        assertTrue(l.retainAll(listOf(1, 2, 3, 4, 5, 6)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun retainAllIterable() {
+        assertFalse(list.retainAll(listOf(1, 2, 3, 4, 5, 6) as Iterable<Int>))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+        assertTrue(l.retainAll(listOf(1, 2, 3, 4, 5, 6) as Iterable<Int>))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun retainAllSequence() {
+        assertFalse(list.retainAll(arrayOf(1, 2, 3, 4, 5, 6).asSequence()))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+        assertTrue(l.retainAll(arrayOf(1, 2, 3, 4, 5, 6).asSequence()))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeRange() {
+        val l = mutableObjectListOf(1, 9, 7, 6, 2, 3, 4, 5)
+        l.removeRange(1, 4)
+        assertNull(l.content[5])
+        assertNull(l.content[6])
+        assertNull(l.content[7])
+        assertEquals(list, l)
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(6, 6)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(100, 200)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(-1, 0)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            l.removeRange(3, 2)
+        }
+    }
+
+    @Test
+    fun testEmptyObjectList() {
+        val l = emptyObjectList<Int>()
+        assertEquals(0, l.size)
+    }
+
+    @Test
+    fun objectListOfEmpty() {
+        val l = objectListOf<Int>()
+        assertEquals(0, l.size)
+    }
+
+    @Test
+    fun objectListOfOneValue() {
+        val l = objectListOf(2)
+        assertEquals(1, l.size)
+        assertEquals(2, l[0])
+    }
+
+    @Test
+    fun objectListOfTwoValues() {
+        val l = objectListOf(2, 1)
+        assertEquals(2, l.size)
+        assertEquals(2, l[0])
+        assertEquals(1, l[1])
+    }
+
+    @Test
+    fun objectListOfThreeValues() {
+        val l = objectListOf(2, 10, -1)
+        assertEquals(3, l.size)
+        assertEquals(2, l[0])
+        assertEquals(10, l[1])
+        assertEquals(-1, l[2])
+    }
+
+    @Test
+    fun objectListOfFourValues() {
+        val l = objectListOf(2, 10, -1, 10)
+        assertEquals(4, l.size)
+        assertEquals(2, l[0])
+        assertEquals(10, l[1])
+        assertEquals(-1, l[2])
+        assertEquals(10, l[3])
+    }
+
+    @Test
+    fun mutableObjectListOfOneValue() {
+        val l = mutableObjectListOf(2)
+        assertEquals(1, l.size)
+        assertEquals(1, l.capacity)
+        assertEquals(2, l[0])
+    }
+
+    @Test
+    fun mutableObjectListOfTwoValues() {
+        val l = mutableObjectListOf(2, 1)
+        assertEquals(2, l.size)
+        assertEquals(2, l.capacity)
+        assertEquals(2, l[0])
+        assertEquals(1, l[1])
+    }
+
+    @Test
+    fun mutableObjectListOfThreeValues() {
+        val l = mutableObjectListOf(2, 10, -1)
+        assertEquals(3, l.size)
+        assertEquals(3, l.capacity)
+        assertEquals(2, l[0])
+        assertEquals(10, l[1])
+        assertEquals(-1, l[2])
+    }
+
+    @Test
+    fun mutableObjectListOfFourValues() {
+        val l = mutableObjectListOf(2, 10, -1, 10)
+        assertEquals(4, l.size)
+        assertEquals(4, l.capacity)
+        assertEquals(2, l[0])
+        assertEquals(10, l[1])
+        assertEquals(-1, l[2])
+        assertEquals(10, l[3])
+    }
+
+    @Test
+    fun iterator() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val iterator = l.asMutableList().iterator()
+        assertTrue(iterator.hasNext())
+        assertEquals(1, iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals(2, iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals(3, iterator.next())
+        assertTrue(iterator.hasNext())
+        iterator.remove()
+        assertTrue(iterator.hasNext())
+        assertEquals(l, mutableObjectListOf(1, 2, 4, 5))
+
+        assertEquals(4, iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals(5, iterator.next())
+        assertFalse(iterator.hasNext())
+        iterator.remove()
+        assertEquals(l, mutableObjectListOf(1, 2, 4))
+    }
+
+    @Test
+    fun listIterator() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val iterator = l.asMutableList().listIterator()
+        assertEquals(1, iterator.next())
+        assertEquals(1, iterator.previous())
+        assertEquals(0, iterator.nextIndex())
+        iterator.add(6)
+        assertEquals(1, iterator.nextIndex())
+        assertEquals(0, iterator.previousIndex())
+        assertEquals(6, iterator.previous())
+        assertEquals(l, mutableObjectListOf(6, 1, 2, 3, 4, 5))
+    }
+
+    @Test
+    fun listIteratorInitialIndex() {
+        val iterator = list.asMutableList().listIterator(2)
+        assertEquals(2, iterator.nextIndex())
+    }
+
+    @Test
+    fun subList() {
+        val l = list.asMutableList().subList(1, 4)
+        assertEquals(3, l.size)
+        assertEquals(2, l[0])
+        assertEquals(3, l[1])
+        assertEquals(4, l[2])
+    }
+
+    @Test
+    fun subListContains() {
+        val l = list.asMutableList().subList(1, 4)
+        assertTrue(l.contains(2))
+        assertTrue(l.contains(3))
+        assertTrue(l.contains(4))
+        assertFalse(l.contains(5))
+        assertFalse(l.contains(1))
+    }
+
+    @Test
+    fun subListContainsAll() {
+        val l = list.asMutableList().subList(1, 4)
+        val smallList = listOf(2, 3, 4)
+        assertTrue(l.containsAll(smallList))
+        val largeList = listOf(3, 4, 5)
+        assertFalse(l.containsAll(largeList))
+    }
+
+    @Test
+    fun subListIndexOf() {
+        val l = list.asMutableList().subList(1, 4)
+        assertEquals(0, l.indexOf(2))
+        assertEquals(2, l.indexOf(4))
+        assertEquals(-1, l.indexOf(1))
+        val l2 = mutableObjectListOf(2, 1, 1, 3).asMutableList().subList(1, 2)
+        assertEquals(0, l2.indexOf(1))
+    }
+
+    @Test
+    fun subListIsEmpty() {
+        val l = list.asMutableList().subList(1, 4)
+        assertFalse(l.isEmpty())
+        assertTrue(list.asMutableList().subList(4, 4).isEmpty())
+    }
+
+    @Test
+    fun subListIterator() {
+        val l = list.asMutableList().subList(1, 4)
+        val l2 = mutableListOf<Int>()
+        l.forEach { l2 += it }
+        assertEquals(3, l2.size)
+        assertEquals(2, l2[0])
+        assertEquals(3, l2[1])
+        assertEquals(4, l2[2])
+    }
+
+    @Test
+    fun subListLastIndexOf() {
+        val l = list.asMutableList().subList(1, 4)
+        assertEquals(0, l.lastIndexOf(2))
+        assertEquals(2, l.lastIndexOf(4))
+        assertEquals(-1, l.lastIndexOf(1))
+        val l2 = mutableObjectListOf(2, 1, 1, 3).asMutableList().subList(1, 3)
+        assertEquals(1, l2.lastIndexOf(1))
+    }
+
+    @Test
+    fun subListAdd() {
+        val v = mutableObjectListOf(1, 2, 3)
+        val l = v.asMutableList().subList(1, 2)
+        assertTrue(l.add(4))
+        assertEquals(2, l.size)
+        assertEquals(4, v.size)
+        assertEquals(2, l[0])
+        assertEquals(4, l[1])
+        assertEquals(2, v[1])
+        assertEquals(4, v[2])
+        assertEquals(3, v[3])
+    }
+
+    @Test
+    fun subListAddIndex() {
+        val v = mutableObjectListOf(6, 1, 2, 3)
+        val l = v.asMutableList().subList(1, 3)
+        l.add(1, 4)
+        assertEquals(3, l.size)
+        assertEquals(5, v.size)
+        assertEquals(1, l[0])
+        assertEquals(4, l[1])
+        assertEquals(2, l[2])
+        assertEquals(1, v[1])
+        assertEquals(4, v[2])
+        assertEquals(2, v[3])
+    }
+
+    @Test
+    fun subListAddAllAtIndex() {
+        val v = mutableObjectListOf(6, 1, 2, 3)
+        val l = v.asMutableList().subList(1, 3)
+        l.addAll(1, listOf(4, 5))
+        assertEquals(4, l.size)
+        assertEquals(6, v.size)
+        assertEquals(1, l[0])
+        assertEquals(4, l[1])
+        assertEquals(5, l[2])
+        assertEquals(2, l[3])
+        assertEquals(1, v[1])
+        assertEquals(4, v[2])
+        assertEquals(5, v[3])
+        assertEquals(2, v[4])
+    }
+
+    @Test
+    fun subListAddAll() {
+        val v = mutableObjectListOf(6, 1, 2, 3)
+        val l = v.asMutableList().subList(1, 3)
+        l.addAll(listOf(4, 5))
+        assertEquals(4, l.size)
+        assertEquals(6, v.size)
+        assertEquals(1, l[0])
+        assertEquals(2, l[1])
+        assertEquals(4, l[2])
+        assertEquals(5, l[3])
+        assertEquals(1, v[1])
+        assertEquals(2, v[2])
+        assertEquals(4, v[3])
+        assertEquals(5, v[4])
+        assertEquals(3, v[5])
+    }
+
+    @Test
+    fun subListClear() {
+        val v = mutableObjectListOf(1, 2, 3, 4, 5)
+        val l = v.asMutableList().subList(1, 4)
+        l.clear()
+        assertEquals(0, l.size)
+        assertEquals(2, v.size)
+        assertEquals(1, v[0])
+        assertEquals(5, v[1])
+        kotlin.test.assertNull(v.content[2])
+        kotlin.test.assertNull(v.content[3])
+        kotlin.test.assertNull(v.content[4])
+    }
+
+    @Test
+    fun subListListIterator() {
+        val l = list.asMutableList().subList(1, 4)
+        val listIterator = l.listIterator()
+        assertTrue(listIterator.hasNext())
+        assertFalse(listIterator.hasPrevious())
+        assertEquals(0, listIterator.nextIndex())
+        assertEquals(2, listIterator.next())
+    }
+
+    @Test
+    fun subListListIteratorWithIndex() {
+        val l = list.asMutableList().subList(1, 4)
+        val listIterator = l.listIterator(1)
+        assertTrue(listIterator.hasNext())
+        assertTrue(listIterator.hasPrevious())
+        assertEquals(1, listIterator.nextIndex())
+        assertEquals(3, listIterator.next())
+    }
+
+    @Test
+    fun subListRemove() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val l2 = l.asMutableList().subList(1, 4)
+        assertTrue(l2.remove(3))
+        assertEquals(l, mutableObjectListOf(1, 2, 4, 5))
+        assertEquals(2, l2.size)
+        assertEquals(2, l2[0])
+        assertEquals(4, l2[1])
+        assertFalse(l2.remove(3))
+        assertEquals(l, mutableObjectListOf(1, 2, 4, 5))
+        assertEquals(2, l2.size)
+    }
+
+    @Test
+    fun subListRemoveAll() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val l2 = l.asMutableList().subList(1, 4)
+        assertFalse(l2.removeAll(listOf(1, 5, -1)))
+        assertEquals(5, l.size)
+        assertEquals(3, l2.size)
+        assertTrue(l2.removeAll(listOf(3, 4, 5)))
+        assertEquals(3, l.size)
+        assertEquals(1, l2.size)
+    }
+
+    @Test
+    fun subListRemoveAt() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val l2 = l.asMutableList().subList(1, 4)
+        assertEquals(3, l2.removeAt(1))
+        assertEquals(4, l.size)
+        assertEquals(2, l2.size)
+        assertEquals(4, l2.removeAt(1))
+        assertEquals(1, l2.size)
+    }
+
+    @Test
+    fun subListRetainAll() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val l2 = l.asMutableList().subList(1, 4)
+        val l3 = objectListOf(1, 2, 3, 4, 5)
+        assertFalse(l2.retainAll(l3.asList()))
+        assertFalse(l2.retainAll(listOf(2, 3, 4)))
+        assertEquals(3, l2.size)
+        assertEquals(5, l.size)
+        assertTrue(l2.retainAll(setOf(1, 2, 4)))
+        assertEquals(4, l.size)
+        assertEquals(2, l2.size)
+        assertEquals(l, objectListOf(1, 2, 4, 5))
+    }
+
+    @Test
+    fun subListSet() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val l2 = l.asMutableList().subList(1, 4)
+        l2[1] = 10
+        assertEquals(10, l2[1])
+        assertEquals(3, l2.size)
+        assertEquals(10, l[2])
+    }
+
+    @Test
+    fun subListSubList() {
+        val l = objectListOf(1, 2, 3, 4, 5).asList().subList(1, 5)
+        val l2 = l.subList(1, 3)
+        assertEquals(2, l2.size)
+        assertEquals(3, l2[0])
+    }
+
+    @Suppress("KotlinConstantConditions")
+    @Test
+    fun list_outOfBounds_Get_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(1, 2, 3, 4).asMutableList()
+            l[-1]
+        }
+    }
+
+    @Suppress("KotlinConstantConditions")
+    @Test
+    fun sublist_outOfBounds_Get_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(1, 2, 3, 4).asMutableList().subList(1, 2)
+            l[-1]
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_Get_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(1, 2, 3, 4).asMutableList()
+            l[4]
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_Get_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(1, 2, 3, 4).asMutableList().subList(1, 2)
+            l[1]
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_RemoveAt_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l.removeAt(-1)
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_RemoveAt_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l.removeAt(-1)
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_RemoveAt_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l.removeAt(4)
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_RemoveAt_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l.removeAt(1)
+        }
+    }
+
+    @Suppress("KotlinConstantConditions")
+    @Test
+    fun list_outOfBounds_Set_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l[-1] = 1
+        }
+    }
+
+    @Suppress("KotlinConstantConditions")
+    @Test
+    fun sublist_outOfBounds_Set_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l[-1] = 1
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_Set_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l[4] = 1
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_Set_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l[1] = 1
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_SubList_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l.subList(-1, 1)
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_SubList_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l.subList(-1, 1)
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_SubList_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l.subList(5, 5)
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_SubList_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l.subList(1, 2)
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_SubList_Order() {
+        assertFailsWith(IllegalArgumentException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l.subList(3, 2)
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_SubList_Order() {
+        assertFailsWith(IllegalArgumentException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l.subList(1, 0)
+        }
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt
new file mode 100644
index 0000000..015b5a8
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt
@@ -0,0 +1,731 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class ObjectLongTest {
+    @Test
+    fun objectLongMap() {
+        val map = MutableObjectLongMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun emptyObjectLongMap() {
+        val map = emptyObjectLongMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyObjectLongMap<String>(), map)
+    }
+
+    @Test
+    fun objectLongMapFunction() {
+        val map = mutableObjectLongMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableObjectLongMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectLongMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableObjectLongMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectLongMapInitFunction() {
+        val map1 = objectLongMapOf(
+            "Hello", 1L,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1L, map1["Hello"])
+
+        val map2 = objectLongMapOf(
+            "Hello", 1L,
+            "Bonjour", 2L,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1L, map2["Hello"])
+        assertEquals(2L, map2["Bonjour"])
+
+        val map3 = objectLongMapOf(
+            "Hello", 1L,
+            "Bonjour", 2L,
+            "Hallo", 3L,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1L, map3["Hello"])
+        assertEquals(2L, map3["Bonjour"])
+        assertEquals(3L, map3["Hallo"])
+
+        val map4 = objectLongMapOf(
+            "Hello", 1L,
+            "Bonjour", 2L,
+            "Hallo", 3L,
+            "Konnichiwa", 4L,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1L, map4["Hello"])
+        assertEquals(2L, map4["Bonjour"])
+        assertEquals(3L, map4["Hallo"])
+        assertEquals(4L, map4["Konnichiwa"])
+
+        val map5 = objectLongMapOf(
+            "Hello", 1L,
+            "Bonjour", 2L,
+            "Hallo", 3L,
+            "Konnichiwa", 4L,
+            "Ciao", 5L,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1L, map5["Hello"])
+        assertEquals(2L, map5["Bonjour"])
+        assertEquals(3L, map5["Hallo"])
+        assertEquals(4L, map5["Konnichiwa"])
+        assertEquals(5L, map5["Ciao"])
+    }
+
+    @Test
+    fun mutableObjectLongMapInitFunction() {
+        val map1 = mutableObjectLongMapOf(
+            "Hello", 1L,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1L, map1["Hello"])
+
+        val map2 = mutableObjectLongMapOf(
+            "Hello", 1L,
+            "Bonjour", 2L,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1L, map2["Hello"])
+        assertEquals(2L, map2["Bonjour"])
+
+        val map3 = mutableObjectLongMapOf(
+            "Hello", 1L,
+            "Bonjour", 2L,
+            "Hallo", 3L,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1L, map3["Hello"])
+        assertEquals(2L, map3["Bonjour"])
+        assertEquals(3L, map3["Hallo"])
+
+        val map4 = mutableObjectLongMapOf(
+            "Hello", 1L,
+            "Bonjour", 2L,
+            "Hallo", 3L,
+            "Konnichiwa", 4L,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1L, map4["Hello"])
+        assertEquals(2L, map4["Bonjour"])
+        assertEquals(3L, map4["Hallo"])
+        assertEquals(4L, map4["Konnichiwa"])
+
+        val map5 = mutableObjectLongMapOf(
+            "Hello", 1L,
+            "Bonjour", 2L,
+            "Hallo", 3L,
+            "Konnichiwa", 4L,
+            "Ciao", 5L,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1L, map5["Hello"])
+        assertEquals(2L, map5["Bonjour"])
+        assertEquals(3L, map5["Hallo"])
+        assertEquals(4L, map5["Konnichiwa"])
+        assertEquals(5L, map5["Ciao"])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map["Hello"])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableObjectLongMap<String>(12)
+        map["Hello"] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map["Hello"])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableObjectLongMap<String>(2)
+        map["Hello"] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1L, map["Hello"])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableObjectLongMap<String>(0)
+        map["Hello"] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map["Hello"])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Hello"] = 2L
+
+        assertEquals(1, map.size)
+        assertEquals(2L, map["Hello"])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableObjectLongMap<String>()
+
+        map.put("Hello", 1L)
+        assertEquals(1L, map["Hello"])
+        map.put("Hello", 2L)
+        assertEquals(2L, map["Hello"])
+    }
+
+    @Test
+    fun nullKey() {
+        val map = MutableObjectLongMap<String?>()
+        map[null] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[null])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+
+        assertFailsWith<NoSuchElementException> {
+            map["Bonjour"]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+
+        assertEquals(2L, map.getOrDefault("Bonjour", 2L))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+
+        assertEquals(3L, map.getOrElse("Hallo") { 3L })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+
+        var counter = 0
+        map.getOrPut("Hello") {
+            counter++
+            2L
+        }
+        assertEquals(1L, map["Hello"])
+        assertEquals(0, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            2L
+        }
+        assertEquals(2L, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            3L
+        }
+        assertEquals(2L, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Hallo") {
+            counter++
+            3L
+        }
+        assertEquals(3L, map["Hallo"])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableObjectLongMap<String?>()
+        map.remove("Hello")
+
+        map["Hello"] = 1L
+        map.remove("Hello")
+        assertEquals(0, map.size)
+
+        map[null] = 1L
+        map.remove(null)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableObjectLongMap<String>(6)
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+        map["Konnichiwa"] = 4L
+        map["Ciao"] = 5L
+        map["Annyeong"] = 6L
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove("Hello")
+        map.remove("Bonjour")
+        map.remove("Hallo")
+        map.remove("Konnichiwa")
+        map.remove("Ciao")
+        map.remove("Annyeong")
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map["Hola"] = 7L
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+        map["Konnichiwa"] = 4L
+        map["Ciao"] = 5L
+        map["Annyeong"] = 6L
+
+        map.removeIf { key, _ -> key.startsWith('H') }
+
+        assertEquals(4, map.size)
+        assertEquals(2L, map["Bonjour"])
+        assertEquals(4L, map["Konnichiwa"])
+        assertEquals(5L, map["Ciao"])
+        assertEquals(6L, map["Annyeong"])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+
+        map -= "Hello"
+
+        assertEquals(2, map.size)
+        assertFalse("Hello" in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+
+        map -= arrayOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusIterable() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+
+        map -= listOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusSequence() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+
+        map -= listOf("Hallo", "Bonjour").asSequence()
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableObjectLongMap<String?>()
+        assertFalse(map.remove("Hello", 1L))
+
+        map["Hello"] = 1L
+        assertTrue(map.remove("Hello", 1L))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableObjectLongMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toLong()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableObjectLongMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toLong()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt().toString())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableObjectLongMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toLong()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertNotNull(key.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableObjectLongMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toLong()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableObjectLongMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toString()] = i.toLong()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableObjectLongMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        val oneString = 1L.toString()
+        val twoString = 2L.toString()
+        assertTrue(
+            "{Hello=$oneString, Bonjour=$twoString}" == map.toString() ||
+                "{Bonjour=$twoString, Hello=$oneString}" == map.toString()
+        )
+
+        map.clear()
+        map[null] = 2L
+        assertEquals("{null=$twoString}", map.toString())
+
+        val selfAsKeyMap = MutableObjectLongMap<Any>()
+        selfAsKeyMap[selfAsKeyMap] = 1L
+        assertEquals("{(this)=$oneString}", selfAsKeyMap.toString())
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableObjectLongMap<String?>()
+        repeat(5) {
+            map[it.toString()] = it.toLong()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { _, value ->
+            order[index++] = value.toInt()
+        }
+        assertEquals(
+            "${order[0]}=${order[0].toLong()}, ${order[1]}=${order[1].toLong()}, " +
+            "${order[2]}=${order[2].toLong()}, ${order[3]}=${order[3].toLong()}, " +
+            "${order[4]}=${order[4].toLong()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0]}=${order[0].toLong()}, ${order[1]}=${order[1].toLong()}, " +
+            "${order[2]}=${order[2].toLong()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0]}=${order[0].toLong()}-${order[1]}=${order[1].toLong()}-" +
+            "${order[2]}=${order[2].toLong()}-${order[3]}=${order[3].toLong()}-" +
+            "${order[4]}=${order[4].toLong()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { _, value -> names[value.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableObjectLongMap<String?>()
+        map["Hello"] = 1L
+        map[null] = 2L
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableObjectLongMap<String?>()
+        map2[null] = 2L
+
+        assertNotEquals(map, map2)
+
+        map2["Hello"] = 1L
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableObjectLongMap<String?>()
+        map["Hello"] = 1L
+        map[null] = 2L
+
+        assertTrue(map.containsKey("Hello"))
+        assertTrue(map.containsKey(null))
+        assertFalse(map.containsKey("Bonjour"))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableObjectLongMap<String?>()
+        map["Hello"] = 1L
+        map[null] = 2L
+
+        assertTrue("Hello" in map)
+        assertTrue(null in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableObjectLongMap<String?>()
+        map["Hello"] = 1L
+        map[null] = 2L
+
+        assertTrue(map.containsValue(1L))
+        assertTrue(map.containsValue(2L))
+        assertFalse(map.containsValue(3L))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableObjectLongMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map["Hello"] = 1L
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableObjectLongMap<String>()
+        assertEquals(0, map.count())
+
+        map["Hello"] = 1L
+        assertEquals(1, map.count())
+
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+        map["Konnichiwa"] = 4L
+        map["Ciao"] = 5L
+        map["Annyeong"] = 6L
+
+        assertEquals(2, map.count { key, _ -> key.startsWith("H") })
+        assertEquals(0, map.count { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+        map["Konnichiwa"] = 4L
+        map["Ciao"] = 5L
+        map["Annyeong"] = 6L
+
+        assertTrue(map.any { key, _ -> key.startsWith("K") })
+        assertFalse(map.any { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+        map["Konnichiwa"] = 4L
+        map["Ciao"] = 5L
+        map["Annyeong"] = 6L
+
+        assertTrue(map.all { key, value -> key.length >= 4 && value.toInt() >= 1 })
+        assertFalse(map.all { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableObjectLongMap<String>()
+        assertEquals(7, map.trim())
+
+        map["Hello"] = 1L
+        map["Hallo"] = 3L
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toLong()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toString()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/PairTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/PairTest.kt
index b132651..51cc5963 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/PairTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/PairTest.kt
@@ -24,18 +24,18 @@
 
     @Test
     fun intCreation() {
-        val pair = PairIntInt(3, 5)
+        val pair = IntIntPair(3, 5)
         assertEquals(3, pair.first)
         assertEquals(5, pair.second)
     }
 
     @Test
     fun intEquality() {
-        val pair = PairIntInt(3, 5)
-        val pairEqual = PairIntInt(3, 5)
-        val pairUnequal1 = PairIntInt(4, 5)
-        val pairUnequal2 = PairIntInt(3, 6)
-        val pairUnequal3 = PairIntInt(4, 6)
+        val pair = IntIntPair(3, 5)
+        val pairEqual = IntIntPair(3, 5)
+        val pairUnequal1 = IntIntPair(4, 5)
+        val pairUnequal2 = IntIntPair(3, 6)
+        val pairUnequal3 = IntIntPair(4, 6)
 
         assertEquals(pair, pairEqual)
         assertNotEquals(pair, pairUnequal1)
@@ -45,7 +45,7 @@
 
     @Test
     fun intDestructing() {
-        val pair = PairIntInt(3, 5)
+        val pair = IntIntPair(3, 5)
         val (first, second) = pair
         assertEquals(3, first)
         assertEquals(5, second)
@@ -53,18 +53,18 @@
 
     @Test
     fun floatCreation() {
-        val pair = PairFloatFloat(3f, 5f)
+        val pair = FloatFloatPair(3f, 5f)
         assertEquals(3f, pair.first)
         assertEquals(5f, pair.second)
     }
 
     @Test
     fun floatEquality() {
-        val pair = PairFloatFloat(3f, 5f)
-        val pairEqual = PairFloatFloat(3f, 5f)
-        val pairUnequal1 = PairFloatFloat(4f, 5f)
-        val pairUnequal2 = PairFloatFloat(3f, 6f)
-        val pairUnequal3 = PairFloatFloat(4f, 6f)
+        val pair = FloatFloatPair(3f, 5f)
+        val pairEqual = FloatFloatPair(3f, 5f)
+        val pairUnequal1 = FloatFloatPair(4f, 5f)
+        val pairUnequal2 = FloatFloatPair(3f, 6f)
+        val pairUnequal3 = FloatFloatPair(4f, 6f)
 
         assertEquals(pair, pairEqual)
         assertNotEquals(pair, pairUnequal1)
@@ -74,7 +74,7 @@
 
     @Test
     fun floatDestructing() {
-        val pair = PairFloatFloat(3f, 5f)
+        val pair = FloatFloatPair(3f, 5f)
         val (first, second) = pair
         assertEquals(3f, first)
         assertEquals(5f, second)
@@ -82,18 +82,18 @@
 
     @Test
     fun longCreation() {
-        val pair = PairLongLong(3, 5)
+        val pair = LongLongPair(3, 5)
         assertEquals(3, pair.first)
         assertEquals(5, pair.second)
     }
 
     @Test
     fun longEquality() {
-        val pair = PairLongLong(3, 5)
-        val pairEqual = PairLongLong(3, 5)
-        val pairUnequal1 = PairLongLong(4, 5)
-        val pairUnequal2 = PairLongLong(3, 6)
-        val pairUnequal3 = PairLongLong(4, 6)
+        val pair = LongLongPair(3, 5)
+        val pairEqual = LongLongPair(3, 5)
+        val pairUnequal1 = LongLongPair(4, 5)
+        val pairUnequal2 = LongLongPair(3, 6)
+        val pairUnequal3 = LongLongPair(4, 6)
 
         assertEquals(pair, pairEqual)
         assertNotEquals(pair, pairUnequal1)
@@ -103,7 +103,7 @@
 
     @Test
     fun longDestructing() {
-        val pair = PairLongLong(3, 5)
+        val pair = LongLongPair(3, 5)
         val (first, second) = pair
         assertEquals(3L, first)
         assertEquals(5L, second)
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterMapTest.kt
index bdd3338..ec03f0d 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterMapTest.kt
@@ -87,6 +87,13 @@
     }
 
     @Test
+    fun insertIndex0() {
+        val map = MutableScatterMap<Float, Long>()
+        map.put(1f, 100L)
+        assertEquals(100L, map[1f])
+    }
+
+    @Test
     fun addToSizedMap() {
         val map = MutableScatterMap<String, String>(12)
         map["Hello"] = "World"
@@ -447,6 +454,34 @@
     }
 
     @Test
+    fun minusScatterSet() {
+        val map = MutableScatterMap<String, String>()
+        map["Hello"] = "World"
+        map["Bonjour"] = "Monde"
+        map["Hallo"] = "Welt"
+
+        map -= scatterSetOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertNull(map["Hallo"])
+        assertNull(map["Bonjour"])
+    }
+
+    @Test
+    fun minusObjectList() {
+        val map = MutableScatterMap<String, String>()
+        map["Hello"] = "World"
+        map["Bonjour"] = "Monde"
+        map["Hallo"] = "Welt"
+
+        map -= objectListOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertNull(map["Hallo"])
+        assertNull(map["Bonjour"])
+    }
+
+    @Test
     fun conditionalRemove() {
         val map = MutableScatterMap<String?, String?>()
         assertFalse(map.remove("Hello", "World"))
@@ -583,6 +618,38 @@
     }
 
     @Test
+    fun joinToString() {
+        val map = mutableScatterMapOf(1 to 1f, 2 to 2f, 3 to 3f, 4 to 4f, 5 to 5f)
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key
+        }
+        assertEquals(
+            "${order[0]}=${order[0].toFloat()}, ${order[1]}=${order[1].toFloat()}, " +
+                "${order[2]}=${order[2].toFloat()}, ${order[3]}=${order[3].toFloat()}, " +
+                "${order[4]}=${order[4].toFloat()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0]}=${order[0].toFloat()}, ${order[1]}=${order[1].toFloat()}, " +
+                "${order[2]}=${order[2].toFloat()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0]}=${order[0].toFloat()}-${order[1]}=${order[1].toFloat()}-" +
+                "${order[2]}=${order[2].toFloat()}-${order[3]}=${order[3].toFloat()}-" +
+                "${order[4]}=${order[4].toFloat()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key] }
+        )
+    }
+
+    @Test
     fun equals() {
         val map = MutableScatterMap<String?, String?>()
         map["Hello"] = "World"
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterSetTest.kt
index 5a4ba52..824502d 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterSetTest.kt
@@ -144,6 +144,16 @@
     }
 
     @Test
+    fun addAllObjectList() {
+        val set = mutableScatterSetOf("Hello")
+        assertFalse(set.addAll(objectListOf("Hello")))
+        assertEquals(1, set.size)
+        assertTrue(set.addAll(objectListOf("Hello", "World")))
+        assertEquals(2, set.size)
+        assertTrue("World" in set)
+    }
+
+    @Test
     fun plusAssignArray() {
         val set = mutableScatterSetOf("Hello")
         set += arrayOf("Hello")
@@ -184,6 +194,16 @@
     }
 
     @Test
+    fun plusAssignObjectList() {
+        val set = mutableScatterSetOf("Hello")
+        set += objectListOf("Hello")
+        assertEquals(1, set.size)
+        set += objectListOf("Hello", "World")
+        assertEquals(2, set.size)
+        assertTrue("World" in set)
+    }
+
+    @Test
     fun nullElement() {
         val set = MutableScatterSet<String?>()
         set += null
@@ -348,6 +368,16 @@
     }
 
     @Test
+    fun removeAllObjectList() {
+        val set = mutableScatterSetOf("Hello", "World")
+        assertFalse(set.removeAll(objectListOf("Hola", "Bonjour")))
+        assertEquals(2, set.size)
+        assertTrue(set.removeAll(objectListOf("Hola", "Hello", "Bonjour")))
+        assertEquals(1, set.size)
+        assertFalse("Hello" in set)
+    }
+
+    @Test
     fun minusAssignArray() {
         val set = mutableScatterSetOf("Hello", "World")
         set -= arrayOf("Hola", "Bonjour")
@@ -388,6 +418,16 @@
     }
 
     @Test
+    fun minusAssignObjectList() {
+        val set = mutableScatterSetOf("Hello", "World")
+        set -= objectListOf("Hola", "Bonjour")
+        assertEquals(2, set.size)
+        set -= objectListOf("Hola", "Hello", "Bonjour")
+        assertEquals(1, set.size)
+        assertFalse("Hello" in set)
+    }
+
+    @Test
     fun insertManyEntries() {
         val set = MutableScatterSet<String>()
 
@@ -461,6 +501,33 @@
     }
 
     @Test
+    fun joinToString() {
+        val set = scatterSetOf(1, 2, 3, 4, 5)
+        val order = IntArray(5)
+        var index = 0
+        set.forEach { element ->
+            order[index++] = element
+        }
+        assertEquals(
+            "${order[0]}, ${order[1]}, ${order[2]}, ${order[3]}, ${order[4]}",
+            set.joinToString()
+        )
+        assertEquals(
+            "x${order[0]}, ${order[1]}, ${order[2]}...",
+            set.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0]}-${order[1]}-${order[2]}-${order[3]}-${order[4]}<",
+            set.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            set.joinToString(limit = 3) { names[it] }
+        )
+    }
+
+    @Test
     fun hashCodeAddValues() {
         val set = mutableScatterSetOf<String?>()
         assertEquals(0, set.hashCode())
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/TestValueClass.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/TestValueClass.kt
new file mode 100644
index 0000000..7580c60
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/TestValueClass.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.collection
+
+import kotlin.jvm.JvmInline
+
+/**
+ * This is a value class to test ValueClassSet and ValueClassList
+ */
+@JvmInline
+value class TestValueClass(val value: ULong) {
+    override fun toString(): String {
+        return ">$value<"
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ValueClassListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ValueClassListTest.kt
new file mode 100644
index 0000000..9f7b4a9
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ValueClassListTest.kt
@@ -0,0 +1,856 @@
+/*
+ * 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.collection
+
+import androidx.collection.template.MutableTestValueClassList
+import androidx.collection.template.emptyTestValueClassList
+import androidx.collection.template.mutableTestValueClassListOf
+import androidx.collection.template.testValueClassListOf
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+
+// ValueClassList was created with:
+// sed -e "s/PACKAGE/androidx.collection.template/" -e "s/VALUE_CLASS/TestValueClass/g" \
+//     -e "s/vALUE_CLASS/testValueClass/g" -e "s/BACKING_PROPERTY/value.toLong()/g" \
+//     -e "s/TO_PARAM/.toULong()/g" -e "s/PRIMITIVE/Long/g" -e "s/VALUE_PKG/androidx.collection/" \
+//     collection/collection/template/ValueClassList.kt.template \
+//     > collection/collection/src/commonTest/kotlin/androidx/collection/template/TestValueClassList.kt
+
+class ValueClassListTest {
+    private val list: MutableTestValueClassList = mutableTestValueClassListOf().also {
+        it += TestValueClass(1UL)
+        it += TestValueClass(2UL)
+        it += TestValueClass(3UL)
+        it += TestValueClass(4UL)
+        it += TestValueClass(5UL)
+    }
+
+    @Test
+    fun emptyConstruction() {
+        val l = mutableTestValueClassListOf()
+        assertEquals(0, l.size)
+        assertEquals(16, l.capacity)
+    }
+
+    @Test
+    fun sizeConstruction() {
+        val l = MutableTestValueClassList(4)
+        assertEquals(4, l.capacity)
+    }
+
+    @Test
+    fun contentConstruction() {
+        val l = mutableTestValueClassListOf(
+            TestValueClass(1UL),
+            TestValueClass(2UL),
+            TestValueClass(3UL)
+        )
+        assertEquals(3, l.size)
+        assertEquals(TestValueClass(1UL), l[0])
+        assertEquals(TestValueClass(2UL), l[1])
+        assertEquals(TestValueClass(3UL), l[2])
+        assertEquals(3, l.capacity)
+        repeat(2) {
+            val l2 = mutableTestValueClassListOf().also {
+                it += TestValueClass(1UL)
+                it += TestValueClass(2UL)
+                it += TestValueClass(3UL)
+                it += TestValueClass(4UL)
+                it += TestValueClass(5UL)
+            }
+            assertEquals(list, l2)
+            l2.removeAt(0)
+        }
+    }
+
+    @Test
+    fun hashCodeTest() {
+        val l2 = mutableTestValueClassListOf().also {
+            it += TestValueClass(1UL)
+            it += TestValueClass(2UL)
+            it += TestValueClass(3UL)
+            it += TestValueClass(4UL)
+            it += TestValueClass(5UL)
+        }
+        assertEquals(list.hashCode(), l2.hashCode())
+        l2.removeAt(4)
+        assertNotEquals(list.hashCode(), l2.hashCode())
+        l2.add(TestValueClass(5UL))
+        assertEquals(list.hashCode(), l2.hashCode())
+        l2.clear()
+        assertNotEquals(list.hashCode(), l2.hashCode())
+    }
+
+    @Test
+    fun equalsTest() {
+        val l2 = mutableTestValueClassListOf().also {
+            it += TestValueClass(1UL)
+            it += TestValueClass(2UL)
+            it += TestValueClass(3UL)
+            it += TestValueClass(4UL)
+            it += TestValueClass(5UL)
+        }
+        assertEquals(list, l2)
+        assertNotEquals(list, mutableTestValueClassListOf())
+        l2.removeAt(4)
+        assertNotEquals(list, l2)
+        l2.add(TestValueClass(5UL))
+        assertEquals(list, l2)
+        l2.clear()
+        assertNotEquals(list, l2)
+    }
+
+    @Test
+    fun string() {
+        assertEquals("[>1<, >2<, >3<, >4<, >5<]", list.toString())
+        assertEquals("[]", mutableTestValueClassListOf().toString())
+    }
+
+    @Test
+    fun size() {
+        assertEquals(5, list.size)
+        assertEquals(5, list.count())
+        val l2 = mutableTestValueClassListOf()
+        assertEquals(0, l2.size)
+        assertEquals(0, l2.count())
+        l2 += TestValueClass(1UL)
+        assertEquals(1, l2.size)
+        assertEquals(1, l2.count())
+    }
+
+    @Test
+    fun get() {
+        assertEquals(TestValueClass(1UL), list[0])
+        assertEquals(TestValueClass(5UL), list[4])
+        assertEquals(TestValueClass(1UL), list.elementAt(0))
+        assertEquals(TestValueClass(5UL), list.elementAt(4))
+    }
+
+    @Test
+    fun getOutOfBounds() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list[5]
+        }
+    }
+
+    @Test
+    fun getOutOfBoundsNegative() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list[-1]
+        }
+    }
+
+    @Test
+    fun elementAtOfBounds() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list.elementAt(5)
+        }
+    }
+
+    @Test
+    fun elementAtOfBoundsNegative() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list.elementAt(-1)
+        }
+    }
+
+    @Test
+    fun elementAtOrElse() {
+        assertEquals(TestValueClass(1UL), list.elementAtOrElse(0) {
+            assertEquals(0, it)
+            TestValueClass(0UL)
+        })
+        assertEquals(TestValueClass(0UL), list.elementAtOrElse(-1) {
+            assertEquals(-1, it)
+            TestValueClass(0UL)
+        })
+        assertEquals(TestValueClass(0UL), list.elementAtOrElse(5) {
+            assertEquals(5, it)
+            TestValueClass(0UL)
+        })
+    }
+
+    @Test
+    fun count() {
+        assertEquals(1, list.count { it.value < 2UL })
+        assertEquals(0, list.count { it.value < 0UL })
+        assertEquals(5, list.count { it.value < 10UL })
+    }
+
+    @Test
+    fun isEmpty() {
+        assertFalse(list.isEmpty())
+        assertFalse(list.none())
+        assertTrue(mutableTestValueClassListOf().isEmpty())
+        assertTrue(mutableTestValueClassListOf().none())
+    }
+
+    @Test
+    fun isNotEmpty() {
+        assertTrue(list.isNotEmpty())
+        assertTrue(list.any())
+        assertFalse(mutableTestValueClassListOf().isNotEmpty())
+    }
+
+    @Test
+    fun indices() {
+        assertEquals(IntRange(0, 4), list.indices)
+        assertEquals(IntRange(0, -1), mutableTestValueClassListOf().indices)
+    }
+
+    @Test
+    fun any() {
+        assertTrue(list.any { it == TestValueClass(5UL) })
+        assertTrue(list.any { it == TestValueClass(1UL) })
+        assertFalse(list.any { it == TestValueClass(0UL) })
+    }
+
+    @Test
+    fun reversedAny() {
+        val reversedList = mutableTestValueClassListOf()
+        assertFalse(
+            list.reversedAny {
+                reversedList.add(it)
+                false
+            }
+        )
+        val reversedContent = mutableTestValueClassListOf().also {
+            it += TestValueClass(5UL)
+            it += TestValueClass(4UL)
+            it += TestValueClass(3UL)
+            it += TestValueClass(2UL)
+            it += TestValueClass(1UL)
+        }
+        assertEquals(reversedContent, reversedList)
+
+        val reversedSublist = mutableTestValueClassListOf()
+        assertTrue(
+            list.reversedAny {
+                reversedSublist.add(it)
+                reversedSublist.size == 2
+            }
+        )
+        assertEquals(
+            reversedSublist,
+            mutableTestValueClassListOf(TestValueClass(5UL), TestValueClass(4UL))
+        )
+    }
+
+    @Test
+    fun forEach() {
+        val copy = mutableTestValueClassListOf()
+        list.forEach { copy += it }
+        assertEquals(list, copy)
+    }
+
+    @Test
+    fun forEachReversed() {
+        val copy = mutableTestValueClassListOf()
+        list.forEachReversed { copy += it }
+        assertEquals(
+            copy,
+            mutableTestValueClassListOf().also {
+                it += TestValueClass(5UL)
+                it += TestValueClass(4UL)
+                it += TestValueClass(3UL)
+                it += TestValueClass(2UL)
+                it += TestValueClass(1UL)
+            }
+        )
+    }
+
+    @Test
+    fun forEachIndexed() {
+        val copy = mutableTestValueClassListOf()
+        val indices = mutableTestValueClassListOf()
+        list.forEachIndexed { index, item ->
+            copy += item
+            indices += TestValueClass(index.toULong())
+        }
+        assertEquals(list, copy)
+        assertEquals(
+            indices,
+            mutableTestValueClassListOf().also {
+                it += TestValueClass(0UL)
+                it += TestValueClass(1UL)
+                it += TestValueClass(2UL)
+                it += TestValueClass(3UL)
+                it += TestValueClass(4UL)
+            }
+        )
+    }
+
+    @Test
+    fun forEachReversedIndexed() {
+        val copy = mutableTestValueClassListOf()
+        val indices = mutableTestValueClassListOf()
+        list.forEachReversedIndexed { index, item ->
+            copy += item
+            indices += TestValueClass(index.toULong())
+        }
+        assertEquals(
+            copy,
+            mutableTestValueClassListOf().also {
+                it += TestValueClass(5UL)
+                it += TestValueClass(4UL)
+                it += TestValueClass(3UL)
+                it += TestValueClass(2UL)
+                it += TestValueClass(1UL)
+            })
+        assertEquals(
+            indices,
+            mutableTestValueClassListOf().also {
+                it += TestValueClass(4UL)
+                it += TestValueClass(3UL)
+                it += TestValueClass(2UL)
+                it += TestValueClass(1UL)
+                it += TestValueClass(0UL)
+            })
+    }
+
+    @Test
+    fun indexOfFirst() {
+        assertEquals(0, list.indexOfFirst { it == TestValueClass(1UL) })
+        assertEquals(4, list.indexOfFirst { it == TestValueClass(5UL) })
+        assertEquals(-1, list.indexOfFirst { it == TestValueClass(0UL) })
+        assertEquals(
+            0,
+            mutableTestValueClassListOf(
+                TestValueClass(8UL),
+                TestValueClass(8UL)
+            ).indexOfFirst { it == TestValueClass(8UL) })
+    }
+
+    @Test
+    fun indexOfLast() {
+        assertEquals(0, list.indexOfLast { it == TestValueClass(1UL) })
+        assertEquals(4, list.indexOfLast { it == TestValueClass(5UL) })
+        assertEquals(-1, list.indexOfLast { it == TestValueClass(0UL) })
+        assertEquals(
+            1,
+            mutableTestValueClassListOf(
+                TestValueClass(8UL),
+                TestValueClass(8UL)
+            ).indexOfLast { it == TestValueClass(8UL) })
+    }
+
+    @Test
+    fun contains() {
+        assertTrue(list.contains(TestValueClass(5UL)))
+        assertTrue(list.contains(TestValueClass(1UL)))
+        assertFalse(list.contains(TestValueClass(0UL)))
+    }
+
+    @Test
+    fun containsAllList() {
+        assertTrue(
+            list.containsAll(
+                mutableTestValueClassListOf(
+                    TestValueClass(2UL),
+                    TestValueClass(3UL),
+                    TestValueClass(1UL)
+                )
+            )
+        )
+        assertFalse(
+            list.containsAll(
+                mutableTestValueClassListOf(
+                    TestValueClass(2UL),
+                    TestValueClass(3UL),
+                    TestValueClass(6UL)
+                )
+            )
+        )
+    }
+
+    @Test
+    fun lastIndexOf() {
+        assertEquals(4, list.lastIndexOf(TestValueClass(5UL)))
+        assertEquals(1, list.lastIndexOf(TestValueClass(2UL)))
+        val copy = mutableTestValueClassListOf()
+        copy.addAll(list)
+        copy.addAll(list)
+        assertEquals(5, copy.lastIndexOf(TestValueClass(1UL)))
+    }
+
+    @Test
+    fun first() {
+        assertEquals(TestValueClass(1UL), list.first())
+    }
+
+    @Test
+    fun firstException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableTestValueClassListOf().first()
+        }
+    }
+
+    @Test
+    fun firstWithPredicate() {
+        assertEquals(TestValueClass(5UL), list.first { it == TestValueClass(5UL) })
+        assertEquals(
+            TestValueClass(1UL),
+            mutableTestValueClassListOf(TestValueClass(1UL), TestValueClass(5UL)).first {
+                it != TestValueClass(0UL)
+            })
+    }
+
+    @Test
+    fun firstWithPredicateException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableTestValueClassListOf().first { it == TestValueClass(8UL) }
+        }
+    }
+
+    @Test
+    fun last() {
+        assertEquals(TestValueClass(5UL), list.last())
+    }
+
+    @Test
+    fun lastException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableTestValueClassListOf().last()
+        }
+    }
+
+    @Test
+    fun lastWithPredicate() {
+        assertEquals(TestValueClass(1UL), list.last { it == TestValueClass(1UL) })
+        assertEquals(
+            TestValueClass(5UL),
+            mutableTestValueClassListOf(TestValueClass(1UL), TestValueClass(5UL)).last {
+                it != TestValueClass(0UL)
+            })
+    }
+
+    @Test
+    fun lastWithPredicateException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableTestValueClassListOf().last { it == TestValueClass(8UL) }
+        }
+    }
+
+    @Test
+    fun fold() {
+        assertEquals("12345", list.fold("") { acc, i -> acc + i.value.toString() })
+    }
+
+    @Test
+    fun foldIndexed() {
+        assertEquals(
+            "01-12-23-34-45-",
+            list.foldIndexed("") { index, acc, i ->
+                "$acc$index${i.value}-"
+            }
+        )
+    }
+
+    @Test
+    fun foldRight() {
+        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.value.toString() })
+    }
+
+    @Test
+    fun foldRightIndexed() {
+        assertEquals(
+            "45-34-23-12-01-",
+            list.foldRightIndexed("") { index, i, acc ->
+                "$acc$index${i.value}-"
+            }
+        )
+    }
+
+    @Test
+    fun add() {
+        val l = mutableTestValueClassListOf(
+            TestValueClass(1UL),
+            TestValueClass(2UL),
+            TestValueClass(3UL)
+        )
+        l += TestValueClass(4UL)
+        l.add(TestValueClass(5UL))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun addAtIndex() {
+        val l = mutableTestValueClassListOf(TestValueClass(2UL), TestValueClass(4UL))
+        l.add(2, TestValueClass(5UL))
+        l.add(0, TestValueClass(1UL))
+        l.add(2, TestValueClass(3UL))
+        assertEquals(list, l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.add(-1, TestValueClass(2UL))
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.add(6, TestValueClass(2UL))
+        }
+    }
+
+    @Test
+    fun addAllListAtIndex() {
+        val l = mutableTestValueClassListOf(TestValueClass(4UL))
+        val l2 = mutableTestValueClassListOf(TestValueClass(1UL), TestValueClass(2UL))
+        val l3 = mutableTestValueClassListOf(TestValueClass(5UL))
+        val l4 = mutableTestValueClassListOf(TestValueClass(3UL))
+        assertTrue(l4.addAll(1, l3))
+        assertTrue(l4.addAll(0, l2))
+        assertTrue(l4.addAll(3, l))
+        assertFalse(l4.addAll(0, mutableTestValueClassListOf()))
+        assertEquals(list, l4)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l4.addAll(6, mutableTestValueClassListOf())
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l4.addAll(-1, mutableTestValueClassListOf())
+        }
+    }
+
+    @Test
+    fun addAllList() {
+        val l = MutableTestValueClassList()
+        l.add(TestValueClass(3UL))
+        l.add(TestValueClass(4UL))
+        l.add(TestValueClass(5UL))
+        val l2 = mutableTestValueClassListOf(TestValueClass(1UL), TestValueClass(2UL))
+        assertTrue(l2.addAll(l))
+        assertEquals(list, l2)
+        assertFalse(l2.addAll(mutableTestValueClassListOf()))
+    }
+
+    @Test
+    fun plusAssignList() {
+        val l = MutableTestValueClassList()
+        l.add(TestValueClass(3UL))
+        l.add(TestValueClass(4UL))
+        l.add(TestValueClass(5UL))
+        val l2 = mutableTestValueClassListOf(TestValueClass(1UL), TestValueClass(2UL))
+        l2 += l
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun clear() {
+        val l = mutableTestValueClassListOf()
+        l.addAll(list)
+        assertTrue(l.isNotEmpty())
+        l.clear()
+        assertTrue(l.isEmpty())
+    }
+
+    @Test
+    fun trim() {
+        val l = mutableTestValueClassListOf(TestValueClass(1UL))
+        l.trim()
+        assertEquals(1, l.capacity)
+        l += TestValueClass(1UL)
+        l += TestValueClass(2UL)
+        l += TestValueClass(3UL)
+        l += TestValueClass(4UL)
+        l += TestValueClass(5UL)
+        l.trim()
+        assertEquals(6, l.capacity)
+        assertEquals(6, l.size)
+        l.clear()
+        l.trim()
+        assertEquals(0, l.capacity)
+        l.trim(100)
+        assertEquals(0, l.capacity)
+        l += TestValueClass(1UL)
+        l += TestValueClass(2UL)
+        l += TestValueClass(3UL)
+        l += TestValueClass(4UL)
+        l += TestValueClass(5UL)
+        l -= TestValueClass(5UL)
+        l.trim(5)
+        assertEquals(5, l.capacity)
+        l.trim(4)
+        assertEquals(4, l.capacity)
+        l.trim(3)
+        assertEquals(4, l.capacity)
+    }
+
+    @Test
+    fun remove() {
+        val l = mutableTestValueClassListOf()
+        l += list
+        l.remove(TestValueClass(3UL))
+        assertEquals(
+            mutableTestValueClassListOf().also {
+                it += TestValueClass(1UL)
+                it += TestValueClass(2UL)
+                it += TestValueClass(4UL)
+                it += TestValueClass(5UL)
+            },
+            l
+        )
+    }
+
+    @Test
+    fun removeAt() {
+        val l = mutableTestValueClassListOf()
+        l += list
+        l.removeAt(2)
+        assertEquals(
+            mutableTestValueClassListOf().also {
+                it += TestValueClass(1UL)
+                it += TestValueClass(2UL)
+                it += TestValueClass(4UL)
+                it += TestValueClass(5UL)
+            },
+            l
+        )
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.removeAt(6)
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.removeAt(-1)
+        }
+    }
+
+    @Test
+    fun set() {
+        val l = mutableTestValueClassListOf()
+        repeat(5) { l += TestValueClass(0UL) }
+        l[0] = TestValueClass(1UL)
+        l[4] = TestValueClass(5UL)
+        l[2] = TestValueClass(3UL)
+        l[1] = TestValueClass(2UL)
+        l[3] = TestValueClass(4UL)
+        assertEquals(list, l)
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.set(-1, TestValueClass(1UL))
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.set(6, TestValueClass(1UL))
+        }
+        assertEquals(TestValueClass(4UL), l.set(3, TestValueClass(1UL)));
+    }
+
+    @Test
+    fun ensureCapacity() {
+        val l = mutableTestValueClassListOf(TestValueClass(1UL))
+        assertEquals(1, l.capacity)
+        l.ensureCapacity(5)
+        assertEquals(5, l.capacity)
+    }
+
+    @Test
+    fun removeAllList() {
+        assertFalse(
+            list.removeAll(
+                mutableTestValueClassListOf(
+                    TestValueClass(0UL),
+                    TestValueClass(10UL),
+                    TestValueClass(15UL)
+                )
+            )
+        )
+        val l = mutableTestValueClassListOf()
+
+        l += TestValueClass(0UL)
+        l += TestValueClass(1UL)
+        l += TestValueClass(15UL)
+        l += TestValueClass(10UL)
+        l += TestValueClass(2UL)
+        l += TestValueClass(3UL)
+        l += TestValueClass(4UL)
+        l += TestValueClass(5UL)
+        l += TestValueClass(20UL)
+        l += TestValueClass(5UL)
+        assertTrue(
+            l.removeAll(
+                mutableTestValueClassListOf().also {
+                    it += TestValueClass(20UL)
+                    it += TestValueClass(0UL)
+                    it += TestValueClass(15UL)
+                    it += TestValueClass(10UL)
+                    it += TestValueClass(5UL)
+                }
+            )
+        )
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun minusAssignList() {
+        val l = mutableTestValueClassListOf().also { it += list }
+        l -= mutableTestValueClassListOf(
+            TestValueClass(0UL),
+            TestValueClass(10UL),
+            TestValueClass(15UL)
+        )
+        assertEquals(list, l)
+        val l2 = mutableTestValueClassListOf()
+
+        l2 += TestValueClass(0UL)
+        l2 += TestValueClass(1UL)
+        l2 += TestValueClass(15UL)
+        l2 += TestValueClass(10UL)
+        l2 += TestValueClass(2UL)
+        l2 += TestValueClass(3UL)
+        l2 += TestValueClass(4UL)
+        l2 += TestValueClass(5UL)
+        l2 += TestValueClass(20UL)
+        l2 += TestValueClass(5UL)
+        l2 -= mutableTestValueClassListOf().also {
+            it += TestValueClass(20UL)
+            it += TestValueClass(0UL)
+            it += TestValueClass(15UL)
+            it += TestValueClass(10UL)
+            it += TestValueClass(5UL)
+        }
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun retainAll() {
+        assertFalse(
+            list.retainAll(
+                mutableTestValueClassListOf().also {
+                    it += TestValueClass(1UL)
+                    it += TestValueClass(2UL)
+                    it += TestValueClass(3UL)
+                    it += TestValueClass(4UL)
+                    it += TestValueClass(5UL)
+                    it += TestValueClass(6UL)
+                }
+            )
+        )
+        val l = mutableTestValueClassListOf()
+            l += TestValueClass(0UL)
+            l += TestValueClass(1UL)
+            l += TestValueClass(15UL)
+            l += TestValueClass(10UL)
+            l += TestValueClass(2UL)
+            l += TestValueClass(3UL)
+            l += TestValueClass(4UL)
+            l += TestValueClass(5UL)
+            l += TestValueClass(20UL)
+        assertTrue(
+            l.retainAll(
+                mutableTestValueClassListOf().also {
+                    it += TestValueClass(1UL)
+                    it += TestValueClass(2UL)
+                    it += TestValueClass(3UL)
+                    it += TestValueClass(4UL)
+                    it += TestValueClass(5UL)
+                    it += TestValueClass(6UL)
+                }
+            )
+        )
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeRange() {
+        val l = mutableTestValueClassListOf()
+        l += TestValueClass(1UL)
+        l += TestValueClass(9UL)
+        l += TestValueClass(7UL)
+        l += TestValueClass(6UL)
+        l += TestValueClass(2UL)
+        l += TestValueClass(3UL)
+        l += TestValueClass(4UL)
+        l += TestValueClass(5UL)
+        l.removeRange(1, 4)
+        assertEquals(list, l)
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(6, 6)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(100, 200)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(-1, 0)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            l.removeRange(3, 2)
+        }
+    }
+
+    @Test
+    fun testEmptyTestValueClassList() {
+        val l = emptyTestValueClassList()
+        assertEquals(0, l.size)
+    }
+
+    @Test
+    fun testValueClassListOfEmpty() {
+        val l = testValueClassListOf()
+        assertEquals(0, l.size)
+    }
+
+    @Test
+    fun testValueClassListOfOneValue() {
+        val l = testValueClassListOf(TestValueClass(2UL))
+        assertEquals(1, l.size)
+        assertEquals(TestValueClass(2UL), l[0])
+    }
+
+    @Test
+    fun testValueClassListOfTwoValues() {
+        val l = testValueClassListOf(TestValueClass(2UL), TestValueClass(1UL))
+        assertEquals(2, l.size)
+        assertEquals(TestValueClass(2UL), l[0])
+        assertEquals(TestValueClass(1UL), l[1])
+    }
+
+    @Test
+    fun testValueClassListOfThreeValues() {
+        val l = testValueClassListOf(TestValueClass(2UL), TestValueClass(10UL), TestValueClass(1UL))
+        assertEquals(3, l.size)
+        assertEquals(TestValueClass(2UL), l[0])
+        assertEquals(TestValueClass(10UL), l[1])
+        assertEquals(TestValueClass(1UL), l[2])
+    }
+
+    @Test
+    fun mutableTestValueClassListOfOneValue() {
+        val l = mutableTestValueClassListOf(TestValueClass(2UL))
+        assertEquals(1, l.size)
+        assertEquals(1, l.capacity)
+        assertEquals(TestValueClass(2UL), l[0])
+    }
+
+    @Test
+    fun mutableTestValueClassListOfTwoValues() {
+        val l = mutableTestValueClassListOf(TestValueClass(2UL), TestValueClass(1UL))
+        assertEquals(2, l.size)
+        assertEquals(2, l.capacity)
+        assertEquals(TestValueClass(2UL), l[0])
+        assertEquals(TestValueClass(1UL), l[1])
+    }
+
+    @Test
+    fun mutableTestValueClassListOfThreeValues() {
+        val l = mutableTestValueClassListOf(
+            TestValueClass(2UL),
+            TestValueClass(10UL),
+            TestValueClass(1UL)
+        )
+        assertEquals(3, l.size)
+        assertEquals(3, l.capacity)
+        assertEquals(TestValueClass(2UL), l[0])
+        assertEquals(TestValueClass(10UL), l[1])
+        assertEquals(TestValueClass(1UL), l[2])
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ValueClassSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ValueClassSetTest.kt
new file mode 100644
index 0000000..efe9085
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ValueClassSetTest.kt
@@ -0,0 +1,535 @@
+/*
+ * 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.collection
+
+import androidx.collection.template.MutableTestValueClassSet
+import androidx.collection.template.TestValueClassSet
+import androidx.collection.template.emptyTestValueClassSet
+import androidx.collection.template.mutableTestValueClassSetOf
+import androidx.collection.template.testValueClassSetOf
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+
+// ValueClassSet was created with:
+// sed -e "s/PACKAGE/androidx.collection.template/" -e "s/VALUE_CLASS/TestValueClass/g" \
+//     -e "s/vALUE_CLASS/testValueClass/g" -e "s/BACKING_PROPERTY/value.toLong()/g" \
+//     -e "s/TO_PARAM/.toULong()/g" -e "s/PRIMITIVE/Long/g" -e "s/VALUE_PKG/androidx.collection/" \
+//     collection/collection/template/ValueClassSet.kt.template \
+//     > collection/collection/src/commonTest/kotlin/androidx/collection/template/TestValueClassSet.kt
+
+@OptIn(ExperimentalUnsignedTypes::class)
+class ValueClassSetTest {
+    @Test
+    fun emptyTestValueClassSetConstructor() {
+        val set = MutableTestValueClassSet()
+        assertEquals(7, set.capacity)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun immutableEmptyTestValueClassSet() {
+        val set: TestValueClassSet = emptyTestValueClassSet()
+        assertEquals(0, set.capacity)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun zeroCapacityTestValueClassSet() {
+        val set = MutableTestValueClassSet(0)
+        assertEquals(0, set.capacity)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun emptyTestValueClassSetWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val set = MutableTestValueClassSet(1800)
+        assertEquals(4095, set.capacity)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun mutableTestValueClassSetBuilder() {
+        val empty = mutableTestValueClassSetOf()
+        assertEquals(0, empty.size)
+
+        val withElements = mutableTestValueClassSetOf(TestValueClass(1UL), TestValueClass(2UL))
+        assertEquals(2, withElements.size)
+        assertTrue(TestValueClass(1UL) in withElements)
+        assertTrue(TestValueClass(2UL) in withElements)
+    }
+
+    @Test
+    fun addToTestValueClassSet() {
+        val set = MutableTestValueClassSet()
+        set += TestValueClass(1UL)
+        assertTrue(set.add(TestValueClass(2UL)))
+
+        assertEquals(2, set.size)
+        val elements = ULongArray(2)
+        var index = 0
+        set.forEach { element ->
+            elements[index++] = element.value
+        }
+        elements.sort()
+        assertEquals(1UL, elements[0])
+        assertEquals(2UL, elements[1])
+    }
+
+    @Test
+    fun addToSizedTestValueClassSet() {
+        val set = MutableTestValueClassSet(12)
+        set += TestValueClass(1UL)
+
+        assertEquals(1, set.size)
+        assertEquals(TestValueClass(1UL), set.first())
+    }
+
+    @Test
+    fun addExistingElement() {
+        val set = MutableTestValueClassSet(12)
+        set += TestValueClass(1UL)
+        assertFalse(set.add(TestValueClass(1UL)))
+        set += TestValueClass(1UL)
+
+        assertEquals(1, set.size)
+        assertEquals(TestValueClass(1UL), set.first())
+    }
+
+    @Test
+    fun addAllTestValueClassSet() {
+        val set = mutableTestValueClassSetOf(TestValueClass(1UL))
+        assertFalse(set.addAll(mutableTestValueClassSetOf(TestValueClass(1UL))))
+        assertEquals(1, set.size)
+        assertTrue(set.addAll(mutableTestValueClassSetOf(TestValueClass(1UL), TestValueClass(2UL))))
+        assertEquals(2, set.size)
+        assertTrue(TestValueClass(2UL) in set)
+    }
+
+    @Test
+    fun plusAssignTestValueClassSet() {
+        val set = mutableTestValueClassSetOf(TestValueClass(1UL))
+        set += mutableTestValueClassSetOf(TestValueClass(1UL))
+        assertEquals(1, set.size)
+        set += mutableTestValueClassSetOf(TestValueClass(1UL), TestValueClass(2UL))
+        assertEquals(2, set.size)
+        assertTrue(TestValueClass(2UL) in set)
+    }
+
+    @Test
+    fun firstWithValue() {
+        val set = MutableTestValueClassSet()
+        set += TestValueClass(1UL)
+        set += TestValueClass(2UL)
+        var element = TestValueClass(ULong.MAX_VALUE)
+        var otherElement = TestValueClass(ULong.MAX_VALUE)
+        set.forEach { if (element.value == ULong.MAX_VALUE) element = it else otherElement = it }
+        assertEquals(element, set.first())
+        set -= element
+        assertEquals(otherElement, set.first())
+    }
+
+    @Test
+    fun firstEmpty() {
+        assertFailsWith(NoSuchElementException::class) {
+            val set = MutableTestValueClassSet()
+            set.first()
+        }
+    }
+
+    @Test
+    fun firstMatching() {
+        val set = MutableTestValueClassSet()
+        set += TestValueClass(1UL)
+        set += TestValueClass(2UL)
+        assertEquals(TestValueClass(1UL), set.first { it.value < 2UL })
+        assertEquals(TestValueClass(2UL), set.first { it.value > 1UL })
+    }
+
+    @Test
+    fun firstMatchingEmpty() {
+        assertFailsWith(NoSuchElementException::class) {
+            val set = MutableTestValueClassSet()
+            set.first { it.value > 0UL }
+        }
+    }
+
+    @Test
+    fun firstMatchingNoMatch() {
+        assertFailsWith(NoSuchElementException::class) {
+            val set = MutableTestValueClassSet()
+            set += TestValueClass(1UL)
+            set += TestValueClass(2UL)
+            set.first { it.value < 0UL }
+        }
+    }
+
+    @Test
+    fun remove() {
+        val set = MutableTestValueClassSet()
+        assertFalse(set.remove(TestValueClass(1UL)))
+
+        set += TestValueClass(1UL)
+        assertTrue(set.remove(TestValueClass(1UL)))
+        assertEquals(0, set.size)
+
+        set += TestValueClass(1UL)
+        set -= TestValueClass(1UL)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val set = MutableTestValueClassSet(6)
+        set += TestValueClass(1UL)
+        set += TestValueClass(5UL)
+        set += TestValueClass(6UL)
+        set += TestValueClass(9UL)
+        set += TestValueClass(11UL)
+        set += TestValueClass(13UL)
+
+        // Removing all the entries will mark the medata as deleted
+        set.remove(TestValueClass(1UL))
+        set.remove(TestValueClass(5UL))
+        set.remove(TestValueClass(6UL))
+        set.remove(TestValueClass(9UL))
+        set.remove(TestValueClass(11UL))
+        set.remove(TestValueClass(13UL))
+
+        assertEquals(0, set.size)
+
+        val capacity = set.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        set += TestValueClass(3UL)
+
+        assertEquals(1, set.size)
+        assertEquals(capacity, set.capacity)
+    }
+
+    @Test
+    fun removeAllTestValueClassSet() {
+        val set = mutableTestValueClassSetOf(TestValueClass(1UL), TestValueClass(2UL))
+        assertFalse(
+            set.removeAll(
+                mutableTestValueClassSetOf(
+                    TestValueClass(3UL),
+                    TestValueClass(5UL)
+                )
+            )
+        )
+        assertEquals(2, set.size)
+        assertTrue(
+            set.removeAll(
+                mutableTestValueClassSetOf(
+                    TestValueClass(3UL),
+                    TestValueClass(1UL),
+                    TestValueClass(5UL)
+                )
+            )
+        )
+        assertEquals(1, set.size)
+        assertFalse(TestValueClass(1UL) in set)
+    }
+
+    @Test
+    fun minusAssignTestValueClassSet() {
+        val set = mutableTestValueClassSetOf(TestValueClass(1UL), TestValueClass(2UL))
+        set -= mutableTestValueClassSetOf(TestValueClass(3UL), TestValueClass(5UL))
+        assertEquals(2, set.size)
+        set -= mutableTestValueClassSetOf(
+            TestValueClass(3UL),
+            TestValueClass(1UL),
+            TestValueClass(5UL)
+        )
+        assertEquals(1, set.size)
+        assertFalse(TestValueClass(1UL) in set)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val set = MutableTestValueClassSet()
+
+        for (i in 0 until 1700) {
+            set += TestValueClass(i.toULong())
+        }
+
+        assertEquals(1700, set.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val set = MutableTestValueClassSet()
+
+            for (j in 0 until i) {
+                set += TestValueClass(j.toULong())
+            }
+
+            val elements = ULongArray(i)
+            var index = 0
+            set.forEach { element ->
+                elements[index++] = element.value
+            }
+            elements.sort()
+
+            index = 0
+            elements.forEach { element ->
+                assertEquals(element, index.toULong())
+                index++
+            }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val set = MutableTestValueClassSet()
+
+        for (i in 0 until 32) {
+            set += TestValueClass(i.toULong())
+        }
+
+        val capacity = set.capacity
+        set.clear()
+
+        assertEquals(0, set.size)
+        assertEquals(capacity, set.capacity)
+    }
+
+    @Test
+    fun string() {
+        val set = MutableTestValueClassSet()
+        assertEquals("[]", set.toString())
+
+        set += TestValueClass(1UL)
+        set += TestValueClass(5UL)
+        assertTrue(
+            "[>1<, >5<]" == set.toString() ||
+                "[>5<, >1<]" == set.toString()
+        )
+    }
+
+    @Test
+    fun equals() {
+        val set = MutableTestValueClassSet()
+        set += TestValueClass(1UL)
+        set += TestValueClass(5UL)
+
+        assertFalse(set.equals(null))
+        assertEquals(set, set)
+
+        val set2 = MutableTestValueClassSet()
+        set2 += TestValueClass(5UL)
+
+        assertNotEquals(set, set2)
+
+        set2 += TestValueClass(1UL)
+        assertEquals(set, set2)
+    }
+
+    @Test
+    fun contains() {
+        val set = MutableTestValueClassSet()
+        set += TestValueClass(1UL)
+        set += TestValueClass(5UL)
+
+        assertTrue(set.contains(TestValueClass(1UL)))
+        assertTrue(set.contains(TestValueClass(5UL)))
+        assertFalse(set.contains(TestValueClass(2UL)))
+    }
+
+    @Test
+    fun empty() {
+        val set = MutableTestValueClassSet()
+        assertTrue(set.isEmpty())
+        assertFalse(set.isNotEmpty())
+        assertTrue(set.none())
+        assertFalse(set.any())
+
+        set += TestValueClass(1UL)
+
+        assertFalse(set.isEmpty())
+        assertTrue(set.isNotEmpty())
+        assertTrue(set.any())
+        assertFalse(set.none())
+    }
+
+    @Test
+    fun count() {
+        val set = MutableTestValueClassSet()
+        assertEquals(0, set.count())
+
+        set += TestValueClass(1UL)
+        assertEquals(1, set.count())
+
+        set += TestValueClass(5UL)
+        set += TestValueClass(6UL)
+        set += TestValueClass(9UL)
+        set += TestValueClass(11UL)
+        set += TestValueClass(13UL)
+
+        assertEquals(2, set.count { it.value < 6UL })
+        assertEquals(0, set.count { it.value < 0UL })
+    }
+
+    @Test
+    fun any() {
+        val set = MutableTestValueClassSet()
+        set += TestValueClass(1UL)
+        set += TestValueClass(5UL)
+        set += TestValueClass(6UL)
+        set += TestValueClass(9UL)
+        set += TestValueClass(11UL)
+        set += TestValueClass(13UL)
+
+        assertTrue(set.any { it.value >= 11UL })
+        assertFalse(set.any { it.value < 0UL })
+    }
+
+    @Test
+    fun all() {
+        val set = MutableTestValueClassSet()
+        set += TestValueClass(1UL)
+        set += TestValueClass(5UL)
+        set += TestValueClass(6UL)
+        set += TestValueClass(9UL)
+        set += TestValueClass(11UL)
+        set += TestValueClass(13UL)
+
+        assertTrue(set.all { it.value > 0UL })
+        assertFalse(set.all { it.value < 0UL })
+    }
+
+    @Test
+    fun trim() {
+        val set = mutableTestValueClassSetOf().also {
+            it += TestValueClass(1UL)
+            it += TestValueClass(2UL)
+            it += TestValueClass(3UL)
+            it += TestValueClass(4UL)
+            it += TestValueClass(5UL)
+            it += TestValueClass(7UL)
+        }
+        val capacity = set.capacity
+        assertEquals(0, set.trim())
+        set.clear()
+        assertEquals(capacity, set.trim())
+        assertEquals(0, set.capacity)
+
+        set += TestValueClass(1UL)
+        set += TestValueClass(2UL)
+        set += TestValueClass(3UL)
+        set += TestValueClass(4UL)
+        set += TestValueClass(5UL)
+        set += TestValueClass(7UL)
+        set += TestValueClass(6UL)
+        set += TestValueClass(8UL)
+        set += TestValueClass(9UL)
+        set += TestValueClass(10UL)
+        set += TestValueClass(11UL)
+        set += TestValueClass(12UL)
+        set += TestValueClass(13UL)
+        set += TestValueClass(14UL)
+
+        set -= TestValueClass(6UL)
+        set -= TestValueClass(8UL)
+        set -= TestValueClass(9UL)
+        set -= TestValueClass(10UL)
+        set -= TestValueClass(11UL)
+        set -= TestValueClass(12UL)
+        set -= TestValueClass(13UL)
+        set -= TestValueClass(14UL)
+        assertTrue(set.trim() > 0)
+        assertEquals(capacity, set.capacity)
+    }
+
+    @Test
+    fun testValueClassSetOfEmpty() {
+        assertEquals(emptyTestValueClassSet(), testValueClassSetOf())
+        assertEquals(0, testValueClassSetOf().size)
+    }
+
+    @Test
+    fun testValueClassSetOfOne() {
+        val set = testValueClassSetOf(TestValueClass(1UL))
+        assertEquals(1, set.size)
+        assertEquals(TestValueClass(1UL), set.first())
+    }
+
+    @Test
+    fun testValueClassSetOfTwo() {
+        val set = testValueClassSetOf(TestValueClass(1UL), TestValueClass(2UL))
+        assertEquals(2, set.size)
+        assertTrue(TestValueClass(1UL) in set)
+        assertTrue(TestValueClass(2UL) in set)
+        assertFalse(TestValueClass(5UL) in set)
+    }
+
+    @Test
+    fun testValueClassSetOfThree() {
+        val set = testValueClassSetOf(TestValueClass(1UL), TestValueClass(2UL), TestValueClass(3UL))
+        assertEquals(3, set.size)
+        assertTrue(TestValueClass(1UL) in set)
+        assertTrue(TestValueClass(2UL) in set)
+        assertTrue(TestValueClass(3UL) in set)
+        assertFalse(TestValueClass(5UL) in set)
+    }
+
+    @Test
+    fun mutableTestValueClassSetOfOne() {
+        val set = mutableTestValueClassSetOf(TestValueClass(1UL))
+        assertEquals(1, set.size)
+        assertEquals(TestValueClass(1UL), set.first())
+    }
+
+    @Test
+    fun mutableTestValueClassSetOfTwo() {
+        val set = mutableTestValueClassSetOf(TestValueClass(1UL), TestValueClass(2UL))
+        assertEquals(2, set.size)
+        assertTrue(TestValueClass(1UL) in set)
+        assertTrue(TestValueClass(2UL) in set)
+        assertFalse(TestValueClass(5UL) in set)
+    }
+
+    @Test
+    fun mutableTestValueClassSetOfThree() {
+        val set = mutableTestValueClassSetOf(
+            TestValueClass(1UL),
+            TestValueClass(2UL),
+            TestValueClass(3UL)
+        )
+        assertEquals(3, set.size)
+        assertTrue(TestValueClass(1UL) in set)
+        assertTrue(TestValueClass(2UL) in set)
+        assertTrue(TestValueClass(3UL) in set)
+        assertFalse(TestValueClass(5UL) in set)
+    }
+
+    @Test
+    fun asTestValueClassSet() {
+        assertEquals(
+            testValueClassSetOf(TestValueClass(1UL)),
+            mutableTestValueClassSetOf(TestValueClass(1UL)).asTestValueClassSet()
+        )
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/template/TestValueClassList.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/template/TestValueClassList.kt
new file mode 100644
index 0000000..10c14dd
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/template/TestValueClassList.kt
@@ -0,0 +1,935 @@
+/*
+ * 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.
+ */
+@file:Suppress("NOTHING_TO_INLINE", "RedundantVisibilityModifier", "UnusedImport")
+/* ktlint-disable max-line-length */
+/* ktlint-disable import-ordering */
+
+package androidx.collection.template
+
+import androidx.collection.LongList
+import androidx.collection.MutableLongList
+import androidx.collection.emptyLongList
+import androidx.collection.mutableLongListOf
+import androidx.collection.TestValueClass
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.jvm.JvmInline
+
+// To use this template, you must substitute several strings. You can copy this and search/replace
+// or use a sed command. These properties must be changed:
+// * androidx.collection.template - target package (e.g. androidx.compose.ui.ui.collection)
+// * androidx.collection - package in which the value class resides
+// * TestValueClass - the value class contained in the list (e.g. Color or Offset)
+// * testValueClass - the value class, with the first letter lower case (e.g. color or offset)
+// * value.toLong() - the field in TestValueClass containing the backing primitive (e.g. packedValue)
+// * Long - the primitive type of the backing list (e.g. Long or Float)
+// * .toULong() - an operation done on the primitive to convert to the value class parameter
+//
+// For example, to create a ColorList:
+// sed -e "s/androidx.collection.template/androidx.compose.ui.graphics/" -e "s/TestValueClass/Color/g" \
+//     -e "s/testValueClass/color/g" -e "s/value.toLong()/value.toLong()/g" -e "s/Long/Long/g" \
+//     -e "s/.toULong()/.toULong()/g" -e "s/androidx.collection/androidx.compose.ui.graphics/g" \
+//     collection/collection/template/ValueClassList.kt.template \
+//     > compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ColorList.kt
+
+/**
+ * [TestValueClassList] is a [List]-like collection for [TestValueClass] values. It allows retrieving
+ * the elements without boxing. [TestValueClassList] is always backed by a [MutableTestValueClassList],
+ * its [MutableList]-like subclass.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ */
+@OptIn(ExperimentalContracts::class)
+@JvmInline
+internal value class TestValueClassList(val list: LongList) {
+    /**
+     * The number of elements in the [TestValueClassList].
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public inline val size: Int get() = list.size
+
+    /**
+     * Returns the last valid index in the [TestValueClassList]. This can be `-1` when the list is empty.
+     */
+    @get:androidx.annotation.IntRange(from = -1)
+    public inline val lastIndex: Int get() = list.lastIndex
+
+    /**
+     * Returns an [IntRange] of the valid indices for this [TestValueClassList].
+     */
+    public inline val indices: IntRange get() = list.indices
+
+    /**
+     * Returns `true` if the collection has no elements in it.
+     */
+    public inline fun none(): Boolean = list.none()
+
+    /**
+     * Returns `true` if there's at least one element in the collection.
+     */
+    public inline fun any(): Boolean = list.any()
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate].
+     */
+    public inline fun any(predicate: (element: TestValueClass) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return list.any { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate] while
+     * iterating in the reverse order.
+     */
+    public inline fun reversedAny(predicate: (element: TestValueClass) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return list.reversedAny { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns `true` if the [TestValueClassList] contains [element] or `false` otherwise.
+     */
+    public inline operator fun contains(element: TestValueClass): Boolean =
+        list.contains(element.value.toLong())
+
+    /**
+     * Returns `true` if the [TestValueClassList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public inline fun containsAll(elements: TestValueClassList): Boolean =
+        list.containsAll(elements.list)
+
+    /**
+     * Returns `true` if the [TestValueClassList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public inline fun containsAll(elements: MutableTestValueClassList): Boolean =
+        list.containsAll(elements.list)
+
+    /**
+     * Returns the number of elements in this list.
+     */
+    public inline fun count(): Int = list.count()
+
+    /**
+     * Counts the number of elements matching [predicate].
+     * @return The number of elements in this list for which [predicate] returns true.
+     */
+    public inline fun count(predicate: (element: TestValueClass) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return list.count { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns the first element in the [TestValueClassList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public inline fun first(): TestValueClass = TestValueClass(list.first().toULong())
+
+    /**
+     * Returns the first element in the [TestValueClassList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfFirst
+     */
+    public inline fun first(predicate: (element: TestValueClass) -> Boolean): TestValueClass {
+        contract { callsInPlace(predicate) }
+        return TestValueClass(list.first { predicate(TestValueClass(it.toULong())) }.toULong())
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [TestValueClassList] in order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes current accumulator value and an element, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> fold(initial: R, operation: (acc: R, element: TestValueClass) -> R): R {
+        contract { callsInPlace(operation) }
+        return list.fold(initial) { acc, element ->
+            operation(acc, TestValueClass(element.toULong()))
+        }
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [TestValueClassList] in order.
+     */
+    public inline fun <R> foldIndexed(
+        initial: R,
+        operation: (index: Int, acc: R, element: TestValueClass) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        return list.foldIndexed(initial) { index, acc, element ->
+            operation(index, acc, TestValueClass(element.toULong()))
+        }
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [TestValueClassList] in reverse order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes an element and the current accumulator value, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> foldRight(initial: R, operation: (element: TestValueClass, acc: R) -> R): R {
+        contract { callsInPlace(operation) }
+        return list.foldRight(initial) { element, acc ->
+            operation(TestValueClass(element.toULong()), acc)
+        }
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [TestValueClassList] in reverse order.
+     */
+    public inline fun <R> foldRightIndexed(
+        initial: R,
+        operation: (index: Int, element: TestValueClass, acc: R) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        return list.foldRightIndexed(initial) { index, element, acc ->
+            operation(index, TestValueClass(element.toULong()), acc)
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [TestValueClassList], in order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEach(block: (element: TestValueClass) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEach { block(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Calls [block] for each element in the [TestValueClassList] along with its index, in order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachIndexed(block: (index: Int, element: TestValueClass) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEachIndexed { index, element ->
+            block(index, TestValueClass(element.toULong()))
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [TestValueClassList] in reverse order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEachReversed(block: (element: TestValueClass) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEachReversed { block(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Calls [block] for each element in the [TestValueClassList] along with its index, in reverse
+     * order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachReversedIndexed(block: (index: Int, element: TestValueClass) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEachReversedIndexed { index, element ->
+            block(index, TestValueClass(element.toULong()))
+        }
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public inline operator fun get(
+        @androidx.annotation.IntRange(from = 0) index: Int
+    ): TestValueClass = TestValueClass(list[index].toULong())
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public inline fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): TestValueClass =
+        TestValueClass(list[index].toULong())
+
+    /**
+     * Returns the element at the given [index] or [defaultValue] if [index] is out of bounds
+     * of the collection.
+     * @param index The index of the element whose value should be returned
+     * @param defaultValue A lambda to call with [index] as a parameter to return a value at
+     * an index not in the list.
+     */
+    public inline fun elementAtOrElse(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        defaultValue: (index: Int) -> TestValueClass
+    ): TestValueClass =
+        TestValueClass(list.elementAtOrElse(index) { defaultValue(it).value.toLong() }.toULong())
+
+    /**
+     * Returns the index of [element] in the [TestValueClassList] or `-1` if [element] is not there.
+     */
+    public inline fun indexOf(element: TestValueClass): Int =
+        list.indexOf(element.value.toLong())
+
+    /**
+     * Returns the index if the first element in the [TestValueClassList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfFirst(predicate: (element: TestValueClass) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return list.indexOfFirst { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns the index if the last element in the [TestValueClassList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfLast(predicate: (element: TestValueClass) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return list.indexOfLast { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns `true` if the [TestValueClassList] has no elements in it or `false` otherwise.
+     */
+    public inline fun isEmpty(): Boolean = list.isEmpty()
+
+    /**
+     * Returns `true` if there are elements in the [TestValueClassList] or `false` if it is empty.
+     */
+    public inline fun isNotEmpty(): Boolean = list.isNotEmpty()
+
+    /**
+     * Returns the last element in the [TestValueClassList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public inline fun last(): TestValueClass = TestValueClass(list.last().toULong())
+
+    /**
+     * Returns the last element in the [TestValueClassList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfLast
+     */
+    public inline fun last(predicate: (element: TestValueClass) -> Boolean): TestValueClass {
+        contract { callsInPlace(predicate) }
+        return TestValueClass(list.last { predicate(TestValueClass(it.toULong())) }.toULong())
+    }
+
+    /**
+     * Returns the index of the last element in the [TestValueClassList] that is the same as
+     * [element] or `-1` if no elements match.
+     */
+    public inline fun lastIndexOf(element: TestValueClass): Int =
+        list.lastIndexOf(element.value.toLong())
+
+    /**
+     * Returns a String representation of the list, surrounded by "[]" and each element
+     * separated by ", ".
+     */
+    override fun toString(): String {
+        if (isEmpty()) {
+            return "[]"
+        }
+        return buildString {
+            append('[')
+            forEachIndexed { index: Int, element: TestValueClass ->
+                if (index != 0) {
+                    append(',').append(' ')
+                }
+                append(element)
+            }
+            append(']')
+        }
+    }
+}
+
+/**
+ * [MutableTestValueClassList] is a [MutableList]-like collection for [TestValueClass] values.
+ * It allows storing and retrieving the elements without boxing. Immutable
+ * access is available through its base class [TestValueClassList], which has a [List]-like
+ * interface.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ *
+ * @constructor Creates a [MutableTestValueClassList] with a [capacity] of `initialCapacity`.
+ */
+@OptIn(ExperimentalContracts::class)
+@JvmInline
+internal value class MutableTestValueClassList(val list: MutableLongList) {
+    public constructor(initialCapacity: Int = 16) : this(MutableLongList(initialCapacity))
+
+    /**
+     * The number of elements in the [TestValueClassList].
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public inline val size: Int get() = list.size
+
+    /**
+     * Returns the last valid index in the [TestValueClassList]. This can be `-1` when the list is empty.
+     */
+    @get:androidx.annotation.IntRange(from = -1)
+    public inline val lastIndex: Int get() = list.lastIndex
+
+    /**
+     * Returns an [IntRange] of the valid indices for this [TestValueClassList].
+     */
+    public inline val indices: IntRange get() = list.indices
+
+    /**
+     * Returns `true` if the collection has no elements in it.
+     */
+    public inline fun none(): Boolean = list.none()
+
+    /**
+     * Returns `true` if there's at least one element in the collection.
+     */
+    public inline fun any(): Boolean = list.any()
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate].
+     */
+    public inline fun any(predicate: (element: TestValueClass) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return list.any { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate] while
+     * iterating in the reverse order.
+     */
+    public inline fun reversedAny(predicate: (element: TestValueClass) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return list.reversedAny { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns `true` if the [TestValueClassList] contains [element] or `false` otherwise.
+     */
+    public inline operator fun contains(element: TestValueClass): Boolean =
+        list.contains(element.value.toLong())
+
+    /**
+     * Returns `true` if the [TestValueClassList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public inline fun containsAll(elements: TestValueClassList): Boolean =
+        list.containsAll(elements.list)
+
+    /**
+     * Returns `true` if the [TestValueClassList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public inline fun containsAll(elements: MutableTestValueClassList): Boolean =
+        list.containsAll(elements.list)
+
+    /**
+     * Returns the number of elements in this list.
+     */
+    public inline fun count(): Int = list.count()
+
+    /**
+     * Counts the number of elements matching [predicate].
+     * @return The number of elements in this list for which [predicate] returns true.
+     */
+    public inline fun count(predicate: (element: TestValueClass) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return list.count { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns the first element in the [TestValueClassList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public inline fun first(): TestValueClass = TestValueClass(list.first().toULong())
+
+    /**
+     * Returns the first element in the [TestValueClassList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfFirst
+     */
+    public inline fun first(predicate: (element: TestValueClass) -> Boolean): TestValueClass {
+        contract { callsInPlace(predicate) }
+        return TestValueClass(list.first { predicate(TestValueClass(it.toULong())) }.toULong())
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [TestValueClassList] in order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes current accumulator value and an element, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> fold(initial: R, operation: (acc: R, element: TestValueClass) -> R): R {
+        contract { callsInPlace(operation) }
+        return list.fold(initial) { acc, element ->
+            operation(acc, TestValueClass(element.toULong()))
+        }
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [TestValueClassList] in order.
+     */
+    public inline fun <R> foldIndexed(
+        initial: R,
+        operation: (index: Int, acc: R, element: TestValueClass) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        return list.foldIndexed(initial) { index, acc, element ->
+            operation(index, acc, TestValueClass(element.toULong()))
+        }
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [TestValueClassList] in reverse order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes an element and the current accumulator value, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> foldRight(initial: R, operation: (element: TestValueClass, acc: R) -> R): R {
+        contract { callsInPlace(operation) }
+        return list.foldRight(initial) { element, acc ->
+            operation(TestValueClass(element.toULong()), acc)
+        }
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [TestValueClassList] in reverse order.
+     */
+    public inline fun <R> foldRightIndexed(
+        initial: R,
+        operation: (index: Int, element: TestValueClass, acc: R) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        return list.foldRightIndexed(initial) { index, element, acc ->
+            operation(index, TestValueClass(element.toULong()), acc)
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [TestValueClassList], in order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEach(block: (element: TestValueClass) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEach { block(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Calls [block] for each element in the [TestValueClassList] along with its index, in order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachIndexed(block: (index: Int, element: TestValueClass) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEachIndexed { index, element ->
+            block(index, TestValueClass(element.toULong()))
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [TestValueClassList] in reverse order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEachReversed(block: (element: TestValueClass) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEachReversed { block(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Calls [block] for each element in the [TestValueClassList] along with its index, in reverse
+     * order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachReversedIndexed(block: (index: Int, element: TestValueClass) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEachReversedIndexed { index, element ->
+            block(index, TestValueClass(element.toULong()))
+        }
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public inline operator fun get(
+        @androidx.annotation.IntRange(from = 0) index: Int
+    ): TestValueClass = TestValueClass(list[index].toULong())
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public inline fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): TestValueClass =
+        TestValueClass(list[index].toULong())
+
+    /**
+     * Returns the element at the given [index] or [defaultValue] if [index] is out of bounds
+     * of the collection.
+     * @param index The index of the element whose value should be returned
+     * @param defaultValue A lambda to call with [index] as a parameter to return a value at
+     * an index not in the list.
+     */
+    public inline fun elementAtOrElse(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        defaultValue: (index: Int) -> TestValueClass
+    ): TestValueClass =
+        TestValueClass(list.elementAtOrElse(index) { defaultValue(it).value.toLong() }.toULong())
+
+    /**
+     * Returns the index of [element] in the [TestValueClassList] or `-1` if [element] is not there.
+     */
+    public inline fun indexOf(element: TestValueClass): Int =
+        list.indexOf(element.value.toLong())
+
+    /**
+     * Returns the index if the first element in the [TestValueClassList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfFirst(predicate: (element: TestValueClass) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return list.indexOfFirst { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns the index if the last element in the [TestValueClassList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfLast(predicate: (element: TestValueClass) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return list.indexOfLast { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns `true` if the [TestValueClassList] has no elements in it or `false` otherwise.
+     */
+    public inline fun isEmpty(): Boolean = list.isEmpty()
+
+    /**
+     * Returns `true` if there are elements in the [TestValueClassList] or `false` if it is empty.
+     */
+    public inline fun isNotEmpty(): Boolean = list.isNotEmpty()
+
+    /**
+     * Returns the last element in the [TestValueClassList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public inline fun last(): TestValueClass = TestValueClass(list.last().toULong())
+
+    /**
+     * Returns the last element in the [TestValueClassList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfLast
+     */
+    public inline fun last(predicate: (element: TestValueClass) -> Boolean): TestValueClass {
+        contract { callsInPlace(predicate) }
+        return TestValueClass(list.last { predicate(TestValueClass(it.toULong())) }.toULong())
+    }
+
+    /**
+     * Returns the index of the last element in the [TestValueClassList] that is the same as
+     * [element] or `-1` if no elements match.
+     */
+    public inline fun lastIndexOf(element: TestValueClass): Int =
+        list.lastIndexOf(element.value.toLong())
+
+    /**
+     * Returns a String representation of the list, surrounded by "[]" and each element
+     * separated by ", ".
+     */
+    override fun toString(): String = asTestValueClassList().toString()
+
+    /**
+     * Returns a read-only interface to the list.
+     */
+    public inline fun asTestValueClassList(): TestValueClassList = TestValueClassList(list)
+
+    /**
+     * Returns the total number of elements that can be held before the [MutableTestValueClassList] must
+     * grow.
+     *
+     * @see ensureCapacity
+     */
+    public inline val capacity: Int
+        get() = list.capacity
+
+    /**
+     * Adds [element] to the [MutableTestValueClassList] and returns `true`.
+     */
+    public inline fun add(element: TestValueClass): Boolean =
+        list.add(element.value.toLong())
+
+    /**
+     * Adds [element] to the [MutableTestValueClassList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public inline fun add(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        element: TestValueClass
+    ) = list.add(index, element.value.toLong())
+
+    /**
+     * Adds all [elements] to the [MutableTestValueClassList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutableTestValueClassList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public inline fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: TestValueClassList
+    ): Boolean = list.addAll(index, elements.list)
+
+    /**
+     * Adds all [elements] to the [MutableTestValueClassList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutableTestValueClassList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public inline fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: MutableTestValueClassList
+    ): Boolean = list.addAll(index, elements.list)
+
+    /**
+     * Adds all [elements] to the end of the [MutableTestValueClassList] and returns `true` if the
+     * [MutableTestValueClassList] was changed or `false` if [elements] was empty.
+     */
+    public inline fun addAll(elements: TestValueClassList): Boolean = list.addAll(elements.list)
+
+    /**
+     * Adds all [elements] to the end of the [MutableTestValueClassList].
+     */
+    public inline operator fun plusAssign(elements: TestValueClassList) =
+        list.plusAssign(elements.list)
+
+    /**
+     * Adds all [elements] to the end of the [MutableTestValueClassList] and returns `true` if the
+     * [MutableTestValueClassList] was changed or `false` if [elements] was empty.
+     */
+    public inline fun addAll(elements: MutableTestValueClassList): Boolean = list.addAll(elements.list)
+
+    /**
+     * Adds all [elements] to the end of the [MutableTestValueClassList].
+     */
+    public inline operator fun plusAssign(elements: MutableTestValueClassList) =
+        list.plusAssign(elements.list)
+
+    /**
+     * Removes all elements in the [MutableTestValueClassList]. The storage isn't released.
+     * @see trim
+     */
+    public inline fun clear() = list.clear()
+
+    /**
+     * Reduces the internal storage. If [capacity] is greater than [minCapacity] and [size], the
+     * internal storage is reduced to the maximum of [size] and [minCapacity].
+     * @see ensureCapacity
+     */
+    public inline fun trim(minCapacity: Int = size) = list.trim(minCapacity)
+
+    /**
+     * Ensures that there is enough space to store [capacity] elements in the [MutableTestValueClassList].
+     * @see trim
+     */
+    public inline fun ensureCapacity(capacity: Int) = list.ensureCapacity(capacity)
+
+    /**
+     * [add] [element] to the [MutableTestValueClassList].
+     */
+    public inline operator fun plusAssign(element: TestValueClass) =
+        list.plusAssign(element.value.toLong())
+
+    /**
+     * [remove] [element] from the [MutableTestValueClassList]
+     */
+    public inline operator fun minusAssign(element: TestValueClass) =
+        list.minusAssign(element.value.toLong())
+
+    /**
+     * Removes [element] from the [MutableTestValueClassList]. If [element] was in the [MutableTestValueClassList]
+     * and was removed, `true` will be returned, or `false` will be returned if the element
+     * was not found.
+     */
+    public inline fun remove(element: TestValueClass): Boolean =
+        list.remove(element.value.toLong())
+
+    /**
+     * Removes all [elements] from the [MutableTestValueClassList] and returns `true` if anything was removed.
+     */
+    public inline fun removeAll(elements: TestValueClassList): Boolean =
+        list.removeAll(elements.list)
+
+    /**
+     * Removes all [elements] from the [MutableTestValueClassList].
+     */
+    public inline operator fun minusAssign(elements: TestValueClassList) =
+        list.minusAssign(elements.list)
+
+    /**
+     * Removes all [elements] from the [MutableTestValueClassList] and returns `true` if anything was removed.
+     */
+    public inline fun removeAll(elements: MutableTestValueClassList): Boolean =
+        list.removeAll(elements.list)
+
+    /**
+     * Removes all [elements] from the [MutableTestValueClassList].
+     */
+    public inline operator fun minusAssign(elements: MutableTestValueClassList) =
+        list.minusAssign(elements.list)
+
+    /**
+     * Removes the element at the given [index] and returns it.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public inline fun removeAt(@androidx.annotation.IntRange(from = 0) index: Int): TestValueClass =
+        TestValueClass(list.removeAt(index).toULong())
+
+    /**
+     * Removes items from index [start] (inclusive) to [end] (exclusive).
+     * @throws IndexOutOfBoundsException if [start] or [end] isn't between 0 and [size], inclusive
+     * @throws IllegalArgumentException if [start] is greater than [end]
+     */
+    public inline fun removeRange(
+        @androidx.annotation.IntRange(from = 0) start: Int,
+        @androidx.annotation.IntRange(from = 0) end: Int
+    ) = list.removeRange(start, end)
+
+    /**
+     * Keeps only [elements] in the [MutableTestValueClassList] and removes all other values.
+     * @return `true` if the [MutableTestValueClassList] has changed.
+     */
+    public inline fun retainAll(elements: TestValueClassList): Boolean =
+        list.retainAll(elements.list)
+
+    /**
+     * Keeps only [elements] in the [MutableTestValueClassList] and removes all other values.
+     * @return `true` if the [MutableTestValueClassList] has changed.
+     */
+    public inline fun retainAll(elements: MutableTestValueClassList): Boolean =
+        list.retainAll(elements.list)
+
+    /**
+     * Sets the value at [index] to [element].
+     * @return the previous value set at [index]
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public inline operator fun set(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        element: TestValueClass
+    ): TestValueClass = TestValueClass(list.set(index, element.value.toLong()).toULong())
+}
+
+/**
+ * @return a read-only [TestValueClassList] with nothing in it.
+ */
+internal inline fun emptyTestValueClassList(): TestValueClassList = TestValueClassList(emptyLongList())
+
+/**
+ * @return a read-only [TestValueClassList] with nothing in it.
+ */
+internal inline fun testValueClassListOf(): TestValueClassList = TestValueClassList(emptyLongList())
+
+/**
+ * @return a new read-only [TestValueClassList] with [element1] as the only item in the list.
+ */
+internal inline fun testValueClassListOf(element1: TestValueClass): TestValueClassList =
+    TestValueClassList(mutableLongListOf(element1.value.toLong()))
+
+/**
+ * @return a new read-only [TestValueClassList] with 2 elements, [element1] and [element2], in order.
+ */
+internal inline fun testValueClassListOf(element1: TestValueClass, element2: TestValueClass): TestValueClassList =
+    TestValueClassList(
+        mutableLongListOf(
+            element1.value.toLong(),
+            element2.value.toLong()
+        )
+    )
+
+/**
+ * @return a new read-only [TestValueClassList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+internal inline fun testValueClassListOf(
+        element1: TestValueClass,
+        element2: TestValueClass,
+        element3: TestValueClass
+): TestValueClassList = TestValueClassList(
+    mutableLongListOf(
+        element1.value.toLong(),
+        element2.value.toLong(),
+        element3.value.toLong()
+    )
+)
+
+/**
+ * @return a new empty [MutableTestValueClassList] with the default capacity.
+ */
+internal inline fun mutableTestValueClassListOf(): MutableTestValueClassList =
+    MutableTestValueClassList(MutableLongList())
+
+/**
+ * @return a new [MutableTestValueClassList] with [element1] as the only item in the list.
+ */
+internal inline fun mutableTestValueClassListOf(element1: TestValueClass): MutableTestValueClassList =
+    MutableTestValueClassList(mutableLongListOf(element1.value.toLong()))
+
+/**
+ * @return a new [MutableTestValueClassList] with 2 elements, [element1] and [element2], in order.
+ */
+internal inline fun mutableTestValueClassListOf(
+        element1: TestValueClass,
+        element2: TestValueClass
+    ): MutableTestValueClassList = MutableTestValueClassList(
+        mutableLongListOf(
+            element1.value.toLong(),
+            element2.value.toLong()
+        )
+    )
+
+/**
+ * @return a new [MutableTestValueClassList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+internal inline fun mutableTestValueClassListOf(
+        element1: TestValueClass,
+        element2: TestValueClass,
+        element3: TestValueClass
+): MutableTestValueClassList = MutableTestValueClassList(
+    mutableLongListOf(
+        element1.value.toLong(),
+        element2.value.toLong(),
+        element3.value.toLong()
+    )
+)
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/template/TestValueClassSet.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/template/TestValueClassSet.kt
new file mode 100644
index 0000000..b01d2b5
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/template/TestValueClassSet.kt
@@ -0,0 +1,554 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "KotlinRedundantDiagnosticSuppress",
+    "KotlinConstantConditions",
+    "PropertyName",
+    "ConstPropertyName",
+    "PrivatePropertyName",
+    "NOTHING_TO_INLINE",
+    "UnusedImport"
+)
+
+package androidx.collection.template
+
+/* ktlint-disable max-line-length */
+/* ktlint-disable import-ordering */
+
+import androidx.collection.LongSet
+import androidx.collection.MutableLongSet
+import androidx.collection.emptyLongSet
+import androidx.collection.mutableLongSetOf
+import androidx.collection.TestValueClass
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.jvm.JvmInline
+
+/* ktlint-disable max-line-length */
+// To use this template, you must substitute several strings. You can copy this and search/replace
+// or use a sed command. These properties must be changed:
+// * androidx.collection.template - target package (e.g. androidx.compose.ui.ui.collection)
+// * androidx.collection - package in which the value class resides
+// * TestValueClass - the value class contained in the set (e.g. Color or Offset)
+// * testValueClass - the value class, with the first letter lower case (e.g. color or offset)
+// * value.toLong() - the field in TestValueClass containing the backing primitive (e.g. packedValue)
+// * Long - the primitive type of the backing set (e.g. Long or Float)
+// * .toULong() - an operation done on the primitive to convert to the value class parameter
+//
+// For example, to create a ColorSet:
+// sed -e "s/androidx.collection.template/androidx.compose.ui.graphics/" -e "s/TestValueClass/Color/g" \
+//     -e "s/testValueClass/color/g" -e "s/value.toLong()/value.toLong()/g" -e "s/Long/Long/g" \
+//     -e "s/.toULong()/.toULong()/g" -e "s/androidx.collection/androidx.collection/g" \
+//     collection/collection/template/ValueClassSet.kt.template \
+//     > compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ColorSet.kt
+
+/**
+ * Returns an empty, read-only [TestValueClassSet].
+ */
+internal inline fun emptyTestValueClassSet(): TestValueClassSet = TestValueClassSet(emptyLongSet())
+
+/**
+ * Returns an empty, read-only [TestValueClassSet].
+ */
+internal inline fun testValueClassSetOf(): TestValueClassSet = TestValueClassSet(emptyLongSet())
+
+/**
+ * Returns a new read-only [TestValueClassSet] with only [element1] in it.
+ */
+internal inline fun testValueClassSetOf(element1: TestValueClass): TestValueClassSet =
+    TestValueClassSet(mutableLongSetOf(element1.value.toLong()))
+
+/**
+ * Returns a new read-only [TestValueClassSet] with only [element1] and [element2] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+internal fun testValueClassSetOf(
+    element1: TestValueClass,
+    element2: TestValueClass
+): TestValueClassSet =
+    TestValueClassSet(
+        mutableLongSetOf(
+            element1.value.toLong(),
+            element2.value.toLong(),
+        )
+    )
+
+/**
+ * Returns a new read-only [TestValueClassSet] with only [element1], [element2], and [element3] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+internal fun testValueClassSetOf(
+    element1: TestValueClass,
+    element2: TestValueClass,
+    element3: TestValueClass
+): TestValueClassSet = TestValueClassSet(
+    mutableLongSetOf(
+        element1.value.toLong(),
+        element2.value.toLong(),
+        element3.value.toLong(),
+    )
+)
+
+/**
+ * Returns a new [MutableTestValueClassSet].
+ */
+internal fun mutableTestValueClassSetOf(): MutableTestValueClassSet = MutableTestValueClassSet(
+    MutableLongSet()
+)
+
+/**
+ * Returns a new [MutableTestValueClassSet] with only [element1] in it.
+ */
+internal fun mutableTestValueClassSetOf(element1: TestValueClass): MutableTestValueClassSet =
+    MutableTestValueClassSet(mutableLongSetOf(element1.value.toLong()))
+
+/**
+ * Returns a new [MutableTestValueClassSet] with only [element1] and [element2] in it.
+ */
+internal fun mutableTestValueClassSetOf(
+    element1: TestValueClass,
+    element2: TestValueClass
+): MutableTestValueClassSet =
+    MutableTestValueClassSet(
+        mutableLongSetOf(
+            element1.value.toLong(),
+            element2.value.toLong(),
+        )
+    )
+
+/**
+ * Returns a new [MutableTestValueClassSet] with only [element1], [element2], and [element3] in it.
+ */
+internal fun mutableTestValueClassSetOf(
+    element1: TestValueClass,
+    element2: TestValueClass,
+    element3: TestValueClass
+): MutableTestValueClassSet =
+    MutableTestValueClassSet(
+        mutableLongSetOf(
+            element1.value.toLong(),
+            element2.value.toLong(),
+            element3.value.toLong(),
+        )
+    )
+
+/**
+ * [TestValueClassSet] is a container with a [Set]-like interface designed to avoid
+ * allocations, including boxing.
+ *
+ * This implementation makes no guarantee as to the order of the elements,
+ * nor does it make guarantees that the order remains constant over time.
+ *
+ * Though [TestValueClassSet] offers a read-only interface, it is always backed
+ * by a [MutableTestValueClassSet]. Read operations alone are thread-safe. However,
+ * any mutations done through the backing [MutableTestValueClassSet] while reading
+ * on another thread are not safe and the developer must protect the set
+ * from such changes during read operations.
+ *
+ * @see [MutableTestValueClassSet]
+ */
+@OptIn(ExperimentalContracts::class)
+@JvmInline
+internal value class TestValueClassSet(val set: LongSet) {
+    /**
+     * Returns the number of elements that can be stored in this set
+     * without requiring internal storage reallocation.
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public inline val capacity: Int
+        get() = set.capacity
+
+    /**
+     * Returns the number of elements in this set.
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public inline val size: Int
+        get() = set.size
+
+    /**
+     * Returns `true` if this set has at least one element.
+     */
+    public inline fun any(): Boolean = set.any()
+
+    /**
+     * Returns `true` if this set has no elements.
+     */
+    public inline fun none(): Boolean = set.none()
+
+    /**
+     * Indicates whether this set is empty.
+     */
+    public inline fun isEmpty(): Boolean = set.isEmpty()
+
+    /**
+     * Returns `true` if this set is not empty.
+     */
+    public inline fun isNotEmpty(): Boolean = set.isNotEmpty()
+
+    /**
+     * Returns the first element in the collection.
+     * @throws NoSuchElementException if the collection is empty
+     */
+    public inline fun first(): TestValueClass = TestValueClass(set.first().toULong())
+
+    /**
+     * Returns the first element in the collection for which [predicate] returns `true`.
+     *
+     * **Note** There is no mechanism for both determining if there is an element that matches
+     * [predicate] _and_ returning it if it exists. Developers should use [forEach] to achieve
+     * this behavior.
+     *
+     * @param predicate Called on elements of the set, returning `true` for an element that matches
+     * or `false` if it doesn't
+     * @return An element in the set for which [predicate] returns `true`.
+     * @throws NoSuchElementException if [predicate] returns `false` for all elements or the
+     * collection is empty.
+     */
+    public inline fun first(predicate: (element: TestValueClass) -> Boolean): TestValueClass =
+        TestValueClass(set.first { predicate(TestValueClass(it.toULong())) }.toULong())
+
+    /**
+     * Iterates over every element stored in this set by invoking
+     * the specified [block] lambda.
+     * @param block called with each element in the set
+     */
+    public inline fun forEach(block: (element: TestValueClass) -> Unit) {
+        contract { callsInPlace(block) }
+        set.forEach { block(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns true if all elements match the given [predicate].
+     * @param predicate called for elements in the set to determine if it returns return `true` for
+     * all elements.
+     */
+    public inline fun all(predicate: (element: TestValueClass) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return set.all { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns true if at least one element matches the given [predicate].
+     * @param predicate called for elements in the set to determine if it returns `true` for any
+     * elements.
+     */
+    public inline fun any(predicate: (element: TestValueClass) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return set.any { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns the number of elements in this set.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public inline fun count(): Int = set.count()
+
+    /**
+     * Returns the number of elements matching the given [predicate].
+     * @param predicate Called for all elements in the set to count the number for which it returns
+     * `true`.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public inline fun count(predicate: (element: TestValueClass) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return set.count { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns `true` if the specified [element] is present in this set, `false`
+     * otherwise.
+     * @param element The element to look for in this set
+     */
+    public inline operator fun contains(element: TestValueClass): Boolean =
+        set.contains(element.value.toLong())
+
+    /**
+     * Returns a string representation of this set. The set is denoted in the
+     * string by the `{}`. Each element is separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "[]"
+        }
+
+        val s = StringBuilder().append('[')
+        var index = 0
+        forEach { element ->
+            if (index++ != 0) {
+                s.append(',').append(' ')
+            }
+            s.append(element)
+        }
+        return s.append(']').toString()
+    }
+}
+
+/**
+ * [MutableTestValueClassSet] is a container with a [MutableSet]-like interface based on a flat
+ * hash table implementation. The underlying implementation is designed to avoid
+ * all allocations on insertion, removal, retrieval, and iteration. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added elements to the set.
+ *
+ * This implementation makes no guarantee as to the order of the elements stored,
+ * nor does it make guarantees that the order remains constant over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the set (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Concurrent reads are however safe.
+ */
+@OptIn(ExperimentalContracts::class)
+@JvmInline
+internal value class MutableTestValueClassSet(val set: MutableLongSet) {
+    /**
+     * Returns the number of elements that can be stored in this set
+     * without requiring internal storage reallocation.
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public inline val capacity: Int
+        get() = set.capacity
+
+    /**
+     * Returns the number of elements in this set.
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public inline val size: Int
+        get() = set.size
+
+    /**
+     * Returns `true` if this set has at least one element.
+     */
+    public inline fun any(): Boolean = set.any()
+
+    /**
+     * Returns `true` if this set has no elements.
+     */
+    public inline fun none(): Boolean = set.none()
+
+    /**
+     * Indicates whether this set is empty.
+     */
+    public inline fun isEmpty(): Boolean = set.isEmpty()
+
+    /**
+     * Returns `true` if this set is not empty.
+     */
+    public inline fun isNotEmpty(): Boolean = set.isNotEmpty()
+
+    /**
+     * Returns the first element in the collection.
+     * @throws NoSuchElementException if the collection is empty
+     */
+    public inline fun first(): TestValueClass = TestValueClass(set.first().toULong())
+
+    /**
+     * Returns the first element in the collection for which [predicate] returns `true`.
+     *
+     * **Note** There is no mechanism for both determining if there is an element that matches
+     * [predicate] _and_ returning it if it exists. Developers should use [forEach] to achieve
+     * this behavior.
+     *
+     * @param predicate Called on elements of the set, returning `true` for an element that matches
+     * or `false` if it doesn't
+     * @return An element in the set for which [predicate] returns `true`.
+     * @throws NoSuchElementException if [predicate] returns `false` for all elements or the
+     * collection is empty.
+     */
+    public inline fun first(predicate: (element: TestValueClass) -> Boolean): TestValueClass =
+        TestValueClass(set.first { predicate(TestValueClass(it.toULong())) }.toULong())
+
+    /**
+     * Iterates over every element stored in this set by invoking
+     * the specified [block] lambda.
+     * @param block called with each element in the set
+     */
+    public inline fun forEach(block: (element: TestValueClass) -> Unit) {
+        contract { callsInPlace(block) }
+        set.forEach { block(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns true if all elements match the given [predicate].
+     * @param predicate called for elements in the set to determine if it returns return `true` for
+     * all elements.
+     */
+    public inline fun all(predicate: (element: TestValueClass) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return set.all { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns true if at least one element matches the given [predicate].
+     * @param predicate called for elements in the set to determine if it returns `true` for any
+     * elements.
+     */
+    public inline fun any(predicate: (element: TestValueClass) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return set.any { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns the number of elements in this set.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public inline fun count(): Int = set.count()
+
+    /**
+     * Returns the number of elements matching the given [predicate].
+     * @param predicate Called for all elements in the set to count the number for which it returns
+     * `true`.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public inline fun count(predicate: (element: TestValueClass) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return set.count { predicate(TestValueClass(it.toULong())) }
+    }
+
+    /**
+     * Returns `true` if the specified [element] is present in this set, `false`
+     * otherwise.
+     * @param element The element to look for in this set
+     */
+    public inline operator fun contains(element: TestValueClass): Boolean =
+        set.contains(element.value.toLong())
+
+    /**
+     * Returns a string representation of this set. The set is denoted in the
+     * string by the `{}`. Each element is separated by `, `.
+     */
+    public override fun toString(): String = asTestValueClassSet().toString()
+
+    /**
+     * Creates a new [MutableTestValueClassSet]
+     * @param initialCapacity The initial desired capacity for this container.
+     * The container will honor this value by guaranteeing its internal structures
+     * can hold that many elements without requiring any allocations. The initial
+     * capacity can be set to 0.
+     */
+    public constructor(initialCapacity: Int = 6) : this(MutableLongSet(initialCapacity))
+
+    /**
+     * Returns a read-only interface to the set.
+     */
+    public inline fun asTestValueClassSet(): TestValueClassSet = TestValueClassSet(set)
+
+    /**
+     * Adds the specified element to the set.
+     * @param element The element to add to the set.
+     * @return `true` if the element has been added or `false` if the element is already
+     * contained within the set.
+     */
+    public inline fun add(element: TestValueClass): Boolean = set.add(element.value.toLong())
+
+    /**
+     * Adds the specified element to the set.
+     * @param element The element to add to the set.
+     */
+    public inline operator fun plusAssign(element: TestValueClass) =
+        set.plusAssign(element.value.toLong())
+
+    /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements A [TestValueClassSet] of elements to add to this set.
+     * @return `true` if any of the specified elements were added to the collection,
+     * `false` if the collection was not modified.
+     */
+    public inline fun addAll(elements: TestValueClassSet): Boolean = set.addAll(elements.set)
+
+    /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements A [TestValueClassSet] of elements to add to this set.
+     * @return `true` if any of the specified elements were added to the collection,
+     * `false` if the collection was not modified.
+     */
+    public inline fun addAll(elements: MutableTestValueClassSet): Boolean = set.addAll(elements.set)
+
+    /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements A [TestValueClassSet] of elements to add to this set.
+     */
+    public inline operator fun plusAssign(elements: TestValueClassSet) =
+        set.plusAssign(elements.set)
+
+    /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements A [TestValueClassSet] of elements to add to this set.
+     */
+    public inline operator fun plusAssign(elements: MutableTestValueClassSet) =
+        set.plusAssign(elements.set)
+
+    /**
+     * Removes the specified [element] from the set.
+     * @param element The element to remove from the set.
+     * @return `true` if the [element] was present in the set, or `false` if it wasn't
+     * present before removal.
+     */
+    public inline fun remove(element: TestValueClass): Boolean = set.remove(element.value.toLong())
+
+    /**
+     * Removes the specified [element] from the set if it is present.
+     * @param element The element to remove from the set.
+     */
+    public inline operator fun minusAssign(element: TestValueClass) =
+        set.minusAssign(element.value.toLong())
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [TestValueClassSet] of elements to be removed from the set.
+     * @return `true` if the set was changed or `false` if none of the elements were present.
+     */
+    public inline fun removeAll(elements: TestValueClassSet): Boolean = set.removeAll(elements.set)
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [TestValueClassSet] of elements to be removed from the set.
+     * @return `true` if the set was changed or `false` if none of the elements were present.
+     */
+    public inline fun removeAll(elements: MutableTestValueClassSet): Boolean =
+        set.removeAll(elements.set)
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [TestValueClassSet] of elements to be removed from the set.
+     */
+    public inline operator fun minusAssign(elements: TestValueClassSet) =
+        set.minusAssign(elements.set)
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [TestValueClassSet] of elements to be removed from the set.
+     */
+    public inline operator fun minusAssign(elements: MutableTestValueClassSet) =
+        set.minusAssign(elements.set)
+
+    /**
+     * Removes all elements from this set.
+     */
+    public inline fun clear() = set.clear()
+
+    /**
+     * Trims this [MutableTestValueClassSet]'s storage so it is sized appropriately
+     * to hold the current elements.
+     *
+     * Returns the number of empty elements removed from this set's storage.
+     * Returns 0 if no trimming is necessary or possible.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public inline fun trim(): Int = set.trim()
+}
diff --git a/collection/collection/template/ObjectPValueMap.kt.template b/collection/collection/template/ObjectPValueMap.kt.template
new file mode 100644
index 0000000..b9f15a4
--- /dev/null
+++ b/collection/collection/template/ObjectPValueMap.kt.template
@@ -0,0 +1,1034 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyObjectPValueMap = MutableObjectPValueMap<Any?>(0)
+
+/**
+ * Returns an empty, read-only [ObjectPValueMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> emptyObjectPValueMap(): ObjectPValueMap<K> =
+    EmptyObjectPValueMap as ObjectPValueMap<K>
+
+/**
+ * Returns an empty, read-only [ObjectPValueMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> objectPValueMap(): ObjectPValueMap<K> =
+    EmptyObjectPValueMap as ObjectPValueMap<K>
+
+/**
+ * Returns a new [ObjectPValueMap] with only [key1] associated with [value1].
+ */
+public fun <K> objectPValueMapOf(
+    key1: K,
+    value1: PValue
+): ObjectPValueMap<K> =
+    MutableObjectPValueMap<K>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [ObjectPValueMap] with only [key1] and [key2] associated with
+ * [value1] and [value2], respectively.
+ */
+public fun <K> objectPValueMapOf(
+    key1: K,
+    value1: PValue,
+    key2: K,
+    value2: PValue,
+): ObjectPValueMap<K> =
+    MutableObjectPValueMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [ObjectPValueMap] with only [key1], [key2], and [key3] associated with
+ * [value1], [value2], and [value3], respectively.
+ */
+public fun <K> objectPValueMapOf(
+    key1: K,
+    value1: PValue,
+    key2: K,
+    value2: PValue,
+    key3: K,
+    value3: PValue,
+): ObjectPValueMap<K> =
+    MutableObjectPValueMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [ObjectPValueMap] with only [key1], [key2], [key3], and [key4] associated with
+ * [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <K> objectPValueMapOf(
+    key1: K,
+    value1: PValue,
+    key2: K,
+    value2: PValue,
+    key3: K,
+    value3: PValue,
+    key4: K,
+    value4: PValue,
+): ObjectPValueMap<K> =
+    MutableObjectPValueMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [ObjectPValueMap] with only [key1], [key2], [key3], [key4], and [key5] associated
+ * with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <K> objectPValueMapOf(
+    key1: K,
+    value1: PValue,
+    key2: K,
+    value2: PValue,
+    key3: K,
+    value3: PValue,
+    key4: K,
+    value4: PValue,
+    key5: K,
+    value5: PValue,
+): ObjectPValueMap<K> =
+    MutableObjectPValueMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new empty [MutableObjectPValueMap].
+ */
+public fun <K> mutableObjectPValueMapOf(): MutableObjectPValueMap<K> = MutableObjectPValueMap()
+
+/**
+ * Returns a new [MutableObjectPValueMap] with only [key1] associated with [value1].
+ */
+public fun <K> mutableObjectPValueMapOf(
+    key1: K,
+    value1: PValue,
+): MutableObjectPValueMap<K> =
+    MutableObjectPValueMap<K>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutableObjectPValueMap] with only [key1] and [key2] associated with
+ * [value1] and [value2], respectively.
+ */
+public fun <K> mutableObjectPValueMapOf(
+    key1: K,
+    value1: PValue,
+    key2: K,
+    value2: PValue,
+): MutableObjectPValueMap<K> =
+    MutableObjectPValueMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutableObjectPValueMap] with only [key1], [key2], and [key3] associated with
+ * [value1], [value2], and [value3], respectively.
+ */
+public fun <K> mutableObjectPValueMapOf(
+    key1: K,
+    value1: PValue,
+    key2: K,
+    value2: PValue,
+    key3: K,
+    value3: PValue,
+): MutableObjectPValueMap<K> =
+    MutableObjectPValueMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutableObjectPValueMap] with only [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <K> mutableObjectPValueMapOf(
+    key1: K,
+    value1: PValue,
+    key2: K,
+    value2: PValue,
+    key3: K,
+    value3: PValue,
+    key4: K,
+    value4: PValue,
+): MutableObjectPValueMap<K> =
+    MutableObjectPValueMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutableObjectPValueMap] with only [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <K> mutableObjectPValueMapOf(
+    key1: K,
+    value1: PValue,
+    key2: K,
+    value2: PValue,
+    key3: K,
+    value3: PValue,
+    key4: K,
+    value4: PValue,
+    key5: K,
+    value5: PValue,
+): MutableObjectPValueMap<K> =
+    MutableObjectPValueMap<K>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [ObjectPValueMap] is a container with a [Map]-like interface for keys with
+ * reference types and [PValue] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableObjectPValueMap].
+ *
+ * @see [MutableObjectPValueMap]
+ * @see ScatterMap
+ */
+public sealed class ObjectPValueMap<K> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: Array<Any?> = EMPTY_OBJECTS
+
+    @PublishedApi
+    @JvmField
+    internal var values: PValueArray = EmptyPValueArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     * @throws NoSuchElementException when [key] is not found
+     */
+    public operator fun get(key: K): PValue {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("There is no key $key in the map")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: K, defaultValue: PValue): PValue {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: K, defaultValue: () -> PValue): PValue {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: K, value: PValue) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K, v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: K) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K)
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: PValue) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (K, PValue) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (K, PValue) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (K, PValue) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: PValue): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@ObjectPValueMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: K, value: PValue) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@ObjectPValueMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [ObjectPValueMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is ObjectPValueMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        val o = other as ObjectPValueMap<Any?>
+
+        forEach { key, value ->
+            if (value != o[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(if (key === this) "(this)" else key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: K): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableObjectPValueMap] is a container with a [MutableMap]-like interface for keys with
+ * reference types and [PValue] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableObjectPValueMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableObjectPValueMap<K>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : ObjectPValueMap<K>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = arrayOfNulls(newCapacity)
+        values = PValueArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: K, defaultValue: () -> PValue): PValue {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        val value = defaultValue()
+        set(key, value)
+        return value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: K, value: PValue) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public fun put(key: K, value: PValue) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: ObjectPValueMap<K>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: ObjectPValueMap<K>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: K) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: K, value: PValue): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (K, PValue) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index] as K, values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: K) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array<out K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Iterable<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Sequence<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: ScatterSet<K>) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        keys[index] = null
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        keys.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: K): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableObjectPValueMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/template/ObjectPValueMapTest.kt.template b/collection/collection/template/ObjectPValueMapTest.kt.template
new file mode 100644
index 0000000..50daf3f
--- /dev/null
+++ b/collection/collection/template/ObjectPValueMapTest.kt.template
@@ -0,0 +1,731 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class ObjectPValueTest {
+    @Test
+    fun objectPValueMap() {
+        val map = MutableObjectPValueMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun emptyObjectPValueMap() {
+        val map = emptyObjectPValueMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyObjectPValueMap<String>(), map)
+    }
+
+    @Test
+    fun objectPValueMapFunction() {
+        val map = mutableObjectPValueMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableObjectPValueMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectPValueMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableObjectPValueMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectPValueMapInitFunction() {
+        val map1 = objectPValueMapOf(
+            "Hello", 1ValueSuffix,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1ValueSuffix, map1["Hello"])
+
+        val map2 = objectPValueMapOf(
+            "Hello", 1ValueSuffix,
+            "Bonjour", 2ValueSuffix,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1ValueSuffix, map2["Hello"])
+        assertEquals(2ValueSuffix, map2["Bonjour"])
+
+        val map3 = objectPValueMapOf(
+            "Hello", 1ValueSuffix,
+            "Bonjour", 2ValueSuffix,
+            "Hallo", 3ValueSuffix,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1ValueSuffix, map3["Hello"])
+        assertEquals(2ValueSuffix, map3["Bonjour"])
+        assertEquals(3ValueSuffix, map3["Hallo"])
+
+        val map4 = objectPValueMapOf(
+            "Hello", 1ValueSuffix,
+            "Bonjour", 2ValueSuffix,
+            "Hallo", 3ValueSuffix,
+            "Konnichiwa", 4ValueSuffix,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1ValueSuffix, map4["Hello"])
+        assertEquals(2ValueSuffix, map4["Bonjour"])
+        assertEquals(3ValueSuffix, map4["Hallo"])
+        assertEquals(4ValueSuffix, map4["Konnichiwa"])
+
+        val map5 = objectPValueMapOf(
+            "Hello", 1ValueSuffix,
+            "Bonjour", 2ValueSuffix,
+            "Hallo", 3ValueSuffix,
+            "Konnichiwa", 4ValueSuffix,
+            "Ciao", 5ValueSuffix,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1ValueSuffix, map5["Hello"])
+        assertEquals(2ValueSuffix, map5["Bonjour"])
+        assertEquals(3ValueSuffix, map5["Hallo"])
+        assertEquals(4ValueSuffix, map5["Konnichiwa"])
+        assertEquals(5ValueSuffix, map5["Ciao"])
+    }
+
+    @Test
+    fun mutableObjectPValueMapInitFunction() {
+        val map1 = mutableObjectPValueMapOf(
+            "Hello", 1ValueSuffix,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1ValueSuffix, map1["Hello"])
+
+        val map2 = mutableObjectPValueMapOf(
+            "Hello", 1ValueSuffix,
+            "Bonjour", 2ValueSuffix,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1ValueSuffix, map2["Hello"])
+        assertEquals(2ValueSuffix, map2["Bonjour"])
+
+        val map3 = mutableObjectPValueMapOf(
+            "Hello", 1ValueSuffix,
+            "Bonjour", 2ValueSuffix,
+            "Hallo", 3ValueSuffix,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1ValueSuffix, map3["Hello"])
+        assertEquals(2ValueSuffix, map3["Bonjour"])
+        assertEquals(3ValueSuffix, map3["Hallo"])
+
+        val map4 = mutableObjectPValueMapOf(
+            "Hello", 1ValueSuffix,
+            "Bonjour", 2ValueSuffix,
+            "Hallo", 3ValueSuffix,
+            "Konnichiwa", 4ValueSuffix,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1ValueSuffix, map4["Hello"])
+        assertEquals(2ValueSuffix, map4["Bonjour"])
+        assertEquals(3ValueSuffix, map4["Hallo"])
+        assertEquals(4ValueSuffix, map4["Konnichiwa"])
+
+        val map5 = mutableObjectPValueMapOf(
+            "Hello", 1ValueSuffix,
+            "Bonjour", 2ValueSuffix,
+            "Hallo", 3ValueSuffix,
+            "Konnichiwa", 4ValueSuffix,
+            "Ciao", 5ValueSuffix,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1ValueSuffix, map5["Hello"])
+        assertEquals(2ValueSuffix, map5["Bonjour"])
+        assertEquals(3ValueSuffix, map5["Hallo"])
+        assertEquals(4ValueSuffix, map5["Konnichiwa"])
+        assertEquals(5ValueSuffix, map5["Ciao"])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableObjectPValueMap<String>(12)
+        map["Hello"] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableObjectPValueMap<String>(2)
+        map["Hello"] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableObjectPValueMap<String>(0)
+        map["Hello"] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Hello"] = 2ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(2ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableObjectPValueMap<String>()
+
+        map.put("Hello", 1ValueSuffix)
+        assertEquals(1ValueSuffix, map["Hello"])
+        map.put("Hello", 2ValueSuffix)
+        assertEquals(2ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun nullKey() {
+        val map = MutableObjectPValueMap<String?>()
+        map[null] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map[null])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+
+        assertFailsWith<NoSuchElementException> {
+            map["Bonjour"]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+
+        assertEquals(2ValueSuffix, map.getOrDefault("Bonjour", 2ValueSuffix))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+
+        assertEquals(3ValueSuffix, map.getOrElse("Hallo") { 3ValueSuffix })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+
+        var counter = 0
+        map.getOrPut("Hello") {
+            counter++
+            2ValueSuffix
+        }
+        assertEquals(1ValueSuffix, map["Hello"])
+        assertEquals(0, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            2ValueSuffix
+        }
+        assertEquals(2ValueSuffix, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            3ValueSuffix
+        }
+        assertEquals(2ValueSuffix, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Hallo") {
+            counter++
+            3ValueSuffix
+        }
+        assertEquals(3ValueSuffix, map["Hallo"])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableObjectPValueMap<String?>()
+        map.remove("Hello")
+
+        map["Hello"] = 1ValueSuffix
+        map.remove("Hello")
+        assertEquals(0, map.size)
+
+        map[null] = 1ValueSuffix
+        map.remove(null)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableObjectPValueMap<String>(6)
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+        map["Konnichiwa"] = 4ValueSuffix
+        map["Ciao"] = 5ValueSuffix
+        map["Annyeong"] = 6ValueSuffix
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove("Hello")
+        map.remove("Bonjour")
+        map.remove("Hallo")
+        map.remove("Konnichiwa")
+        map.remove("Ciao")
+        map.remove("Annyeong")
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map["Hola"] = 7ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+        map["Konnichiwa"] = 4ValueSuffix
+        map["Ciao"] = 5ValueSuffix
+        map["Annyeong"] = 6ValueSuffix
+
+        map.removeIf { key, _ -> key.startsWith('H') }
+
+        assertEquals(4, map.size)
+        assertEquals(2ValueSuffix, map["Bonjour"])
+        assertEquals(4ValueSuffix, map["Konnichiwa"])
+        assertEquals(5ValueSuffix, map["Ciao"])
+        assertEquals(6ValueSuffix, map["Annyeong"])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+
+        map -= "Hello"
+
+        assertEquals(2, map.size)
+        assertFalse("Hello" in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+
+        map -= arrayOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusIterable() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+
+        map -= listOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusSequence() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+
+        map -= listOf("Hallo", "Bonjour").asSequence()
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableObjectPValueMap<String?>()
+        assertFalse(map.remove("Hello", 1ValueSuffix))
+
+        map["Hello"] = 1ValueSuffix
+        assertTrue(map.remove("Hello", 1ValueSuffix))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableObjectPValueMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toPValue()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableObjectPValueMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toPValue()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt().toString())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableObjectPValueMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toPValue()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertNotNull(key.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableObjectPValueMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toPValue()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableObjectPValueMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toString()] = i.toPValue()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableObjectPValueMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        val oneString = 1ValueSuffix.toString()
+        val twoString = 2ValueSuffix.toString()
+        assertTrue(
+            "{Hello=$oneString, Bonjour=$twoString}" == map.toString() ||
+                "{Bonjour=$twoString, Hello=$oneString}" == map.toString()
+        )
+
+        map.clear()
+        map[null] = 2ValueSuffix
+        assertEquals("{null=$twoString}", map.toString())
+
+        val selfAsKeyMap = MutableObjectPValueMap<Any>()
+        selfAsKeyMap[selfAsKeyMap] = 1ValueSuffix
+        assertEquals("{(this)=$oneString}", selfAsKeyMap.toString())
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutableObjectPValueMap<String?>()
+        repeat(5) {
+            map[it.toString()] = it.toPValue()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { _, value ->
+            order[index++] = value.toInt()
+        }
+        assertEquals(
+            "${order[0]}=${order[0].toPValue()}, ${order[1]}=${order[1].toPValue()}, " +
+            "${order[2]}=${order[2].toPValue()}, ${order[3]}=${order[3].toPValue()}, " +
+            "${order[4]}=${order[4].toPValue()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0]}=${order[0].toPValue()}, ${order[1]}=${order[1].toPValue()}, " +
+            "${order[2]}=${order[2].toPValue()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0]}=${order[0].toPValue()}-${order[1]}=${order[1].toPValue()}-" +
+            "${order[2]}=${order[2].toPValue()}-${order[3]}=${order[3].toPValue()}-" +
+            "${order[4]}=${order[4].toPValue()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { _, value -> names[value.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableObjectPValueMap<String?>()
+        map["Hello"] = 1ValueSuffix
+        map[null] = 2ValueSuffix
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableObjectPValueMap<String?>()
+        map2[null] = 2ValueSuffix
+
+        assertNotEquals(map, map2)
+
+        map2["Hello"] = 1ValueSuffix
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableObjectPValueMap<String?>()
+        map["Hello"] = 1ValueSuffix
+        map[null] = 2ValueSuffix
+
+        assertTrue(map.containsKey("Hello"))
+        assertTrue(map.containsKey(null))
+        assertFalse(map.containsKey("Bonjour"))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableObjectPValueMap<String?>()
+        map["Hello"] = 1ValueSuffix
+        map[null] = 2ValueSuffix
+
+        assertTrue("Hello" in map)
+        assertTrue(null in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableObjectPValueMap<String?>()
+        map["Hello"] = 1ValueSuffix
+        map[null] = 2ValueSuffix
+
+        assertTrue(map.containsValue(1ValueSuffix))
+        assertTrue(map.containsValue(2ValueSuffix))
+        assertFalse(map.containsValue(3ValueSuffix))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableObjectPValueMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map["Hello"] = 1ValueSuffix
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableObjectPValueMap<String>()
+        assertEquals(0, map.count())
+
+        map["Hello"] = 1ValueSuffix
+        assertEquals(1, map.count())
+
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+        map["Konnichiwa"] = 4ValueSuffix
+        map["Ciao"] = 5ValueSuffix
+        map["Annyeong"] = 6ValueSuffix
+
+        assertEquals(2, map.count { key, _ -> key.startsWith("H") })
+        assertEquals(0, map.count { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+        map["Konnichiwa"] = 4ValueSuffix
+        map["Ciao"] = 5ValueSuffix
+        map["Annyeong"] = 6ValueSuffix
+
+        assertTrue(map.any { key, _ -> key.startsWith("K") })
+        assertFalse(map.any { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+        map["Konnichiwa"] = 4ValueSuffix
+        map["Ciao"] = 5ValueSuffix
+        map["Annyeong"] = 6ValueSuffix
+
+        assertTrue(map.all { key, value -> key.length >= 4 && value.toInt() >= 1 })
+        assertFalse(map.all { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableObjectPValueMap<String>()
+        assertEquals(7, map.trim())
+
+        map["Hello"] = 1ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toPValue()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toString()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/template/PKeyList.kt.template b/collection/collection/template/PKeyList.kt.template
new file mode 100644
index 0000000..1bf836e
--- /dev/null
+++ b/collection/collection/template/PKeyList.kt.template
@@ -0,0 +1,968 @@
+/*
+ * 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.
+ */
+@file:Suppress("NOTHING_TO_INLINE", "RedundantVisibilityModifier")
+@file:OptIn(ExperimentalContracts::class)
+
+package androidx.collection
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+/**
+ * [PKeyList] is a [List]-like collection for [PKey] values. It allows retrieving
+ * the elements without boxing. [PKeyList] is always backed by a [MutablePKeyList],
+ * its [MutableList]-like subclass.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ */
+public sealed class PKeyList(initialCapacity: Int) {
+    @JvmField
+    @PublishedApi
+    internal var content: PKeyArray = if (initialCapacity == 0) {
+        EmptyPKeyArray
+    } else {
+        PKeyArray(initialCapacity)
+    }
+
+    @Suppress("PropertyName")
+    @JvmField
+    @PublishedApi
+    internal var _size: Int = 0
+
+    /**
+     * The number of elements in the [PKeyList].
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns the last valid index in the [PKeyList]. This can be `-1` when the list is empty.
+     */
+    @get:androidx.annotation.IntRange(from = -1)
+    public inline val lastIndex: Int get() = _size - 1
+
+    /**
+     * Returns an [IntRange] of the valid indices for this [PKeyList].
+     */
+    public inline val indices: IntRange get() = 0 until _size
+
+    /**
+     * Returns `true` if the collection has no elements in it.
+     */
+    public fun none(): Boolean {
+        return isEmpty()
+    }
+
+    /**
+     * Returns `true` if there's at least one element in the collection.
+     */
+    public fun any(): Boolean {
+        return isNotEmpty()
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate].
+     */
+    public inline fun any(predicate: (element: PKey) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEach {
+            if (predicate(it)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate] while
+     * iterating in the reverse order.
+     */
+    public inline fun reversedAny(predicate: (element: PKey) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEachReversed {
+            if (predicate(it)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if the [PKeyList] contains [element] or `false` otherwise.
+     */
+    public operator fun contains(element: PKey): Boolean {
+        forEach {
+            if (it == element) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if the [PKeyList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public fun containsAll(elements: PKeyList): Boolean {
+        for (i in elements.indices) {
+            if (!contains(elements[i])) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns the number of elements in this list.
+     */
+    public fun count(): Int = _size
+
+    /**
+     * Counts the number of elements matching [predicate].
+     * @return The number of elements in this list for which [predicate] returns true.
+     */
+    public inline fun count(predicate: (element: PKey) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        var count = 0
+        forEach { if (predicate(it)) count++ }
+        return count
+    }
+
+    /**
+     * Returns the first element in the [PKeyList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public fun first(): PKey {
+        if (isEmpty()) {
+            throw NoSuchElementException("PKeyList is empty.")
+        }
+        return content[0]
+    }
+
+    /**
+     * Returns the first element in the [PKeyList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfFirst
+     */
+    public inline fun first(predicate: (element: PKey) -> Boolean): PKey {
+        contract { callsInPlace(predicate) }
+        forEach { item ->
+            if (predicate(item)) return item
+        }
+        throw NoSuchElementException("PKeyList contains no element matching the predicate.")
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [PKeyList] in order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes current accumulator value and an element, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> fold(initial: R, operation: (acc: R, element: PKey) -> R): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEach { item ->
+            acc = operation(acc, item)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [PKeyList] in order.
+     */
+    public inline fun <R> foldIndexed(
+        initial: R,
+        operation: (index: Int, acc: R, element: PKey) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachIndexed { i, item ->
+            acc = operation(i, acc, item)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [PKeyList] in reverse order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes an element and the current accumulator value, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> foldRight(initial: R, operation: (element: PKey, acc: R) -> R): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachReversed { item ->
+            acc = operation(item, acc)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [PKeyList] in reverse order.
+     */
+    public inline fun <R> foldRightIndexed(
+        initial: R,
+        operation: (index: Int, element: PKey, acc: R) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachReversedIndexed { i, item ->
+            acc = operation(i, item, acc)
+        }
+        return acc
+    }
+
+    /**
+     * Calls [block] for each element in the [PKeyList], in order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEach(block: (element: PKey) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in 0 until _size) {
+            block(content[i])
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [PKeyList] along with its index, in order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachIndexed(block: (index: Int, element: PKey) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in 0 until _size) {
+            block(i, content[i])
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [PKeyList] in reverse order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEachReversed(block: (element: PKey) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in _size - 1 downTo 0) {
+            block(content[i])
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [PKeyList] along with its index, in reverse
+     * order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachReversedIndexed(block: (index: Int, element: PKey) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in _size - 1 downTo 0) {
+            block(i, content[i])
+        }
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public operator fun get(@androidx.annotation.IntRange(from = 0) index: Int): PKey {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        return content[index]
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): PKey {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        return content[index]
+    }
+
+    /**
+     * Returns the element at the given [index] or [defaultValue] if [index] is out of bounds
+     * of the collection.
+     * @param index The index of the element whose value should be returned
+     * @param defaultValue A lambda to call with [index] as a parameter to return a value at
+     * an index not in the list.
+     */
+    public inline fun elementAtOrElse(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        defaultValue: (index: Int) -> PKey
+    ): PKey {
+        if (index !in 0 until _size) {
+            return defaultValue(index)
+        }
+        return content[index]
+    }
+
+    /**
+     * Returns the index of [element] in the [PKeyList] or `-1` if [element] is not there.
+     */
+    public fun indexOf(element: PKey): Int {
+        forEachIndexed { i, item ->
+            if (element == item) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns the index if the first element in the [PKeyList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfFirst(predicate: (element: PKey) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        forEachIndexed { i, item ->
+            if (predicate(item)) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns the index if the last element in the [PKeyList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfLast(predicate: (element: PKey) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        forEachReversedIndexed { i, item ->
+            if (predicate(item)) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns `true` if the [PKeyList] has no elements in it or `false` otherwise.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if there are elements in the [PKeyList] or `false` if it is empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the last element in the [PKeyList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public fun last(): PKey {
+        if (isEmpty()) {
+            throw NoSuchElementException("PKeyList is empty.")
+        }
+        return content[lastIndex]
+    }
+
+    /**
+     * Returns the last element in the [PKeyList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfLast
+     */
+    public inline fun last(predicate: (element: PKey) -> Boolean): PKey {
+        contract { callsInPlace(predicate) }
+        forEachReversed { item ->
+            if (predicate(item)) {
+                return item
+            }
+        }
+        throw NoSuchElementException("PKeyList contains no element matching the predicate.")
+    }
+
+    /**
+     * Returns the index of the last element in the [PKeyList] that is the same as
+     * [element] or `-1` if no elements match.
+     */
+    public fun lastIndexOf(element: PKey): Int {
+        forEachReversedIndexed { i, item ->
+            if (item == element) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        this@PKeyList.forEachIndexed { index, element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(element)
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. [transform] dictates how each element will be represented.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (PKey) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        this@PKeyList.forEachIndexed { index, element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(element))
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns a hash code based on the contents of the [PKeyList].
+     */
+    override fun hashCode(): Int {
+        var hashCode = 0
+        forEach { element ->
+            hashCode += 31 * element.hashCode()
+        }
+        return hashCode
+    }
+
+    /**
+     * Returns `true` if [other] is a [PKeyList] and the contents of this and [other] are the
+     * same.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (other !is PKeyList || other._size != _size) {
+            return false
+        }
+        val content = content
+        val otherContent = other.content
+        for (i in indices) {
+            if (content[i] != otherContent[i]) {
+                return false
+            }
+        }
+        return true
+    }
+
+    /**
+     * Returns a String representation of the list, surrounded by "[]" and each element
+     * separated by ", ".
+     */
+    override fun toString(): String = joinToString(prefix = "[", postfix = "]")
+}
+
+/**
+ * [MutablePKeyList] is a [MutableList]-like collection for [PKey] values.
+ * It allows storing and retrieving the elements without boxing. Immutable
+ * access is available through its base class [PKeyList], which has a [List]-like
+ * interface.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ *
+ * @constructor Creates a [MutablePKeyList] with a [capacity] of `initialCapacity`.
+ */
+public class MutablePKeyList(
+    initialCapacity: Int = 16
+) : PKeyList(initialCapacity) {
+    /**
+     * Returns the total number of elements that can be held before the [MutablePKeyList] must
+     * grow.
+     *
+     * @see ensureCapacity
+     */
+    public inline val capacity: Int
+        get() = content.size
+
+    /**
+     * Adds [element] to the [MutablePKeyList] and returns `true`.
+     */
+    public fun add(element: PKey): Boolean {
+        ensureCapacity(_size + 1)
+        content[_size] = element
+        _size++
+        return true
+    }
+
+    /**
+     * Adds [element] to the [MutablePKeyList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public fun add(@androidx.annotation.IntRange(from = 0) index: Int, element: PKey) {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        ensureCapacity(_size + 1)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + 1,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        content[index] = element
+        _size++
+    }
+
+    /**
+     * Adds all [elements] to the [MutablePKeyList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutablePKeyList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive.
+     */
+    public fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: PKeyArray
+    ): Boolean {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        if (elements.isEmpty()) return false
+        ensureCapacity(_size + elements.size)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + elements.size,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        elements.copyInto(content, index)
+        _size += elements.size
+        return true
+    }
+
+    /**
+     * Adds all [elements] to the [MutablePKeyList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutablePKeyList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: PKeyList
+    ): Boolean {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        if (elements.isEmpty()) return false
+        ensureCapacity(_size + elements._size)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + elements._size,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        elements.content.copyInto(
+            destination = content,
+            destinationOffset = index,
+            startIndex = 0,
+            endIndex = elements._size
+        )
+        _size += elements._size
+        return true
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutablePKeyList] and returns `true` if the
+     * [MutablePKeyList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: PKeyList): Boolean {
+        return addAll(_size, elements)
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutablePKeyList] and returns `true` if the
+     * [MutablePKeyList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: PKeyArray): Boolean {
+        return addAll(_size, elements)
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutablePKeyList].
+     */
+    public operator fun plusAssign(elements: PKeyList) {
+        addAll(_size, elements)
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutablePKeyList].
+     */
+    public operator fun plusAssign(elements: PKeyArray) {
+        addAll(_size, elements)
+    }
+
+    /**
+     * Removes all elements in the [MutablePKeyList]. The storage isn't released.
+     * @see trim
+     */
+    public fun clear() {
+        _size = 0
+    }
+
+    /**
+     * Reduces the internal storage. If [capacity] is greater than [minCapacity] and [size], the
+     * internal storage is reduced to the maximum of [size] and [minCapacity].
+     * @see ensureCapacity
+     */
+    public fun trim(minCapacity: Int = _size) {
+        val minSize = maxOf(minCapacity, _size)
+        if (capacity > minSize) {
+            content = content.copyOf(minSize)
+        }
+    }
+
+    /**
+     * Ensures that there is enough space to store [capacity] elements in the [MutablePKeyList].
+     * @see trim
+     */
+    public fun ensureCapacity(capacity: Int) {
+        val oldContent = content
+        if (oldContent.size < capacity) {
+            val newSize = maxOf(capacity, oldContent.size * 3 / 2)
+            content = oldContent.copyOf(newSize)
+        }
+    }
+
+    /**
+     * [add] [element] to the [MutablePKeyList].
+     */
+    public inline operator fun plusAssign(element: PKey) {
+        add(element)
+    }
+
+    /**
+     * [remove] [element] from the [MutablePKeyList]
+     */
+    public inline operator fun minusAssign(element: PKey) {
+        remove(element)
+    }
+
+    /**
+     * Removes [element] from the [MutablePKeyList]. If [element] was in the [MutablePKeyList]
+     * and was removed, `true` will be returned, or `false` will be returned if the element
+     * was not found.
+     */
+    public fun remove(element: PKey): Boolean {
+        val index = indexOf(element)
+        if (index >= 0) {
+            removeAt(index)
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Removes all [elements] from the [MutablePKeyList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: PKeyArray): Boolean {
+        val initialSize = _size
+        for (i in elements.indices) {
+            remove(elements[i])
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutablePKeyList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: PKeyList): Boolean {
+        val initialSize = _size
+        for (i in 0..elements.lastIndex) {
+            remove(elements[i])
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutablePKeyList].
+     */
+    public operator fun minusAssign(elements: PKeyArray) {
+        elements.forEach { element ->
+            remove(element)
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutablePKeyList].
+     */
+    public operator fun minusAssign(elements: PKeyList) {
+        elements.forEach { element ->
+            remove(element)
+        }
+    }
+
+    /**
+     * Removes the element at the given [index] and returns it.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public fun removeAt(@androidx.annotation.IntRange(from = 0) index: Int): PKey {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        val content = content
+        val item = content[index]
+        if (index != lastIndex) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index,
+                startIndex = index + 1,
+                endIndex = _size
+            )
+        }
+        _size--
+        return item
+    }
+
+    /**
+     * Removes items from index [start] (inclusive) to [end] (exclusive).
+     * @throws IndexOutOfBoundsException if [start] or [end] isn't between 0 and [size], inclusive
+     * @throws IllegalArgumentException if [start] is greater than [end]
+     */
+    public fun removeRange(
+        @androidx.annotation.IntRange(from = 0) start: Int,
+        @androidx.annotation.IntRange(from = 0) end: Int
+    ) {
+        if (start !in 0.._size || end !in 0.._size) {
+            throw IndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size")
+        }
+        if (end < start) {
+            throw IllegalArgumentException("Start ($start) is more than end ($end)")
+        }
+        if (end != start) {
+            if (end < _size) {
+                content.copyInto(
+                    destination = content,
+                    destinationOffset = start,
+                    startIndex = end,
+                    endIndex = _size
+                )
+            }
+            _size -= (end - start)
+        }
+    }
+
+    /**
+     * Keeps only [elements] in the [MutablePKeyList] and removes all other values.
+     * @return `true` if the [MutablePKeyList] has changed.
+     */
+    public fun retainAll(elements: PKeyArray): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val item = content[i]
+            if (elements.indexOfFirst { it == item } < 0) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Keeps only [elements] in the [MutablePKeyList] and removes all other values.
+     * @return `true` if the [MutablePKeyList] has changed.
+     */
+    public fun retainAll(elements: PKeyList): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val item = content[i]
+            if (item !in elements) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Sets the value at [index] to [element].
+     * @return the previous value set at [index]
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public operator fun set(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        element: PKey
+    ): PKey {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex")
+        }
+        val content = content
+        val old = content[index]
+        content[index] = element
+        return old
+    }
+
+    /**
+     * Sorts the [MutablePKeyList] elements in ascending order.
+     */
+    public fun sort() {
+        content.sort(fromIndex = 0, toIndex = _size)
+    }
+
+    /**
+     * Sorts the [MutablePKeyList] elements in descending order.
+     */
+    public fun sortDescending() {
+        content.sortDescending(fromIndex = 0, toIndex = _size)
+    }
+}
+
+private val EmptyPKeyList: PKeyList = MutablePKeyList(0)
+
+/**
+ * @return a read-only [PKeyList] with nothing in it.
+ */
+public fun emptyPKeyList(): PKeyList = EmptyPKeyList
+
+/**
+ * @return a read-only [PKeyList] with nothing in it.
+ */
+public fun pKeyListOf(): PKeyList = EmptyPKeyList
+
+/**
+ * @return a new read-only [PKeyList] with [element1] as the only item in the list.
+ */
+public fun pKeyListOf(element1: PKey): PKeyList = mutablePKeyListOf(element1)
+
+/**
+ * @return a new read-only [PKeyList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun pKeyListOf(element1: PKey, element2: PKey): PKeyList =
+    mutablePKeyListOf(element1, element2)
+
+/**
+ * @return a new read-only [PKeyList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun pKeyListOf(element1: PKey, element2: PKey, element3: PKey): PKeyList =
+    mutablePKeyListOf(element1, element2, element3)
+
+/**
+ * @return a new read-only [PKeyList] with [elements] in order.
+ */
+public fun pKeyListOf(vararg elements: PKey): PKeyList =
+    MutablePKeyList(elements.size).apply { plusAssign(elements) }
+
+/**
+ * @return a new empty [MutablePKeyList] with the default capacity.
+ */
+public inline fun mutablePKeyListOf(): MutablePKeyList = MutablePKeyList()
+
+/**
+ * @return a new [MutablePKeyList] with [element1] as the only item in the list.
+ */
+public fun mutablePKeyListOf(element1: PKey): MutablePKeyList {
+    val list = MutablePKeyList(1)
+    list += element1
+    return list
+}
+
+/**
+ * @return a new [MutablePKeyList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun mutablePKeyListOf(element1: PKey, element2: PKey): MutablePKeyList {
+    val list = MutablePKeyList(2)
+    list += element1
+    list += element2
+    return list
+}
+
+/**
+ * @return a new [MutablePKeyList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun mutablePKeyListOf(element1: PKey, element2: PKey, element3: PKey): MutablePKeyList {
+    val list = MutablePKeyList(3)
+    list += element1
+    list += element2
+    list += element3
+    return list
+}
+
+/**
+ * @return a new [MutablePKeyList] with the given elements, in order.
+ */
+public inline fun mutablePKeyListOf(vararg elements: PKey): MutablePKeyList =
+    MutablePKeyList(elements.size).apply { plusAssign(elements) }
diff --git a/collection/collection/template/PKeyListTest.kt.template b/collection/collection/template/PKeyListTest.kt.template
new file mode 100644
index 0000000..cd430c9
--- /dev/null
+++ b/collection/collection/template/PKeyListTest.kt.template
@@ -0,0 +1,744 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class PKeyListTest {
+    private val list: MutablePKeyList = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+
+    @Test
+    fun emptyConstruction() {
+        val l = mutablePKeyListOf()
+        assertEquals(0, l.size)
+        assertEquals(16, l.capacity)
+    }
+
+    @Test
+    fun sizeConstruction() {
+        val l = MutablePKeyList(4)
+        assertEquals(4, l.capacity)
+    }
+
+    @Test
+    fun contentConstruction() {
+        val l = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix)
+        assertEquals(3, l.size)
+        assertEquals(1KeySuffix, l[0])
+        assertEquals(2KeySuffix, l[1])
+        assertEquals(3KeySuffix, l[2])
+        assertEquals(3, l.capacity)
+        repeat(2) {
+            val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+            assertEquals(list, l2)
+            l2.removeAt(0)
+        }
+    }
+
+    @Test
+    fun hashCodeTest() {
+        val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        assertEquals(list.hashCode(), l2.hashCode())
+        l2.removeAt(4)
+        assertNotEquals(list.hashCode(), l2.hashCode())
+        l2.add(5KeySuffix)
+        assertEquals(list.hashCode(), l2.hashCode())
+        l2.clear()
+        assertNotEquals(list.hashCode(), l2.hashCode())
+    }
+
+    @Test
+    fun equalsTest() {
+        val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        assertEquals(list, l2)
+        assertNotEquals(list, mutablePKeyListOf())
+        l2.removeAt(4)
+        assertNotEquals(list, l2)
+        l2.add(5KeySuffix)
+        assertEquals(list, l2)
+        l2.clear()
+        assertNotEquals(list, l2)
+    }
+
+    @Test
+    fun string() {
+        assertEquals("[${1KeySuffix}, ${2KeySuffix}, ${3KeySuffix}, ${4KeySuffix}, ${5KeySuffix}]", list.toString())
+        assertEquals("[]", mutablePKeyListOf().toString())
+    }
+
+    @Test
+    fun joinToString() {
+        assertEquals("${1KeySuffix}, ${2KeySuffix}, ${3KeySuffix}, ${4KeySuffix}, ${5KeySuffix}", list.joinToString())
+        assertEquals(
+            "x${1KeySuffix}, ${2KeySuffix}, ${3KeySuffix}...",
+            list.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${1KeySuffix}-${2KeySuffix}-${3KeySuffix}-${4KeySuffix}-${5KeySuffix}<",
+            list.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        assertEquals("one, two, three...", list.joinToString(limit = 3) {
+            when (it.toInt()) {
+                1 -> "one"
+                2 -> "two"
+                3 -> "three"
+                else -> "whoops"
+            }
+        })
+    }
+
+    @Test
+    fun size() {
+        assertEquals(5, list.size)
+        assertEquals(5, list.count())
+        val l2 = mutablePKeyListOf()
+        assertEquals(0, l2.size)
+        assertEquals(0, l2.count())
+        l2 += 1KeySuffix
+        assertEquals(1, l2.size)
+        assertEquals(1, l2.count())
+    }
+
+    @Test
+    fun get() {
+        assertEquals(1KeySuffix, list[0])
+        assertEquals(5KeySuffix, list[4])
+        assertEquals(1KeySuffix, list.elementAt(0))
+        assertEquals(5KeySuffix, list.elementAt(4))
+    }
+
+    @Test
+    fun getOutOfBounds() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list[5]
+        }
+    }
+
+    @Test
+    fun getOutOfBoundsNegative() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list[-1]
+        }
+    }
+
+    @Test
+    fun elementAtOfBounds() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list.elementAt(5)
+        }
+    }
+
+    @Test
+    fun elementAtOfBoundsNegative() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list.elementAt(-1)
+        }
+    }
+
+    @Test
+    fun elementAtOrElse() {
+        assertEquals(1KeySuffix, list.elementAtOrElse(0) {
+            assertEquals(0, it)
+            0KeySuffix
+        })
+        assertEquals(0KeySuffix, list.elementAtOrElse(-1) {
+            assertEquals(-1, it)
+            0KeySuffix
+        })
+        assertEquals(0KeySuffix, list.elementAtOrElse(5) {
+            assertEquals(5, it)
+            0KeySuffix
+        })
+    }
+
+    @Test
+    fun count() {
+        assertEquals(1, list.count { it < 2KeySuffix })
+        assertEquals(0, list.count { it < 0KeySuffix })
+        assertEquals(5, list.count { it < 10KeySuffix })
+    }
+
+    @Test
+    fun isEmpty() {
+        assertFalse(list.isEmpty())
+        assertFalse(list.none())
+        assertTrue(mutablePKeyListOf().isEmpty())
+        assertTrue(mutablePKeyListOf().none())
+    }
+
+    @Test
+    fun isNotEmpty() {
+        assertTrue(list.isNotEmpty())
+        assertTrue(list.any())
+        assertFalse(mutablePKeyListOf().isNotEmpty())
+    }
+
+    @Test
+    fun indices() {
+        assertEquals(IntRange(0, 4), list.indices)
+        assertEquals(IntRange(0, -1), mutablePKeyListOf().indices)
+    }
+
+    @Test
+    fun any() {
+        assertTrue(list.any { it == 5KeySuffix })
+        assertTrue(list.any { it == 1KeySuffix })
+        assertFalse(list.any { it == 0KeySuffix })
+    }
+
+    @Test
+    fun reversedAny() {
+        val reversedList = mutablePKeyListOf()
+        assertFalse(
+            list.reversedAny {
+                reversedList.add(it)
+                false
+            }
+        )
+        val reversedContent = mutablePKeyListOf(5KeySuffix, 4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix)
+        assertEquals(reversedContent, reversedList)
+
+        val reversedSublist = mutablePKeyListOf()
+        assertTrue(
+            list.reversedAny {
+                reversedSublist.add(it)
+                reversedSublist.size == 2
+            }
+        )
+        assertEquals(reversedSublist, mutablePKeyListOf(5KeySuffix, 4KeySuffix))
+    }
+
+    @Test
+    fun forEach() {
+        val copy = mutablePKeyListOf()
+        list.forEach { copy += it }
+        assertEquals(list, copy)
+    }
+
+    @Test
+    fun forEachReversed() {
+        val copy = mutablePKeyListOf()
+        list.forEachReversed { copy += it }
+        assertEquals(copy, mutablePKeyListOf(5KeySuffix, 4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix))
+    }
+
+    @Test
+    fun forEachIndexed() {
+        val copy = mutablePKeyListOf()
+        val indices = mutablePKeyListOf()
+        list.forEachIndexed { index, item ->
+            copy += item
+            indices += index.toPKey()
+        }
+        assertEquals(list, copy)
+        assertEquals(indices, mutablePKeyListOf(0KeySuffix, 1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix))
+    }
+
+    @Test
+    fun forEachReversedIndexed() {
+        val copy = mutablePKeyListOf()
+        val indices = mutablePKeyListOf()
+        list.forEachReversedIndexed { index, item ->
+            copy += item
+            indices += index.toPKey()
+        }
+        assertEquals(copy, mutablePKeyListOf(5KeySuffix, 4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix))
+        assertEquals(indices, mutablePKeyListOf(4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix, 0KeySuffix))
+    }
+
+    @Test
+    fun indexOfFirst() {
+        assertEquals(0, list.indexOfFirst { it == 1KeySuffix })
+        assertEquals(4, list.indexOfFirst { it == 5KeySuffix })
+        assertEquals(-1, list.indexOfFirst { it == 0KeySuffix })
+        assertEquals(0, mutablePKeyListOf(8KeySuffix, 8KeySuffix).indexOfFirst { it == 8KeySuffix })
+    }
+
+    @Test
+    fun indexOfLast() {
+        assertEquals(0, list.indexOfLast { it == 1KeySuffix })
+        assertEquals(4, list.indexOfLast { it == 5KeySuffix })
+        assertEquals(-1, list.indexOfLast { it == 0KeySuffix })
+        assertEquals(1, mutablePKeyListOf(8KeySuffix, 8KeySuffix).indexOfLast { it == 8KeySuffix })
+    }
+
+    @Test
+    fun contains() {
+        assertTrue(list.contains(5KeySuffix))
+        assertTrue(list.contains(1KeySuffix))
+        assertFalse(list.contains(0KeySuffix))
+    }
+
+    @Test
+    fun containsAllList() {
+        assertTrue(list.containsAll(mutablePKeyListOf(2KeySuffix, 3KeySuffix, 1KeySuffix)))
+        assertFalse(list.containsAll(mutablePKeyListOf(2KeySuffix, 3KeySuffix, 6KeySuffix)))
+    }
+
+    @Test
+    fun lastIndexOf() {
+        assertEquals(4, list.lastIndexOf(5KeySuffix))
+        assertEquals(1, list.lastIndexOf(2KeySuffix))
+        val copy = mutablePKeyListOf()
+        copy.addAll(list)
+        copy.addAll(list)
+        assertEquals(5, copy.lastIndexOf(1KeySuffix))
+    }
+
+    @Test
+    fun first() {
+        assertEquals(1KeySuffix, list.first())
+    }
+
+    @Test
+    fun firstException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutablePKeyListOf().first()
+        }
+    }
+
+    @Test
+    fun firstWithPredicate() {
+        assertEquals(5KeySuffix, list.first { it == 5KeySuffix })
+        assertEquals(1KeySuffix, mutablePKeyListOf(1KeySuffix, 5KeySuffix).first { it != 0KeySuffix })
+    }
+
+    @Test
+    fun firstWithPredicateException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutablePKeyListOf().first { it == 8KeySuffix }
+        }
+    }
+
+    @Test
+    fun last() {
+        assertEquals(5KeySuffix, list.last())
+    }
+
+    @Test
+    fun lastException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutablePKeyListOf().last()
+        }
+    }
+
+    @Test
+    fun lastWithPredicate() {
+        assertEquals(1KeySuffix, list.last { it == 1KeySuffix })
+        assertEquals(5KeySuffix, mutablePKeyListOf(1KeySuffix, 5KeySuffix).last { it != 0KeySuffix })
+    }
+
+    @Test
+    fun lastWithPredicateException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutablePKeyListOf().last { it == 8KeySuffix }
+        }
+    }
+
+    @Test
+    fun fold() {
+        assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
+    }
+
+    @Test
+    fun foldIndexed() {
+        assertEquals(
+            "01-12-23-34-45-",
+            list.foldIndexed("") { index, acc, i ->
+                "$acc$index${i.toInt()}-"
+            }
+        )
+    }
+
+    @Test
+    fun foldRight() {
+        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
+    }
+
+    @Test
+    fun foldRightIndexed() {
+        assertEquals(
+            "45-34-23-12-01-",
+            list.foldRightIndexed("") { index, i, acc ->
+                "$acc$index${i.toInt()}-"
+            }
+        )
+    }
+
+    @Test
+    fun add() {
+        val l = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix)
+        l += 4KeySuffix
+        l.add(5KeySuffix)
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun addAtIndex() {
+        val l = mutablePKeyListOf(2KeySuffix, 4KeySuffix)
+        l.add(2, 5KeySuffix)
+        l.add(0, 1KeySuffix)
+        l.add(2, 3KeySuffix)
+        assertEquals(list, l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.add(-1, 2KeySuffix)
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.add(6, 2KeySuffix)
+        }
+    }
+
+    @Test
+    fun addAllListAtIndex() {
+        val l = mutablePKeyListOf(4KeySuffix)
+        val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+        val l3 = mutablePKeyListOf(5KeySuffix)
+        val l4 = mutablePKeyListOf(3KeySuffix)
+        assertTrue(l4.addAll(1, l3))
+        assertTrue(l4.addAll(0, l2))
+        assertTrue(l4.addAll(3, l))
+        assertFalse(l4.addAll(0, mutablePKeyListOf()))
+        assertEquals(list, l4)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l4.addAll(6, mutablePKeyListOf())
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l4.addAll(-1, mutablePKeyListOf())
+        }
+    }
+
+    @Test
+    fun addAllList() {
+        val l = MutablePKeyList()
+        l.add(3KeySuffix)
+        l.add(4KeySuffix)
+        l.add(5KeySuffix)
+        val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+        assertTrue(l2.addAll(l))
+        assertEquals(list, l2)
+        assertFalse(l2.addAll(mutablePKeyListOf()))
+    }
+
+    @Test
+    fun plusAssignList() {
+        val l = MutablePKeyList()
+        l.add(3KeySuffix)
+        l.add(4KeySuffix)
+        l.add(5KeySuffix)
+        val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+        l2 += l
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun addAllArrayAtIndex() {
+        val a1 = pKeyArrayOf(4KeySuffix)
+        val a2 = pKeyArrayOf(1KeySuffix, 2KeySuffix)
+        val a3 = pKeyArrayOf(5KeySuffix)
+        val l = mutablePKeyListOf(3KeySuffix)
+        assertTrue(l.addAll(1, a3))
+        assertTrue(l.addAll(0, a2))
+        assertTrue(l.addAll(3, a1))
+        assertFalse(l.addAll(0, pKeyArrayOf()))
+        assertEquals(list, l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.addAll(6, pKeyArrayOf())
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.addAll(-1, pKeyArrayOf())
+        }
+    }
+
+    @Test
+    fun addAllArray() {
+        val a = pKeyArrayOf(3KeySuffix, 4KeySuffix, 5KeySuffix)
+        val v = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+        v.addAll(a)
+        assertEquals(5, v.size)
+        assertEquals(3KeySuffix, v[2])
+        assertEquals(4KeySuffix, v[3])
+        assertEquals(5KeySuffix, v[4])
+    }
+
+    @Test
+    fun plusAssignArray() {
+        val a = pKeyArrayOf(3KeySuffix, 4KeySuffix, 5KeySuffix)
+        val v = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+        v += a
+        assertEquals(list, v)
+    }
+
+    @Test
+    fun clear() {
+        val l = mutablePKeyListOf()
+        l.addAll(list)
+        assertTrue(l.isNotEmpty())
+        l.clear()
+        assertTrue(l.isEmpty())
+    }
+
+    @Test
+    fun trim() {
+        val l = mutablePKeyListOf(1KeySuffix)
+        l.trim()
+        assertEquals(1, l.capacity)
+        l += pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        l.trim()
+        assertEquals(6, l.capacity)
+        assertEquals(6, l.size)
+        l.clear()
+        l.trim()
+        assertEquals(0, l.capacity)
+        l.trim(100)
+        assertEquals(0, l.capacity)
+        l += pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        l -= 5KeySuffix
+        l.trim(5)
+        assertEquals(5, l.capacity)
+        l.trim(4)
+        assertEquals(4, l.capacity)
+        l.trim(3)
+        assertEquals(4, l.capacity)
+    }
+
+    @Test
+    fun remove() {
+        val l = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        l.remove(3KeySuffix)
+        assertEquals(mutablePKeyListOf(1KeySuffix, 2KeySuffix, 4KeySuffix, 5KeySuffix), l)
+    }
+
+    @Test
+    fun removeAt() {
+        val l = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        l.removeAt(2)
+        assertEquals(mutablePKeyListOf(1KeySuffix, 2KeySuffix, 4KeySuffix, 5KeySuffix), l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.removeAt(6)
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.removeAt(-1)
+        }
+    }
+
+    @Test
+    fun set() {
+        val l = mutablePKeyListOf(0KeySuffix, 0KeySuffix, 0KeySuffix, 0KeySuffix, 0KeySuffix)
+        l[0] = 1KeySuffix
+        l[4] = 5KeySuffix
+        l[2] = 3KeySuffix
+        l[1] = 2KeySuffix
+        l[3] = 4KeySuffix
+        assertEquals(list, l)
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.set(-1, 1KeySuffix)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.set(6, 1KeySuffix)
+        }
+        assertEquals(4KeySuffix, l.set(3, 1KeySuffix));
+    }
+
+    @Test
+    fun ensureCapacity() {
+        val l = mutablePKeyListOf(1KeySuffix)
+        assertEquals(1, l.capacity)
+        l.ensureCapacity(5)
+        assertEquals(5, l.capacity)
+    }
+
+    @Test
+    fun removeAllList() {
+        assertFalse(list.removeAll(mutablePKeyListOf(0KeySuffix, 10KeySuffix, 15KeySuffix)))
+        val l = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix, 5KeySuffix)
+        assertTrue(l.removeAll(mutablePKeyListOf(20KeySuffix, 0KeySuffix, 15KeySuffix, 10KeySuffix, 5KeySuffix)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllPKeyArray() {
+        assertFalse(list.removeAll(pKeyArrayOf(0KeySuffix, 10KeySuffix, 15KeySuffix)))
+        val l = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix, 5KeySuffix)
+        assertTrue(l.removeAll(pKeyArrayOf(20KeySuffix, 0KeySuffix, 15KeySuffix, 10KeySuffix, 5KeySuffix)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun minusAssignList() {
+        val l = mutablePKeyListOf().also { it += list }
+        l -= mutablePKeyListOf(0KeySuffix, 10KeySuffix, 15KeySuffix)
+        assertEquals(list, l)
+        val l2 = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix, 5KeySuffix)
+        l2 -= mutablePKeyListOf(20KeySuffix, 0KeySuffix, 15KeySuffix, 10KeySuffix, 5KeySuffix)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignPKeyArray() {
+        val l = mutablePKeyListOf().also { it += list }
+        l -= pKeyArrayOf(0KeySuffix, 10KeySuffix, 15KeySuffix)
+        assertEquals(list, l)
+        val l2 = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix, 5KeySuffix)
+        l2 -= pKeyArrayOf(20KeySuffix, 0KeySuffix, 15KeySuffix, 10KeySuffix, 5KeySuffix)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun retainAll() {
+        assertFalse(list.retainAll(mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 6KeySuffix)))
+        val l = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix)
+        assertTrue(l.retainAll(mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 6KeySuffix)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun retainAllPKeyArray() {
+        assertFalse(list.retainAll(pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 6KeySuffix)))
+        val l = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix)
+        assertTrue(l.retainAll(pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 6KeySuffix)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeRange() {
+        val l = mutablePKeyListOf(1KeySuffix, 9KeySuffix, 7KeySuffix, 6KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        l.removeRange(1, 4)
+        assertEquals(list, l)
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(6, 6)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(100, 200)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(-1, 0)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            l.removeRange(3, 2)
+        }
+    }
+
+    @Test
+    fun sort() {
+        val l = mutablePKeyListOf(1KeySuffix, 4KeySuffix, 2KeySuffix, 5KeySuffix, 3KeySuffix)
+        l.sort()
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun sortDescending() {
+        val l = mutablePKeyListOf(1KeySuffix, 4KeySuffix, 2KeySuffix, 5KeySuffix, 3KeySuffix)
+        l.sortDescending()
+        assertEquals(mutablePKeyListOf(5KeySuffix, 4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix), l)
+    }
+
+    @Test
+    fun testEmptyPKeyList() {
+        val l = emptyPKeyList()
+        assertEquals(0, l.size)
+    }
+
+    @Test
+    fun pKeyListOfEmpty() {
+        val l = pKeyListOf()
+        assertEquals(0, l.size)
+    }
+
+    @Test
+    fun pKeyListOfOneValue() {
+        val l = pKeyListOf(2KeySuffix)
+        assertEquals(1, l.size)
+        assertEquals(2KeySuffix, l[0])
+    }
+
+    @Test
+    fun pKeyListOfTwoValues() {
+        val l = pKeyListOf(2KeySuffix, 1KeySuffix)
+        assertEquals(2, l.size)
+        assertEquals(2KeySuffix, l[0])
+        assertEquals(1KeySuffix, l[1])
+    }
+
+    @Test
+    fun pKeyListOfThreeValues() {
+        val l = pKeyListOf(2KeySuffix, 10KeySuffix, -1KeySuffix)
+        assertEquals(3, l.size)
+        assertEquals(2KeySuffix, l[0])
+        assertEquals(10KeySuffix, l[1])
+        assertEquals(-1KeySuffix, l[2])
+    }
+
+    @Test
+    fun pKeyListOfFourValues() {
+        val l = pKeyListOf(2KeySuffix, 10KeySuffix, -1KeySuffix, 10KeySuffix)
+        assertEquals(4, l.size)
+        assertEquals(2KeySuffix, l[0])
+        assertEquals(10KeySuffix, l[1])
+        assertEquals(-1KeySuffix, l[2])
+        assertEquals(10KeySuffix, l[3])
+    }
+
+    @Test
+    fun mutablePKeyListOfOneValue() {
+        val l = mutablePKeyListOf(2KeySuffix)
+        assertEquals(1, l.size)
+        assertEquals(1, l.capacity)
+        assertEquals(2KeySuffix, l[0])
+    }
+
+    @Test
+    fun mutablePKeyListOfTwoValues() {
+        val l = mutablePKeyListOf(2KeySuffix, 1KeySuffix)
+        assertEquals(2, l.size)
+        assertEquals(2, l.capacity)
+        assertEquals(2KeySuffix, l[0])
+        assertEquals(1KeySuffix, l[1])
+    }
+
+    @Test
+    fun mutablePKeyListOfThreeValues() {
+        val l = mutablePKeyListOf(2KeySuffix, 10KeySuffix, -1KeySuffix)
+        assertEquals(3, l.size)
+        assertEquals(3, l.capacity)
+        assertEquals(2KeySuffix, l[0])
+        assertEquals(10KeySuffix, l[1])
+        assertEquals(-1KeySuffix, l[2])
+    }
+
+    @Test
+    fun mutablePKeyListOfFourValues() {
+        val l = mutablePKeyListOf(2KeySuffix, 10KeySuffix, -1KeySuffix, 10KeySuffix)
+        assertEquals(4, l.size)
+        assertEquals(4, l.capacity)
+        assertEquals(2KeySuffix, l[0])
+        assertEquals(10KeySuffix, l[1])
+        assertEquals(-1KeySuffix, l[2])
+        assertEquals(10KeySuffix, l[3])
+    }
+}
diff --git a/collection/collection/template/PKeyObjectMap.kt.template b/collection/collection/template/PKeyObjectMap.kt.template
new file mode 100644
index 0000000..5908308
--- /dev/null
+++ b/collection/collection/template/PKeyObjectMap.kt.template
@@ -0,0 +1,1016 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyPKeyObjectMap = MutablePKeyObjectMap<Nothing>(0)
+
+/**
+ * Returns an empty, read-only [PKeyObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> emptyPKeyObjectMap(): PKeyObjectMap<V> = EmptyPKeyObjectMap as PKeyObjectMap<V>
+
+/**
+ * Returns an empty, read-only [PKeyObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> pKeyObjectMapOf(): PKeyObjectMap<V> = EmptyPKeyObjectMap as PKeyObjectMap<V>
+
+/**
+ * Returns a new [PKeyObjectMap] with [key1] associated with [value1].
+ */
+public fun <V> pKeyObjectMapOf(
+    key1: PKey,
+    value1: V
+): PKeyObjectMap<V> = MutablePKeyObjectMap<V>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [PKeyObjectMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun <V> pKeyObjectMapOf(
+    key1: PKey,
+    value1: V,
+    key2: PKey,
+    value2: V,
+): PKeyObjectMap<V> = MutablePKeyObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [PKeyObjectMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun <V> pKeyObjectMapOf(
+    key1: PKey,
+    value1: V,
+    key2: PKey,
+    value2: V,
+    key3: PKey,
+    value3: V,
+): PKeyObjectMap<V> = MutablePKeyObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [PKeyObjectMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <V> pKeyObjectMapOf(
+    key1: PKey,
+    value1: V,
+    key2: PKey,
+    value2: V,
+    key3: PKey,
+    value3: V,
+    key4: PKey,
+    value4: V,
+): PKeyObjectMap<V> = MutablePKeyObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [PKeyObjectMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <V> pKeyObjectMapOf(
+    key1: PKey,
+    value1: V,
+    key2: PKey,
+    value2: V,
+    key3: PKey,
+    value3: V,
+    key4: PKey,
+    value4: V,
+    key5: PKey,
+    value5: V,
+): PKeyObjectMap<V> = MutablePKeyObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutablePKeyObjectMap].
+ */
+public fun <V> mutablePKeyObjectMapOf(): MutablePKeyObjectMap<V> = MutablePKeyObjectMap()
+
+/**
+ * Returns a new [MutablePKeyObjectMap] with [key1] associated with [value1].
+ */
+public fun <V> mutablePKeyObjectMapOf(
+    key1: PKey,
+    value1: V
+): MutablePKeyObjectMap<V> = MutablePKeyObjectMap<V>().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutablePKeyObjectMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun <V> mutablePKeyObjectMapOf(
+    key1: PKey,
+    value1: V,
+    key2: PKey,
+    value2: V,
+): MutablePKeyObjectMap<V> = MutablePKeyObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutablePKeyObjectMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun <V> mutablePKeyObjectMapOf(
+    key1: PKey,
+    value1: V,
+    key2: PKey,
+    value2: V,
+    key3: PKey,
+    value3: V,
+): MutablePKeyObjectMap<V> = MutablePKeyObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutablePKeyObjectMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun <V> mutablePKeyObjectMapOf(
+    key1: PKey,
+    value1: V,
+    key2: PKey,
+    value2: V,
+    key3: PKey,
+    value3: V,
+    key4: PKey,
+    value4: V,
+): MutablePKeyObjectMap<V> = MutablePKeyObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutablePKeyObjectMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun <V> mutablePKeyObjectMapOf(
+    key1: PKey,
+    value1: V,
+    key2: PKey,
+    value2: V,
+    key3: PKey,
+    value3: V,
+    key4: PKey,
+    value4: V,
+    key5: PKey,
+    value5: V,
+): MutablePKeyObjectMap<V> = MutablePKeyObjectMap<V>().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [PKeyObjectMap] is a container with a [Map]-like interface for keys with
+ * [PKey] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutablePKeyObjectMap].
+ *
+ * @see [MutablePKeyObjectMap]
+ */
+public sealed class PKeyObjectMap<V> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: PKeyArray = EmptyPKeyArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: Array<Any?> = EMPTY_OBJECTS
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     */
+    public operator fun get(key: PKey): V? {
+        val index = findKeyIndex(key)
+        @Suppress("UNCHECKED_CAST")
+        return if (index >= 0) values[index] as V? else null
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: PKey, defaultValue: V): V {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            @Suppress("UNCHECKED_CAST")
+            return values[index] as V
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: PKey, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: PKey, value: V) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index], v[index] as V)
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: PKey) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: V) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(v[index] as V)
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (PKey, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (PKey, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (PKey, V) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: PKey): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: PKey): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: V): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@PKeyObjectMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: PKey, value: V) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@PKeyObjectMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [PKeyObjectMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is PKeyObjectMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value == null) {
+                if (other[key] != null || !other.containsKey(key)) {
+                    return false
+                }
+            } else if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(if (value === this) "(this)" else value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    internal inline fun findKeyIndex(key: PKey): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutablePKeyObjectMap] is a container with a [MutableMap]-like interface for keys with
+ * [PKey] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutablePKeyObjectMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see ScatterMap
+ */
+public class MutablePKeyObjectMap<V>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : PKeyObjectMap<V>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = PKeyArray(newCapacity)
+        values = arrayOfNulls(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: PKey, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue().also { set(key, it) }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: PKey, value: V) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: PKey, value: V): V? {
+        val index = findAbsoluteInsertIndex(key)
+        val oldValue = values[index]
+        keys[index] = key
+        values[index] = value
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: PKeyObjectMap<V>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: PKeyObjectMap<V>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map. If the
+     * [key] was present in the map, this function returns the value that was
+     * present before removal.
+     */
+    public fun remove(key: PKey): V? {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return removeValueAt(index)
+        }
+        return null
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: PKey, value: V): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (PKey, V) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index], values[index] as V)) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: PKey) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: PKeyArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: PKeySet) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: PKeyList) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int): V? {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        val oldValue = values[index]
+        values[index] = null
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        values.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: PKey): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutablePKeyObjectMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/template/PKeyObjectMapTest.kt.template b/collection/collection/template/PKeyObjectMapTest.kt.template
new file mode 100644
index 0000000..817e459
--- /dev/null
+++ b/collection/collection/template/PKeyObjectMapTest.kt.template
@@ -0,0 +1,734 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class PKeyObjectMapTest {
+    @Test
+    fun pKeyObjectMap() {
+        val map = MutablePKeyObjectMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyPKeyObjectMap() {
+        val map = emptyPKeyObjectMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyPKeyObjectMap<String>(), map)
+    }
+
+    @Test
+    fun pKeyObjectMapFunction() {
+        val map = mutablePKeyObjectMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutablePKeyObjectMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun pKeyObjectMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutablePKeyObjectMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun pKeyObjectMapInitFunction() {
+        val map1 = pKeyObjectMapOf(
+            1KeySuffix, "World",
+        )
+        assertEquals(1, map1.size)
+        assertEquals("World", map1[1KeySuffix])
+
+        val map2 = pKeyObjectMapOf(
+            1KeySuffix, "World",
+            2KeySuffix, "Monde",
+        )
+        assertEquals(2, map2.size)
+        assertEquals("World", map2[1KeySuffix])
+        assertEquals("Monde", map2[2KeySuffix])
+
+        val map3 = pKeyObjectMapOf(
+            1KeySuffix, "World",
+            2KeySuffix, "Monde",
+            3KeySuffix, "Welt",
+        )
+        assertEquals(3, map3.size)
+        assertEquals("World", map3[1KeySuffix])
+        assertEquals("Monde", map3[2KeySuffix])
+        assertEquals("Welt", map3[3KeySuffix])
+
+        val map4 = pKeyObjectMapOf(
+            1KeySuffix, "World",
+            2KeySuffix, "Monde",
+            3KeySuffix, "Welt",
+            4KeySuffix, "Sekai",
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals("World", map4[1KeySuffix])
+        assertEquals("Monde", map4[2KeySuffix])
+        assertEquals("Welt", map4[3KeySuffix])
+        assertEquals("Sekai", map4[4KeySuffix])
+
+        val map5 = pKeyObjectMapOf(
+            1KeySuffix, "World",
+            2KeySuffix, "Monde",
+            3KeySuffix, "Welt",
+            4KeySuffix, "Sekai",
+            5KeySuffix, "Mondo",
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals("World", map5[1KeySuffix])
+        assertEquals("Monde", map5[2KeySuffix])
+        assertEquals("Welt", map5[3KeySuffix])
+        assertEquals("Sekai", map5[4KeySuffix])
+        assertEquals("Mondo", map5[5KeySuffix])
+    }
+
+    @Test
+    fun mutablePKeyObjectMapInitFunction() {
+        val map1 = mutablePKeyObjectMapOf(
+            1KeySuffix, "World",
+        )
+        assertEquals(1, map1.size)
+        assertEquals("World", map1[1KeySuffix])
+
+        val map2 = mutablePKeyObjectMapOf(
+            1KeySuffix, "World",
+            2KeySuffix, "Monde",
+        )
+        assertEquals(2, map2.size)
+        assertEquals("World", map2[1KeySuffix])
+        assertEquals("Monde", map2[2KeySuffix])
+
+        val map3 = mutablePKeyObjectMapOf(
+            1KeySuffix, "World",
+            2KeySuffix, "Monde",
+            3KeySuffix, "Welt",
+        )
+        assertEquals(3, map3.size)
+        assertEquals("World", map3[1KeySuffix])
+        assertEquals("Monde", map3[2KeySuffix])
+        assertEquals("Welt", map3[3KeySuffix])
+
+        val map4 = mutablePKeyObjectMapOf(
+            1KeySuffix, "World",
+            2KeySuffix, "Monde",
+            3KeySuffix, "Welt",
+            4KeySuffix, "Sekai",
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals("World", map4[1KeySuffix])
+        assertEquals("Monde", map4[2KeySuffix])
+        assertEquals("Welt", map4[3KeySuffix])
+        assertEquals("Sekai", map4[4KeySuffix])
+
+        val map5 = mutablePKeyObjectMapOf(
+            1KeySuffix, "World",
+            2KeySuffix, "Monde",
+            3KeySuffix, "Welt",
+            4KeySuffix, "Sekai",
+            5KeySuffix, "Mondo",
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals("World", map5[1KeySuffix])
+        assertEquals("Monde", map5[2KeySuffix])
+        assertEquals("Welt", map5[3KeySuffix])
+        assertEquals("Sekai", map5[4KeySuffix])
+        assertEquals("Mondo", map5[5KeySuffix])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1KeySuffix])
+    }
+
+    @Test
+    fun insertIndex0() {
+        val map = MutablePKeyObjectMap<String>()
+        map.put(1KeySuffix, "World")
+        assertEquals("World", map[1KeySuffix])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutablePKeyObjectMap<String>(12)
+        map[1KeySuffix] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1KeySuffix])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutablePKeyObjectMap<String>(2)
+        map[1KeySuffix] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals("World", map[1KeySuffix])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutablePKeyObjectMap<String>(0)
+        map[1KeySuffix] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1KeySuffix])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[1KeySuffix] = "Monde"
+
+        assertEquals(1, map.size)
+        assertEquals("Monde", map[1KeySuffix])
+    }
+
+    @Test
+    fun put() {
+        val map = MutablePKeyObjectMap<String?>()
+
+        assertNull(map.put(1KeySuffix, "World"))
+        assertEquals("World", map.put(1KeySuffix, "Monde"))
+        assertNull(map.put(2KeySuffix, null))
+        assertNull(map.put(2KeySuffix, "Monde"))
+    }
+
+    @Test
+    fun putAllMap() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        map.putAll(mutablePKeyObjectMapOf(3KeySuffix, "Welt", 7KeySuffix, "Mundo"))
+
+        assertEquals(4, map.size)
+        assertEquals("Welt", map[3KeySuffix])
+        assertEquals("Mundo", map[7KeySuffix])
+    }
+
+    @Test
+    fun plusMap() {
+        val map = MutablePKeyObjectMap<String>()
+        map += pKeyObjectMapOf(3KeySuffix, "Welt", 7KeySuffix, "Mundo")
+
+        assertEquals(2, map.size)
+        assertEquals("Welt", map[3KeySuffix])
+        assertEquals("Mundo", map[7KeySuffix])
+    }
+
+    @Test
+    fun nullValue() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = null
+
+        assertEquals(1, map.size)
+        assertNull(map[1KeySuffix])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+
+        assertNull(map[2KeySuffix])
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+
+        assertEquals("Monde", map.getOrDefault(2KeySuffix, "Monde"))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        assertEquals("Monde", map.getOrElse(2KeySuffix) { "Monde" })
+        assertEquals("Welt", map.getOrElse(3KeySuffix) { "Welt" })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+
+        var counter = 0
+        map.getOrPut(1KeySuffix) {
+            counter++
+            "Monde"
+        }
+        assertEquals("World", map[1KeySuffix])
+        assertEquals(0, counter)
+
+        map.getOrPut(2KeySuffix) {
+            counter++
+            "Monde"
+        }
+        assertEquals("Monde", map[2KeySuffix])
+        assertEquals(1, counter)
+
+        map.getOrPut(2KeySuffix) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Monde", map[2KeySuffix])
+        assertEquals(1, counter)
+
+        map.getOrPut(3KeySuffix) {
+            counter++
+            null
+        }
+        assertNull(map[3KeySuffix])
+        assertEquals(2, counter)
+
+        map.getOrPut(3KeySuffix) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Welt", map[3KeySuffix])
+        assertEquals(3, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutablePKeyObjectMap<String?>()
+        assertNull(map.remove(1KeySuffix))
+
+        map[1KeySuffix] = "World"
+        assertEquals("World", map.remove(1KeySuffix))
+        assertEquals(0, map.size)
+
+        map[1KeySuffix] = null
+        assertNull(map.remove(1KeySuffix))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutablePKeyObjectMap<String>(6)
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+        map[4KeySuffix] = "Sekai"
+        map[5KeySuffix] = "Mondo"
+        map[6KeySuffix] = "Sesang"
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1KeySuffix)
+        map.remove(2KeySuffix)
+        map.remove(3KeySuffix)
+        map.remove(4KeySuffix)
+        map.remove(5KeySuffix)
+        map.remove(6KeySuffix)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7KeySuffix] = "Mundo"
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+        map[4KeySuffix] = "Sekai"
+        map[5KeySuffix] = "Mondo"
+        map[6KeySuffix] = "Sesang"
+
+        map.removeIf { key, value ->
+            key == 1KeySuffix || key == 3KeySuffix || value.startsWith('S')
+        }
+
+        assertEquals(2, map.size)
+        assertEquals("Monde", map[2KeySuffix])
+        assertEquals("Mondo", map[5KeySuffix])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+
+        map -= 1KeySuffix
+
+        assertEquals(2, map.size)
+        assertNull(map[1KeySuffix])
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+
+        map -= pKeyArrayOf(3KeySuffix, 2KeySuffix)
+
+        assertEquals(1, map.size)
+        assertNull(map[3KeySuffix])
+        assertNull(map[2KeySuffix])
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+
+        map -= pKeySetOf(3KeySuffix, 2KeySuffix)
+
+        assertEquals(1, map.size)
+        assertNull(map[3KeySuffix])
+        assertNull(map[2KeySuffix])
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+
+        map -= pKeyListOf(3KeySuffix, 2KeySuffix)
+
+        assertEquals(1, map.size)
+        assertNull(map[3KeySuffix])
+        assertNull(map[2KeySuffix])
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutablePKeyObjectMap<String?>()
+        assertFalse(map.remove(1KeySuffix, "World"))
+
+        map[1KeySuffix] = "World"
+        assertTrue(map.remove(1KeySuffix, "World"))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutablePKeyObjectMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toPKey()] = i.toString()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutablePKeyObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toPKey()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key.toInt().toString(), value)
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutablePKeyObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toPKey()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertEquals(key.toInt().toString(), map[key])
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutablePKeyObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toPKey()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachValue { value ->
+                assertNotNull(value.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutablePKeyObjectMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toPKey()] = i.toString()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutablePKeyObjectMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        val oneKey = 1KeySuffix.toString()
+        val twoKey = 2KeySuffix.toString()
+        assertTrue(
+            "{$oneKey=World, $twoKey=Monde}" == map.toString() ||
+                "{$twoKey=Monde, $oneKey=World}" == map.toString()
+        )
+
+        map.clear()
+        map[1KeySuffix] = null
+        assertEquals("{$oneKey=null}", map.toString())
+
+        val selfAsValueMap = MutablePKeyObjectMap<Any>()
+        selfAsValueMap[1KeySuffix] = selfAsValueMap
+        assertEquals("{$oneKey=(this)}", selfAsValueMap.toString())
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutablePKeyObjectMap<String>()
+        repeat(5) {
+            map[it.toPKey()] = it.toString()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toPKey()}=${order[0]}, ${order[1].toPKey()}=${order[1]}, " +
+            "${order[2].toPKey()}=${order[2]}, ${order[3].toPKey()}=${order[3]}, " +
+            "${order[4].toPKey()}=${order[4]}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toPKey()}=${order[0]}, ${order[1].toPKey()}=${order[1]}, " +
+            "${order[2].toPKey()}=${order[2]}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toPKey()}=${order[0]}-${order[1].toPKey()}=${order[1]}-" +
+            "${order[2].toPKey()}=${order[2]}-${order[3].toPKey()}=${order[3]}-" +
+            "${order[4].toPKey()}=${order[4]}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutablePKeyObjectMap<String?>()
+        map2[2KeySuffix] = null
+
+        assertNotEquals(map, map2)
+
+        map2[1KeySuffix] = "World"
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        assertTrue(map.containsKey(1KeySuffix))
+        assertFalse(map.containsKey(3KeySuffix))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        assertTrue(1KeySuffix in map)
+        assertFalse(3KeySuffix in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        assertTrue(map.containsValue("World"))
+        assertTrue(map.containsValue(null))
+        assertFalse(map.containsValue("Monde"))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutablePKeyObjectMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1KeySuffix] = "World"
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutablePKeyObjectMap<String>()
+        assertEquals(0, map.count())
+
+        map[1KeySuffix] = "World"
+        assertEquals(1, map.count())
+
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+        map[4KeySuffix] = "Sekai"
+        map[5KeySuffix] = "Mondo"
+        map[6KeySuffix] = "Sesang"
+
+        assertEquals(2, map.count { key, _ -> key < 3KeySuffix })
+        assertEquals(0, map.count { key, _ -> key < 0KeySuffix })
+    }
+
+    @Test
+    fun any() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+        map[4KeySuffix] = "Sekai"
+        map[5KeySuffix] = "Mondo"
+        map[6KeySuffix] = "Sesang"
+
+        assertTrue(map.any { key, _ -> key > 5KeySuffix })
+        assertFalse(map.any { key, _ -> key < 0KeySuffix })
+    }
+
+    @Test
+    fun all() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+        map[4KeySuffix] = "Sekai"
+        map[5KeySuffix] = "Mondo"
+        map[6KeySuffix] = "Sesang"
+
+        assertTrue(map.all { key, value -> key < 7KeySuffix && value.isNotEmpty() })
+        assertFalse(map.all { key, _ -> key < 6KeySuffix })
+    }
+}
diff --git a/collection/collection/template/PKeyPValueMap.kt.template b/collection/collection/template/PKeyPValueMap.kt.template
new file mode 100644
index 0000000..48eaa25
--- /dev/null
+++ b/collection/collection/template/PKeyPValueMap.kt.template
@@ -0,0 +1,1005 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyPKeyPValueMap = MutablePKeyPValueMap(0)
+
+/**
+ * Returns an empty, read-only [PKeyPValueMap].
+ */
+public fun emptyPKeyPValueMap(): PKeyPValueMap = EmptyPKeyPValueMap
+
+/**
+ * Returns a new [MutablePKeyPValueMap].
+ */
+public fun pKeyPValueMapOf(): PKeyPValueMap = EmptyPKeyPValueMap
+
+/**
+ * Returns a new [PKeyPValueMap] with [key1] associated with [value1].
+ */
+public fun pKeyPValueMapOf(
+    key1: PKey,
+    value1: PValue
+): PKeyPValueMap = MutablePKeyPValueMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [PKeyPValueMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun pKeyPValueMapOf(
+    key1: PKey,
+    value1: PValue,
+    key2: PKey,
+    value2: PValue,
+): PKeyPValueMap = MutablePKeyPValueMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [PKeyPValueMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun pKeyPValueMapOf(
+    key1: PKey,
+    value1: PValue,
+    key2: PKey,
+    value2: PValue,
+    key3: PKey,
+    value3: PValue,
+): PKeyPValueMap = MutablePKeyPValueMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [PKeyPValueMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun pKeyPValueMapOf(
+    key1: PKey,
+    value1: PValue,
+    key2: PKey,
+    value2: PValue,
+    key3: PKey,
+    value3: PValue,
+    key4: PKey,
+    value4: PValue,
+): PKeyPValueMap = MutablePKeyPValueMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [PKeyPValueMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun pKeyPValueMapOf(
+    key1: PKey,
+    value1: PValue,
+    key2: PKey,
+    value2: PValue,
+    key3: PKey,
+    value3: PValue,
+    key4: PKey,
+    value4: PValue,
+    key5: PKey,
+    value5: PValue,
+): PKeyPValueMap = MutablePKeyPValueMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * Returns a new [MutablePKeyPValueMap].
+ */
+public fun mutablePKeyPValueMapOf(): MutablePKeyPValueMap = MutablePKeyPValueMap()
+
+/**
+ * Returns a new [MutablePKeyPValueMap] with [key1] associated with [value1].
+ */
+public fun mutablePKeyPValueMapOf(
+    key1: PKey,
+    value1: PValue
+): MutablePKeyPValueMap = MutablePKeyPValueMap().also { map ->
+        map[key1] = value1
+    }
+
+/**
+ * Returns a new [MutablePKeyPValueMap] with [key1], and [key2]
+ * associated with [value1], and [value2], respectively.
+ */
+public fun mutablePKeyPValueMapOf(
+    key1: PKey,
+    value1: PValue,
+    key2: PKey,
+    value2: PValue,
+): MutablePKeyPValueMap = MutablePKeyPValueMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+    }
+
+/**
+ * Returns a new [MutablePKeyPValueMap] with [key1], [key2], and [key3]
+ * associated with [value1], [value2], and [value3], respectively.
+ */
+public fun mutablePKeyPValueMapOf(
+    key1: PKey,
+    value1: PValue,
+    key2: PKey,
+    value2: PValue,
+    key3: PKey,
+    value3: PValue,
+): MutablePKeyPValueMap = MutablePKeyPValueMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+    }
+
+/**
+ * Returns a new [MutablePKeyPValueMap] with [key1], [key2], [key3], and [key4]
+ * associated with [value1], [value2], [value3], and [value4], respectively.
+ */
+public fun mutablePKeyPValueMapOf(
+    key1: PKey,
+    value1: PValue,
+    key2: PKey,
+    value2: PValue,
+    key3: PKey,
+    value3: PValue,
+    key4: PKey,
+    value4: PValue,
+): MutablePKeyPValueMap = MutablePKeyPValueMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+    }
+
+/**
+ * Returns a new [MutablePKeyPValueMap] with [key1], [key2], [key3], [key4], and [key5]
+ * associated with [value1], [value2], [value3], [value4], and [value5], respectively.
+ */
+public fun mutablePKeyPValueMapOf(
+    key1: PKey,
+    value1: PValue,
+    key2: PKey,
+    value2: PValue,
+    key3: PKey,
+    value3: PValue,
+    key4: PKey,
+    value4: PValue,
+    key5: PKey,
+    value5: PValue,
+): MutablePKeyPValueMap = MutablePKeyPValueMap().also { map ->
+        map[key1] = value1
+        map[key2] = value2
+        map[key3] = value3
+        map[key4] = value4
+        map[key5] = value5
+    }
+
+/**
+ * [PKeyPValueMap] is a container with a [Map]-like interface for
+ * [PKey] primitive keys and [PValue] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutablePKeyPValueMap].
+ *
+ * @see [MutablePKeyPValueMap]
+ * @see [ScatterMap]
+ */
+public sealed class PKeyPValueMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: PKeyArray = EmptyPKeyArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: PValueArray = EmptyPValueArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: PKey): PValue {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: PKey, defaultValue: PValue): PValue {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: PKey, defaultValue: () -> PValue): PValue {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: PKey, value: PValue) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: PKey) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: PValue) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (PKey, PValue) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (PKey, PValue) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (PKey, PValue) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: PKey): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: PKey): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: PValue): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@PKeyPValueMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(key)
+            append('=')
+            append(value)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the entries, separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. Each entry is created with [transform].
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (key: PKey, value: PValue) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@PKeyPValueMap.forEach { key, value ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(key, value))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [PKeyPValueMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is PKeyPValueMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: PKey): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutablePKeyPValueMap] is a container with a [MutableMap]-like interface for
+ * [PKey] primitive keys and [PValue] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutablePKeyPValueMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutablePKeyPValueMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : PKeyPValueMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = PKeyArray(newCapacity)
+        values = PValueArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: PKey, defaultValue: () -> PValue): PValue {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: PKey, value: PValue) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: PKey, value: PValue) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: PKeyPValueMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: PKeyPValueMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: PKey) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: PKey, value: PValue): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (PKey, PValue) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: PKey) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: PKeyArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: PKeySet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: PKeyList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: PKey): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutablePKeyPValueMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/template/PKeyPValueMapTest.kt.template b/collection/collection/template/PKeyPValueMapTest.kt.template
new file mode 100644
index 0000000..e6609c5
--- /dev/null
+++ b/collection/collection/template/PKeyPValueMapTest.kt.template
@@ -0,0 +1,706 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class PKeyPValueMapTest {
+    @Test
+    fun pKeyPValueMap() {
+        val map = MutablePKeyPValueMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyPKeyPValueMap() {
+        val map = emptyPKeyPValueMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyPKeyPValueMap(), map)
+    }
+
+    @Test
+    fun pKeyPValueMapFunction() {
+        val map = mutablePKeyPValueMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutablePKeyPValueMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun pKeyPValueMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutablePKeyPValueMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun pKeyPValueMapInitFunction() {
+        val map1 = pKeyPValueMapOf(
+            1KeySuffix, 1ValueSuffix,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1ValueSuffix, map1[1KeySuffix])
+
+        val map2 = pKeyPValueMapOf(
+            1KeySuffix, 1ValueSuffix,
+            2KeySuffix, 2ValueSuffix,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1ValueSuffix, map2[1KeySuffix])
+        assertEquals(2ValueSuffix, map2[2KeySuffix])
+
+        val map3 = pKeyPValueMapOf(
+            1KeySuffix, 1ValueSuffix,
+            2KeySuffix, 2ValueSuffix,
+            3KeySuffix, 3ValueSuffix,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1ValueSuffix, map3[1KeySuffix])
+        assertEquals(2ValueSuffix, map3[2KeySuffix])
+        assertEquals(3ValueSuffix, map3[3KeySuffix])
+
+        val map4 = pKeyPValueMapOf(
+            1KeySuffix, 1ValueSuffix,
+            2KeySuffix, 2ValueSuffix,
+            3KeySuffix, 3ValueSuffix,
+            4KeySuffix, 4ValueSuffix,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1ValueSuffix, map4[1KeySuffix])
+        assertEquals(2ValueSuffix, map4[2KeySuffix])
+        assertEquals(3ValueSuffix, map4[3KeySuffix])
+        assertEquals(4ValueSuffix, map4[4KeySuffix])
+
+        val map5 = pKeyPValueMapOf(
+            1KeySuffix, 1ValueSuffix,
+            2KeySuffix, 2ValueSuffix,
+            3KeySuffix, 3ValueSuffix,
+            4KeySuffix, 4ValueSuffix,
+            5KeySuffix, 5ValueSuffix,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1ValueSuffix, map5[1KeySuffix])
+        assertEquals(2ValueSuffix, map5[2KeySuffix])
+        assertEquals(3ValueSuffix, map5[3KeySuffix])
+        assertEquals(4ValueSuffix, map5[4KeySuffix])
+        assertEquals(5ValueSuffix, map5[5KeySuffix])
+    }
+
+    @Test
+    fun mutablePKeyPValueMapInitFunction() {
+        val map1 = mutablePKeyPValueMapOf(
+            1KeySuffix, 1ValueSuffix,
+        )
+        assertEquals(1, map1.size)
+        assertEquals(1ValueSuffix, map1[1KeySuffix])
+
+        val map2 = mutablePKeyPValueMapOf(
+            1KeySuffix, 1ValueSuffix,
+            2KeySuffix, 2ValueSuffix,
+        )
+        assertEquals(2, map2.size)
+        assertEquals(1ValueSuffix, map2[1KeySuffix])
+        assertEquals(2ValueSuffix, map2[2KeySuffix])
+
+        val map3 = mutablePKeyPValueMapOf(
+            1KeySuffix, 1ValueSuffix,
+            2KeySuffix, 2ValueSuffix,
+            3KeySuffix, 3ValueSuffix,
+        )
+        assertEquals(3, map3.size)
+        assertEquals(1ValueSuffix, map3[1KeySuffix])
+        assertEquals(2ValueSuffix, map3[2KeySuffix])
+        assertEquals(3ValueSuffix, map3[3KeySuffix])
+
+        val map4 = mutablePKeyPValueMapOf(
+            1KeySuffix, 1ValueSuffix,
+            2KeySuffix, 2ValueSuffix,
+            3KeySuffix, 3ValueSuffix,
+            4KeySuffix, 4ValueSuffix,
+        )
+
+        assertEquals(4, map4.size)
+        assertEquals(1ValueSuffix, map4[1KeySuffix])
+        assertEquals(2ValueSuffix, map4[2KeySuffix])
+        assertEquals(3ValueSuffix, map4[3KeySuffix])
+        assertEquals(4ValueSuffix, map4[4KeySuffix])
+
+        val map5 = mutablePKeyPValueMapOf(
+            1KeySuffix, 1ValueSuffix,
+            2KeySuffix, 2ValueSuffix,
+            3KeySuffix, 3ValueSuffix,
+            4KeySuffix, 4ValueSuffix,
+            5KeySuffix, 5ValueSuffix,
+        )
+
+        assertEquals(5, map5.size)
+        assertEquals(1ValueSuffix, map5[1KeySuffix])
+        assertEquals(2ValueSuffix, map5[2KeySuffix])
+        assertEquals(3ValueSuffix, map5[3KeySuffix])
+        assertEquals(4ValueSuffix, map5[4KeySuffix])
+        assertEquals(5ValueSuffix, map5[5KeySuffix])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutablePKeyPValueMap(12)
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutablePKeyPValueMap(2)
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutablePKeyPValueMap(0)
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[1KeySuffix] = 2ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(2ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun put() {
+        val map = MutablePKeyPValueMap()
+
+        map.put(1KeySuffix, 1ValueSuffix)
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+        map.put(1KeySuffix, 2ValueSuffix)
+        assertEquals(2ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertFailsWith<NoSuchElementException> {
+            map[2KeySuffix]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertEquals(2ValueSuffix, map.getOrDefault(2KeySuffix, 2ValueSuffix))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertEquals(3ValueSuffix, map.getOrElse(3KeySuffix) { 3ValueSuffix })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        var counter = 0
+        map.getOrPut(1KeySuffix) {
+            counter++
+            2ValueSuffix
+        }
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+        assertEquals(0, counter)
+
+        map.getOrPut(2KeySuffix) {
+            counter++
+            2ValueSuffix
+        }
+        assertEquals(2ValueSuffix, map[2KeySuffix])
+        assertEquals(1, counter)
+
+        map.getOrPut(2KeySuffix) {
+            counter++
+            3ValueSuffix
+        }
+        assertEquals(2ValueSuffix, map[2KeySuffix])
+        assertEquals(1, counter)
+
+        map.getOrPut(3KeySuffix) {
+            counter++
+            3ValueSuffix
+        }
+        assertEquals(3ValueSuffix, map[3KeySuffix])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutablePKeyPValueMap()
+        map.remove(1KeySuffix)
+
+        map[1KeySuffix] = 1ValueSuffix
+        map.remove(1KeySuffix)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutablePKeyPValueMap(6)
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+        map[4KeySuffix] = 4ValueSuffix
+        map[5KeySuffix] = 5ValueSuffix
+        map[6KeySuffix] = 6ValueSuffix
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1KeySuffix)
+        map.remove(2KeySuffix)
+        map.remove(3KeySuffix)
+        map.remove(4KeySuffix)
+        map.remove(5KeySuffix)
+        map.remove(6KeySuffix)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7KeySuffix] = 7ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+        map[4KeySuffix] = 4ValueSuffix
+        map[5KeySuffix] = 5ValueSuffix
+        map[6KeySuffix] = 6ValueSuffix
+
+        map.removeIf { key, _ -> key == 1KeySuffix || key == 3KeySuffix }
+
+        assertEquals(4, map.size)
+        assertEquals(2ValueSuffix, map[2KeySuffix])
+        assertEquals(4ValueSuffix, map[4KeySuffix])
+        assertEquals(5ValueSuffix, map[5KeySuffix])
+        assertEquals(6ValueSuffix, map[6KeySuffix])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+
+        map -= 1KeySuffix
+
+        assertEquals(2, map.size)
+        assertFalse(1KeySuffix in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+
+        map -= pKeyArrayOf(3KeySuffix, 2KeySuffix)
+
+        assertEquals(1, map.size)
+        assertFalse(3KeySuffix in map)
+        assertFalse(2KeySuffix in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+
+        map -= pKeySetOf(3KeySuffix, 2KeySuffix)
+
+        assertEquals(1, map.size)
+        assertFalse(3KeySuffix in map)
+        assertFalse(2KeySuffix in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+
+        map -= pKeyListOf(3KeySuffix, 2KeySuffix)
+
+        assertEquals(1, map.size)
+        assertFalse(3KeySuffix in map)
+        assertFalse(2KeySuffix in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutablePKeyPValueMap()
+        assertFalse(map.remove(1KeySuffix, 1ValueSuffix))
+
+        map[1KeySuffix] = 1ValueSuffix
+        assertTrue(map.remove(1KeySuffix, 1ValueSuffix))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutablePKeyPValueMap()
+
+        for (i in 0 until 1700) {
+            map[i.toPKey()] = i.toPValue()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutablePKeyPValueMap()
+
+            for (j in 0 until i) {
+                map[j.toPKey()] = j.toPValue()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toPKey())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutablePKeyPValueMap()
+
+            for (j in 0 until i) {
+                map[j.toPKey()] = j.toPValue()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutablePKeyPValueMap()
+
+            for (j in 0 until i) {
+                map[j.toPKey()] = j.toPValue()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutablePKeyPValueMap()
+
+        for (i in 0 until 32) {
+            map[i.toPKey()] = i.toPValue()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutablePKeyPValueMap()
+        assertEquals("{}", map.toString())
+
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        val oneValueString = 1ValueSuffix.toString()
+        val twoValueString = 2ValueSuffix.toString()
+        val oneKeyString = 1KeySuffix.toString()
+        val twoKeyString = 2KeySuffix.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val map = MutablePKeyPValueMap()
+        repeat(5) {
+            map[it.toPKey()] = it.toPValue()
+        }
+        val order = IntArray(5)
+        var index = 0
+        map.forEach { key, _ ->
+            order[index++] = key.toInt()
+        }
+        assertEquals(
+            "${order[0].toPKey()}=${order[0].toPValue()}, ${order[1].toPKey()}=" +
+            "${order[1].toPValue()}, ${order[2].toPKey()}=${order[2].toPValue()}," +
+            " ${order[3].toPKey()}=${order[3].toPValue()}, ${order[4].toPKey()}=" +
+            "${order[4].toPValue()}",
+            map.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toPKey()}=${order[0].toPValue()}, ${order[1].toPKey()}=" +
+            "${order[1].toPValue()}, ${order[2].toPKey()}=${order[2].toPValue()}...",
+            map.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toPKey()}=${order[0].toPValue()}-${order[1].toPKey()}=" +
+            "${order[1].toPValue()}-${order[2].toPKey()}=${order[2].toPValue()}-" +
+            "${order[3].toPKey()}=${order[3].toPValue()}-${order[4].toPKey()}=" +
+            "${order[4].toPValue()}<",
+            map.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            map.joinToString(limit = 3) { key, _ -> names[key.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutablePKeyPValueMap()
+        assertNotEquals(map, map2)
+
+        map2[1KeySuffix] = 1ValueSuffix
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertTrue(map.containsKey(1KeySuffix))
+        assertFalse(map.containsKey(2KeySuffix))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertTrue(1KeySuffix in map)
+        assertFalse(2KeySuffix in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertTrue(map.containsValue(1ValueSuffix))
+        assertFalse(map.containsValue(3ValueSuffix))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutablePKeyPValueMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutablePKeyPValueMap()
+        assertEquals(0, map.count())
+
+        map[1KeySuffix] = 1ValueSuffix
+        assertEquals(1, map.count())
+
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+        map[4KeySuffix] = 4ValueSuffix
+        map[5KeySuffix] = 5ValueSuffix
+        map[6KeySuffix] = 6ValueSuffix
+
+        assertEquals(2, map.count { key, _ -> key <= 2KeySuffix })
+        assertEquals(0, map.count { key, _ -> key < 0KeySuffix })
+    }
+
+    @Test
+    fun any() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+        map[4KeySuffix] = 4ValueSuffix
+        map[5KeySuffix] = 5ValueSuffix
+        map[6KeySuffix] = 6ValueSuffix
+
+        assertTrue(map.any { key, _ -> key == 4KeySuffix })
+        assertFalse(map.any { key, _ -> key < 0KeySuffix })
+    }
+
+    @Test
+    fun all() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+        map[4KeySuffix] = 4ValueSuffix
+        map[5KeySuffix] = 5ValueSuffix
+        map[6KeySuffix] = 6ValueSuffix
+
+        assertTrue(map.all { key, value -> key > 0KeySuffix && value >= 1ValueSuffix })
+        assertFalse(map.all { key, _ -> key < 6KeySuffix })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutablePKeyPValueMap()
+        assertEquals(7, map.trim())
+
+        map[1KeySuffix] = 1ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toPKey()] = i.toPValue()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toPKey()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/template/PKeySet.kt.template b/collection/collection/template/PKeySet.kt.template
new file mode 100644
index 0000000..c3fedd9
--- /dev/null
+++ b/collection/collection/template/PKeySet.kt.template
@@ -0,0 +1,834 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "KotlinRedundantDiagnosticSuppress",
+    "KotlinConstantConditions",
+    "PropertyName",
+    "ConstPropertyName",
+    "PrivatePropertyName",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.contracts.contract
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// This is a copy of ScatterSet, but with primitive elements
+
+// Default empty set to avoid allocations
+private val EmptyPKeySet = MutablePKeySet(0)
+
+// An empty array of pKeys
+internal val EmptyPKeyArray = PKeyArray(0)
+
+/**
+ * Returns an empty, read-only [PKeySet].
+ */
+public fun emptyPKeySet(): PKeySet = EmptyPKeySet
+
+/**
+ * Returns an empty, read-only [ScatterSet].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(): PKeySet = EmptyPKeySet
+
+/**
+ * Returns a new read-only [PKeySet] with only [element1] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(element1: PKey): PKeySet = mutablePKeySetOf(element1)
+
+/**
+ * Returns a new read-only [PKeySet] with only [element1] and [element2] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(element1: PKey, element2: PKey): PKeySet =
+    mutablePKeySetOf(element1, element2)
+
+/**
+ * Returns a new read-only [PKeySet] with only [element1], [element2], and [element3] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(element1: PKey, element2: PKey, element3: PKey): PKeySet =
+    mutablePKeySetOf(element1, element2, element3)
+
+/**
+ * Returns a new read-only [PKeySet] with only [elements] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(vararg elements: PKey): PKeySet =
+    MutablePKeySet(elements.size).apply { plusAssign(elements) }
+
+/**
+ * Returns a new [MutablePKeySet].
+ */
+public fun mutablePKeySetOf(): MutablePKeySet = MutablePKeySet()
+
+/**
+ * Returns a new [MutablePKeySet] with only [element1] in it.
+ */
+public fun mutablePKeySetOf(element1: PKey): MutablePKeySet =
+    MutablePKeySet(1).apply {
+        plusAssign(element1)
+    }
+
+/**
+ * Returns a new [MutablePKeySet] with only [element1] and [element2] in it.
+ */
+public fun mutablePKeySetOf(element1: PKey, element2: PKey): MutablePKeySet =
+    MutablePKeySet(2).apply {
+        plusAssign(element1)
+        plusAssign(element2)
+    }
+
+/**
+ * Returns a new [MutablePKeySet] with only [element1], [element2], and [element3] in it.
+ */
+public fun mutablePKeySetOf(element1: PKey, element2: PKey, element3: PKey): MutablePKeySet =
+    MutablePKeySet(3).apply {
+        plusAssign(element1)
+        plusAssign(element2)
+        plusAssign(element3)
+    }
+
+/**
+ * Returns a new [MutablePKeySet] with the specified elements.
+ */
+public fun mutablePKeySetOf(vararg elements: PKey): MutablePKeySet =
+    MutablePKeySet(elements.size).apply { plusAssign(elements) }
+
+/**
+ * [PKeySet] is a container with a [Set]-like interface designed to avoid
+ * allocations, including boxing.
+ *
+ * This implementation makes no guarantee as to the order of the elements,
+ * nor does it make guarantees that the order remains constant over time.
+ *
+ * Though [PKeySet] offers a read-only interface, it is always backed
+ * by a [MutablePKeySet]. Read operations alone are thread-safe. However,
+ * any mutations done through the backing [MutablePKeySet] while reading
+ * on another thread are not safe and the developer must protect the set
+ * from such changes during read operations.
+ *
+ * @see [MutablePKeySet]
+ */
+public sealed class PKeySet {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` elements, including when
+    // the set is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var elements: PKeyArray = EmptyPKeyArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of elements that can be stored in this set
+     * without requiring internal storage reallocation.
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of elements in this set.
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this set has at least one element.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this set has no elements.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this set is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this set is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the first element in the collection.
+     * @throws NoSuchElementException if the collection is empty
+     */
+    public inline fun first(): PKey {
+        forEach { return it }
+        throw NoSuchElementException("The PKeySet is empty")
+    }
+
+    /**
+     * Returns the first element in the collection for which [predicate] returns `true`.
+     *
+     * **Note** There is no mechanism for both determining if there is an element that matches
+     * [predicate] _and_ returning it if it exists. Developers should use [forEach] to achieve
+     * this behavior.
+     *
+     * @param predicate Called on elements of the set, returning `true` for an element that matches
+     * or `false` if it doesn't
+     * @return An element in the set for which [predicate] returns `true`.
+     * @throws NoSuchElementException if [predicate] returns `false` for all elements or the
+     * collection is empty.
+     */
+    public inline fun first(predicate: (element: PKey) -> Boolean): PKey {
+        contract { callsInPlace(predicate) }
+        forEach { if (predicate(it)) return it }
+        throw NoSuchElementException("Could not find a match")
+    }
+
+    /**
+     * Iterates over every element stored in this set by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndex(block: (index: Int) -> Unit) {
+        contract { callsInPlace(block) }
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 elements
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every element stored in this set by invoking
+     * the specified [block] lambda.
+     * @param block called with each element in the set
+     */
+    public inline fun forEach(block: (element: PKey) -> Unit) {
+        contract { callsInPlace(block) }
+        val k = elements
+
+        forEachIndex { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Returns true if all elements match the given [predicate].
+     * @param predicate called for elements in the set to determine if it returns return `true` for
+     * all elements.
+     */
+    public inline fun all(predicate: (element: PKey) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEach { element ->
+            if (!predicate(element)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one element matches the given [predicate].
+     * @param predicate called for elements in the set to determine if it returns `true` for any
+     * elements.
+     */
+    public inline fun any(predicate: (element: PKey) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEach { element ->
+            if (predicate(element)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of elements in this set.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public fun count(): Int = _size
+
+    /**
+     * Returns the number of elements matching the given [predicate].
+     * @param predicate Called for all elements in the set to count the number for which it returns
+     * `true`.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public inline fun count(predicate: (element: PKey) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        var count = 0
+        forEach { element ->
+            if (predicate(element)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns `true` if the specified [element] is present in this set, `false`
+     * otherwise.
+     * @param element The element to look for in this set
+     */
+    public operator fun contains(element: PKey): Boolean = findElementIndex(element) >= 0
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@PKeySet.forEach { element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(element)
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. [transform] dictates how each element will be represented.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (PKey) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        var index = 0
+        this@PKeySet.forEach { element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(element))
+            index++
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns the hash code value for this set. The hash code of a set is defined to be the
+     * sum of the hash codes of the elements in the set.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { element ->
+            hash += element.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this set for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [PKeySet]
+     * - Has the same [size] as this set
+     * - Contains elements equal to this set's elements
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is PKeySet) {
+            return false
+        }
+        if (other._size != _size) {
+            return false
+        }
+
+        forEach { element ->
+            if (element !in other) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this set. The set is denoted in the
+     * string by the `{}`. Each element is separated by `, `.
+     */
+    override fun toString(): String = joinToString(prefix = "[", postfix = "]")
+
+    /**
+     * Scans the set to find the index in the backing arrays of the
+     * specified [element]. Returns -1 if the element is not present.
+     */
+    internal inline fun findElementIndex(element: PKey): Int {
+        val hash = hash(element)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (elements[index] == element) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutablePKeySet] is a container with a [MutableSet]-like interface based on a flat
+ * hash table implementation. The underlying implementation is designed to avoid
+ * all allocations on insertion, removal, retrieval, and iteration. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added elements to the set.
+ *
+ * This implementation makes no guarantee as to the order of the elements stored,
+ * nor does it make guarantees that the order remains constant over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the set (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Concurrent reads are however safe.
+ *
+ * @constructor Creates a new [MutablePKeySet]
+ * @param initialCapacity The initial desired capacity for this container.
+ * The container will honor this value by guaranteeing its internal structures
+ * can hold that many elements without requiring any allocations. The initial
+ * capacity can be set to 0.
+ */
+public class MutablePKeySet(
+    initialCapacity: Int = DefaultScatterCapacity
+) : PKeySet() {
+    // Number of elements we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        elements = PKeyArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Adds the specified element to the set.
+     * @param element The element to add to the set.
+     * @return `true` if the element has been added or `false` if the element is already
+     * contained within the set.
+     */
+    public fun add(element: PKey): Boolean {
+        val oldSize = _size
+        val index = findAbsoluteInsertIndex(element)
+        elements[index] = element
+        return _size != oldSize
+    }
+
+    /**
+     * Adds the specified element to the set.
+     * @param element The element to add to the set.
+     */
+    public operator fun plusAssign(element: PKey) {
+        val index = findAbsoluteInsertIndex(element)
+        elements[index] = element
+    }
+
+    /**
+     * Adds all the [elements] into this set.
+     * @param elements An array of elements to add to the set.
+     * @return `true` if any of the specified elements were added to the collection,
+     * `false` if the collection was not modified.
+     */
+    public fun addAll(@Suppress("ArrayReturn") elements: PKeyArray): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all the [elements] into this set.
+     * @param elements An array of elements to add to the set.
+     */
+    public operator fun plusAssign(@Suppress("ArrayReturn") elements: PKeyArray) {
+        elements.forEach { element ->
+            plusAssign(element)
+        }
+    }
+
+    /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements A [PKeySet] of elements to add to this set.
+     * @return `true` if any of the specified elements were added to the collection,
+     * `false` if the collection was not modified.
+     */
+    public fun addAll(elements: PKeySet): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements A [PKeySet] of elements to add to this set.
+     */
+    public operator fun plusAssign(elements: PKeySet) {
+        elements.forEach { element ->
+            plusAssign(element)
+        }
+    }
+
+    /**
+     * Removes the specified [element] from the set.
+     * @param element The element to remove from the set.
+     * @return `true` if the [element] was present in the set, or `false` if it wasn't
+     * present before removal.
+     */
+    public fun remove(element: PKey): Boolean {
+        val index = findElementIndex(element)
+        val exists = index >= 0
+        if (exists) {
+            removeElementAt(index)
+        }
+        return exists
+    }
+
+    /**
+     * Removes the specified [element] from the set if it is present.
+     * @param element The element to remove from the set.
+     */
+    public operator fun minusAssign(element: PKey) {
+        val index = findElementIndex(element)
+        if (index >= 0) {
+            removeElementAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An array of elements to be removed from the set.
+     * @return `true` if the set was changed or `false` if none of the elements were present.
+     */
+    public fun removeAll(@Suppress("ArrayReturn") elements: PKeyArray): Boolean {
+        val oldSize = _size
+        minusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An array of elements to be removed from the set.
+     */
+    public operator fun minusAssign(@Suppress("ArrayReturn") elements: PKeyArray) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [PKeySet] of elements to be removed from the set.
+     * @return `true` if the set was changed or `false` if none of the elements were present.
+     */
+    public fun removeAll(elements: PKeySet): Boolean {
+        val oldSize = _size
+        minusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [PKeySet] of elements to be removed from the set.
+     */
+    public operator fun minusAssign(elements: PKeySet) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    private fun removeElementAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the element as empty if there's a group
+        //       window around this element that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all elements from this set.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the set to find the index at which we can store a given [element].
+     * If the element already exists in the set, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the set is full.
+     */
+    private fun findAbsoluteInsertIndex(element: PKey): Int {
+        val hash = hash(element)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (elements[index] == element) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the set in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutablePKeySet]'s storage so it is sized appropriately
+     * to hold the current elements.
+     *
+     * Returns the number of empty elements removed from this set's storage.
+     * Returns 0 if no trimming is necessary or possible.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted elements from the set to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the set capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_map`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousElements = elements
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newElements = elements
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousElement = previousElements[i]
+                val hash = hash(previousElement)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newElements[index] = previousElement
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
+
+/**
+ * Returns the hash code of [k]. This follows the [HashSet] default behavior on Android
+ * of returning [Object.hashcode()] with the higher bits of hash spread to the lower bits.
+ */
+internal inline fun hash(k: PKey): Int {
+    val hash = k.hashCode()
+    return hash xor (hash ushr 16)
+}
diff --git a/collection/collection/template/PKeySetTest.kt.template b/collection/collection/template/PKeySetTest.kt.template
new file mode 100644
index 0000000..6962ded
--- /dev/null
+++ b/collection/collection/template/PKeySetTest.kt.template
@@ -0,0 +1,563 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class PKeySetTest {
+    @Test
+    fun emptyPKeySetConstructor() {
+        val set = MutablePKeySet()
+        assertEquals(7, set.capacity)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun immutableEmptyPKeySet() {
+        val set: PKeySet = emptyPKeySet()
+        assertEquals(0, set.capacity)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun zeroCapacityPKeySet() {
+        val set = MutablePKeySet(0)
+        assertEquals(0, set.capacity)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun emptyPKeySetWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val set = MutablePKeySet(1800)
+        assertEquals(4095, set.capacity)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun mutablePKeySetBuilder() {
+        val empty = mutablePKeySetOf()
+        assertEquals(0, empty.size)
+
+        val withElements = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        assertEquals(2, withElements.size)
+        assertTrue(1KeySuffix in withElements)
+        assertTrue(2KeySuffix in withElements)
+    }
+
+    @Test
+    fun addToPKeySet() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        assertTrue(set.add(2KeySuffix))
+
+        assertEquals(2, set.size)
+        val elements = PKeyArray(2)
+        var index = 0
+        set.forEach { element ->
+            elements[index++] = element
+        }
+        elements.sort()
+        assertEquals(1KeySuffix, elements[0])
+        assertEquals(2KeySuffix, elements[1])
+    }
+
+    @Test
+    fun addToSizedPKeySet() {
+        val set = MutablePKeySet(12)
+        set += 1KeySuffix
+
+        assertEquals(1, set.size)
+        assertEquals(1KeySuffix, set.first())
+    }
+
+    @Test
+    fun addExistingElement() {
+        val set = MutablePKeySet(12)
+        set += 1KeySuffix
+        assertFalse(set.add(1KeySuffix))
+        set += 1KeySuffix
+
+        assertEquals(1, set.size)
+        assertEquals(1KeySuffix, set.first())
+    }
+
+    @Test
+    fun addAllArray() {
+        val set = mutablePKeySetOf(1KeySuffix)
+        assertFalse(set.addAll(pKeyArrayOf(1KeySuffix)))
+        assertEquals(1, set.size)
+        assertTrue(set.addAll(pKeyArrayOf(1KeySuffix, 2KeySuffix)))
+        assertEquals(2, set.size)
+        assertTrue(2KeySuffix in set)
+    }
+
+    @Test
+    fun addAllPKeySet() {
+        val set = mutablePKeySetOf(1KeySuffix)
+        assertFalse(set.addAll(mutablePKeySetOf(1KeySuffix)))
+        assertEquals(1, set.size)
+        assertTrue(set.addAll(mutablePKeySetOf(1KeySuffix, 2KeySuffix)))
+        assertEquals(2, set.size)
+        assertTrue(2KeySuffix in set)
+    }
+
+    @Test
+    fun plusAssignArray() {
+        val set = mutablePKeySetOf(1KeySuffix)
+        set += pKeyArrayOf(1KeySuffix)
+        assertEquals(1, set.size)
+        set += pKeyArrayOf(1KeySuffix, 2KeySuffix)
+        assertEquals(2, set.size)
+        assertTrue(2KeySuffix in set)
+    }
+
+    @Test
+    fun plusAssignPKeySet() {
+        val set = mutablePKeySetOf(1KeySuffix)
+        set += mutablePKeySetOf(1KeySuffix)
+        assertEquals(1, set.size)
+        set += mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        assertEquals(2, set.size)
+        assertTrue(2KeySuffix in set)
+    }
+
+    @Test
+    fun firstWithValue() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        set += 2KeySuffix
+        var element: PKey = -1KeySuffix
+        var otherElement: PKey = -1KeySuffix
+        set.forEach { if (element == -1KeySuffix) element = it else otherElement = it }
+        assertEquals(element, set.first())
+        set -= element
+        assertEquals(otherElement, set.first())
+    }
+
+    @Test
+    fun firstEmpty() {
+        assertFailsWith(NoSuchElementException::class) {
+            val set = MutablePKeySet()
+            set.first()
+        }
+    }
+
+    @Test
+    fun firstMatching() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        set += 2KeySuffix
+        assertEquals(1KeySuffix, set.first { it < 2KeySuffix })
+        assertEquals(2KeySuffix, set.first { it > 1KeySuffix })
+    }
+
+    @Test
+    fun firstMatchingEmpty() {
+        assertFailsWith(NoSuchElementException::class) {
+            val set = MutablePKeySet()
+            set.first { it > 0KeySuffix }
+        }
+    }
+
+    @Test
+    fun firstMatchingNoMatch() {
+        assertFailsWith(NoSuchElementException::class) {
+            val set = MutablePKeySet()
+            set += 1KeySuffix
+            set += 2KeySuffix
+            set.first { it < 0KeySuffix }
+        }
+    }
+
+    @Test
+    fun remove() {
+        val set = MutablePKeySet()
+        assertFalse(set.remove(1KeySuffix))
+
+        set += 1KeySuffix
+        assertTrue(set.remove(1KeySuffix))
+        assertEquals(0, set.size)
+
+        set += 1KeySuffix
+        set -= 1KeySuffix
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val set = MutablePKeySet(6)
+        set += 1KeySuffix
+        set += 5KeySuffix
+        set += 6KeySuffix
+        set += 9KeySuffix
+        set += 11KeySuffix
+        set += 13KeySuffix
+
+        // Removing all the entries will mark the medata as deleted
+        set.remove(1KeySuffix)
+        set.remove(5KeySuffix)
+        set.remove(6KeySuffix)
+        set.remove(9KeySuffix)
+        set.remove(11KeySuffix)
+        set.remove(13KeySuffix)
+
+        assertEquals(0, set.size)
+
+        val capacity = set.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        set += 3KeySuffix
+
+        assertEquals(1, set.size)
+        assertEquals(capacity, set.capacity)
+    }
+
+    @Test
+    fun removeAllArray() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        assertFalse(set.removeAll(pKeyArrayOf(3KeySuffix, 5KeySuffix)))
+        assertEquals(2, set.size)
+        assertTrue(set.removeAll(pKeyArrayOf(3KeySuffix, 1KeySuffix, 5KeySuffix)))
+        assertEquals(1, set.size)
+        assertFalse(1KeySuffix in set)
+    }
+
+    @Test
+    fun removeAllPKeySet() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        assertFalse(set.removeAll(mutablePKeySetOf(3KeySuffix, 5KeySuffix)))
+        assertEquals(2, set.size)
+        assertTrue(set.removeAll(mutablePKeySetOf(3KeySuffix, 1KeySuffix, 5KeySuffix)))
+        assertEquals(1, set.size)
+        assertFalse(1KeySuffix in set)
+    }
+
+    @Test
+    fun minusAssignArray() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        set -= pKeyArrayOf(3KeySuffix, 5KeySuffix)
+        assertEquals(2, set.size)
+        set -= pKeyArrayOf(3KeySuffix, 1KeySuffix, 5KeySuffix)
+        assertEquals(1, set.size)
+        assertFalse(1KeySuffix in set)
+    }
+
+    @Test
+    fun minusAssignPKeySet() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        set -= mutablePKeySetOf(3KeySuffix, 5KeySuffix)
+        assertEquals(2, set.size)
+        set -= mutablePKeySetOf(3KeySuffix, 1KeySuffix, 5KeySuffix)
+        assertEquals(1, set.size)
+        assertFalse(1KeySuffix in set)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val set = MutablePKeySet()
+
+        for (i in 0 until 1700) {
+            set += i.toPKey()
+        }
+
+        assertEquals(1700, set.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val set = MutablePKeySet()
+
+            for (j in 0 until i) {
+                set += j.toPKey()
+            }
+
+            val elements = PKeyArray(i)
+            var index = 0
+            set.forEach { element ->
+                elements[index++] = element
+            }
+            elements.sort()
+
+            index = 0
+            elements.forEach { element ->
+                assertEquals(element, index.toPKey())
+                index++
+            }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val set = MutablePKeySet()
+
+        for (i in 0 until 32) {
+            set += i.toPKey()
+        }
+
+        val capacity = set.capacity
+        set.clear()
+
+        assertEquals(0, set.size)
+        assertEquals(capacity, set.capacity)
+    }
+
+    @Test
+    fun string() {
+        val set = MutablePKeySet()
+        assertEquals("[]", set.toString())
+
+        set += 1KeySuffix
+        set += 5KeySuffix
+        assertTrue(
+            "[${1KeySuffix}, ${5KeySuffix}]" == set.toString() ||
+                "[${5KeySuffix}, ${1KeySuffix}]" == set.toString()
+        )
+    }
+
+    @Test
+    fun joinToString() {
+        val set = pKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        val order = IntArray(5)
+        var index = 0
+        set.forEach { element ->
+            order[index++] = element.toInt()
+        }
+        assertEquals(
+            "${order[0].toPKey()}, ${order[1].toPKey()}, ${order[2].toPKey()}, " +
+            "${order[3].toPKey()}, ${order[4].toPKey()}",
+            set.joinToString()
+        )
+        assertEquals(
+            "x${order[0].toPKey()}, ${order[1].toPKey()}, ${order[2].toPKey()}...",
+            set.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${order[0].toPKey()}-${order[1].toPKey()}-${order[2].toPKey()}-" +
+            "${order[3].toPKey()}-${order[4].toPKey()}<",
+            set.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        val names = arrayOf("one", "two", "three", "four", "five")
+        assertEquals(
+            "${names[order[0]]}, ${names[order[1]]}, ${names[order[2]]}...",
+            set.joinToString(limit = 3) { names[it.toInt()] }
+        )
+    }
+
+    @Test
+    fun equals() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        set += 5KeySuffix
+
+        assertFalse(set.equals(null))
+        assertEquals(set, set)
+
+        val set2 = MutablePKeySet()
+        set2 += 5KeySuffix
+
+        assertNotEquals(set, set2)
+
+        set2 += 1KeySuffix
+        assertEquals(set, set2)
+    }
+
+    @Test
+    fun contains() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        set += 5KeySuffix
+
+        assertTrue(set.contains(1KeySuffix))
+        assertTrue(set.contains(5KeySuffix))
+        assertFalse(set.contains(2KeySuffix))
+    }
+
+    @Test
+    fun empty() {
+        val set = MutablePKeySet()
+        assertTrue(set.isEmpty())
+        assertFalse(set.isNotEmpty())
+        assertTrue(set.none())
+        assertFalse(set.any())
+
+        set += 1KeySuffix
+
+        assertFalse(set.isEmpty())
+        assertTrue(set.isNotEmpty())
+        assertTrue(set.any())
+        assertFalse(set.none())
+    }
+
+    @Test
+    fun count() {
+        val set = MutablePKeySet()
+        assertEquals(0, set.count())
+
+        set += 1KeySuffix
+        assertEquals(1, set.count())
+
+        set += 5KeySuffix
+        set += 6KeySuffix
+        set += 9KeySuffix
+        set += 11KeySuffix
+        set += 13KeySuffix
+
+        assertEquals(2, set.count { it < 6KeySuffix })
+        assertEquals(0, set.count { it < 0KeySuffix })
+    }
+
+    @Test
+    fun any() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        set += 5KeySuffix
+        set += 6KeySuffix
+        set += 9KeySuffix
+        set += 11KeySuffix
+        set += 13KeySuffix
+
+        assertTrue(set.any { it >= 11KeySuffix })
+        assertFalse(set.any { it < 0KeySuffix })
+    }
+
+    @Test
+    fun all() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        set += 5KeySuffix
+        set += 6KeySuffix
+        set += 9KeySuffix
+        set += 11KeySuffix
+        set += 13KeySuffix
+
+        assertTrue(set.all { it > 0KeySuffix })
+        assertFalse(set.all { it < 0KeySuffix })
+    }
+
+    @Test
+    fun trim() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 7KeySuffix)
+        val capacity = set.capacity
+        assertEquals(0, set.trim())
+        set.clear()
+        assertEquals(capacity, set.trim())
+        assertEquals(0, set.capacity)
+        set.addAll(pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 7KeySuffix, 6KeySuffix, 8KeySuffix,
+            9KeySuffix, 10KeySuffix, 11KeySuffix, 12KeySuffix, 13KeySuffix, 14KeySuffix))
+        set.removeAll(pKeyArrayOf(6KeySuffix, 8KeySuffix, 9KeySuffix, 10KeySuffix, 11KeySuffix, 12KeySuffix, 13KeySuffix, 14KeySuffix))
+        assertTrue(set.trim() > 0)
+        assertEquals(capacity, set.capacity)
+    }
+
+    @Test
+    fun pKeySetOfEmpty() {
+        assertSame(emptyPKeySet(), pKeySetOf())
+        assertEquals(0, pKeySetOf().size)
+    }
+
+    @Test
+    fun pKeySetOfOne() {
+        val set = pKeySetOf(1KeySuffix)
+        assertEquals(1, set.size)
+        assertEquals(1KeySuffix, set.first())
+    }
+
+    @Test
+    fun pKeySetOfTwo() {
+        val set = pKeySetOf(1KeySuffix, 2KeySuffix)
+        assertEquals(2, set.size)
+        assertTrue(1KeySuffix in set)
+        assertTrue(2KeySuffix in set)
+        assertFalse(5KeySuffix in set)
+    }
+
+    @Test
+    fun pKeySetOfThree() {
+        val set = pKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix)
+        assertEquals(3, set.size)
+        assertTrue(1KeySuffix in set)
+        assertTrue(2KeySuffix in set)
+        assertTrue(3KeySuffix in set)
+        assertFalse(5KeySuffix in set)
+    }
+
+    @Test
+    fun pKeySetOfFour() {
+        val set = pKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix)
+        assertEquals(4, set.size)
+        assertTrue(1KeySuffix in set)
+        assertTrue(2KeySuffix in set)
+        assertTrue(3KeySuffix in set)
+        assertTrue(4KeySuffix in set)
+        assertFalse(5KeySuffix in set)
+    }
+
+    @Test
+    fun mutablePKeySetOfOne() {
+        val set = mutablePKeySetOf(1KeySuffix)
+        assertEquals(1, set.size)
+        assertEquals(1KeySuffix, set.first())
+    }
+
+    @Test
+    fun mutablePKeySetOfTwo() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        assertEquals(2, set.size)
+        assertTrue(1KeySuffix in set)
+        assertTrue(2KeySuffix in set)
+        assertFalse(5KeySuffix in set)
+    }
+
+    @Test
+    fun mutablePKeySetOfThree() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix)
+        assertEquals(3, set.size)
+        assertTrue(1KeySuffix in set)
+        assertTrue(2KeySuffix in set)
+        assertTrue(3KeySuffix in set)
+        assertFalse(5KeySuffix in set)
+    }
+
+    @Test
+    fun mutablePKeySetOfFour() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix)
+        assertEquals(4, set.size)
+        assertTrue(1KeySuffix in set)
+        assertTrue(2KeySuffix in set)
+        assertTrue(3KeySuffix in set)
+        assertTrue(4KeySuffix in set)
+        assertFalse(5KeySuffix in set)
+    }
+}
diff --git a/collection/collection/template/ValueClassList.kt.template b/collection/collection/template/ValueClassList.kt.template
new file mode 100644
index 0000000..34d2ea2
--- /dev/null
+++ b/collection/collection/template/ValueClassList.kt.template
@@ -0,0 +1,935 @@
+/*
+ * 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.
+ */
+@file:Suppress("NOTHING_TO_INLINE", "RedundantVisibilityModifier", "UnusedImport")
+/* ktlint-disable max-line-length */
+/* ktlint-disable import-ordering */
+
+package PACKAGE
+
+import androidx.collection.PRIMITIVEList
+import androidx.collection.MutablePRIMITIVEList
+import androidx.collection.emptyPRIMITIVEList
+import androidx.collection.mutablePRIMITIVEListOf
+import VALUE_PKG.VALUE_CLASS
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.jvm.JvmInline
+
+// To use this template, you must substitute several strings. You can copy this and search/replace
+// or use a sed command. These properties must be changed:
+// * PACKAGE - target package (e.g. androidx.compose.ui.ui.collection)
+// * VALUE_PKG - package in which the value class resides
+// * VALUE_CLASS - the value class contained in the list (e.g. Color or Offset)
+// * vALUE_CLASS - the value class, with the first letter lower case (e.g. color or offset)
+// * BACKING_PROPERTY - the field in VALUE_CLASS containing the backing primitive (e.g. packedValue)
+// * PRIMITIVE - the primitive type of the backing list (e.g. Long or Float)
+// * TO_PARAM - an operation done on the primitive to convert to the value class parameter
+//
+// For example, to create a ColorList:
+// sed -e "s/PACKAGE/androidx.compose.ui.graphics/" -e "s/VALUE_CLASS/Color/g" \
+//     -e "s/vALUE_CLASS/color/g" -e "s/BACKING_PROPERTY/value.toLong()/g" -e "s/PRIMITIVE/Long/g" \
+//     -e "s/TO_PARAM/.toULong()/g" -e "s/VALUE_PKG/androidx.compose.ui.graphics/g" \
+//     collection/collection/template/ValueClassList.kt.template \
+//     > compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ColorList.kt
+
+/**
+ * [VALUE_CLASSList] is a [List]-like collection for [VALUE_CLASS] values. It allows retrieving
+ * the elements without boxing. [VALUE_CLASSList] is always backed by a [MutableVALUE_CLASSList],
+ * its [MutableList]-like subclass.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ */
+@OptIn(ExperimentalContracts::class)
+@JvmInline
+internal value class VALUE_CLASSList(val list: PRIMITIVEList) {
+    /**
+     * The number of elements in the [VALUE_CLASSList].
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public inline val size: Int get() = list.size
+
+    /**
+     * Returns the last valid index in the [VALUE_CLASSList]. This can be `-1` when the list is empty.
+     */
+    @get:androidx.annotation.IntRange(from = -1)
+    public inline val lastIndex: Int get() = list.lastIndex
+
+    /**
+     * Returns an [IntRange] of the valid indices for this [VALUE_CLASSList].
+     */
+    public inline val indices: IntRange get() = list.indices
+
+    /**
+     * Returns `true` if the collection has no elements in it.
+     */
+    public inline fun none(): Boolean = list.none()
+
+    /**
+     * Returns `true` if there's at least one element in the collection.
+     */
+    public inline fun any(): Boolean = list.any()
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate].
+     */
+    public inline fun any(predicate: (element: VALUE_CLASS) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return list.any { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate] while
+     * iterating in the reverse order.
+     */
+    public inline fun reversedAny(predicate: (element: VALUE_CLASS) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return list.reversedAny { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns `true` if the [VALUE_CLASSList] contains [element] or `false` otherwise.
+     */
+    public inline operator fun contains(element: VALUE_CLASS): Boolean =
+        list.contains(element.BACKING_PROPERTY)
+
+    /**
+     * Returns `true` if the [VALUE_CLASSList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public inline fun containsAll(elements: VALUE_CLASSList): Boolean =
+        list.containsAll(elements.list)
+
+    /**
+     * Returns `true` if the [VALUE_CLASSList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public inline fun containsAll(elements: MutableVALUE_CLASSList): Boolean =
+        list.containsAll(elements.list)
+
+    /**
+     * Returns the number of elements in this list.
+     */
+    public inline fun count(): Int = list.count()
+
+    /**
+     * Counts the number of elements matching [predicate].
+     * @return The number of elements in this list for which [predicate] returns true.
+     */
+    public inline fun count(predicate: (element: VALUE_CLASS) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return list.count { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns the first element in the [VALUE_CLASSList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public inline fun first(): VALUE_CLASS = VALUE_CLASS(list.first()TO_PARAM)
+
+    /**
+     * Returns the first element in the [VALUE_CLASSList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfFirst
+     */
+    public inline fun first(predicate: (element: VALUE_CLASS) -> Boolean): VALUE_CLASS {
+        contract { callsInPlace(predicate) }
+        return VALUE_CLASS(list.first { predicate(VALUE_CLASS(itTO_PARAM)) }TO_PARAM)
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [VALUE_CLASSList] in order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes current accumulator value and an element, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> fold(initial: R, operation: (acc: R, element: VALUE_CLASS) -> R): R {
+        contract { callsInPlace(operation) }
+        return list.fold(initial) { acc, element ->
+            operation(acc, VALUE_CLASS(elementTO_PARAM))
+        }
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [VALUE_CLASSList] in order.
+     */
+    public inline fun <R> foldIndexed(
+        initial: R,
+        operation: (index: Int, acc: R, element: VALUE_CLASS) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        return list.foldIndexed(initial) { index, acc, element ->
+            operation(index, acc, VALUE_CLASS(elementTO_PARAM))
+        }
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [VALUE_CLASSList] in reverse order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes an element and the current accumulator value, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> foldRight(initial: R, operation: (element: VALUE_CLASS, acc: R) -> R): R {
+        contract { callsInPlace(operation) }
+        return list.foldRight(initial) { element, acc ->
+            operation(VALUE_CLASS(elementTO_PARAM), acc)
+        }
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [VALUE_CLASSList] in reverse order.
+     */
+    public inline fun <R> foldRightIndexed(
+        initial: R,
+        operation: (index: Int, element: VALUE_CLASS, acc: R) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        return list.foldRightIndexed(initial) { index, element, acc ->
+            operation(index, VALUE_CLASS(elementTO_PARAM), acc)
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [VALUE_CLASSList], in order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEach(block: (element: VALUE_CLASS) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEach { block(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Calls [block] for each element in the [VALUE_CLASSList] along with its index, in order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachIndexed(block: (index: Int, element: VALUE_CLASS) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEachIndexed { index, element ->
+            block(index, VALUE_CLASS(elementTO_PARAM))
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [VALUE_CLASSList] in reverse order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEachReversed(block: (element: VALUE_CLASS) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEachReversed { block(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Calls [block] for each element in the [VALUE_CLASSList] along with its index, in reverse
+     * order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachReversedIndexed(block: (index: Int, element: VALUE_CLASS) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEachReversedIndexed { index, element ->
+            block(index, VALUE_CLASS(elementTO_PARAM))
+        }
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public inline operator fun get(
+        @androidx.annotation.IntRange(from = 0) index: Int
+    ): VALUE_CLASS = VALUE_CLASS(list[index]TO_PARAM)
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public inline fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): VALUE_CLASS =
+        VALUE_CLASS(list[index]TO_PARAM)
+
+    /**
+     * Returns the element at the given [index] or [defaultValue] if [index] is out of bounds
+     * of the collection.
+     * @param index The index of the element whose value should be returned
+     * @param defaultValue A lambda to call with [index] as a parameter to return a value at
+     * an index not in the list.
+     */
+    public inline fun elementAtOrElse(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        defaultValue: (index: Int) -> VALUE_CLASS
+    ): VALUE_CLASS =
+        VALUE_CLASS(list.elementAtOrElse(index) { defaultValue(it).BACKING_PROPERTY }TO_PARAM)
+
+    /**
+     * Returns the index of [element] in the [VALUE_CLASSList] or `-1` if [element] is not there.
+     */
+    public inline fun indexOf(element: VALUE_CLASS): Int =
+        list.indexOf(element.BACKING_PROPERTY)
+
+    /**
+     * Returns the index if the first element in the [VALUE_CLASSList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfFirst(predicate: (element: VALUE_CLASS) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return list.indexOfFirst { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns the index if the last element in the [VALUE_CLASSList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfLast(predicate: (element: VALUE_CLASS) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return list.indexOfLast { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns `true` if the [VALUE_CLASSList] has no elements in it or `false` otherwise.
+     */
+    public inline fun isEmpty(): Boolean = list.isEmpty()
+
+    /**
+     * Returns `true` if there are elements in the [VALUE_CLASSList] or `false` if it is empty.
+     */
+    public inline fun isNotEmpty(): Boolean = list.isNotEmpty()
+
+    /**
+     * Returns the last element in the [VALUE_CLASSList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public inline fun last(): VALUE_CLASS = VALUE_CLASS(list.last()TO_PARAM)
+
+    /**
+     * Returns the last element in the [VALUE_CLASSList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfLast
+     */
+    public inline fun last(predicate: (element: VALUE_CLASS) -> Boolean): VALUE_CLASS {
+        contract { callsInPlace(predicate) }
+        return VALUE_CLASS(list.last { predicate(VALUE_CLASS(itTO_PARAM)) }TO_PARAM)
+    }
+
+    /**
+     * Returns the index of the last element in the [VALUE_CLASSList] that is the same as
+     * [element] or `-1` if no elements match.
+     */
+    public inline fun lastIndexOf(element: VALUE_CLASS): Int =
+        list.lastIndexOf(element.BACKING_PROPERTY)
+
+    /**
+     * Returns a String representation of the list, surrounded by "[]" and each element
+     * separated by ", ".
+     */
+    override fun toString(): String {
+        if (isEmpty()) {
+            return "[]"
+        }
+        return buildString {
+            append('[')
+            forEachIndexed { index: Int, element: VALUE_CLASS ->
+                if (index != 0) {
+                    append(',').append(' ')
+                }
+                append(element)
+            }
+            append(']')
+        }
+    }
+}
+
+/**
+ * [MutableVALUE_CLASSList] is a [MutableList]-like collection for [VALUE_CLASS] values.
+ * It allows storing and retrieving the elements without boxing. Immutable
+ * access is available through its base class [VALUE_CLASSList], which has a [List]-like
+ * interface.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ *
+ * @constructor Creates a [MutableVALUE_CLASSList] with a [capacity] of `initialCapacity`.
+ */
+@OptIn(ExperimentalContracts::class)
+@JvmInline
+internal value class MutableVALUE_CLASSList(val list: MutablePRIMITIVEList) {
+    public constructor(initialCapacity: Int = 16) : this(MutablePRIMITIVEList(initialCapacity))
+
+    /**
+     * The number of elements in the [VALUE_CLASSList].
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public inline val size: Int get() = list.size
+
+    /**
+     * Returns the last valid index in the [VALUE_CLASSList]. This can be `-1` when the list is empty.
+     */
+    @get:androidx.annotation.IntRange(from = -1)
+    public inline val lastIndex: Int get() = list.lastIndex
+
+    /**
+     * Returns an [IntRange] of the valid indices for this [VALUE_CLASSList].
+     */
+    public inline val indices: IntRange get() = list.indices
+
+    /**
+     * Returns `true` if the collection has no elements in it.
+     */
+    public inline fun none(): Boolean = list.none()
+
+    /**
+     * Returns `true` if there's at least one element in the collection.
+     */
+    public inline fun any(): Boolean = list.any()
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate].
+     */
+    public inline fun any(predicate: (element: VALUE_CLASS) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return list.any { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate] while
+     * iterating in the reverse order.
+     */
+    public inline fun reversedAny(predicate: (element: VALUE_CLASS) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return list.reversedAny { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns `true` if the [VALUE_CLASSList] contains [element] or `false` otherwise.
+     */
+    public inline operator fun contains(element: VALUE_CLASS): Boolean =
+        list.contains(element.BACKING_PROPERTY)
+
+    /**
+     * Returns `true` if the [VALUE_CLASSList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public inline fun containsAll(elements: VALUE_CLASSList): Boolean =
+        list.containsAll(elements.list)
+
+    /**
+     * Returns `true` if the [VALUE_CLASSList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public inline fun containsAll(elements: MutableVALUE_CLASSList): Boolean =
+        list.containsAll(elements.list)
+
+    /**
+     * Returns the number of elements in this list.
+     */
+    public inline fun count(): Int = list.count()
+
+    /**
+     * Counts the number of elements matching [predicate].
+     * @return The number of elements in this list for which [predicate] returns true.
+     */
+    public inline fun count(predicate: (element: VALUE_CLASS) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return list.count { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns the first element in the [VALUE_CLASSList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public inline fun first(): VALUE_CLASS = VALUE_CLASS(list.first()TO_PARAM)
+
+    /**
+     * Returns the first element in the [VALUE_CLASSList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfFirst
+     */
+    public inline fun first(predicate: (element: VALUE_CLASS) -> Boolean): VALUE_CLASS {
+        contract { callsInPlace(predicate) }
+        return VALUE_CLASS(list.first { predicate(VALUE_CLASS(itTO_PARAM)) }TO_PARAM)
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [VALUE_CLASSList] in order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes current accumulator value and an element, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> fold(initial: R, operation: (acc: R, element: VALUE_CLASS) -> R): R {
+        contract { callsInPlace(operation) }
+        return list.fold(initial) { acc, element ->
+            operation(acc, VALUE_CLASS(elementTO_PARAM))
+        }
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [VALUE_CLASSList] in order.
+     */
+    public inline fun <R> foldIndexed(
+        initial: R,
+        operation: (index: Int, acc: R, element: VALUE_CLASS) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        return list.foldIndexed(initial) { index, acc, element ->
+            operation(index, acc, VALUE_CLASS(elementTO_PARAM))
+        }
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [VALUE_CLASSList] in reverse order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes an element and the current accumulator value, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> foldRight(initial: R, operation: (element: VALUE_CLASS, acc: R) -> R): R {
+        contract { callsInPlace(operation) }
+        return list.foldRight(initial) { element, acc ->
+            operation(VALUE_CLASS(elementTO_PARAM), acc)
+        }
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [VALUE_CLASSList] in reverse order.
+     */
+    public inline fun <R> foldRightIndexed(
+        initial: R,
+        operation: (index: Int, element: VALUE_CLASS, acc: R) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        return list.foldRightIndexed(initial) { index, element, acc ->
+            operation(index, VALUE_CLASS(elementTO_PARAM), acc)
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [VALUE_CLASSList], in order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEach(block: (element: VALUE_CLASS) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEach { block(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Calls [block] for each element in the [VALUE_CLASSList] along with its index, in order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachIndexed(block: (index: Int, element: VALUE_CLASS) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEachIndexed { index, element ->
+            block(index, VALUE_CLASS(elementTO_PARAM))
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [VALUE_CLASSList] in reverse order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEachReversed(block: (element: VALUE_CLASS) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEachReversed { block(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Calls [block] for each element in the [VALUE_CLASSList] along with its index, in reverse
+     * order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachReversedIndexed(block: (index: Int, element: VALUE_CLASS) -> Unit) {
+        contract { callsInPlace(block) }
+        list.forEachReversedIndexed { index, element ->
+            block(index, VALUE_CLASS(elementTO_PARAM))
+        }
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public inline operator fun get(
+        @androidx.annotation.IntRange(from = 0) index: Int
+    ): VALUE_CLASS = VALUE_CLASS(list[index]TO_PARAM)
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public inline fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): VALUE_CLASS =
+        VALUE_CLASS(list[index]TO_PARAM)
+
+    /**
+     * Returns the element at the given [index] or [defaultValue] if [index] is out of bounds
+     * of the collection.
+     * @param index The index of the element whose value should be returned
+     * @param defaultValue A lambda to call with [index] as a parameter to return a value at
+     * an index not in the list.
+     */
+    public inline fun elementAtOrElse(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        defaultValue: (index: Int) -> VALUE_CLASS
+    ): VALUE_CLASS =
+        VALUE_CLASS(list.elementAtOrElse(index) { defaultValue(it).BACKING_PROPERTY }TO_PARAM)
+
+    /**
+     * Returns the index of [element] in the [VALUE_CLASSList] or `-1` if [element] is not there.
+     */
+    public inline fun indexOf(element: VALUE_CLASS): Int =
+        list.indexOf(element.BACKING_PROPERTY)
+
+    /**
+     * Returns the index if the first element in the [VALUE_CLASSList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfFirst(predicate: (element: VALUE_CLASS) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return list.indexOfFirst { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns the index if the last element in the [VALUE_CLASSList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfLast(predicate: (element: VALUE_CLASS) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return list.indexOfLast { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns `true` if the [VALUE_CLASSList] has no elements in it or `false` otherwise.
+     */
+    public inline fun isEmpty(): Boolean = list.isEmpty()
+
+    /**
+     * Returns `true` if there are elements in the [VALUE_CLASSList] or `false` if it is empty.
+     */
+    public inline fun isNotEmpty(): Boolean = list.isNotEmpty()
+
+    /**
+     * Returns the last element in the [VALUE_CLASSList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public inline fun last(): VALUE_CLASS = VALUE_CLASS(list.last()TO_PARAM)
+
+    /**
+     * Returns the last element in the [VALUE_CLASSList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfLast
+     */
+    public inline fun last(predicate: (element: VALUE_CLASS) -> Boolean): VALUE_CLASS {
+        contract { callsInPlace(predicate) }
+        return VALUE_CLASS(list.last { predicate(VALUE_CLASS(itTO_PARAM)) }TO_PARAM)
+    }
+
+    /**
+     * Returns the index of the last element in the [VALUE_CLASSList] that is the same as
+     * [element] or `-1` if no elements match.
+     */
+    public inline fun lastIndexOf(element: VALUE_CLASS): Int =
+        list.lastIndexOf(element.BACKING_PROPERTY)
+
+    /**
+     * Returns a String representation of the list, surrounded by "[]" and each element
+     * separated by ", ".
+     */
+    override fun toString(): String = asVALUE_CLASSList().toString()
+
+    /**
+     * Returns a read-only interface to the list.
+     */
+    public inline fun asVALUE_CLASSList(): VALUE_CLASSList = VALUE_CLASSList(list)
+
+    /**
+     * Returns the total number of elements that can be held before the [MutableVALUE_CLASSList] must
+     * grow.
+     *
+     * @see ensureCapacity
+     */
+    public inline val capacity: Int
+        get() = list.capacity
+
+    /**
+     * Adds [element] to the [MutableVALUE_CLASSList] and returns `true`.
+     */
+    public inline fun add(element: VALUE_CLASS): Boolean =
+        list.add(element.BACKING_PROPERTY)
+
+    /**
+     * Adds [element] to the [MutableVALUE_CLASSList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public inline fun add(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        element: VALUE_CLASS
+    ) = list.add(index, element.BACKING_PROPERTY)
+
+    /**
+     * Adds all [elements] to the [MutableVALUE_CLASSList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutableVALUE_CLASSList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public inline fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: VALUE_CLASSList
+    ): Boolean = list.addAll(index, elements.list)
+
+    /**
+     * Adds all [elements] to the [MutableVALUE_CLASSList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutableVALUE_CLASSList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public inline fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: MutableVALUE_CLASSList
+    ): Boolean = list.addAll(index, elements.list)
+
+    /**
+     * Adds all [elements] to the end of the [MutableVALUE_CLASSList] and returns `true` if the
+     * [MutableVALUE_CLASSList] was changed or `false` if [elements] was empty.
+     */
+    public inline fun addAll(elements: VALUE_CLASSList): Boolean = list.addAll(elements.list)
+
+    /**
+     * Adds all [elements] to the end of the [MutableVALUE_CLASSList].
+     */
+    public inline operator fun plusAssign(elements: VALUE_CLASSList) =
+        list.plusAssign(elements.list)
+
+    /**
+     * Adds all [elements] to the end of the [MutableVALUE_CLASSList] and returns `true` if the
+     * [MutableVALUE_CLASSList] was changed or `false` if [elements] was empty.
+     */
+    public inline fun addAll(elements: MutableVALUE_CLASSList): Boolean = list.addAll(elements.list)
+
+    /**
+     * Adds all [elements] to the end of the [MutableVALUE_CLASSList].
+     */
+    public inline operator fun plusAssign(elements: MutableVALUE_CLASSList) =
+        list.plusAssign(elements.list)
+
+    /**
+     * Removes all elements in the [MutableVALUE_CLASSList]. The storage isn't released.
+     * @see trim
+     */
+    public inline fun clear() = list.clear()
+
+    /**
+     * Reduces the internal storage. If [capacity] is greater than [minCapacity] and [size], the
+     * internal storage is reduced to the maximum of [size] and [minCapacity].
+     * @see ensureCapacity
+     */
+    public inline fun trim(minCapacity: Int = size) = list.trim(minCapacity)
+
+    /**
+     * Ensures that there is enough space to store [capacity] elements in the [MutableVALUE_CLASSList].
+     * @see trim
+     */
+    public inline fun ensureCapacity(capacity: Int) = list.ensureCapacity(capacity)
+
+    /**
+     * [add] [element] to the [MutableVALUE_CLASSList].
+     */
+    public inline operator fun plusAssign(element: VALUE_CLASS) =
+        list.plusAssign(element.BACKING_PROPERTY)
+
+    /**
+     * [remove] [element] from the [MutableVALUE_CLASSList]
+     */
+    public inline operator fun minusAssign(element: VALUE_CLASS) =
+        list.minusAssign(element.BACKING_PROPERTY)
+
+    /**
+     * Removes [element] from the [MutableVALUE_CLASSList]. If [element] was in the [MutableVALUE_CLASSList]
+     * and was removed, `true` will be returned, or `false` will be returned if the element
+     * was not found.
+     */
+    public inline fun remove(element: VALUE_CLASS): Boolean =
+        list.remove(element.BACKING_PROPERTY)
+
+    /**
+     * Removes all [elements] from the [MutableVALUE_CLASSList] and returns `true` if anything was removed.
+     */
+    public inline fun removeAll(elements: VALUE_CLASSList): Boolean =
+        list.removeAll(elements.list)
+
+    /**
+     * Removes all [elements] from the [MutableVALUE_CLASSList].
+     */
+    public inline operator fun minusAssign(elements: VALUE_CLASSList) =
+        list.minusAssign(elements.list)
+
+    /**
+     * Removes all [elements] from the [MutableVALUE_CLASSList] and returns `true` if anything was removed.
+     */
+    public inline fun removeAll(elements: MutableVALUE_CLASSList): Boolean =
+        list.removeAll(elements.list)
+
+    /**
+     * Removes all [elements] from the [MutableVALUE_CLASSList].
+     */
+    public inline operator fun minusAssign(elements: MutableVALUE_CLASSList) =
+        list.minusAssign(elements.list)
+
+    /**
+     * Removes the element at the given [index] and returns it.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public inline fun removeAt(@androidx.annotation.IntRange(from = 0) index: Int): VALUE_CLASS =
+        VALUE_CLASS(list.removeAt(index)TO_PARAM)
+
+    /**
+     * Removes items from index [start] (inclusive) to [end] (exclusive).
+     * @throws IndexOutOfBoundsException if [start] or [end] isn't between 0 and [size], inclusive
+     * @throws IllegalArgumentException if [start] is greater than [end]
+     */
+    public inline fun removeRange(
+        @androidx.annotation.IntRange(from = 0) start: Int,
+        @androidx.annotation.IntRange(from = 0) end: Int
+    ) = list.removeRange(start, end)
+
+    /**
+     * Keeps only [elements] in the [MutableVALUE_CLASSList] and removes all other values.
+     * @return `true` if the [MutableVALUE_CLASSList] has changed.
+     */
+    public inline fun retainAll(elements: VALUE_CLASSList): Boolean =
+        list.retainAll(elements.list)
+
+    /**
+     * Keeps only [elements] in the [MutableVALUE_CLASSList] and removes all other values.
+     * @return `true` if the [MutableVALUE_CLASSList] has changed.
+     */
+    public inline fun retainAll(elements: MutableVALUE_CLASSList): Boolean =
+        list.retainAll(elements.list)
+
+    /**
+     * Sets the value at [index] to [element].
+     * @return the previous value set at [index]
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public inline operator fun set(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        element: VALUE_CLASS
+    ): VALUE_CLASS = VALUE_CLASS(list.set(index, element.BACKING_PROPERTY)TO_PARAM)
+}
+
+/**
+ * @return a read-only [VALUE_CLASSList] with nothing in it.
+ */
+internal inline fun emptyVALUE_CLASSList(): VALUE_CLASSList = VALUE_CLASSList(emptyPRIMITIVEList())
+
+/**
+ * @return a read-only [VALUE_CLASSList] with nothing in it.
+ */
+internal inline fun vALUE_CLASSListOf(): VALUE_CLASSList = VALUE_CLASSList(emptyPRIMITIVEList())
+
+/**
+ * @return a new read-only [VALUE_CLASSList] with [element1] as the only item in the list.
+ */
+internal inline fun vALUE_CLASSListOf(element1: VALUE_CLASS): VALUE_CLASSList =
+    VALUE_CLASSList(mutablePRIMITIVEListOf(element1.BACKING_PROPERTY))
+
+/**
+ * @return a new read-only [VALUE_CLASSList] with 2 elements, [element1] and [element2], in order.
+ */
+internal inline fun vALUE_CLASSListOf(element1: VALUE_CLASS, element2: VALUE_CLASS): VALUE_CLASSList =
+    VALUE_CLASSList(
+        mutablePRIMITIVEListOf(
+            element1.BACKING_PROPERTY,
+            element2.BACKING_PROPERTY
+        )
+    )
+
+/**
+ * @return a new read-only [VALUE_CLASSList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+internal inline fun vALUE_CLASSListOf(
+        element1: VALUE_CLASS,
+        element2: VALUE_CLASS,
+        element3: VALUE_CLASS
+): VALUE_CLASSList = VALUE_CLASSList(
+    mutablePRIMITIVEListOf(
+        element1.BACKING_PROPERTY,
+        element2.BACKING_PROPERTY,
+        element3.BACKING_PROPERTY
+    )
+)
+
+/**
+ * @return a new empty [MutableVALUE_CLASSList] with the default capacity.
+ */
+internal inline fun mutableVALUE_CLASSListOf(): MutableVALUE_CLASSList =
+    MutableVALUE_CLASSList(MutablePRIMITIVEList())
+
+/**
+ * @return a new [MutableVALUE_CLASSList] with [element1] as the only item in the list.
+ */
+internal inline fun mutableVALUE_CLASSListOf(element1: VALUE_CLASS): MutableVALUE_CLASSList =
+    MutableVALUE_CLASSList(mutablePRIMITIVEListOf(element1.BACKING_PROPERTY))
+
+/**
+ * @return a new [MutableVALUE_CLASSList] with 2 elements, [element1] and [element2], in order.
+ */
+internal inline fun mutableVALUE_CLASSListOf(
+        element1: VALUE_CLASS,
+        element2: VALUE_CLASS
+    ): MutableVALUE_CLASSList = MutableVALUE_CLASSList(
+        mutablePRIMITIVEListOf(
+            element1.BACKING_PROPERTY,
+            element2.BACKING_PROPERTY
+        )
+    )
+
+/**
+ * @return a new [MutableVALUE_CLASSList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+internal inline fun mutableVALUE_CLASSListOf(
+        element1: VALUE_CLASS,
+        element2: VALUE_CLASS,
+        element3: VALUE_CLASS
+): MutableVALUE_CLASSList = MutableVALUE_CLASSList(
+    mutablePRIMITIVEListOf(
+        element1.BACKING_PROPERTY,
+        element2.BACKING_PROPERTY,
+        element3.BACKING_PROPERTY
+    )
+)
diff --git a/collection/collection/template/ValueClassSet.kt.template b/collection/collection/template/ValueClassSet.kt.template
new file mode 100644
index 0000000..33d7af4
--- /dev/null
+++ b/collection/collection/template/ValueClassSet.kt.template
@@ -0,0 +1,554 @@
+/*
+ * 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "KotlinRedundantDiagnosticSuppress",
+    "KotlinConstantConditions",
+    "PropertyName",
+    "ConstPropertyName",
+    "PrivatePropertyName",
+    "NOTHING_TO_INLINE",
+    "UnusedImport"
+)
+
+package PACKAGE
+
+/* ktlint-disable max-line-length */
+/* ktlint-disable import-ordering */
+
+import androidx.collection.PRIMITIVESet
+import androidx.collection.MutablePRIMITIVESet
+import androidx.collection.emptyPRIMITIVESet
+import androidx.collection.mutablePRIMITIVESetOf
+import VALUE_PKG.VALUE_CLASS
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.jvm.JvmInline
+
+/* ktlint-disable max-line-length */
+// To use this template, you must substitute several strings. You can copy this and search/replace
+// or use a sed command. These properties must be changed:
+// * PACKAGE - target package (e.g. androidx.compose.ui.ui.collection)
+// * VALUE_PKG - package in which the value class resides
+// * VALUE_CLASS - the value class contained in the set (e.g. Color or Offset)
+// * vALUE_CLASS - the value class, with the first letter lower case (e.g. color or offset)
+// * BACKING_PROPERTY - the field in VALUE_CLASS containing the backing primitive (e.g. packedValue)
+// * PRIMITIVE - the primitive type of the backing set (e.g. Long or Float)
+// * TO_PARAM - an operation done on the primitive to convert to the value class parameter
+//
+// For example, to create a ColorSet:
+// sed -e "s/PACKAGE/androidx.compose.ui.graphics/" -e "s/VALUE_CLASS/Color/g" \
+//     -e "s/vALUE_CLASS/color/g" -e "s/BACKING_PROPERTY/value.toLong()/g" -e "s/PRIMITIVE/Long/g" \
+//     -e "s/TO_PARAM/.toULong()/g" -e "s/VALUE_PKG/androidx.collection/g" \
+//     collection/collection/template/ValueClassSet.kt.template \
+//     > compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ColorSet.kt
+
+/**
+ * Returns an empty, read-only [VALUE_CLASSSet].
+ */
+internal inline fun emptyVALUE_CLASSSet(): VALUE_CLASSSet = VALUE_CLASSSet(emptyPRIMITIVESet())
+
+/**
+ * Returns an empty, read-only [VALUE_CLASSSet].
+ */
+internal inline fun vALUE_CLASSSetOf(): VALUE_CLASSSet = VALUE_CLASSSet(emptyPRIMITIVESet())
+
+/**
+ * Returns a new read-only [VALUE_CLASSSet] with only [element1] in it.
+ */
+internal inline fun vALUE_CLASSSetOf(element1: VALUE_CLASS): VALUE_CLASSSet =
+    VALUE_CLASSSet(mutablePRIMITIVESetOf(element1.BACKING_PROPERTY))
+
+/**
+ * Returns a new read-only [VALUE_CLASSSet] with only [element1] and [element2] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+internal fun vALUE_CLASSSetOf(
+    element1: VALUE_CLASS,
+    element2: VALUE_CLASS
+): VALUE_CLASSSet =
+    VALUE_CLASSSet(
+        mutablePRIMITIVESetOf(
+            element1.BACKING_PROPERTY,
+            element2.BACKING_PROPERTY,
+        )
+    )
+
+/**
+ * Returns a new read-only [VALUE_CLASSSet] with only [element1], [element2], and [element3] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+internal fun vALUE_CLASSSetOf(
+    element1: VALUE_CLASS,
+    element2: VALUE_CLASS,
+    element3: VALUE_CLASS
+): VALUE_CLASSSet = VALUE_CLASSSet(
+    mutablePRIMITIVESetOf(
+        element1.BACKING_PROPERTY,
+        element2.BACKING_PROPERTY,
+        element3.BACKING_PROPERTY,
+    )
+)
+
+/**
+ * Returns a new [MutableVALUE_CLASSSet].
+ */
+internal fun mutableVALUE_CLASSSetOf(): MutableVALUE_CLASSSet = MutableVALUE_CLASSSet(
+    MutablePRIMITIVESet()
+)
+
+/**
+ * Returns a new [MutableVALUE_CLASSSet] with only [element1] in it.
+ */
+internal fun mutableVALUE_CLASSSetOf(element1: VALUE_CLASS): MutableVALUE_CLASSSet =
+    MutableVALUE_CLASSSet(mutablePRIMITIVESetOf(element1.BACKING_PROPERTY))
+
+/**
+ * Returns a new [MutableVALUE_CLASSSet] with only [element1] and [element2] in it.
+ */
+internal fun mutableVALUE_CLASSSetOf(
+    element1: VALUE_CLASS,
+    element2: VALUE_CLASS
+): MutableVALUE_CLASSSet =
+    MutableVALUE_CLASSSet(
+        mutablePRIMITIVESetOf(
+            element1.BACKING_PROPERTY,
+            element2.BACKING_PROPERTY,
+        )
+    )
+
+/**
+ * Returns a new [MutableVALUE_CLASSSet] with only [element1], [element2], and [element3] in it.
+ */
+internal fun mutableVALUE_CLASSSetOf(
+    element1: VALUE_CLASS,
+    element2: VALUE_CLASS,
+    element3: VALUE_CLASS
+): MutableVALUE_CLASSSet =
+    MutableVALUE_CLASSSet(
+        mutablePRIMITIVESetOf(
+            element1.BACKING_PROPERTY,
+            element2.BACKING_PROPERTY,
+            element3.BACKING_PROPERTY,
+        )
+    )
+
+/**
+ * [VALUE_CLASSSet] is a container with a [Set]-like interface designed to avoid
+ * allocations, including boxing.
+ *
+ * This implementation makes no guarantee as to the order of the elements,
+ * nor does it make guarantees that the order remains constant over time.
+ *
+ * Though [VALUE_CLASSSet] offers a read-only interface, it is always backed
+ * by a [MutableVALUE_CLASSSet]. Read operations alone are thread-safe. However,
+ * any mutations done through the backing [MutableVALUE_CLASSSet] while reading
+ * on another thread are not safe and the developer must protect the set
+ * from such changes during read operations.
+ *
+ * @see [MutableVALUE_CLASSSet]
+ */
+@OptIn(ExperimentalContracts::class)
+@JvmInline
+internal value class VALUE_CLASSSet(val set: PRIMITIVESet) {
+    /**
+     * Returns the number of elements that can be stored in this set
+     * without requiring internal storage reallocation.
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public inline val capacity: Int
+        get() = set.capacity
+
+    /**
+     * Returns the number of elements in this set.
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public inline val size: Int
+        get() = set.size
+
+    /**
+     * Returns `true` if this set has at least one element.
+     */
+    public inline fun any(): Boolean = set.any()
+
+    /**
+     * Returns `true` if this set has no elements.
+     */
+    public inline fun none(): Boolean = set.none()
+
+    /**
+     * Indicates whether this set is empty.
+     */
+    public inline fun isEmpty(): Boolean = set.isEmpty()
+
+    /**
+     * Returns `true` if this set is not empty.
+     */
+    public inline fun isNotEmpty(): Boolean = set.isNotEmpty()
+
+    /**
+     * Returns the first element in the collection.
+     * @throws NoSuchElementException if the collection is empty
+     */
+    public inline fun first(): VALUE_CLASS = VALUE_CLASS(set.first()TO_PARAM)
+
+    /**
+     * Returns the first element in the collection for which [predicate] returns `true`.
+     *
+     * **Note** There is no mechanism for both determining if there is an element that matches
+     * [predicate] _and_ returning it if it exists. Developers should use [forEach] to achieve
+     * this behavior.
+     *
+     * @param predicate Called on elements of the set, returning `true` for an element that matches
+     * or `false` if it doesn't
+     * @return An element in the set for which [predicate] returns `true`.
+     * @throws NoSuchElementException if [predicate] returns `false` for all elements or the
+     * collection is empty.
+     */
+    public inline fun first(predicate: (element: VALUE_CLASS) -> Boolean): VALUE_CLASS =
+        VALUE_CLASS(set.first { predicate(VALUE_CLASS(itTO_PARAM)) }TO_PARAM)
+
+    /**
+     * Iterates over every element stored in this set by invoking
+     * the specified [block] lambda.
+     * @param block called with each element in the set
+     */
+    public inline fun forEach(block: (element: VALUE_CLASS) -> Unit) {
+        contract { callsInPlace(block) }
+        set.forEach { block(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns true if all elements match the given [predicate].
+     * @param predicate called for elements in the set to determine if it returns return `true` for
+     * all elements.
+     */
+    public inline fun all(predicate: (element: VALUE_CLASS) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return set.all { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns true if at least one element matches the given [predicate].
+     * @param predicate called for elements in the set to determine if it returns `true` for any
+     * elements.
+     */
+    public inline fun any(predicate: (element: VALUE_CLASS) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return set.any { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns the number of elements in this set.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public inline fun count(): Int = set.count()
+
+    /**
+     * Returns the number of elements matching the given [predicate].
+     * @param predicate Called for all elements in the set to count the number for which it returns
+     * `true`.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public inline fun count(predicate: (element: VALUE_CLASS) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return set.count { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns `true` if the specified [element] is present in this set, `false`
+     * otherwise.
+     * @param element The element to look for in this set
+     */
+    public inline operator fun contains(element: VALUE_CLASS): Boolean =
+        set.contains(element.BACKING_PROPERTY)
+
+    /**
+     * Returns a string representation of this set. The set is denoted in the
+     * string by the `{}`. Each element is separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "[]"
+        }
+
+        val s = StringBuilder().append('[')
+        var index = 0
+        forEach { element ->
+            if (index++ != 0) {
+                s.append(',').append(' ')
+            }
+            s.append(element)
+        }
+        return s.append(']').toString()
+    }
+}
+
+/**
+ * [MutableVALUE_CLASSSet] is a container with a [MutableSet]-like interface based on a flat
+ * hash table implementation. The underlying implementation is designed to avoid
+ * all allocations on insertion, removal, retrieval, and iteration. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added elements to the set.
+ *
+ * This implementation makes no guarantee as to the order of the elements stored,
+ * nor does it make guarantees that the order remains constant over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the set (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Concurrent reads are however safe.
+ */
+@OptIn(ExperimentalContracts::class)
+@JvmInline
+internal value class MutableVALUE_CLASSSet(val set: MutablePRIMITIVESet) {
+    /**
+     * Returns the number of elements that can be stored in this set
+     * without requiring internal storage reallocation.
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public inline val capacity: Int
+        get() = set.capacity
+
+    /**
+     * Returns the number of elements in this set.
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public inline val size: Int
+        get() = set.size
+
+    /**
+     * Returns `true` if this set has at least one element.
+     */
+    public inline fun any(): Boolean = set.any()
+
+    /**
+     * Returns `true` if this set has no elements.
+     */
+    public inline fun none(): Boolean = set.none()
+
+    /**
+     * Indicates whether this set is empty.
+     */
+    public inline fun isEmpty(): Boolean = set.isEmpty()
+
+    /**
+     * Returns `true` if this set is not empty.
+     */
+    public inline fun isNotEmpty(): Boolean = set.isNotEmpty()
+
+    /**
+     * Returns the first element in the collection.
+     * @throws NoSuchElementException if the collection is empty
+     */
+    public inline fun first(): VALUE_CLASS = VALUE_CLASS(set.first()TO_PARAM)
+
+    /**
+     * Returns the first element in the collection for which [predicate] returns `true`.
+     *
+     * **Note** There is no mechanism for both determining if there is an element that matches
+     * [predicate] _and_ returning it if it exists. Developers should use [forEach] to achieve
+     * this behavior.
+     *
+     * @param predicate Called on elements of the set, returning `true` for an element that matches
+     * or `false` if it doesn't
+     * @return An element in the set for which [predicate] returns `true`.
+     * @throws NoSuchElementException if [predicate] returns `false` for all elements or the
+     * collection is empty.
+     */
+    public inline fun first(predicate: (element: VALUE_CLASS) -> Boolean): VALUE_CLASS =
+        VALUE_CLASS(set.first { predicate(VALUE_CLASS(itTO_PARAM)) }TO_PARAM)
+
+    /**
+     * Iterates over every element stored in this set by invoking
+     * the specified [block] lambda.
+     * @param block called with each element in the set
+     */
+    public inline fun forEach(block: (element: VALUE_CLASS) -> Unit) {
+        contract { callsInPlace(block) }
+        set.forEach { block(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns true if all elements match the given [predicate].
+     * @param predicate called for elements in the set to determine if it returns return `true` for
+     * all elements.
+     */
+    public inline fun all(predicate: (element: VALUE_CLASS) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return set.all { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns true if at least one element matches the given [predicate].
+     * @param predicate called for elements in the set to determine if it returns `true` for any
+     * elements.
+     */
+    public inline fun any(predicate: (element: VALUE_CLASS) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        return set.any { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns the number of elements in this set.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public inline fun count(): Int = set.count()
+
+    /**
+     * Returns the number of elements matching the given [predicate].
+     * @param predicate Called for all elements in the set to count the number for which it returns
+     * `true`.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public inline fun count(predicate: (element: VALUE_CLASS) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        return set.count { predicate(VALUE_CLASS(itTO_PARAM)) }
+    }
+
+    /**
+     * Returns `true` if the specified [element] is present in this set, `false`
+     * otherwise.
+     * @param element The element to look for in this set
+     */
+    public inline operator fun contains(element: VALUE_CLASS): Boolean =
+        set.contains(element.BACKING_PROPERTY)
+
+    /**
+     * Returns a string representation of this set. The set is denoted in the
+     * string by the `{}`. Each element is separated by `, `.
+     */
+    public override fun toString(): String = asVALUE_CLASSSet().toString()
+
+    /**
+     * Creates a new [MutableVALUE_CLASSSet]
+     * @param initialCapacity The initial desired capacity for this container.
+     * The container will honor this value by guaranteeing its internal structures
+     * can hold that many elements without requiring any allocations. The initial
+     * capacity can be set to 0.
+     */
+    public constructor(initialCapacity: Int = 6) : this(MutablePRIMITIVESet(initialCapacity))
+
+    /**
+     * Returns a read-only interface to the set.
+     */
+    public inline fun asVALUE_CLASSSet(): VALUE_CLASSSet = VALUE_CLASSSet(set)
+
+    /**
+     * Adds the specified element to the set.
+     * @param element The element to add to the set.
+     * @return `true` if the element has been added or `false` if the element is already
+     * contained within the set.
+     */
+    public inline fun add(element: VALUE_CLASS): Boolean = set.add(element.BACKING_PROPERTY)
+
+    /**
+     * Adds the specified element to the set.
+     * @param element The element to add to the set.
+     */
+    public inline operator fun plusAssign(element: VALUE_CLASS) =
+        set.plusAssign(element.BACKING_PROPERTY)
+
+    /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements A [VALUE_CLASSSet] of elements to add to this set.
+     * @return `true` if any of the specified elements were added to the collection,
+     * `false` if the collection was not modified.
+     */
+    public inline fun addAll(elements: VALUE_CLASSSet): Boolean = set.addAll(elements.set)
+
+    /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements A [VALUE_CLASSSet] of elements to add to this set.
+     * @return `true` if any of the specified elements were added to the collection,
+     * `false` if the collection was not modified.
+     */
+    public inline fun addAll(elements: MutableVALUE_CLASSSet): Boolean = set.addAll(elements.set)
+
+    /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements A [VALUE_CLASSSet] of elements to add to this set.
+     */
+    public inline operator fun plusAssign(elements: VALUE_CLASSSet) =
+        set.plusAssign(elements.set)
+
+    /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements A [VALUE_CLASSSet] of elements to add to this set.
+     */
+    public inline operator fun plusAssign(elements: MutableVALUE_CLASSSet) =
+        set.plusAssign(elements.set)
+
+    /**
+     * Removes the specified [element] from the set.
+     * @param element The element to remove from the set.
+     * @return `true` if the [element] was present in the set, or `false` if it wasn't
+     * present before removal.
+     */
+    public inline fun remove(element: VALUE_CLASS): Boolean = set.remove(element.BACKING_PROPERTY)
+
+    /**
+     * Removes the specified [element] from the set if it is present.
+     * @param element The element to remove from the set.
+     */
+    public inline operator fun minusAssign(element: VALUE_CLASS) =
+        set.minusAssign(element.BACKING_PROPERTY)
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [VALUE_CLASSSet] of elements to be removed from the set.
+     * @return `true` if the set was changed or `false` if none of the elements were present.
+     */
+    public inline fun removeAll(elements: VALUE_CLASSSet): Boolean = set.removeAll(elements.set)
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [VALUE_CLASSSet] of elements to be removed from the set.
+     * @return `true` if the set was changed or `false` if none of the elements were present.
+     */
+    public inline fun removeAll(elements: MutableVALUE_CLASSSet): Boolean =
+        set.removeAll(elements.set)
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [VALUE_CLASSSet] of elements to be removed from the set.
+     */
+    public inline operator fun minusAssign(elements: VALUE_CLASSSet) =
+        set.minusAssign(elements.set)
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [VALUE_CLASSSet] of elements to be removed from the set.
+     */
+    public inline operator fun minusAssign(elements: MutableVALUE_CLASSSet) =
+        set.minusAssign(elements.set)
+
+    /**
+     * Removes all elements from this set.
+     */
+    public inline fun clear() = set.clear()
+
+    /**
+     * Trims this [MutableVALUE_CLASSSet]'s storage so it is sized appropriately
+     * to hold the current elements.
+     *
+     * Returns the number of empty elements removed from this set's storage.
+     * Returns 0 if no trimming is necessary or possible.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public inline fun trim(): Int = set.trim()
+}
diff --git a/collection/collection/template/generateCollections.sh b/collection/collection/template/generateCollections.sh
new file mode 100755
index 0000000..39db416d
--- /dev/null
+++ b/collection/collection/template/generateCollections.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+
+primitives=("Float" "Long" "Int")
+suffixes=("f" "L" "")
+
+scriptDir=`dirname ${PWD}/${0}`
+
+for index in ${!primitives[@]}
+do
+  primitive=${primitives[$index]}
+  firstLower=`echo ${primitive:0:1} | tr '[:upper:]' '[:lower:]'`
+  lower="${firstLower}${primitive:1}"
+  echo "generating ${primitive}ObjectMap.kt"
+  sed -e "s/PKey/${primitive}/g" -e "s/pKey/${lower}/g" ${scriptDir}/PKeyObjectMap.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/${primitive}ObjectMap.kt
+  echo "generating ${primitive}ObjectMapTest.kt"
+  sed -e "s/PValue/${primitive}/g" ${scriptDir}/ObjectPValueMap.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/Object${primitive}Map.kt
+
+  suffix=${suffixes[$index]}
+  echo "generating Object${primitive}Map.kt"
+  sed -e "s/PValue/${primitive}/g" -e "s/ValueSuffix/${suffix}/g" ${scriptDir}/ObjectPValueMapTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/Object${primitive}MapTest.kt
+  echo "generating Object${primitive}MapTest.kt"
+  sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" -e "s/KeySuffix/${suffix}/g" ${scriptDir}/PKeyObjectMapTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/${primitive}ObjectMapTest.kt
+
+  echo "generating ${primitive}Set.kt"
+  sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" ${scriptDir}/PKeySet.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/${primitive}Set.kt
+  echo "generating ${primitive}SetTest.kt"
+  sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" -e "s/KeySuffix/${suffix}/g" ${scriptDir}/PKeySetTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/${primitive}SetTest.kt
+
+  echo "generating ${primitive}List.kt"
+  sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" ${scriptDir}/PKeyList.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/${primitive}List.kt
+  echo "generating ${primitive}ListTest.kt"
+  sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" -e "s/KeySuffix/${suffix}/g" ${scriptDir}/PKeyListTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/${primitive}ListTest.kt
+done
+
+for keyIndex in ${!primitives[@]}
+do
+  key=${primitives[$keyIndex]}
+  firstLower=`echo ${key:0:1} | tr '[:upper:]' '[:lower:]'`
+  lowerKey="${firstLower}${key:1}"
+  keySuffix=${suffixes[$keyIndex]}
+  for valueIndex in ${!primitives[@]}
+  do
+    value=${primitives[$valueIndex]}
+    valueSuffix=${suffixes[$valueIndex]}
+    echo "generating ${key}${value}Map.kt"
+    sed -e "s/PKey/${key}/g" -e "s/pKey/${lowerKey}/g" -e "s/PValue/${value}/g" ${scriptDir}/PKeyPValueMap.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/${key}${value}Map.kt
+    echo "generating ${key}${value}MapTest.kt"
+    sed -e "s/PKey/${key}/g" -e "s/pKey/${lowerKey}/g" -e "s/PValue/${value}/g" -e "s/ValueSuffix/${valueSuffix}/g" -e "s/KeySuffix/${keySuffix}/g" ${scriptDir}/PKeyPValueMapTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/${key}${value}MapTest.kt
+  done
+done
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 1af0fb2..9547fc0 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -85,13 +85,14 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
                 implementation(libs.testCore)
                 implementation(libs.junit)
+                implementation(libs.truth)
                 implementation(project(":compose:animation:animation"))
                 implementation("androidx.compose.ui:ui-test-junit4:1.2.1")
                 implementation(project(":compose:test-utils"))
@@ -102,7 +103,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
diff --git a/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/InfiniteTransitionTest.kt b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/InfiniteTransitionTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/InfiniteTransitionTest.kt
rename to compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/InfiniteTransitionTest.kt
diff --git a/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/PathEasingTest.kt b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/PathEasingTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/PathEasingTest.kt
rename to compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/PathEasingTest.kt
diff --git a/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
rename to compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
diff --git a/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/SingleValueAnimationTest.kt b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SingleValueAnimationTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/SingleValueAnimationTest.kt
rename to compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SingleValueAnimationTest.kt
diff --git a/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/TransitionTest.kt b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/TransitionTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/TransitionTest.kt
rename to compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/TransitionTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimationTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimationTestUtils.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimationTestUtils.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimationVectorTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimationVectorTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/DecayAnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/DecayAnimationTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/DurationScaleTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/DurationScaleTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/EasingTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/EasingTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/IsInfiniteTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/IsInfiniteTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/KeyframeAnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/KeyframeAnimationTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/MotionTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MotionTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/MotionTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MotionTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/PhysicsAnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/PhysicsAnimationTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SnapAnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SnapAnimationTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SpringEstimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SpringEstimationTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SpringEstimationTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SpringEstimationTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SuspendAnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SuspendAnimationTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/TweenAnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/TweenAnimationTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/TypeConverterTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt
similarity index 100%
rename from compose/animation/animation-core/src/test/java/androidx/compose/animation/core/TypeConverterTest.kt
rename to compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt
diff --git a/compose/animation/animation-graphics/build.gradle b/compose/animation/animation-graphics/build.gradle
index 4faa4b5..9126638 100644
--- a/compose/animation/animation-graphics/build.gradle
+++ b/compose/animation/animation-graphics/build.gradle
@@ -91,7 +91,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
@@ -108,7 +108,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/res/AnimatedVectorPainterResourcesTest.kt b/compose/animation/animation-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/animation/graphics/res/AnimatedVectorPainterResourcesTest.kt
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/res/AnimatedVectorPainterResourcesTest.kt
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/animation/graphics/res/AnimatedVectorPainterResourcesTest.kt
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/AnimatorAnimationSpecsTest.kt b/compose/animation/animation-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/animation/graphics/vector/AnimatorAnimationSpecsTest.kt
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/AnimatorAnimationSpecsTest.kt
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/animation/graphics/vector/AnimatorAnimationSpecsTest.kt
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/AnimatorTest.kt b/compose/animation/animation-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/animation/graphics/vector/AnimatorTest.kt
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/AnimatorTest.kt
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/animation/graphics/vector/AnimatorTest.kt
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatedVectorParserTest.kt b/compose/animation/animation-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatedVectorParserTest.kt
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatedVectorParserTest.kt
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatedVectorParserTest.kt
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatorParserTest.kt b/compose/animation/animation-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatorParserTest.kt
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatorParserTest.kt
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatorParserTest.kt
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/complex_background.xml b/compose/animation/animation-graphics/src/androidInstrumentedTest/res/animator/complex_background.xml
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/res/animator/complex_background.xml
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/res/animator/complex_background.xml
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/object_animator_1d.xml b/compose/animation/animation-graphics/src/androidInstrumentedTest/res/animator/object_animator_1d.xml
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/res/animator/object_animator_1d.xml
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/res/animator/object_animator_1d.xml
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/object_animator_2d.xml b/compose/animation/animation-graphics/src/androidInstrumentedTest/res/animator/object_animator_2d.xml
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/res/animator/object_animator_2d.xml
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/res/animator/object_animator_2d.xml
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/property_values_holders.xml b/compose/animation/animation-graphics/src/androidInstrumentedTest/res/animator/property_values_holders.xml
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/res/animator/property_values_holders.xml
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/res/animator/property_values_holders.xml
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/set.xml b/compose/animation/animation-graphics/src/androidInstrumentedTest/res/animator/set.xml
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/res/animator/set.xml
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/res/animator/set.xml
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/drawable/avd_complex.xml b/compose/animation/animation-graphics/src/androidInstrumentedTest/res/drawable/avd_complex.xml
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/res/drawable/avd_complex.xml
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/res/drawable/avd_complex.xml
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/drawable/target_duplicated.xml b/compose/animation/animation-graphics/src/androidInstrumentedTest/res/drawable/target_duplicated.xml
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/res/drawable/target_duplicated.xml
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/res/drawable/target_duplicated.xml
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/drawable/vd_complex.xml b/compose/animation/animation-graphics/src/androidInstrumentedTest/res/drawable/vd_complex.xml
similarity index 100%
rename from compose/animation/animation-graphics/src/androidAndroidTest/res/drawable/vd_complex.xml
rename to compose/animation/animation-graphics/src/androidInstrumentedTest/res/drawable/vd_complex.xml
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 2fecd73..929927e 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -87,7 +87,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
@@ -105,7 +105,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
deleted file mode 100644
index 2767e5aa..0000000
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
+++ /dev/null
@@ -1,1304 +0,0 @@
-/*
- * Copyright 2021 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(ExperimentalAnimationApi::class)
-
-package androidx.compose.animation
-
-import androidx.compose.animation.core.InternalAnimationApi
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.MutableTransitionState
-import androidx.compose.animation.core.Transition
-import androidx.compose.animation.core.keyframes
-import androidx.compose.animation.core.snap
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.core.updateTransition
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.TabRow
-import androidx.compose.material3.Text
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.saveable.rememberSaveableStateHolder
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.withFrameMillis
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.SubcomposeLayout
-import androidx.compose.ui.layout.boundsInRoot
-import androidx.compose.ui.layout.layout
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.layout.onPlaced
-import androidx.compose.ui.layout.positionInRoot
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.round
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import com.google.common.truth.Truth.assertThat
-import kotlin.math.roundToInt
-import kotlinx.coroutines.delay
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertNull
-import org.junit.Assert.assertTrue
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@LargeTest
-class AnimatedContentTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @OptIn(InternalAnimationApi::class)
-    @Test
-    fun AnimatedContentSizeTransformTest() {
-        val size1 = 40
-        val size2 = 200
-        val testModifier by mutableStateOf(TestModifier())
-        val transitionState = MutableTransitionState(true)
-        var playTimeMillis by mutableStateOf(0)
-        rule.mainClock.autoAdvance = false
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides Density(1f)) {
-                val transition = updateTransition(transitionState)
-                playTimeMillis = (transition.playTimeNanos / 1_000_000L).toInt()
-                transition.AnimatedContent(
-                    testModifier,
-                    transitionSpec = {
-                        if (true isTransitioningTo false) {
-                            fadeIn() togetherWith fadeOut() using
-                                SizeTransform { initialSize, targetSize ->
-                                    keyframes {
-                                        durationMillis = 320
-                                        IntSize(targetSize.width, initialSize.height) at 160 with
-                                            LinearEasing
-                                        targetSize at 320 with LinearEasing
-                                    }
-                                }
-                        } else {
-                            fadeIn() togetherWith fadeOut() using SizeTransform { _, _ ->
-                                tween(durationMillis = 80, easing = LinearEasing)
-                            }
-                        }
-                    }
-                ) {
-                    if (it) {
-                        Box(modifier = Modifier.size(size = size1.dp))
-                    } else {
-                        Box(modifier = Modifier.size(size = size2.dp))
-                    }
-                }
-            }
-        }
-        rule.runOnIdle {
-            assertEquals(40, testModifier.height)
-            assertEquals(40, testModifier.width)
-            assertTrue(transitionState.targetState)
-            transitionState.targetState = false
-        }
-
-        // Transition from item1 to item2 in 320ms, animating to full width in the first 160ms
-        // then full height in the next 160ms
-        while (transitionState.currentState != transitionState.targetState) {
-            rule.runOnIdle {
-                if (playTimeMillis <= 160) {
-                    assertEquals(playTimeMillis + 40, testModifier.width)
-                    assertEquals(40, testModifier.height)
-                } else {
-                    assertEquals(200, testModifier.width)
-                    assertEquals(playTimeMillis - 120, testModifier.height)
-                }
-            }
-            rule.mainClock.advanceTimeByFrame()
-        }
-
-        rule.runOnIdle {
-            assertEquals(200, testModifier.width)
-            assertEquals(200, testModifier.height)
-            transitionState.targetState = true
-        }
-
-        // Transition from item2 to item1 in 80ms
-        while (transitionState.currentState != transitionState.targetState) {
-            rule.runOnIdle {
-                if (playTimeMillis <= 80) {
-                    assertEquals(200 - playTimeMillis * 2, testModifier.width)
-                    assertEquals(200 - playTimeMillis * 2, testModifier.height)
-                }
-            }
-            rule.mainClock.advanceTimeByFrame()
-        }
-    }
-
-    @OptIn(InternalAnimationApi::class)
-    @Test
-    fun AnimatedContentSizeTransformEmptyComposableTest() {
-        val size1 = 160
-        val testModifier by mutableStateOf(TestModifier())
-        val transitionState = MutableTransitionState(true)
-        var playTimeMillis by mutableStateOf(0)
-        rule.mainClock.autoAdvance = false
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides Density(1f)) {
-                val transition = updateTransition(transitionState)
-                playTimeMillis = (transition.playTimeNanos / 1_000_000L).toInt()
-                transition.AnimatedContent(
-                    testModifier,
-                    transitionSpec = {
-                        EnterTransition.None togetherWith ExitTransition.None using
-                            SizeTransform { _, _ ->
-                                tween(durationMillis = 160, easing = LinearEasing)
-                            }
-                    }
-                ) {
-                    if (it) {
-                        Box(modifier = Modifier.size(size = size1.dp))
-                    }
-                    // Empty composable for it == false
-                }
-            }
-        }
-        rule.runOnIdle {
-            assertEquals(160, testModifier.height)
-            assertEquals(160, testModifier.width)
-            assertTrue(transitionState.targetState)
-            transitionState.targetState = false
-        }
-
-        // Transition from item1 to item2 in 320ms, animating to full width in the first 160ms
-        // then full height in the next 160ms
-        while (transitionState.currentState != transitionState.targetState) {
-            rule.runOnIdle {
-                assertEquals(160 - playTimeMillis, testModifier.width)
-                assertEquals(160 - playTimeMillis, testModifier.height)
-            }
-            rule.mainClock.advanceTimeByFrame()
-        }
-
-        // Now there's only an empty composable
-        rule.runOnIdle {
-            assertEquals(0, testModifier.width)
-            assertEquals(0, testModifier.height)
-            transitionState.targetState = true
-        }
-
-        // Transition from item2 to item1 in 80ms
-        while (transitionState.currentState != transitionState.targetState) {
-            rule.runOnIdle {
-                assertEquals(playTimeMillis, testModifier.width)
-                assertEquals(playTimeMillis, testModifier.height)
-            }
-            rule.mainClock.advanceTimeByFrame()
-        }
-    }
-
-    @OptIn(InternalAnimationApi::class)
-    @Test
-    fun AnimatedContentContentAlignmentTest() {
-        val size1 = IntSize(80, 80)
-        val size2 = IntSize(160, 240)
-        val testModifier by mutableStateOf(TestModifier())
-        var offset1 by mutableStateOf(Offset.Zero)
-        var offset2 by mutableStateOf(Offset.Zero)
-        var playTimeMillis by mutableStateOf(0)
-        val transitionState = MutableTransitionState(true)
-        val alignment = listOf(
-            Alignment.TopStart, Alignment.BottomStart, Alignment.Center,
-            Alignment.BottomEnd, Alignment.TopEnd
-        )
-        var contentAlignment by mutableStateOf(Alignment.TopStart)
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides Density(1f)) {
-                val transition = updateTransition(transitionState)
-                playTimeMillis = (transition.playTimeNanos / 1_000_000L).toInt()
-                transition.AnimatedContent(
-                    testModifier,
-                    contentAlignment = contentAlignment,
-                    transitionSpec = {
-                        fadeIn(animationSpec = tween(durationMillis = 80)) togetherWith fadeOut(
-                            animationSpec = tween(durationMillis = 80)
-                        ) using SizeTransform { _, _ ->
-                            tween(durationMillis = 80, easing = LinearEasing)
-                        }
-                    }
-                ) {
-                    if (it) {
-                        Box(
-                            modifier = Modifier
-                                .onGloballyPositioned {
-                                    offset1 = it.positionInRoot()
-                                }
-                                .size(size1.width.dp, size1.height.dp)
-                        )
-                    } else {
-                        Box(
-                            modifier = Modifier
-                                .onGloballyPositioned {
-                                    offset2 = it.positionInRoot()
-                                }
-                                .size(size2.width.dp, size2.height.dp)
-                        )
-                    }
-                }
-            }
-        }
-
-        rule.mainClock.autoAdvance = false
-
-        alignment.forEach {
-            rule.runOnIdle {
-                assertEquals(80, testModifier.height)
-                assertEquals(80, testModifier.width)
-                assertTrue(transitionState.targetState)
-                contentAlignment = it
-            }
-
-            rule.mainClock.advanceTimeByFrame()
-            rule.waitForIdle()
-            rule.runOnIdle { transitionState.targetState = false }
-
-            // Transition from item1 to item2 in 320ms, animating to full width in the first 160ms
-            // then full height in the next 160ms
-            while (transitionState.currentState != transitionState.targetState) {
-                rule.runOnIdle {
-                    val space = IntSize(testModifier.width, testModifier.height)
-                    val position1 = it.align(size1, space, LayoutDirection.Ltr)
-                    val position2 = it.align(size2, space, LayoutDirection.Ltr)
-                    if (playTimeMillis < 80) {
-                        // This gets removed when the animation is finished at 80ms
-                        assertEquals(
-                            position1,
-                            IntOffset(offset1.x.roundToInt(), offset1.y.roundToInt())
-                        )
-                    }
-                    if (playTimeMillis > 0) {
-                        assertEquals(
-                            position2,
-                            IntOffset(offset2.x.roundToInt(), offset2.y.roundToInt())
-                        )
-                    }
-                }
-                rule.mainClock.advanceTimeByFrame()
-            }
-
-            rule.runOnIdle {
-                assertEquals(size2.width, testModifier.width)
-                assertEquals(size2.height, testModifier.height)
-                // After the animation the size should be the same as parent, offset should be 0
-                assertEquals(offset2, Offset.Zero)
-                transitionState.targetState = true
-            }
-
-            // Transition from item2 to item1 in 80ms
-            while (transitionState.currentState != transitionState.targetState) {
-                rule.runOnIdle {
-                    val space = IntSize(testModifier.width, testModifier.height)
-                    val position1 = it.align(size1, space, LayoutDirection.Ltr)
-                    val position2 = it.align(size2, space, LayoutDirection.Ltr)
-                    if (playTimeMillis > 0) {
-                        assertEquals(
-                            position1,
-                            IntOffset(offset1.x.roundToInt(), offset1.y.roundToInt())
-                        )
-                    }
-                    if (playTimeMillis < 80) {
-                        assertEquals(
-                            position2,
-                            IntOffset(offset2.x.roundToInt(), offset2.y.roundToInt())
-                        )
-                    }
-                }
-                rule.mainClock.advanceTimeByFrame()
-            }
-
-            rule.runOnIdle {
-                assertEquals(size1.width, testModifier.width)
-                assertEquals(size1.height, testModifier.height)
-                // After the animation the size should be the same as parent, offset should be 0
-                assertEquals(offset1, Offset.Zero)
-            }
-        }
-    }
-
-    @OptIn(ExperimentalAnimationApi::class)
-    @Test
-    fun AnimatedContentSlideInAndOutOfContainerTest() {
-        val transitionState = MutableTransitionState(true)
-        // LinearEasing is required to ensure the animation doesn't reach final values before the
-        // duration.
-        val animSpec = tween<IntOffset>(200, easing = LinearEasing)
-        lateinit var trueTransition: Transition<EnterExitState>
-        lateinit var falseTransition: Transition<EnterExitState>
-        rule.mainClock.autoAdvance = false
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides Density(1f, 1f)) {
-                @Suppress("UpdateTransitionLabel")
-                val rootTransition = updateTransition(transitionState)
-                rootTransition.AnimatedContent(
-                    transitionSpec = {
-                        if (true isTransitioningTo false) {
-                            slideIntoContainer(
-                                AnimatedContentTransitionScope.SlideDirection.Start, animSpec
-                            ) togetherWith
-                                slideOutOfContainer(
-                                    AnimatedContentTransitionScope.SlideDirection.Start, animSpec
-                                )
-                        } else {
-                            slideIntoContainer(
-                                AnimatedContentTransitionScope.SlideDirection.End, animSpec
-                            ) togetherWith
-                                slideOutOfContainer(
-                                    towards = AnimatedContentTransitionScope.SlideDirection.End,
-                                    animSpec
-                                )
-                        }
-                    }
-                ) { target ->
-                    if (target) {
-                        trueTransition = transition
-                    } else {
-                        falseTransition = transition
-                    }
-                    Box(
-                        Modifier
-                            .requiredSize(200.dp)
-                            .testTag(target.toString())
-                    )
-                }
-            }
-        }
-
-        // Kick off the first animation.
-        transitionState.targetState = false
-        // The initial composition creates the transition…
-        rule.mainClock.advanceTimeByFrame()
-        rule.onNodeWithTag("true").assertExists()
-        rule.onNodeWithTag("false").assertExists()
-        // …but the animation won't actually start until one frame later.
-        rule.mainClock.advanceTimeByFrame()
-        assertThat(trueTransition.animations).isNotEmpty()
-        assertThat(falseTransition.animations).isNotEmpty()
-
-        // Loop to ensure the content is offset correctly at each frame.
-        var trueAnim = trueTransition.animations[0]
-        var falseAnim = falseTransition.animations[0]
-        assertThat(transitionState.currentState).isTrue()
-        while (transitionState.currentState) {
-            // True is leaving: it should start at 0 and slide out to -200.
-            assertThat(trueAnim.value).isEqualTo(IntOffset(-trueTransition.playTimeMillis, 0))
-            // False is entering: it should start at 200 and slide in to 0.
-            assertThat(falseAnim.value)
-                .isEqualTo(IntOffset(200 - falseTransition.playTimeMillis, 0))
-            rule.mainClock.advanceTimeByFrame()
-        }
-        // The animation should remove the newly-hidden node from the composition.
-        rule.onNodeWithTag("true").assertDoesNotExist()
-
-        // Kick off the second transition.
-        transitionState.targetState = true
-        rule.mainClock.advanceTimeByFrame()
-        rule.onNodeWithTag("true").assertExists()
-        rule.onNodeWithTag("false").assertExists()
-        rule.mainClock.advanceTimeByFrame()
-        assertThat(trueTransition.animations).isNotEmpty()
-
-        trueAnim = trueTransition.animations[0]
-        falseAnim = falseTransition.animations[0]
-        assertThat(transitionState.currentState).isFalse()
-        while (!transitionState.currentState) {
-            // True is entering, it should start at -200 and slide in to 0.
-            assertThat(trueAnim.value).isEqualTo(IntOffset(trueTransition.playTimeMillis - 200, 0))
-            // False is leaving, it should start at 0 and slide out to 200.
-            assertThat(falseAnim.value).isEqualTo(IntOffset(falseTransition.playTimeMillis, 0))
-            rule.mainClock.advanceTimeByFrame()
-        }
-        rule.onNodeWithTag("false").assertDoesNotExist()
-    }
-
-    @Test
-    fun AnimatedContentWithContentKey() {
-        var targetState by mutableStateOf(1)
-        var actualIncomingPosition: Offset? = null
-        var actualOutgoingPosition: Offset? = null
-        var targetPosition: Offset? = null
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides Density(1f)) {
-                AnimatedContent(targetState,
-                    Modifier.onGloballyPositioned {
-                        targetPosition = it.positionInRoot()
-                    },
-                    transitionSpec = {
-                        slideInHorizontally { -200 } togetherWith
-                            slideOutHorizontally(snap()) { 200 } + fadeOut(tween(200))
-                    },
-                    contentKey = { it > 3 }) { target ->
-                    Box(
-                        Modifier
-                            .requiredSize(200.dp)
-                            .onGloballyPositioned {
-                                if (target == targetState) {
-                                    actualIncomingPosition = it.localToRoot(Offset.Zero)
-                                } else {
-                                    actualOutgoingPosition = it.localToRoot(Offset.Zero)
-                                }
-                            })
-                }
-            }
-        }
-        rule.waitForIdle()
-        rule.runOnIdle {
-            repeat(3) {
-                // Check that no animation happens until the content key changes
-                assertEquals(targetPosition, actualIncomingPosition)
-                assertNotNull(actualIncomingPosition)
-                assertNull(actualOutgoingPosition)
-                targetState++
-            }
-        }
-
-        rule.runOnIdle {
-            // Check that animation happened because targetState going from 3 to 4 caused the
-            // resulting key to change
-            assertEquals(targetPosition, actualIncomingPosition)
-            assertNotNull(actualIncomingPosition)
-            assertEquals(
-                targetPosition!!.copy(x = targetPosition!!.x + 200),
-                actualOutgoingPosition
-            )
-        }
-    }
-
-    @Test
-    fun LookaheadWithMinMaxIntrinsics() {
-        rule.setContent {
-            LookaheadScope {
-                Scaffold(
-                    Modifier
-                        .fillMaxSize()
-                        .testTag(""),
-                    topBar = {},
-                    floatingActionButton = {}
-                ) {
-                    Surface() {
-                        SubcomposeLayout(Modifier.fillMaxWidth()) { constraints ->
-                            val tabRowWidth = constraints.maxWidth
-                            val tabMeasurables = subcompose("Tabs") {
-                                repeat(15) {
-                                    Text(it.toString(), Modifier.width(100.dp))
-                                }
-                            }
-                            val tabCount = tabMeasurables.size
-                            var tabWidth = 0
-                            if (tabCount > 0) {
-                                tabWidth = (tabRowWidth / tabCount)
-                            }
-                            val tabRowHeight = tabMeasurables.fold(initial = 0) { max, curr ->
-                                maxOf(curr.maxIntrinsicHeight(tabWidth), max)
-                            }
-
-                            val tabPlaceables = tabMeasurables.map {
-                                it.measure(
-                                    constraints.copy(
-                                        minWidth = tabWidth,
-                                        maxWidth = tabWidth,
-                                        minHeight = tabRowHeight,
-                                        maxHeight = tabRowHeight,
-                                    )
-                                )
-                            }
-
-                            repeat(tabCount) { index ->
-                                var contentWidth =
-                                    minOf(
-                                        tabMeasurables[index].maxIntrinsicWidth(tabRowHeight),
-                                        tabWidth
-                                    ).toDp()
-                                contentWidth -= 32.dp
-                            }
-
-                            layout(tabRowWidth, tabRowHeight) {
-                                tabPlaceables.forEachIndexed { index, placeable ->
-                                    placeable.placeRelative(index * tabWidth, 0)
-                                }
-                            }
-                        }
-                    }
-                }
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .background(Color.Blue)
-                ) {
-                    Text(text = "test")
-                }
-            }
-        }
-        rule.waitForIdle()
-    }
-
-    // This test uses a Scaffold around a TabRow setup to reproduce a scenario where tabs' lookahead
-    // measurements will be invalidated right before placement, to ensure the correctness of the
-    // impl that lookahead remeasures children right before layout.
-    @Test
-    fun AnimatedContentWithSubcomposition() {
-        var target by mutableStateOf(true)
-        rule.setContent {
-            AnimatedContent(target) {
-                if (it) {
-                    Scaffold(
-                        Modifier
-                            .fillMaxSize()
-                            .testTag(""),
-                        topBar = {},
-                        floatingActionButton = {}
-                    ) {
-                        TabRow(selectedTabIndex = 0) {
-                            repeat(15) {
-                                Text(it.toString(), Modifier.width(100.dp))
-                            }
-                        }
-                    }
-                    Box(
-                        Modifier
-                            .fillMaxSize()
-                            .background(Color.Blue)
-                    ) {
-                        Text(text = "test")
-                    }
-                } else {
-                    Box(Modifier.size(200.dp))
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            target = !target
-        }
-        rule.waitForIdle()
-        rule.runOnIdle {
-            target = !target
-        }
-        rule.waitForIdle()
-    }
-
-    @Test
-    fun AnimatedContentWithKeysTest() {
-        var targetState by mutableStateOf(1)
-        val list = mutableListOf<Int>()
-        rule.setContent {
-            val transition = updateTransition(targetState)
-            val holder = rememberSaveableStateHolder()
-            transition.AnimatedContent(contentKey = { it > 2 }) {
-                if (it <= 2) {
-                    holder.SaveableStateProvider(11) {
-                        var count by rememberSaveable { mutableStateOf(0) }
-                        LaunchedEffect(Unit) {
-                            list.add(++count)
-                        }
-                    }
-                }
-                Box(Modifier.requiredSize(200.dp))
-            }
-            LaunchedEffect(Unit) {
-                assertFalse(transition.isRunning)
-                targetState = 2
-                withFrameMillis {
-                    assertFalse(transition.isRunning)
-                    assertEquals(1, transition.currentState)
-                    assertEquals(1, transition.targetState)
-
-                    // This state change should now cause an animation
-                    targetState = 3
-                }
-                withFrameMillis {
-                    assertTrue(transition.isRunning)
-                }
-            }
-        }
-        rule.waitForIdle()
-        rule.runOnIdle {
-            assertEquals(1, list.size)
-            assertEquals(1, list[0])
-            targetState = 1
-        }
-
-        rule.runOnIdle {
-            // Check that save worked
-            assertEquals(2, list.size)
-            assertEquals(1, list[0])
-            assertEquals(2, list[1])
-        }
-    }
-
-    @OptIn(ExperimentalAnimationApi::class)
-    @Test
-    fun AnimatedContentWithInterruption() {
-        var flag by mutableStateOf(true)
-        var rootCoords: LayoutCoordinates? = null
-        rule.setContent {
-            AnimatedContent(targetState = flag,
-                modifier = Modifier.onGloballyPositioned { rootCoords = it },
-                transitionSpec = {
-                    if (targetState) {
-                        fadeIn(tween(2000)) togetherWith slideOut(
-                            tween(2000)
-                        ) { fullSize ->
-                            IntOffset(0, fullSize.height / 2)
-                        } + fadeOut(
-                            tween(2000)
-                        )
-                    } else {
-                        fadeIn(tween(2000)) togetherWith fadeOut(tween(2000))
-                    }
-                }) { state ->
-                if (state) {
-                    Box(modifier = Modifier
-                        .onGloballyPositioned {
-                            assertEquals(
-                                Offset.Zero,
-                                rootCoords!!.localPositionOf(it, Offset.Zero)
-                            )
-                        }
-                        .fillMaxSize()
-                        .background(Color.Green)
-                    )
-                } else {
-                    LaunchedEffect(key1 = Unit) {
-                        delay(200)
-                        assertFalse(flag)
-                        assertTrue(transition.isRunning)
-                        // Interrupt
-                        flag = true
-                    }
-                    Box(modifier = Modifier
-                        .onGloballyPositioned {
-                            assertEquals(
-                                Offset.Zero,
-                                rootCoords!!.localPositionOf(it, Offset.Zero)
-                            )
-                        }
-                        .fillMaxSize()
-                        .background(Color.Red)
-                    )
-                }
-            }
-        }
-        rule.runOnIdle {
-            flag = false
-        }
-    }
-
-    @OptIn(ExperimentalAnimationApi::class)
-    @Test
-    fun testExitHold() {
-        var target by mutableStateOf(true)
-        var box1Disposed = false
-        var box2EnterFinished = false
-        rule.setContent {
-            AnimatedContent(
-                targetState = target,
-                transitionSpec = {
-                    fadeIn(tween(200)) togetherWith
-                        fadeOut(tween(5)) + ExitTransition.Hold
-                }
-            ) {
-                if (it) {
-                    Box(Modifier.size(200.dp)) {
-                        DisposableEffect(key1 = Unit) {
-                            onDispose {
-                                box1Disposed = true
-                            }
-                        }
-                    }
-                } else {
-                    Box(Modifier.size(200.dp)) {
-                        box2EnterFinished =
-                            transition.targetState == transition.currentState &&
-                                transition.targetState == EnterExitState.Visible
-                    }
-                }
-            }
-        }
-
-        rule.waitForIdle()
-        rule.mainClock.autoAdvance = false
-        rule.runOnIdle {
-            target = !target
-        }
-
-        rule.waitForIdle()
-        repeat(10) {
-            rule.mainClock.advanceTimeByFrame()
-            assertFalse(box1Disposed)
-            assertFalse(box2EnterFinished)
-        }
-
-        repeat(10) {
-            rule.mainClock.advanceTimeByFrame()
-            rule.waitForIdle()
-            assertEquals(box1Disposed, box2EnterFinished)
-        }
-
-        assertTrue(box1Disposed)
-        assertTrue(box2EnterFinished)
-    }
-
-    @Test
-    fun testScaleToFitDefault() {
-        var target by mutableStateOf(1)
-        var box1Coords: LayoutCoordinates? = null
-        var box2Coords: LayoutCoordinates? = null
-        var box1Disposed = true
-        var box2Disposed = true
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides Density(1f)) {
-                AnimatedContent(
-                    targetState = target,
-                    transitionSpec = {
-                        if (1 isTransitioningTo 2) {
-                            fadeIn(tween(300)) + scaleInToFitContainer() togetherWith
-                                scaleOutToFitContainer()
-                        } else {
-                            fadeIn() + scaleInToFitContainer() togetherWith
-                                fadeOut(tween(150))
-                        } using SizeTransform { initialSize, targetSize ->
-                            keyframes {
-                                durationMillis = 300
-                                initialSize at 100 with LinearEasing
-                                targetSize at 200 with LinearEasing
-                            }
-                        }
-                    }) {
-                    if (it == 1) {
-                        Box(
-                            Modifier
-                                .onPlaced {
-                                    box1Coords = it
-                                }
-                                .size(200.dp, 400.dp)) {
-                            DisposableEffect(key1 = Unit) {
-                                box1Disposed = false
-                                onDispose {
-                                    box1Disposed = true
-                                }
-                            }
-                        }
-                    } else {
-                        Box(
-                            Modifier
-                                .onPlaced { box2Coords = it }
-                                .size(100.dp, 50.dp)) {
-
-                            DisposableEffect(key1 = Unit) {
-                                box2Disposed = false
-                                onDispose {
-                                    box2Disposed = true
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        rule.waitForIdle()
-        rule.mainClock.autoAdvance = false
-        assertEquals(IntSize(200, 400), box1Coords?.size)
-        assertNull(box2Coords)
-
-        assertFalse(box1Disposed)
-        assertTrue(box2Disposed)
-
-        rule.runOnIdle {
-            // Start transition from 1 -> 2, size 200,400 -> 100,50
-            target = 2
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.waitForIdle()
-
-        // Box1 doesn't have any other ExitTransition than scale, so it'll be disposed
-        // after a couple of frames
-        assertFalse(box1Disposed)
-        assertFalse(box2Disposed)
-
-        repeat(20) {
-            rule.mainClock.advanceTimeByFrame()
-
-            val playTime = 16 * it
-            val bounds2 = box2Coords?.boundsInRoot()
-            if (playTime <= 100) {
-                assertEquals(Rect(0f, 0f, 200f, 100f), bounds2)
-            } else if (playTime <= 200) {
-                val fraction = (playTime - 100) / 100f
-                val width = 200 * (1 - fraction) + 100 * fraction
-                // Since we are testing default behavior, the scaling is based on width.
-                val height = width / 100f * 50
-                assertEquals(Offset.Zero, bounds2?.topLeft)
-                assertEquals(width, bounds2?.width)
-                assertEquals(height, bounds2?.height)
-            } else {
-                assertEquals(Rect(0f, 0f, 100f, 50f), bounds2)
-            }
-        }
-
-        rule.runOnIdle {
-            // Start transition from false -> true, size 100, 50 -> 200,400
-            target = 1
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.waitForIdle()
-
-        assertFalse(box1Disposed)
-        assertFalse(box2Disposed)
-
-        repeat(20) {
-            rule.mainClock.advanceTimeByFrame()
-            val playTime = 16 * it
-            val bounds = box1Coords?.boundsInRoot()
-            if (playTime <= 100) {
-                assertEquals(100f, bounds?.width)
-                assertFalse(box2Disposed)
-            } else if (playTime <= 150) {
-                val fraction = (playTime - 100) / 100f
-                val width = 100 * (1 - fraction) + 200 * fraction
-                // Since we are testing default behavior, the scaling is based on width.
-                assertEquals(Offset.Zero, bounds?.topLeft)
-                assertEquals(width, bounds?.width)
-            } else {
-                rule.waitForIdle()
-                assertThat(box2Disposed)
-            }
-        }
-    }
-
-    @Test
-    fun testScaleToFitCenterAlignment() {
-        var target by mutableStateOf(true)
-        var box1Coords: LayoutCoordinates? = null
-        var box2Coords: LayoutCoordinates? = null
-        var layoutDirection: LayoutDirection? = null
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides Density(1f)) {
-                layoutDirection = LocalLayoutDirection.current
-                AnimatedContent(
-                    targetState = target,
-                    transitionSpec = {
-                        fadeIn() + scaleInToFitContainer(Alignment.Center) togetherWith
-                            fadeOut(tween(100)) using
-                            SizeTransform { _, _ ->
-                                tween(100, easing = LinearEasing)
-                            }
-                    }) {
-                    if (target) {
-                        Box(
-                            Modifier
-                                .onPlaced {
-                                    box1Coords = it
-                                }
-                                .size(200.dp, 400.dp))
-                    } else {
-                        Box(
-                            Modifier
-                                .onPlaced { box2Coords = it }
-                                .size(100.dp, 50.dp))
-                    }
-                }
-            }
-        }
-
-        rule.waitForIdle()
-        assertEquals(IntSize(200, 400), box1Coords?.size)
-        assertNull(box2Coords)
-
-        rule.runOnIdle {
-            // Start transition from true -> false, size 200,400 -> 100,50
-            target = false
-        }
-        rule.mainClock.advanceTimeByFrame()
-        repeat(10) {
-            rule.mainClock.advanceTimeByFrame()
-            val playTime = 16 * it
-            val bounds = box2Coords?.boundsInRoot()
-            assertNotNull(bounds)
-            val fraction = (playTime / 100f).coerceAtMost(1f)
-            val width = 200 * (1 - fraction) + 100 * fraction
-            val containerHeight = 400 * (1 - fraction) + 50 * fraction
-            // Since we are testing default behavior, the scaling is based on width.
-            val height = width / 100f * 50
-            assertEquals(width, bounds!!.width, 0.01f)
-            assertEquals(height, bounds.height, 0.01f)
-            val offset = Alignment.Center.align(
-                IntSize(width.roundToInt(), height.roundToInt()),
-                IntSize(width.roundToInt(), containerHeight.roundToInt()), layoutDirection!!
-            )
-            assertEquals(offset, bounds.topLeft.round())
-        }
-    }
-
-    @Test
-    fun testScaleToFitBottomCenterAlignment() {
-        var target by mutableStateOf(true)
-        var box1Coords: LayoutCoordinates? = null
-        var box2Coords: LayoutCoordinates? = null
-        var layoutDirection: LayoutDirection? = null
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides Density(1f)) {
-                layoutDirection = LocalLayoutDirection.current
-                AnimatedContent(
-                    targetState = target,
-                    transitionSpec = {
-                        fadeIn() + scaleInToFitContainer(
-                            Alignment.BottomCenter
-                        ) togetherWith
-                            fadeOut(tween(100)) using
-                            SizeTransform { _, _ ->
-                                tween(100, easing = LinearEasing)
-                            }
-                    }) {
-                    if (target) {
-                        Box(
-                            Modifier
-                                .onPlaced {
-                                    box1Coords = it
-                                }
-                                .size(200.dp, 400.dp))
-                    } else {
-                        Box(
-                            Modifier
-                                .onPlaced { box2Coords = it }
-                                .size(100.dp, 50.dp))
-                    }
-                }
-            }
-        }
-
-        rule.waitForIdle()
-        rule.mainClock.autoAdvance = false
-        assertEquals(IntSize(200, 400), box1Coords?.size)
-        assertNull(box2Coords)
-
-        rule.runOnIdle {
-            // Start transition from true -> false, size 200,400 -> 100,50
-            target = false
-        }
-        rule.mainClock.advanceTimeByFrame()
-        repeat(10) {
-            rule.mainClock.advanceTimeByFrame()
-            val playTime = 16 * it
-            val bounds = box2Coords?.boundsInRoot()
-            assertNotNull(bounds)
-            val fraction = (playTime / 100f).coerceAtMost(1f)
-            val width = 200 * (1 - fraction) + 100 * fraction
-            val containerHeight = 400 * (1 - fraction) + 50 * fraction
-            // Since we are testing default behavior, the scaling is based on width.
-            val height = width / 100f * 50
-            assertEquals(width, bounds!!.width, 0.01f)
-            assertEquals(height, bounds.height, 0.01f)
-            val offset = Alignment.BottomCenter.align(
-                IntSize(width.roundToInt(), height.roundToInt()),
-                IntSize(width.roundToInt(), containerHeight.roundToInt()), layoutDirection!!
-            )
-            assertEquals(offset, bounds.topLeft.round())
-        }
-    }
-
-    @Test
-    fun testScaleToFitInsideBottomEndAlignment() {
-        var target by mutableStateOf(true)
-        var box1Coords: LayoutCoordinates? = null
-        var box2Coords: LayoutCoordinates? = null
-        var layoutDirection: LayoutDirection? = null
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides Density(1f)) {
-                layoutDirection = LocalLayoutDirection.current
-                AnimatedContent(
-                    targetState = target,
-                    transitionSpec = {
-                        fadeIn() + scaleInToFitContainer(
-                            Alignment.BottomEnd, ContentScale.Inside
-                        ) togetherWith
-                            fadeOut(tween(100)) using
-                            SizeTransform { _, _ ->
-                                tween(100, easing = LinearEasing)
-                            }
-                    }) {
-                    if (target) {
-                        Box(
-                            Modifier
-                                .onPlaced {
-                                    box1Coords = it
-                                }
-                                .size(200.dp, 400.dp))
-                    } else {
-                        Box(
-                            Modifier
-                                .onPlaced { box2Coords = it }
-                                .size(100.dp, 50.dp))
-                    }
-                }
-            }
-        }
-
-        rule.waitForIdle()
-        rule.mainClock.autoAdvance = false
-        assertEquals(IntSize(200, 400), box1Coords?.size)
-        assertNull(box2Coords)
-
-        rule.runOnIdle {
-            // Start transition from true -> false, size 200,400 -> 100,50
-            target = false
-        }
-        rule.mainClock.advanceTimeByFrame()
-        repeat(10) {
-            rule.mainClock.advanceTimeByFrame()
-            val playTime = 16 * it
-            val bounds = box2Coords?.boundsInRoot()
-            assertNotNull(bounds)
-            val fraction = (playTime / 100f).coerceAtMost(1f)
-            val width = 100f
-            val containerWidth = 200 * (1 - fraction) + 100 * fraction
-            val containerHeight = 400 * (1 - fraction) + 50 * fraction
-            // Since we are testing default behavior, the scaling is based on width.
-            val height = 50f
-            assertEquals(width, bounds!!.width, 0.01f)
-            assertEquals(height, bounds.height, 0.01f)
-            val offset = Alignment.BottomEnd.align(
-                IntSize(width.roundToInt(), height.roundToInt()),
-                IntSize(containerWidth.roundToInt(), containerHeight.roundToInt()),
-                layoutDirection!!
-            )
-            assertEquals(offset, bounds.topLeft.round())
-        }
-    }
-
-    @Test
-    fun testScaleToFitWithFitHeight() {
-        var target by mutableStateOf(true)
-        var box1Coords: LayoutCoordinates? = null
-        var box2Coords: LayoutCoordinates? = null
-        var layoutDirection: LayoutDirection? = null
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides Density(1f)) {
-                layoutDirection = LocalLayoutDirection.current
-                AnimatedContent(
-                    targetState = target,
-                    transitionSpec = {
-                        fadeIn() + scaleInToFitContainer(
-                            Alignment.Center, ContentScale.FillHeight
-                        ) togetherWith fadeOut(tween(100)) using
-                            SizeTransform { _, _ ->
-                                tween(100, easing = LinearEasing)
-                            }
-                    }) {
-                    if (target) {
-                        Box(
-                            Modifier
-                                .onPlaced {
-                                    box1Coords = it
-                                }
-                                .size(200.dp, 400.dp))
-                    } else {
-                        Box(
-                            Modifier
-                                .onPlaced { box2Coords = it }
-                                .size(100.dp, 250.dp))
-                    }
-                }
-            }
-        }
-
-        rule.waitForIdle()
-        rule.mainClock.autoAdvance = false
-        assertEquals(IntSize(200, 400), box1Coords?.size)
-        assertNull(box2Coords)
-
-        rule.runOnIdle {
-            // Start transition from true -> false, size 200,400 -> 100,250
-            target = false
-        }
-        rule.mainClock.advanceTimeByFrame()
-        repeat(10) {
-            rule.mainClock.advanceTimeByFrame()
-            val playTime = 16 * it
-            val bounds = box2Coords?.boundsInRoot()
-            assertNotNull(bounds)
-            val fraction = (playTime / 100f).coerceAtMost(1f)
-            val height = 400 * (1 - fraction) + 250 * fraction
-            val containerWidth = 200 * (1 - fraction) + 100 * fraction
-            // Since we are testing default behavior, the scaling is based on width.
-            val width = height / 250f * 100
-            assertEquals(width, bounds!!.width, 0.01f)
-            assertEquals(height, bounds.height, 0.01f)
-            val offset = Alignment.Center.align(
-                IntSize(width.roundToInt(), height.roundToInt()),
-                IntSize(containerWidth.roundToInt(), height.roundToInt()), layoutDirection!!
-            )
-            assertEquals(offset, bounds.topLeft.round())
-        }
-    }
-
-    @OptIn(ExperimentalAnimationApi::class)
-    @Test
-    fun testExitHoldDefersUntilAllFinished() {
-        var target by mutableStateOf(true)
-        var box1Disposed = false
-        var box2EnterFinished = false
-        var transitionFinished = false
-        rule.setContent {
-            val outerTransition = updateTransition(targetState = target)
-            transitionFinished = !outerTransition.targetState && !outerTransition.currentState
-            outerTransition.AnimatedContent(
-                transitionSpec = {
-                    fadeIn(tween(160)) togetherWith
-                        fadeOut(tween(5)) + ExitTransition.Hold using
-                        SizeTransform { _, _ ->
-                            tween(300)
-                        }
-                }
-            ) {
-                if (it) {
-                    Box(Modifier.size(200.dp)) {
-                        DisposableEffect(key1 = Unit) {
-                            onDispose {
-                                box1Disposed = true
-                            }
-                        }
-                    }
-                } else {
-                    Box(Modifier.size(400.dp)) {
-                        box2EnterFinished =
-                            transition.targetState == transition.currentState &&
-                                transition.targetState == EnterExitState.Visible
-                    }
-                }
-            }
-        }
-
-        rule.waitForIdle()
-        rule.mainClock.autoAdvance = false
-        rule.runOnIdle {
-            target = !target
-        }
-
-        rule.waitForIdle()
-        rule.mainClock.advanceTimeByFrame()
-        repeat(10) {
-            rule.mainClock.advanceTimeByFrame()
-            assertFalse(box1Disposed)
-            assertFalse(box2EnterFinished)
-        }
-
-        repeat(3) {
-            rule.mainClock.advanceTimeByFrame()
-            rule.waitForIdle()
-            assertTrue(box2EnterFinished)
-            // Enter finished, but box1 is only disposed when the transition is completely finished,
-            // which includes enter, exit & size change.
-            assertFalse(box1Disposed)
-            assertFalse(transitionFinished)
-        }
-
-        repeat(10) {
-            rule.mainClock.advanceTimeByFrame()
-            rule.waitForIdle()
-            assertTrue(box2EnterFinished)
-            // Enter finished, but box1 is only disposed when the transition is completely finished,
-            // which includes enter, exit & size change.
-            assertEquals(box1Disposed, transitionFinished)
-        }
-
-        assertTrue(box1Disposed)
-        assertTrue(box2EnterFinished)
-    }
-
-    /**
-     * This test checks that scaleInToFitContainer and scaleOutToFitContainer handle empty
-     * content correctly.
-     */
-    @Test
-    fun testAnimateToEmptyComposable() {
-        var isEmpty by mutableStateOf(false)
-        var targetSize: IntSize? = null
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides Density(1f)) {
-                AnimatedContent(targetState = isEmpty,
-                    transitionSpec = {
-                        scaleInToFitContainer() togetherWith scaleOutToFitContainer()
-                    },
-                    modifier = Modifier.layout { measurable, constraints ->
-                        measurable.measure(constraints).run {
-                            if (isLookingAhead) {
-                                targetSize = IntSize(width, height)
-                            }
-                            layout(width, height) {
-                                place(0, 0)
-                            }
-                        }
-                    }
-                ) {
-                    if (!it) {
-                        Box(Modifier.size(200.dp))
-                    }
-                }
-            }
-        }
-        rule.runOnIdle {
-            assertEquals(IntSize(200, 200), targetSize)
-            isEmpty = true
-        }
-
-        rule.runOnIdle {
-            assertEquals(IntSize.Zero, targetSize)
-            isEmpty = !isEmpty
-        }
-        rule.runOnIdle {
-            assertEquals(IntSize(200, 200), targetSize)
-        }
-    }
-
-    @OptIn(InternalAnimationApi::class)
-    private val Transition<*>.playTimeMillis get() = (playTimeNanos / 1_000_000L).toInt()
-}
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
new file mode 100644
index 0000000..67921dc
--- /dev/null
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
@@ -0,0 +1,1377 @@
+/*
+ * Copyright 2021 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(ExperimentalAnimationApi::class)
+
+package androidx.compose.animation
+
+import androidx.compose.animation.core.InternalAnimationApi
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.TabRow
+import androidx.compose.material3.Text
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameMillis
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
+import kotlinx.coroutines.delay
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class AnimatedContentTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @OptIn(InternalAnimationApi::class)
+    @Test
+    fun AnimatedContentSizeTransformTest() {
+        val size1 = 40
+        val size2 = 200
+        val testModifier by mutableStateOf(TestModifier())
+        val transitionState = MutableTransitionState(true)
+        var playTimeMillis by mutableStateOf(0)
+        rule.mainClock.autoAdvance = false
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                val transition = updateTransition(transitionState)
+                playTimeMillis = (transition.playTimeNanos / 1_000_000L).toInt()
+                transition.AnimatedContent(
+                    testModifier,
+                    transitionSpec = {
+                        if (true isTransitioningTo false) {
+                            fadeIn() togetherWith fadeOut() using
+                                SizeTransform { initialSize, targetSize ->
+                                    keyframes {
+                                        durationMillis = 320
+                                        IntSize(targetSize.width, initialSize.height) at 160 with
+                                            LinearEasing
+                                        targetSize at 320 with LinearEasing
+                                    }
+                                }
+                        } else {
+                            fadeIn() togetherWith fadeOut() using SizeTransform { _, _ ->
+                                tween(durationMillis = 80, easing = LinearEasing)
+                            }
+                        }
+                    }
+                ) {
+                    if (it) {
+                        Box(modifier = Modifier.size(size = size1.dp))
+                    } else {
+                        Box(modifier = Modifier.size(size = size2.dp))
+                    }
+                }
+            }
+        }
+        rule.runOnIdle {
+            assertEquals(40, testModifier.height)
+            assertEquals(40, testModifier.width)
+            assertTrue(transitionState.targetState)
+            transitionState.targetState = false
+        }
+
+        // Transition from item1 to item2 in 320ms, animating to full width in the first 160ms
+        // then full height in the next 160ms
+        while (transitionState.currentState != transitionState.targetState) {
+            rule.runOnIdle {
+                if (playTimeMillis <= 160) {
+                    assertEquals(playTimeMillis + 40, testModifier.width)
+                    assertEquals(40, testModifier.height)
+                } else {
+                    assertEquals(200, testModifier.width)
+                    assertEquals(playTimeMillis - 120, testModifier.height)
+                }
+            }
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        rule.runOnIdle {
+            assertEquals(200, testModifier.width)
+            assertEquals(200, testModifier.height)
+            transitionState.targetState = true
+        }
+
+        // Transition from item2 to item1 in 80ms
+        while (transitionState.currentState != transitionState.targetState) {
+            rule.runOnIdle {
+                if (playTimeMillis <= 80) {
+                    assertEquals(200 - playTimeMillis * 2, testModifier.width)
+                    assertEquals(200 - playTimeMillis * 2, testModifier.height)
+                }
+            }
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @OptIn(InternalAnimationApi::class)
+    @Test
+    fun AnimatedContentSizeTransformEmptyComposableTest() {
+        val size1 = 160
+        val testModifier by mutableStateOf(TestModifier())
+        val transitionState = MutableTransitionState(true)
+        var playTimeMillis by mutableStateOf(0)
+        rule.mainClock.autoAdvance = false
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                val transition = updateTransition(transitionState)
+                playTimeMillis = (transition.playTimeNanos / 1_000_000L).toInt()
+                transition.AnimatedContent(
+                    testModifier,
+                    transitionSpec = {
+                        EnterTransition.None togetherWith ExitTransition.None using
+                            SizeTransform { _, _ ->
+                                tween(durationMillis = 160, easing = LinearEasing)
+                            }
+                    }
+                ) {
+                    if (it) {
+                        Box(modifier = Modifier.size(size = size1.dp))
+                    }
+                    // Empty composable for it == false
+                }
+            }
+        }
+        rule.runOnIdle {
+            assertEquals(160, testModifier.height)
+            assertEquals(160, testModifier.width)
+            assertTrue(transitionState.targetState)
+            transitionState.targetState = false
+        }
+
+        // Transition from item1 to item2 in 320ms, animating to full width in the first 160ms
+        // then full height in the next 160ms
+        while (transitionState.currentState != transitionState.targetState) {
+            rule.runOnIdle {
+                assertEquals(160 - playTimeMillis, testModifier.width)
+                assertEquals(160 - playTimeMillis, testModifier.height)
+            }
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Now there's only an empty composable
+        rule.runOnIdle {
+            assertEquals(0, testModifier.width)
+            assertEquals(0, testModifier.height)
+            transitionState.targetState = true
+        }
+
+        // Transition from item2 to item1 in 80ms
+        while (transitionState.currentState != transitionState.targetState) {
+            rule.runOnIdle {
+                assertEquals(playTimeMillis, testModifier.width)
+                assertEquals(playTimeMillis, testModifier.height)
+            }
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @OptIn(InternalAnimationApi::class)
+    @Test
+    fun AnimatedContentContentAlignmentTest() {
+        val size1 = IntSize(80, 80)
+        val size2 = IntSize(160, 240)
+        val testModifier by mutableStateOf(TestModifier())
+        var offset1 by mutableStateOf(Offset.Zero)
+        var offset2 by mutableStateOf(Offset.Zero)
+        var playTimeMillis by mutableStateOf(0)
+        val transitionState = MutableTransitionState(true)
+        val alignment = listOf(
+            Alignment.TopStart, Alignment.BottomStart, Alignment.Center,
+            Alignment.BottomEnd, Alignment.TopEnd
+        )
+        var contentAlignment by mutableStateOf(Alignment.TopStart)
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                val transition = updateTransition(transitionState)
+                playTimeMillis = (transition.playTimeNanos / 1_000_000L).toInt()
+                transition.AnimatedContent(
+                    testModifier,
+                    contentAlignment = contentAlignment,
+                    transitionSpec = {
+                        fadeIn(animationSpec = tween(durationMillis = 80)) togetherWith fadeOut(
+                            animationSpec = tween(durationMillis = 80)
+                        ) using SizeTransform { _, _ ->
+                            tween(durationMillis = 80, easing = LinearEasing)
+                        }
+                    }
+                ) {
+                    if (it) {
+                        Box(
+                            modifier = Modifier
+                                .onGloballyPositioned {
+                                    offset1 = it.positionInRoot()
+                                }
+                                .size(size1.width.dp, size1.height.dp)
+                        )
+                    } else {
+                        Box(
+                            modifier = Modifier
+                                .onGloballyPositioned {
+                                    offset2 = it.positionInRoot()
+                                }
+                                .size(size2.width.dp, size2.height.dp)
+                        )
+                    }
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+
+        alignment.forEach {
+            rule.runOnIdle {
+                assertEquals(80, testModifier.height)
+                assertEquals(80, testModifier.width)
+                assertTrue(transitionState.targetState)
+                contentAlignment = it
+            }
+
+            rule.mainClock.advanceTimeByFrame()
+            rule.waitForIdle()
+            rule.runOnIdle { transitionState.targetState = false }
+
+            // Transition from item1 to item2 in 320ms, animating to full width in the first 160ms
+            // then full height in the next 160ms
+            while (transitionState.currentState != transitionState.targetState) {
+                rule.runOnIdle {
+                    val space = IntSize(testModifier.width, testModifier.height)
+                    val position1 = it.align(size1, space, LayoutDirection.Ltr)
+                    val position2 = it.align(size2, space, LayoutDirection.Ltr)
+                    if (playTimeMillis < 80) {
+                        // This gets removed when the animation is finished at 80ms
+                        assertEquals(
+                            position1,
+                            IntOffset(offset1.x.roundToInt(), offset1.y.roundToInt())
+                        )
+                    }
+                    if (playTimeMillis > 0) {
+                        assertEquals(
+                            position2,
+                            IntOffset(offset2.x.roundToInt(), offset2.y.roundToInt())
+                        )
+                    }
+                }
+                rule.mainClock.advanceTimeByFrame()
+            }
+
+            rule.runOnIdle {
+                assertEquals(size2.width, testModifier.width)
+                assertEquals(size2.height, testModifier.height)
+                // After the animation the size should be the same as parent, offset should be 0
+                assertEquals(offset2, Offset.Zero)
+                transitionState.targetState = true
+            }
+
+            // Transition from item2 to item1 in 80ms
+            while (transitionState.currentState != transitionState.targetState) {
+                rule.runOnIdle {
+                    val space = IntSize(testModifier.width, testModifier.height)
+                    val position1 = it.align(size1, space, LayoutDirection.Ltr)
+                    val position2 = it.align(size2, space, LayoutDirection.Ltr)
+                    if (playTimeMillis > 0) {
+                        assertEquals(
+                            position1,
+                            IntOffset(offset1.x.roundToInt(), offset1.y.roundToInt())
+                        )
+                    }
+                    if (playTimeMillis < 80) {
+                        assertEquals(
+                            position2,
+                            IntOffset(offset2.x.roundToInt(), offset2.y.roundToInt())
+                        )
+                    }
+                }
+                rule.mainClock.advanceTimeByFrame()
+            }
+
+            rule.runOnIdle {
+                assertEquals(size1.width, testModifier.width)
+                assertEquals(size1.height, testModifier.height)
+                // After the animation the size should be the same as parent, offset should be 0
+                assertEquals(offset1, Offset.Zero)
+            }
+        }
+    }
+
+    @OptIn(ExperimentalAnimationApi::class)
+    @Test
+    fun AnimatedContentSlideInAndOutOfContainerTest() {
+        val transitionState = MutableTransitionState(true)
+        // LinearEasing is required to ensure the animation doesn't reach final values before the
+        // duration.
+        val animSpec = tween<IntOffset>(200, easing = LinearEasing)
+        lateinit var trueTransition: Transition<EnterExitState>
+        lateinit var falseTransition: Transition<EnterExitState>
+        rule.mainClock.autoAdvance = false
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f, 1f)) {
+                @Suppress("UpdateTransitionLabel")
+                val rootTransition = updateTransition(transitionState)
+                rootTransition.AnimatedContent(
+                    transitionSpec = {
+                        if (true isTransitioningTo false) {
+                            slideIntoContainer(
+                                AnimatedContentTransitionScope.SlideDirection.Start, animSpec
+                            ) togetherWith
+                                slideOutOfContainer(
+                                    AnimatedContentTransitionScope.SlideDirection.Start, animSpec
+                                )
+                        } else {
+                            slideIntoContainer(
+                                AnimatedContentTransitionScope.SlideDirection.End, animSpec
+                            ) togetherWith
+                                slideOutOfContainer(
+                                    towards = AnimatedContentTransitionScope.SlideDirection.End,
+                                    animSpec
+                                )
+                        }
+                    }
+                ) { target ->
+                    if (target) {
+                        trueTransition = transition
+                    } else {
+                        falseTransition = transition
+                    }
+                    Box(
+                        Modifier
+                            .requiredSize(200.dp)
+                            .testTag(target.toString())
+                    )
+                }
+            }
+        }
+
+        // Kick off the first animation.
+        transitionState.targetState = false
+        // The initial composition creates the transition…
+        rule.mainClock.advanceTimeByFrame()
+        rule.onNodeWithTag("true").assertExists()
+        rule.onNodeWithTag("false").assertExists()
+        // …but the animation won't actually start until one frame later.
+        rule.mainClock.advanceTimeByFrame()
+        assertThat(trueTransition.animations).isNotEmpty()
+        assertThat(falseTransition.animations).isNotEmpty()
+
+        // Loop to ensure the content is offset correctly at each frame.
+        var trueAnim = trueTransition.animations[0]
+        var falseAnim = falseTransition.animations[0]
+        assertThat(transitionState.currentState).isTrue()
+        while (transitionState.currentState) {
+            // True is leaving: it should start at 0 and slide out to -200.
+            assertThat(trueAnim.value).isEqualTo(IntOffset(-trueTransition.playTimeMillis, 0))
+            // False is entering: it should start at 200 and slide in to 0.
+            assertThat(falseAnim.value)
+                .isEqualTo(IntOffset(200 - falseTransition.playTimeMillis, 0))
+            rule.mainClock.advanceTimeByFrame()
+        }
+        // The animation should remove the newly-hidden node from the composition.
+        rule.onNodeWithTag("true").assertDoesNotExist()
+
+        // Kick off the second transition.
+        transitionState.targetState = true
+        rule.mainClock.advanceTimeByFrame()
+        rule.onNodeWithTag("true").assertExists()
+        rule.onNodeWithTag("false").assertExists()
+        rule.mainClock.advanceTimeByFrame()
+        assertThat(trueTransition.animations).isNotEmpty()
+
+        trueAnim = trueTransition.animations[0]
+        falseAnim = falseTransition.animations[0]
+        assertThat(transitionState.currentState).isFalse()
+        while (!transitionState.currentState) {
+            // True is entering, it should start at -200 and slide in to 0.
+            assertThat(trueAnim.value).isEqualTo(IntOffset(trueTransition.playTimeMillis - 200, 0))
+            // False is leaving, it should start at 0 and slide out to 200.
+            assertThat(falseAnim.value).isEqualTo(IntOffset(falseTransition.playTimeMillis, 0))
+            rule.mainClock.advanceTimeByFrame()
+        }
+        rule.onNodeWithTag("false").assertDoesNotExist()
+    }
+
+    @Test
+    fun AnimatedContentWithContentKey() {
+        var targetState by mutableStateOf(1)
+        var actualIncomingPosition: Offset? = null
+        var actualOutgoingPosition: Offset? = null
+        var targetPosition: Offset? = null
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                AnimatedContent(targetState,
+                    Modifier.onGloballyPositioned {
+                        targetPosition = it.positionInRoot()
+                    },
+                    transitionSpec = {
+                        slideInHorizontally { -200 } togetherWith
+                            slideOutHorizontally(snap()) { 200 } + fadeOut(tween(200))
+                    },
+                    contentKey = { it > 3 }) { target ->
+                    Box(
+                        Modifier
+                            .requiredSize(200.dp)
+                            .onGloballyPositioned {
+                                if (target == targetState) {
+                                    actualIncomingPosition = it.localToRoot(Offset.Zero)
+                                } else {
+                                    actualOutgoingPosition = it.localToRoot(Offset.Zero)
+                                }
+                            })
+                }
+            }
+        }
+        rule.waitForIdle()
+        rule.runOnIdle {
+            repeat(3) {
+                // Check that no animation happens until the content key changes
+                assertEquals(targetPosition, actualIncomingPosition)
+                assertNotNull(actualIncomingPosition)
+                assertNull(actualOutgoingPosition)
+                targetState++
+            }
+        }
+
+        rule.runOnIdle {
+            // Check that animation happened because targetState going from 3 to 4 caused the
+            // resulting key to change
+            assertEquals(targetPosition, actualIncomingPosition)
+            assertNotNull(actualIncomingPosition)
+            assertEquals(
+                targetPosition!!.copy(x = targetPosition!!.x + 200),
+                actualOutgoingPosition
+            )
+        }
+    }
+
+    @Test
+    fun LookaheadWithMinMaxIntrinsics() {
+        rule.setContent {
+            LookaheadScope {
+                Scaffold(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(""),
+                    topBar = {},
+                    floatingActionButton = {}
+                ) {
+                    Surface() {
+                        SubcomposeLayout(Modifier.fillMaxWidth()) { constraints ->
+                            val tabRowWidth = constraints.maxWidth
+                            val tabMeasurables = subcompose("Tabs") {
+                                repeat(15) {
+                                    Text(it.toString(), Modifier.width(100.dp))
+                                }
+                            }
+                            val tabCount = tabMeasurables.size
+                            var tabWidth = 0
+                            if (tabCount > 0) {
+                                tabWidth = (tabRowWidth / tabCount)
+                            }
+                            val tabRowHeight = tabMeasurables.fold(initial = 0) { max, curr ->
+                                maxOf(curr.maxIntrinsicHeight(tabWidth), max)
+                            }
+
+                            val tabPlaceables = tabMeasurables.map {
+                                it.measure(
+                                    constraints.copy(
+                                        minWidth = tabWidth,
+                                        maxWidth = tabWidth,
+                                        minHeight = tabRowHeight,
+                                        maxHeight = tabRowHeight,
+                                    )
+                                )
+                            }
+
+                            repeat(tabCount) { index ->
+                                var contentWidth =
+                                    minOf(
+                                        tabMeasurables[index].maxIntrinsicWidth(tabRowHeight),
+                                        tabWidth
+                                    ).toDp()
+                                contentWidth -= 32.dp
+                            }
+
+                            layout(tabRowWidth, tabRowHeight) {
+                                tabPlaceables.forEachIndexed { index, placeable ->
+                                    placeable.placeRelative(index * tabWidth, 0)
+                                }
+                            }
+                        }
+                    }
+                }
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .background(Color.Blue)
+                ) {
+                    Text(text = "test")
+                }
+            }
+        }
+        rule.waitForIdle()
+    }
+
+    // This test uses a Scaffold around a TabRow setup to reproduce a scenario where tabs' lookahead
+    // measurements will be invalidated right before placement, to ensure the correctness of the
+    // impl that lookahead remeasures children right before layout.
+    @Test
+    fun AnimatedContentWithSubcomposition() {
+        var target by mutableStateOf(true)
+        rule.setContent {
+            AnimatedContent(target) {
+                if (it) {
+                    Scaffold(
+                        Modifier
+                            .fillMaxSize()
+                            .testTag(""),
+                        topBar = {},
+                        floatingActionButton = {}
+                    ) {
+                        TabRow(selectedTabIndex = 0) {
+                            repeat(15) {
+                                Text(it.toString(), Modifier.width(100.dp))
+                            }
+                        }
+                    }
+                    Box(
+                        Modifier
+                            .fillMaxSize()
+                            .background(Color.Blue)
+                    ) {
+                        Text(text = "test")
+                    }
+                } else {
+                    Box(Modifier.size(200.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            target = !target
+        }
+        rule.waitForIdle()
+        rule.runOnIdle {
+            target = !target
+        }
+        rule.waitForIdle()
+    }
+
+    @Test
+    fun AnimatedContentWithKeysTest() {
+        var targetState by mutableStateOf(1)
+        val list = mutableListOf<Int>()
+        rule.setContent {
+            val transition = updateTransition(targetState)
+            val holder = rememberSaveableStateHolder()
+            transition.AnimatedContent(contentKey = { it > 2 }) {
+                if (it <= 2) {
+                    holder.SaveableStateProvider(11) {
+                        var count by rememberSaveable { mutableStateOf(0) }
+                        LaunchedEffect(Unit) {
+                            list.add(++count)
+                        }
+                    }
+                }
+                Box(Modifier.requiredSize(200.dp))
+            }
+            LaunchedEffect(Unit) {
+                assertFalse(transition.isRunning)
+                targetState = 2
+                withFrameMillis {
+                    assertFalse(transition.isRunning)
+                    assertEquals(1, transition.currentState)
+                    assertEquals(1, transition.targetState)
+
+                    // This state change should now cause an animation
+                    targetState = 3
+                }
+                withFrameMillis {
+                    assertTrue(transition.isRunning)
+                }
+            }
+        }
+        rule.waitForIdle()
+        rule.runOnIdle {
+            assertEquals(1, list.size)
+            assertEquals(1, list[0])
+            targetState = 1
+        }
+
+        rule.runOnIdle {
+            // Check that save worked
+            assertEquals(2, list.size)
+            assertEquals(1, list[0])
+            assertEquals(2, list[1])
+        }
+    }
+
+    @OptIn(ExperimentalAnimationApi::class)
+    @Test
+    fun AnimatedContentWithInterruption() {
+        var flag by mutableStateOf(true)
+        var rootCoords: LayoutCoordinates? = null
+        rule.setContent {
+            AnimatedContent(targetState = flag,
+                modifier = Modifier.onGloballyPositioned { rootCoords = it },
+                transitionSpec = {
+                    if (targetState) {
+                        fadeIn(tween(2000)) togetherWith slideOut(
+                            tween(2000)
+                        ) { fullSize ->
+                            IntOffset(0, fullSize.height / 2)
+                        } + fadeOut(
+                            tween(2000)
+                        )
+                    } else {
+                        fadeIn(tween(2000)) togetherWith fadeOut(tween(2000))
+                    }
+                }) { state ->
+                if (state) {
+                    Box(modifier = Modifier
+                        .onGloballyPositioned {
+                            assertEquals(
+                                Offset.Zero,
+                                rootCoords!!.localPositionOf(it, Offset.Zero)
+                            )
+                        }
+                        .fillMaxSize()
+                        .background(Color.Green)
+                    )
+                } else {
+                    LaunchedEffect(key1 = Unit) {
+                        delay(200)
+                        assertFalse(flag)
+                        assertTrue(transition.isRunning)
+                        // Interrupt
+                        flag = true
+                    }
+                    Box(modifier = Modifier
+                        .onGloballyPositioned {
+                            assertEquals(
+                                Offset.Zero,
+                                rootCoords!!.localPositionOf(it, Offset.Zero)
+                            )
+                        }
+                        .fillMaxSize()
+                        .background(Color.Red)
+                    )
+                }
+            }
+        }
+        rule.runOnIdle {
+            flag = false
+        }
+    }
+
+    @OptIn(ExperimentalAnimationApi::class)
+    @Test
+    fun testExitHold() {
+        var target by mutableStateOf(true)
+        var box1Disposed = false
+        var box2EnterFinished = false
+        rule.setContent {
+            AnimatedContent(
+                targetState = target,
+                transitionSpec = {
+                    fadeIn(tween(200)) togetherWith
+                        fadeOut(tween(5)) + ExitTransition.Hold
+                }
+            ) {
+                if (it) {
+                    Box(Modifier.size(200.dp)) {
+                        DisposableEffect(key1 = Unit) {
+                            onDispose {
+                                box1Disposed = true
+                            }
+                        }
+                    }
+                } else {
+                    Box(Modifier.size(200.dp)) {
+                        box2EnterFinished =
+                            transition.targetState == transition.currentState &&
+                                transition.targetState == EnterExitState.Visible
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.mainClock.autoAdvance = false
+        rule.runOnIdle {
+            target = !target
+        }
+
+        rule.waitForIdle()
+        repeat(10) {
+            rule.mainClock.advanceTimeByFrame()
+            assertFalse(box1Disposed)
+            assertFalse(box2EnterFinished)
+        }
+
+        repeat(10) {
+            rule.mainClock.advanceTimeByFrame()
+            rule.waitForIdle()
+            assertEquals(box1Disposed, box2EnterFinished)
+        }
+
+        assertTrue(box1Disposed)
+        assertTrue(box2EnterFinished)
+    }
+
+    @Test
+    fun testScaleToFitDefault() {
+        var target by mutableStateOf(1)
+        var box1Coords: LayoutCoordinates? = null
+        var box2Coords: LayoutCoordinates? = null
+        var box1Disposed = true
+        var box2Disposed = true
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                AnimatedContent(
+                    targetState = target,
+                    transitionSpec = {
+                        if (1 isTransitioningTo 2) {
+                            fadeIn(tween(300)) + scaleInToFitContainer() togetherWith
+                                scaleOutToFitContainer()
+                        } else {
+                            fadeIn() + scaleInToFitContainer() togetherWith
+                                fadeOut(tween(150))
+                        } using SizeTransform { initialSize, targetSize ->
+                            keyframes {
+                                durationMillis = 300
+                                initialSize at 100 with LinearEasing
+                                targetSize at 200 with LinearEasing
+                            }
+                        }
+                    }) {
+                    if (it == 1) {
+                        Box(
+                            Modifier
+                                .onPlaced {
+                                    box1Coords = it
+                                }
+                                .size(200.dp, 400.dp)) {
+                            DisposableEffect(key1 = Unit) {
+                                box1Disposed = false
+                                onDispose {
+                                    box1Disposed = true
+                                }
+                            }
+                        }
+                    } else {
+                        Box(
+                            Modifier
+                                .onPlaced { box2Coords = it }
+                                .size(100.dp, 50.dp)) {
+
+                            DisposableEffect(key1 = Unit) {
+                                box2Disposed = false
+                                onDispose {
+                                    box2Disposed = true
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.mainClock.autoAdvance = false
+        assertEquals(IntSize(200, 400), box1Coords?.size)
+        assertNull(box2Coords)
+
+        assertFalse(box1Disposed)
+        assertTrue(box2Disposed)
+
+        rule.runOnIdle {
+            // Start transition from 1 -> 2, size 200,400 -> 100,50
+            target = 2
+        }
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Box1 doesn't have any other ExitTransition than scale, so it'll be disposed
+        // after a couple of frames
+        assertFalse(box1Disposed)
+        assertFalse(box2Disposed)
+
+        repeat(20) {
+            rule.mainClock.advanceTimeByFrame()
+
+            val playTime = 16 * it
+            val bounds2 = box2Coords?.boundsInRoot()
+            if (playTime <= 100) {
+                assertEquals(Rect(0f, 0f, 200f, 100f), bounds2)
+            } else if (playTime <= 200) {
+                val fraction = (playTime - 100) / 100f
+                val width = 200 * (1 - fraction) + 100 * fraction
+                // Since we are testing default behavior, the scaling is based on width.
+                val height = width / 100f * 50
+                assertEquals(Offset.Zero, bounds2?.topLeft)
+                assertEquals(width, bounds2?.width)
+                assertEquals(height, bounds2?.height)
+            } else {
+                assertEquals(Rect(0f, 0f, 100f, 50f), bounds2)
+            }
+        }
+
+        rule.runOnIdle {
+            // Start transition from false -> true, size 100, 50 -> 200,400
+            target = 1
+        }
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        assertFalse(box1Disposed)
+        assertFalse(box2Disposed)
+
+        repeat(20) {
+            rule.mainClock.advanceTimeByFrame()
+            val playTime = 16 * it
+            val bounds = box1Coords?.boundsInRoot()
+            if (playTime <= 100) {
+                assertEquals(100f, bounds?.width)
+                assertFalse(box2Disposed)
+            } else if (playTime <= 150) {
+                val fraction = (playTime - 100) / 100f
+                val width = 100 * (1 - fraction) + 200 * fraction
+                // Since we are testing default behavior, the scaling is based on width.
+                assertEquals(Offset.Zero, bounds?.topLeft)
+                assertEquals(width, bounds?.width)
+            } else {
+                rule.waitForIdle()
+                assertThat(box2Disposed)
+            }
+        }
+    }
+
+    @Test
+    fun testScaleToFitCenterAlignment() {
+        var target by mutableStateOf(true)
+        var box1Coords: LayoutCoordinates? = null
+        var box2Coords: LayoutCoordinates? = null
+        var layoutDirection: LayoutDirection? = null
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                layoutDirection = LocalLayoutDirection.current
+                AnimatedContent(
+                    targetState = target,
+                    transitionSpec = {
+                        fadeIn() + scaleInToFitContainer(Alignment.Center) togetherWith
+                            fadeOut(tween(100)) using
+                            SizeTransform { _, _ ->
+                                tween(100, easing = LinearEasing)
+                            }
+                    }) {
+                    if (target) {
+                        Box(
+                            Modifier
+                                .onPlaced {
+                                    box1Coords = it
+                                }
+                                .size(200.dp, 400.dp))
+                    } else {
+                        Box(
+                            Modifier
+                                .onPlaced { box2Coords = it }
+                                .size(100.dp, 50.dp))
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        assertEquals(IntSize(200, 400), box1Coords?.size)
+        assertNull(box2Coords)
+
+        rule.runOnIdle {
+            // Start transition from true -> false, size 200,400 -> 100,50
+            target = false
+        }
+        rule.mainClock.advanceTimeByFrame()
+        repeat(10) {
+            rule.mainClock.advanceTimeByFrame()
+            val playTime = 16 * it
+            val bounds = box2Coords?.boundsInRoot()
+            assertNotNull(bounds)
+            val fraction = (playTime / 100f).coerceAtMost(1f)
+            val width = 200 * (1 - fraction) + 100 * fraction
+            val containerHeight = 400 * (1 - fraction) + 50 * fraction
+            // Since we are testing default behavior, the scaling is based on width.
+            val height = width / 100f * 50
+            assertEquals(width, bounds!!.width, 0.01f)
+            assertEquals(height, bounds.height, 0.01f)
+            val offset = Alignment.Center.align(
+                IntSize(width.roundToInt(), height.roundToInt()),
+                IntSize(width.roundToInt(), containerHeight.roundToInt()), layoutDirection!!
+            )
+            assertEquals(offset, bounds.topLeft.round())
+        }
+    }
+
+    @Test
+    fun testScaleToFitBottomCenterAlignment() {
+        var target by mutableStateOf(true)
+        var box1Coords: LayoutCoordinates? = null
+        var box2Coords: LayoutCoordinates? = null
+        var layoutDirection: LayoutDirection? = null
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                layoutDirection = LocalLayoutDirection.current
+                AnimatedContent(
+                    targetState = target,
+                    transitionSpec = {
+                        fadeIn() + scaleInToFitContainer(
+                            Alignment.BottomCenter
+                        ) togetherWith
+                            fadeOut(tween(100)) using
+                            SizeTransform { _, _ ->
+                                tween(100, easing = LinearEasing)
+                            }
+                    }) {
+                    if (target) {
+                        Box(
+                            Modifier
+                                .onPlaced {
+                                    box1Coords = it
+                                }
+                                .size(200.dp, 400.dp))
+                    } else {
+                        Box(
+                            Modifier
+                                .onPlaced { box2Coords = it }
+                                .size(100.dp, 50.dp))
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.mainClock.autoAdvance = false
+        assertEquals(IntSize(200, 400), box1Coords?.size)
+        assertNull(box2Coords)
+
+        rule.runOnIdle {
+            // Start transition from true -> false, size 200,400 -> 100,50
+            target = false
+        }
+        rule.mainClock.advanceTimeByFrame()
+        repeat(10) {
+            rule.mainClock.advanceTimeByFrame()
+            val playTime = 16 * it
+            val bounds = box2Coords?.boundsInRoot()
+            assertNotNull(bounds)
+            val fraction = (playTime / 100f).coerceAtMost(1f)
+            val width = 200 * (1 - fraction) + 100 * fraction
+            val containerHeight = 400 * (1 - fraction) + 50 * fraction
+            // Since we are testing default behavior, the scaling is based on width.
+            val height = width / 100f * 50
+            assertEquals(width, bounds!!.width, 0.01f)
+            assertEquals(height, bounds.height, 0.01f)
+            val offset = Alignment.BottomCenter.align(
+                IntSize(width.roundToInt(), height.roundToInt()),
+                IntSize(width.roundToInt(), containerHeight.roundToInt()), layoutDirection!!
+            )
+            assertEquals(offset, bounds.topLeft.round())
+        }
+    }
+
+    @Test
+    fun testScaleToFitInsideBottomEndAlignment() {
+        var target by mutableStateOf(true)
+        var box1Coords: LayoutCoordinates? = null
+        var box2Coords: LayoutCoordinates? = null
+        var layoutDirection: LayoutDirection? = null
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                layoutDirection = LocalLayoutDirection.current
+                AnimatedContent(
+                    targetState = target,
+                    transitionSpec = {
+                        fadeIn() + scaleInToFitContainer(
+                            Alignment.BottomEnd, ContentScale.Inside
+                        ) togetherWith
+                            fadeOut(tween(100)) using
+                            SizeTransform { _, _ ->
+                                tween(100, easing = LinearEasing)
+                            }
+                    }) {
+                    if (target) {
+                        Box(
+                            Modifier
+                                .onPlaced {
+                                    box1Coords = it
+                                }
+                                .size(200.dp, 400.dp))
+                    } else {
+                        Box(
+                            Modifier
+                                .onPlaced { box2Coords = it }
+                                .size(100.dp, 50.dp))
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.mainClock.autoAdvance = false
+        assertEquals(IntSize(200, 400), box1Coords?.size)
+        assertNull(box2Coords)
+
+        rule.runOnIdle {
+            // Start transition from true -> false, size 200,400 -> 100,50
+            target = false
+        }
+        rule.mainClock.advanceTimeByFrame()
+        repeat(10) {
+            rule.mainClock.advanceTimeByFrame()
+            val playTime = 16 * it
+            val bounds = box2Coords?.boundsInRoot()
+            assertNotNull(bounds)
+            val fraction = (playTime / 100f).coerceAtMost(1f)
+            val width = 100f
+            val containerWidth = 200 * (1 - fraction) + 100 * fraction
+            val containerHeight = 400 * (1 - fraction) + 50 * fraction
+            // Since we are testing default behavior, the scaling is based on width.
+            val height = 50f
+            assertEquals(width, bounds!!.width, 0.01f)
+            assertEquals(height, bounds.height, 0.01f)
+            val offset = Alignment.BottomEnd.align(
+                IntSize(width.roundToInt(), height.roundToInt()),
+                IntSize(containerWidth.roundToInt(), containerHeight.roundToInt()),
+                layoutDirection!!
+            )
+            assertEquals(offset, bounds.topLeft.round())
+        }
+    }
+
+    @Test
+    fun testRightEnterExitTransitionIsChosenDuringInterruption() {
+        var flag by mutableStateOf(false)
+        var fixedPosition: Offset? = null
+        var slidePosition: Offset? = null
+        rule.setContent {
+            AnimatedContent(
+                targetState = flag,
+                label = "",
+                transitionSpec = {
+                    if (false isTransitioningTo true) {
+                        ContentTransform(
+                            targetContentEnter = EnterTransition.None,
+                            initialContentExit = slideOutOfContainer(
+                                AnimatedContentTransitionScope.SlideDirection.Start,
+                                animationSpec = tween(durationMillis = 500)
+                            ),
+                            targetContentZIndex = -1.0f,
+                            sizeTransform = SizeTransform(clip = false)
+                        )
+                    } else {
+                        ContentTransform(
+                            targetContentEnter = slideIntoContainer(
+                                AnimatedContentTransitionScope.SlideDirection.End
+                            ),
+                            initialContentExit = ExitTransition.Hold,
+                            targetContentZIndex = 0.0f,
+                            sizeTransform = SizeTransform(clip = false)
+                        )
+                    }
+                },
+                modifier = Modifier.fillMaxSize()
+            ) { flag ->
+                Spacer(
+                    modifier = Modifier
+                        .wrapContentSize(Alignment.Center)
+                        .size(256.dp)
+                        .onGloballyPositioned {
+                            if (flag) {
+                                fixedPosition = it.positionInRoot()
+                            } else {
+                                slidePosition = it.positionInRoot()
+                            }
+                        }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            flag = true
+        }
+        rule.waitUntil { fixedPosition != null }
+        val initialFixedPosition = fixedPosition
+        // Advance 10 frames
+        repeat(10) {
+            val lastSlidePos = slidePosition
+            rule.waitUntil { slidePosition != lastSlidePos }
+            assertEquals(initialFixedPosition, fixedPosition)
+        }
+
+        // Change the target state amid transition, creating an interruption
+        flag = false
+        // Advance 10 frames
+        repeat(10) {
+            val lastSlidePos = slidePosition
+            rule.waitUntil { slidePosition != lastSlidePos }
+            assertEquals(initialFixedPosition, fixedPosition)
+        }
+        rule.waitForIdle()
+    }
+
+    @Test
+    fun testScaleToFitWithFitHeight() {
+        var target by mutableStateOf(true)
+        var box1Coords: LayoutCoordinates? = null
+        var box2Coords: LayoutCoordinates? = null
+        var layoutDirection: LayoutDirection? = null
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                layoutDirection = LocalLayoutDirection.current
+                AnimatedContent(
+                    targetState = target,
+                    transitionSpec = {
+                        fadeIn() + scaleInToFitContainer(
+                            Alignment.Center, ContentScale.FillHeight
+                        ) togetherWith fadeOut(tween(100)) using
+                            SizeTransform { _, _ ->
+                                tween(100, easing = LinearEasing)
+                            }
+                    }) {
+                    if (target) {
+                        Box(
+                            Modifier
+                                .onPlaced {
+                                    box1Coords = it
+                                }
+                                .size(200.dp, 400.dp))
+                    } else {
+                        Box(
+                            Modifier
+                                .onPlaced { box2Coords = it }
+                                .size(100.dp, 250.dp))
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.mainClock.autoAdvance = false
+        assertEquals(IntSize(200, 400), box1Coords?.size)
+        assertNull(box2Coords)
+
+        rule.runOnIdle {
+            // Start transition from true -> false, size 200,400 -> 100,250
+            target = false
+        }
+        rule.mainClock.advanceTimeByFrame()
+        repeat(10) {
+            rule.mainClock.advanceTimeByFrame()
+            val playTime = 16 * it
+            val bounds = box2Coords?.boundsInRoot()
+            assertNotNull(bounds)
+            val fraction = (playTime / 100f).coerceAtMost(1f)
+            val height = 400 * (1 - fraction) + 250 * fraction
+            val containerWidth = 200 * (1 - fraction) + 100 * fraction
+            // Since we are testing default behavior, the scaling is based on width.
+            val width = height / 250f * 100
+            assertEquals(width, bounds!!.width, 0.01f)
+            assertEquals(height, bounds.height, 0.01f)
+            val offset = Alignment.Center.align(
+                IntSize(width.roundToInt(), height.roundToInt()),
+                IntSize(containerWidth.roundToInt(), height.roundToInt()), layoutDirection!!
+            )
+            assertEquals(offset, bounds.topLeft.round())
+        }
+    }
+
+    @OptIn(ExperimentalAnimationApi::class)
+    @Test
+    fun testExitHoldDefersUntilAllFinished() {
+        var target by mutableStateOf(true)
+        var box1Disposed = false
+        var box2EnterFinished = false
+        var transitionFinished = false
+        rule.setContent {
+            val outerTransition = updateTransition(targetState = target)
+            transitionFinished = !outerTransition.targetState && !outerTransition.currentState
+            outerTransition.AnimatedContent(
+                transitionSpec = {
+                    fadeIn(tween(160)) togetherWith
+                        fadeOut(tween(5)) + ExitTransition.Hold using
+                        SizeTransform { _, _ ->
+                            tween(300)
+                        }
+                }
+            ) {
+                if (it) {
+                    Box(Modifier.size(200.dp)) {
+                        DisposableEffect(key1 = Unit) {
+                            onDispose {
+                                box1Disposed = true
+                            }
+                        }
+                    }
+                } else {
+                    Box(Modifier.size(400.dp)) {
+                        box2EnterFinished =
+                            transition.targetState == transition.currentState &&
+                                transition.targetState == EnterExitState.Visible
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.mainClock.autoAdvance = false
+        rule.runOnIdle {
+            target = !target
+        }
+
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+        repeat(10) {
+            rule.mainClock.advanceTimeByFrame()
+            assertFalse(box1Disposed)
+            assertFalse(box2EnterFinished)
+        }
+
+        repeat(3) {
+            rule.mainClock.advanceTimeByFrame()
+            rule.waitForIdle()
+            assertTrue(box2EnterFinished)
+            // Enter finished, but box1 is only disposed when the transition is completely finished,
+            // which includes enter, exit & size change.
+            assertFalse(box1Disposed)
+            assertFalse(transitionFinished)
+        }
+
+        repeat(10) {
+            rule.mainClock.advanceTimeByFrame()
+            rule.waitForIdle()
+            assertTrue(box2EnterFinished)
+            // Enter finished, but box1 is only disposed when the transition is completely finished,
+            // which includes enter, exit & size change.
+            assertEquals(box1Disposed, transitionFinished)
+        }
+
+        assertTrue(box1Disposed)
+        assertTrue(box2EnterFinished)
+    }
+
+    /**
+     * This test checks that scaleInToFitContainer and scaleOutToFitContainer handle empty
+     * content correctly.
+     */
+    @Test
+    fun testAnimateToEmptyComposable() {
+        var isEmpty by mutableStateOf(false)
+        var targetSize: IntSize? = null
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                AnimatedContent(targetState = isEmpty,
+                    transitionSpec = {
+                        scaleInToFitContainer() togetherWith scaleOutToFitContainer()
+                    },
+                    modifier = Modifier.layout { measurable, constraints ->
+                        measurable.measure(constraints).run {
+                            if (isLookingAhead) {
+                                targetSize = IntSize(width, height)
+                            }
+                            layout(width, height) {
+                                place(0, 0)
+                            }
+                        }
+                    }
+                ) {
+                    if (!it) {
+                        Box(Modifier.size(200.dp))
+                    }
+                }
+            }
+        }
+        rule.runOnIdle {
+            assertEquals(IntSize(200, 200), targetSize)
+            isEmpty = true
+        }
+
+        rule.runOnIdle {
+            assertEquals(IntSize.Zero, targetSize)
+            isEmpty = !isEmpty
+        }
+        rule.runOnIdle {
+            assertEquals(IntSize(200, 200), targetSize)
+        }
+    }
+
+    @OptIn(InternalAnimationApi::class)
+    private val Transition<*>.playTimeMillis get() = (playTimeNanos / 1_000_000L).toInt()
+}
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
similarity index 100%
rename from compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
rename to compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
similarity index 100%
rename from compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
rename to compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/CrossfadeTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/CrossfadeTest.kt
similarity index 100%
rename from compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/CrossfadeTest.kt
rename to compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/CrossfadeTest.kt
diff --git a/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt b/compose/animation/animation/src/androidUnitTest/kotlin/androidx/compose/animation/ConverterTest.kt
similarity index 100%
rename from compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt
rename to compose/animation/animation/src/androidUnitTest/kotlin/androidx/compose/animation/ConverterTest.kt
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
index b82c044..e557851 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
@@ -820,13 +820,14 @@
         }
         if (!contentMap.containsKey(targetState) || !contentMap.containsKey(currentState)) {
             contentMap.clear()
-            val enter = transitionSpec(rootScope).targetContentEnter
-            val exit = rootScope.transitionSpec().initialContentExit
-            val zIndex = transitionSpec(rootScope).targetContentZIndex
             currentlyVisible.fastForEach { stateForContent ->
                 contentMap[stateForContent] = {
+                    // Only update content transform when enter/exit _direction_ changes.
+                    val contentTransform = remember(stateForContent == targetState) {
+                        rootScope.transitionSpec()
+                    }
                     PopulateContentFor(
-                        stateForContent, rootScope, enter, exit, zIndex, currentlyVisible, content
+                        stateForContent, rootScope, contentTransform, currentlyVisible, content
                     )
                 }
             }
@@ -871,33 +872,32 @@
 private inline fun <S> Transition<S>.PopulateContentFor(
     stateForContent: S,
     rootScope: AnimatedContentRootScope<S>,
-    enter: EnterTransition,
-    exit: ExitTransition,
-    zIndex: Float,
+    contentTransform: ContentTransform,
     currentlyVisible: SnapshotStateList<S>,
     crossinline content: @Composable() AnimatedContentScope.(targetState: S) -> Unit
 ) {
-    var activeEnter by remember { mutableStateOf(enter) }
+    var activeEnter by remember { mutableStateOf(contentTransform.targetContentEnter) }
     var activeExit by remember { mutableStateOf(ExitTransition.None) }
-    val targetZIndex = remember { zIndex }
+    val targetZIndex = remember { contentTransform.targetContentZIndex }
 
     val isEntering = targetState == stateForContent
     if (targetState == currentState) {
         // Transition finished, reset active enter & exit.
-        activeEnter = androidx.compose.animation.EnterTransition.None
-        activeExit = androidx.compose.animation.ExitTransition.None
+        activeEnter = EnterTransition.None
+        activeExit = ExitTransition.None
     } else if (isEntering) {
         // If the previous enter transition never finishes when multiple
         // interruptions happen, avoid adding new enter transitions for simplicity.
-        if (activeEnter == androidx.compose.animation.EnterTransition.None)
-            activeEnter += enter
+        if (activeEnter == EnterTransition.None)
+            activeEnter += contentTransform.targetContentEnter
     } else {
         // If the previous exit transition never finishes when multiple
         // interruptions happen, avoid adding new enter transitions for simplicity.
-        if (activeExit == androidx.compose.animation.ExitTransition.None) {
-            activeExit += exit
+        if (activeExit == ExitTransition.None) {
+            activeExit += contentTransform.initialContentExit
         }
     }
+
     val childData = remember { AnimatedContentRootScope.ChildData(stateForContent) }
     AnimatedEnterExitImpl(
         this,
@@ -915,16 +915,15 @@
             .then(
                 if (isEntering) {
                     activeEnter[ScaleToFitTransitionKey]
-                        ?: activeExit[ScaleToFitTransitionKey] ?: androidx.compose.ui.Modifier
+                        ?: activeExit[ScaleToFitTransitionKey] ?: Modifier
                 } else {
                     activeExit[ScaleToFitTransitionKey]
-                        ?: activeEnter[ScaleToFitTransitionKey] ?: androidx.compose.ui.Modifier
+                        ?: activeEnter[ScaleToFitTransitionKey] ?: Modifier
                 }
             ),
         shouldDisposeBlock = { currentState, targetState ->
-            currentState == androidx.compose.animation.EnterExitState.PostExit &&
-                targetState == androidx.compose.animation.EnterExitState.PostExit &&
-                !activeExit.data.hold
+            currentState == EnterExitState.PostExit &&
+                targetState == EnterExitState.PostExit && !activeExit.data.hold
         },
         onLookaheadMeasured = {
             if (isEntering) rootScope.targetSizeMap.getOrPut(targetState) {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
index d428ebd..2eaec85 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
@@ -302,7 +302,8 @@
                 }
                 A(%composer, 0)
                 M3({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
                   A(%composer, 0)
                   if (condition) {
                     %composer.endToMarker(tmp0_marker)
@@ -315,7 +316,7 @@
                     return
                   }
                   A(%composer, 0)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -374,7 +375,8 @@
                 }
                 A(%composer, 0)
                 M3({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
                   A(%composer, 0)
                   if (a) {
                     %composer.endToMarker(tmp0_marker)
@@ -387,10 +389,11 @@
                     return
                   }
                   A(%composer, 0)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 M3({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
                   A(%composer, 0)
                   if (b) {
                     %composer.endToMarker(tmp0_marker)
@@ -403,7 +406,7 @@
                     return
                   }
                   A(%composer, 0)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -451,14 +454,15 @@
                 }
                 A(%composer, 0)
                 M3({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
                   A(%composer, 0)
                   if (condition) {
-                    sourceInformationMarkerEnd(%composer)
+                    %composer.endReplaceableGroup()
                     return@M3
                   }
                   A(%composer, 0)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -498,7 +502,8 @@
                       traceEventStart(<>, %changed, -1, <>)
                     }
                     M1({ %composer: Composer?, %changed: Int ->
-                      sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+                      %composer.startReplaceableGroup(<>)
+                      sourceInformation(%composer, "C:Test.kt")
                       if (condition) {
                         %composer.endToMarker(tmp0_marker)
                         if (isTraceInProgress()) {
@@ -506,7 +511,7 @@
                         }
                         return@composableLambdaInstance
                       }
-                      sourceInformationMarkerEnd(%composer)
+                      %composer.endReplaceableGroup()
                     }, %composer, 0)
                     if (isTraceInProgress()) {
                       traceEventEnd()
@@ -572,12 +577,13 @@
                   sourceInformationMarkerStart(%composer, <>, "C<A()>,<M1>,<A()>:Test.kt")
                   A(%composer, 0)
                   M1({ %composer: Composer?, %changed: Int ->
-                    sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+                    %composer.startReplaceableGroup(<>)
+                    sourceInformation(%composer, "C:Test.kt")
                     if (condition) {
-                      sourceInformationMarkerEnd(%composer)
+                      %composer.endReplaceableGroup()
                       return@M1
                     }
-                    sourceInformationMarkerEnd(%composer)
+                    %composer.endReplaceableGroup()
                   }, %composer, 0)
                   A(%composer, 0)
                   sourceInformationMarkerEnd(%composer)
@@ -630,18 +636,20 @@
                 A(%composer, 0)
                 M3({ %composer: Composer?, %changed: Int ->
                   val tmp0_marker = %composer.currentMarker
-                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<M1>,<A()>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<A()>,<M1>,<A()>:Test.kt")
                   A(%composer, 0)
                   M1({ %composer: Composer?, %changed: Int ->
-                    sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+                    %composer.startReplaceableGroup(<>)
+                    sourceInformation(%composer, "C:Test.kt")
                     if (condition) {
                       %composer.endToMarker(tmp0_marker)
                       return@M3
                     }
-                    sourceInformationMarkerEnd(%composer)
+                    %composer.endReplaceableGroup()
                   }, %composer, 0)
                   A(%composer, 0)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -694,7 +702,8 @@
                 }
                 A(%composer, 0)
                 M1({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
                   A(%composer, 0)
                   %composer.startReplaceableGroup(<>)
                   sourceInformation(%composer, "*<A()>,<A()>")
@@ -714,7 +723,7 @@
                   }
                   %composer.endReplaceableGroup()
                   A(%composer, 0)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -769,24 +778,26 @@
                 }
                 A(%composer, 0)
                 M3({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
                   A(%composer, 0)
                   if (condition) {
-                    sourceInformationMarkerEnd(%composer)
+                    %composer.endReplaceableGroup()
                     return@M3
                   }
                   A(%composer, 0)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 M3({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
                   A(%composer, 0)
                   if (condition) {
-                    sourceInformationMarkerEnd(%composer)
+                    %composer.endReplaceableGroup()
                     return@M3
                   }
                   A(%composer, 0)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -839,14 +850,16 @@
                 }
                 Text("Root - before", %composer, 0b0110)
                 M1({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C<Text("...>,<Text("...>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<Text("...>,<Text("...>:Test.kt")
                   Text("M1 - begin", %composer, 0b0110)
                   %composer.startReplaceableGroup(<>)
                   sourceInformation(%composer, "<Text("...>,<M1>")
                   if (condition) {
                     Text("if - begin", %composer, 0b0110)
                     M1({ %composer: Composer?, %changed: Int ->
-                      sourceInformationMarkerStart(%composer, <>, "C<Text("...>:Test.kt")
+                      %composer.startReplaceableGroup(<>)
+                      sourceInformation(%composer, "C<Text("...>:Test.kt")
                       Text("In CCM1", %composer, 0b0110)
                       %composer.endToMarker(tmp0_marker)
                       if (isTraceInProgress()) {
@@ -856,12 +869,12 @@
                         test_CM1_CCM1_RetFun(condition, %composer, updateChangedFlags(%changed or 0b0001))
                       }
                       return
-                      sourceInformationMarkerEnd(%composer)
+                      %composer.endReplaceableGroup()
                     }, %composer, 0)
                   }
                   %composer.endReplaceableGroup()
                   Text("M1 - end", %composer, 0b0110)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 Text("Root - end", %composer, 0b0110)
                 if (isTraceInProgress()) {
@@ -905,13 +918,14 @@
                 traceEventStart(<>, %changed, -1, <>)
               }
               FakeBox({ %composer: Composer?, %changed: Int ->
-                sourceInformationMarkerStart(%composer, <>, "C<A()>:Test.kt")
+                %composer.startReplaceableGroup(<>)
+                sourceInformation(%composer, "C<A()>:Test.kt")
                 if (condition) {
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                   return@FakeBox
                 }
                 A(%composer, 0)
-                sourceInformationMarkerEnd(%composer)
+                %composer.endReplaceableGroup()
               }, %composer, 0)
               if (isTraceInProgress()) {
                 traceEventEnd()
@@ -1087,13 +1101,14 @@
                   traceEventStart(<>, %dirty, -1, <>)
                 }
                 IW({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C<A()>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<A()>:Test.kt")
                   if (condition) {
-                    sourceInformationMarkerEnd(%composer)
+                    %composer.endReplaceableGroup()
                     return@IW
                   }
                   A(%composer, 0)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 if (isTraceInProgress()) {
                   traceEventEnd()
@@ -1189,7 +1204,8 @@
                 }
                 Text("Some text", %composer, 0b0110)
                 M1({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C:Test.kt")
                   Identity {
                     if (condition) {
                       %composer.endToMarker(tmp0_marker)
@@ -1202,7 +1218,7 @@
                       return
                     }
                   }
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 Text("Some more text", %composer, 0b0110)
                 if (isTraceInProgress()) {
@@ -1247,14 +1263,15 @@
                 }
                 Text("Some text", %composer, 0b0110)
                 M1({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C:Test.kt")
                   Identity {
                     if (condition) {
-                      sourceInformationMarkerEnd(%composer)
+                      %composer.endReplaceableGroup()
                       return@M1
                     }
                   }
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 Text("Some more text", %composer, 0b0110)
                 if (isTraceInProgress()) {
@@ -1271,6 +1288,154 @@
     )
 
     @Test
+    fun verifyEarlyExitFromNestedInlineFunction() = verifyComposeIrTransform(
+        source = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            @NonRestartableComposable
+            fun Test(condition: Boolean) {
+                Text("Before outer")
+                InlineLinearA {
+                    Text("Before inner")
+                    InlineLinearB inner@{
+                        Text("Before return")
+                        if (condition) return@inner
+                        Text("After return")
+                    }
+                    Text("After inner")
+                }
+                Text("Before outer")
+            }
+        """,
+        expectedTransformed = """
+            @Composable
+            @NonRestartableComposable
+            fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+              %composer.startReplaceableGroup(<>)
+              sourceInformation(%composer, "C(Test)<Text("...>,<Inline...>,<Text("...>:Test.kt")
+              if (isTraceInProgress()) {
+                traceEventStart(<>, %changed, -1, <>)
+              }
+              Text("Before outer", %composer, 0b0110)
+              InlineLinearA({ %composer: Composer?, %changed: Int ->
+                sourceInformationMarkerStart(%composer, <>, "C<Text("...>,<Inline...>,<Text("...>:Test.kt")
+                Text("Before inner", %composer, 0b0110)
+                InlineLinearB({ %composer: Composer?, %changed: Int ->
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<Text("...>,<Text("...>:Test.kt")
+                  Text("Before return", %composer, 0b0110)
+                  if (condition) {
+                    %composer.endReplaceableGroup()
+                    return@InlineLinearB
+                  }
+                  Text("After return", %composer, 0b0110)
+                  %composer.endReplaceableGroup()
+                }, %composer, 0)
+                Text("After inner", %composer, 0b0110)
+                sourceInformationMarkerEnd(%composer)
+              }, %composer, 0)
+              Text("Before outer", %composer, 0b0110)
+              if (isTraceInProgress()) {
+                traceEventEnd()
+              }
+              %composer.endReplaceableGroup()
+            }
+        """,
+        extra = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            fun Text(value: String) { }
+
+            @Composable
+            inline fun InlineLinearA(content: @Composable () -> Unit) {
+                content()
+            }
+
+            @Composable
+            inline fun InlineLinearB(content: @Composable () -> Unit) {
+                content()
+            }
+        """
+    )
+
+    @Test
+    fun verifyEarlyExitFromMultiLevelNestedInlineFunction() = verifyComposeIrTransform(
+        source = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            @NonRestartableComposable
+            fun Test(condition: Boolean) {
+                Text("Before outer")
+                InlineLinearA outer@{
+                    Text("Before inner")
+                    InlineLinearB {
+                        Text("Before return")
+                        if (condition) return@outer
+                        Text("After return")
+                    }
+                    Text("After inner")
+                }
+                Text("Before outer")
+            }
+        """,
+        expectedTransformed = """
+            @Composable
+            @NonRestartableComposable
+            fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+              %composer.startReplaceableGroup(<>)
+              sourceInformation(%composer, "C(Test)<Text("...>,<Inline...>,<Text("...>:Test.kt")
+              if (isTraceInProgress()) {
+                traceEventStart(<>, %changed, -1, <>)
+              }
+              Text("Before outer", %composer, 0b0110)
+              InlineLinearA({ %composer: Composer?, %changed: Int ->
+                val tmp0_marker = %composer.currentMarker
+                %composer.startReplaceableGroup(<>)
+                sourceInformation(%composer, "C<Text("...>,<Inline...>,<Text("...>:Test.kt")
+                Text("Before inner", %composer, 0b0110)
+                InlineLinearB({ %composer: Composer?, %changed: Int ->
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<Text("...>,<Text("...>:Test.kt")
+                  Text("Before return", %composer, 0b0110)
+                  if (condition) {
+                    %composer.endToMarker(tmp0_marker)
+                    return@InlineLinearA
+                  }
+                  Text("After return", %composer, 0b0110)
+                  %composer.endReplaceableGroup()
+                }, %composer, 0)
+                Text("After inner", %composer, 0b0110)
+                %composer.endReplaceableGroup()
+              }, %composer, 0)
+              Text("Before outer", %composer, 0b0110)
+              if (isTraceInProgress()) {
+                traceEventEnd()
+              }
+              %composer.endReplaceableGroup()
+            }
+        """,
+        extra = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            fun Text(value: String) { }
+
+            @Composable
+            inline fun InlineLinearA(content: @Composable () -> Unit) {
+                content()
+            }
+
+            @Composable
+            inline fun InlineLinearB(content: @Composable () -> Unit) {
+                content()
+            }
+        """
+    )
+
+    @Test
     fun testEnsureRuntimeTestWillCompile_CL() {
         classLoader(
             """
@@ -1332,7 +1497,8 @@
                 }
                 Text("Root - before", %composer, 0b0110)
                 M1({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C<Text("...>,<Text("...>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<Text("...>,<Text("...>:Test.kt")
                   Text("M1 - before", %composer, 0b0110)
                   if (condition) {
                     %composer.endToMarker(tmp0_marker)
@@ -1345,7 +1511,7 @@
                     return
                   }
                   Text("M1 - after", %composer, 0b0110)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 Text("Root - after", %composer, 0b0110)
                 if (isTraceInProgress()) {
@@ -6182,16 +6348,18 @@
                 }
                 Inline1({ %composer: Composer?, %changed: Int ->
                   val tmp0_marker = %composer.currentMarker
-                  sourceInformationMarkerStart(%composer, <>, "C<Inline...>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<Inline...>:Test.kt")
                   Inline2({ %composer: Composer?, %changed: Int ->
-                    sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+                    %composer.startReplaceableGroup(<>)
+                    sourceInformation(%composer, "C:Test.kt")
                     if (true) {
                       %composer.endToMarker(tmp0_marker)
                       return@Inline1
                     }
-                    sourceInformationMarkerEnd(%composer)
+                    %composer.endReplaceableGroup()
                   }, %composer, 0)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 if (isTraceInProgress()) {
                   traceEventEnd()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
index 4fb99f6..a3a2934 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
@@ -174,4 +174,77 @@
             }
         """
     )
+
+    @Test
+    fun verifyEarlyExitFromMultiLevelNestedInlineFunction() = verifyComposeIrTransform(
+        source = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            @NonRestartableComposable
+            fun Test(condition: Boolean) {
+                Text("Before outer")
+                InlineLinearA outer@{
+                    Text("Before inner")
+                    InlineLinearB {
+                        Text("Before return")
+                        if (condition) return@outer
+                        Text("After return")
+                    }
+                    Text("After inner")
+                }
+                Text("Before outer")
+            }
+        """,
+        expectedTransformed = """
+            @Composable
+            @NonRestartableComposable
+            fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+              %composer.startReplaceableGroup(<>)
+              sourceInformation(%composer, "C(Test)")
+              if (isTraceInProgress()) {
+                traceEventStart(<>, %changed, -1, <>)
+              }
+              Text("Before outer", %composer, 0b0110)
+              InlineLinearA({ %composer: Composer?, %changed: Int ->
+                val tmp0_marker = %composer.currentMarker
+                %composer.startReplaceableGroup(<>)
+                Text("Before inner", %composer, 0b0110)
+                InlineLinearB({ %composer: Composer?, %changed: Int ->
+                  %composer.startReplaceableGroup(<>)
+                  Text("Before return", %composer, 0b0110)
+                  if (condition) {
+                    %composer.endToMarker(tmp0_marker)
+                    return@InlineLinearA
+                  }
+                  Text("After return", %composer, 0b0110)
+                  %composer.endReplaceableGroup()
+                }, %composer, 0)
+                Text("After inner", %composer, 0b0110)
+                %composer.endReplaceableGroup()
+              }, %composer, 0)
+              Text("Before outer", %composer, 0b0110)
+              if (isTraceInProgress()) {
+                traceEventEnd()
+              }
+              %composer.endReplaceableGroup()
+            }
+        """,
+        extra = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            fun Text(value: String) { }
+
+            @Composable
+            inline fun InlineLinearA(content: @Composable () -> Unit) {
+                content()
+            }
+
+            @Composable
+            inline fun InlineLinearB(content: @Composable () -> Unit) {
+                content()
+            }
+        """
+    )
 }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
index 2aa05c6..aaaba3b2 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
@@ -175,7 +175,8 @@
                 }
                 A(%composer, 0)
                 Wrapper({ %composer: Composer?, %changed: Int ->
-                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
                   A(%composer, 0)
                   if (!condition) {
                     %composer.endToMarker(tmp0_marker)
@@ -188,7 +189,7 @@
                     return
                   }
                   A(%composer, 0)
-                  sourceInformationMarkerEnd(%composer)
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index b1b54c0..1b4c6c2f 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -978,17 +978,10 @@
         val bodyPreamble = mutableStatementContainer()
         val bodyEpilogue = mutableStatementContainer()
 
-        // First generate the source information call
         val isInlineLambda = scope.isInlinedLambda
-        if (collectSourceInformation) {
-            if (isInlineLambda) {
-                sourceInformationPreamble.statements.add(
-                    irSourceInformationMarkerStart(body, scope)
-                )
-                bodyEpilogue.statements.add(irSourceInformationMarkerEnd(body, scope))
-            } else {
-                sourceInformationPreamble.statements.add(irSourceInformation(scope))
-            }
+
+        if (collectSourceInformation && !isInlineLambda) {
+            sourceInformationPreamble.statements.add(irSourceInformation(scope))
         }
 
         // we start off assuming that we *can* skip execution of the function
@@ -1028,7 +1021,12 @@
         // are using the dispatchReceiverParameter or the extensionReceiverParameter
         val transformed = nonReturningBody.apply {
             transformChildrenVoid()
+        }.let {
+            if (isInlineLambda) {
+                it.asSourceOrEarlyExitGroup(scope)
+            } else it
         }
+
         canSkipExecution = buildPreambleStatementsAndReturnIfSkippingPossible(
             body,
             skipPreamble,
@@ -1058,12 +1056,6 @@
             }
         }
 
-        if (collectSourceInformation && isInlineLambda) {
-            scope.realizeEndCalls {
-                irSourceInformationMarkerEnd(body, scope)
-            }
-        }
-
         if (canSkipExecution) {
             // We CANNOT skip if any of the following conditions are met
             // 1. if any of the stable parameters have *differences* from last execution.
@@ -1290,7 +1282,6 @@
                 returnVar?.let { irReturnVar(declaration.symbol, it) }
             )
         )
-
         scope.metrics.recordFunction(
             composable = true,
             restartable = true,
@@ -2290,7 +2281,7 @@
         }
     }
 
-    fun irTemporary(
+    private fun irTemporary(
         value: IrExpression,
         nameHint: String? = null,
         irType: IrType = value.type,
@@ -2307,7 +2298,9 @@
             name,
             irType,
             isVar
-        )
+        ).also {
+            it.parent = currentFunctionScope.function.parent
+        }
     }
 
     private fun IrBlock.withReplaceableGroupStatements(scope: Scope.BlockScope): IrExpression {
@@ -2404,7 +2397,7 @@
     private fun IrExpression.wrap(
         before: List<IrExpression> = emptyList(),
         after: List<IrExpression> = emptyList()
-    ): IrExpression {
+    ): IrContainerExpression {
         return if (after.isEmpty() || type.isNothing() || type.isUnit()) {
             wrap(startOffset, endOffset, type, before, after)
         } else {
@@ -2425,7 +2418,7 @@
         type: IrType,
         before: List<IrExpression> = emptyList(),
         after: List<IrExpression> = emptyList()
-    ): IrExpression {
+    ): IrContainerExpression {
         return IrBlockImpl(
             startOffset,
             endOffset,
@@ -2462,6 +2455,58 @@
         )
     }
 
+    private fun IrContainerExpression.asSourceOrEarlyExitGroup(
+        scope: Scope.FunctionScope
+    ): IrContainerExpression {
+        if (scope.hasEarlyReturn) {
+            currentFunctionScope.metrics.recordGroup()
+        } else if (!collectSourceInformation) {
+            // If we are not generating source information and the lambda does not contain an
+            // early exit this we don't need a group or source markers.
+            return this
+        }
+        // if the scope has no composable calls, then the only important thing is that a
+        // start/end call gets executed. as a result, we can just put them both at the top of
+        // the group, and we don't have to deal with any of the complicated jump logic that
+        // could be inside of the block
+        val makeStart = {
+            if (scope.hasEarlyReturn) irStartReplaceableGroup(
+                this,
+                scope,
+                startOffset = startOffset,
+                endOffset = endOffset
+            )
+            else irSourceInformationMarkerStart(this, scope)
+        }
+        val makeEnd = {
+            if (scope.hasEarlyReturn) irEndReplaceableGroup(scope = scope)
+            else irSourceInformationMarkerEnd(this, scope)
+        }
+        if (!scope.hasComposableCalls && !scope.hasReturn && !scope.hasJump) {
+            return wrap(
+                before = listOf(makeStart()),
+                after = listOf(makeEnd()),
+            )
+        }
+        scope.realizeGroup(makeEnd)
+        return when {
+            // if the scope ends with a return call, then it will get properly ended if we
+            // just push the end call on the scope because of the way returns get transformed in
+            // this class. As a result, here we can safely just "prepend" the start call
+            endsWithReturnOrJump() -> {
+                wrap(before = listOf(makeStart()))
+            }
+            // otherwise, we want to push an end call for any early returns/jumps, but also add
+            // an end call to the end of the group
+            else -> {
+                wrap(
+                    before = listOf(makeStart()),
+                    after = listOf(makeEnd()),
+                )
+            }
+        }
+    }
+
     private fun mutableStatementContainer() = mutableStatementContainer(context)
 
     private fun encounteredComposableCall(withGroups: Boolean, isCached: Boolean) {
@@ -2574,12 +2619,16 @@
                                 it.markReturn(extraEndLocation)
                             }
                             scope.markReturn(extraEndLocation)
+                            if (scope.isInlinedLambda && scope.inComposableCall) {
+                                scope.hasEarlyReturn = true
+                            }
                         } else {
                             val functionScope = scope
                             val targetScope = currentScope as? Scope.BlockScope ?: functionScope
                             if (functionScope.isInlinedLambda) {
                                 val marker = irGet(functionScope.allocateMarker())
                                 extraEndLocation(irEndToMarker(marker, targetScope))
+                                scope.hasEarlyReturn = true
                             } else {
                                 val marker = functionScope.allocateMarker()
                                 functionScope.markReturn {
@@ -2591,8 +2640,10 @@
                         scope.updateIntrinsiceRememberSafety(false)
                         break@loop
                     }
-                    if (scope.isInlinedLambda && scope.inComposableCall)
+                    if (scope.isInlinedLambda && scope.inComposableCall) {
                         leavingInlinedLambda = true
+                        scope.hasEarlyReturn = true
+                    }
                 }
                 is Scope.BlockScope -> {
                     blockScopeMarks.add(scope)
@@ -3708,6 +3759,8 @@
 
             val metrics: FunctionMetrics = transformer.metricsFor(function)
 
+            var hasEarlyReturn: Boolean = false
+
             private var lastTemporaryIndex: Int = 0
 
             private fun nextTemporaryIndex(): Int = lastTemporaryIndex++
@@ -4038,13 +4091,12 @@
                 makeEnd: () -> IrExpression
             ) {
                 addProvisionalSourceLocations(scope.sourceLocations)
-                coalescableChilds.add(
-                    CoalescableGroupInfo(
-                        scope,
-                        realizeGroup,
-                        makeEnd
-                    )
+                val groupInfo = CoalescableGroupInfo(
+                    scope,
+                    realizeGroup,
+                    makeEnd
                 )
+                coalescableChilds.add(groupInfo)
             }
 
             open fun calculateHasSourceInformation(sourceInformationEnabled: Boolean): Boolean =
@@ -4442,6 +4494,7 @@
                     isConst = false,
                     isLateinit = false
                 ).apply {
+                    parent = currentFunctionScope.function.parent
                     initializer = irGet(param)
                 }
             }
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 055f276..320eead 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -81,7 +81,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:foundation:foundation"))
@@ -100,7 +100,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/AndroidManifest.xml b/compose/foundation/foundation-layout/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/AndroidManifest.xml
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/AlignmentLineTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/AlignmentLineTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/AlignmentLineTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/AlignmentLineTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ArrangementTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/ArrangementTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ArrangementTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/ArrangementTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/AspectRatioTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/AspectRatioTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/AspectRatioTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/AspectRatioTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/BoxTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/BoxTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/BoxTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/BoxTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/BoxWithConstraintsTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/BoxWithConstraintsTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/BoxWithConstraintsTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/BoxWithConstraintsTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/FlowRowColumnTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/FlowRowColumnTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/FlowRowColumnTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/FlowRowColumnTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/IntrinsicTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/IntrinsicTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/IntrinsicTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/IntrinsicTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/LayoutTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/LayoutTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/OffsetTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/OffsetTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/OffsetTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/OffsetTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/PaddingTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/PaddingTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/PaddingTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/PaddingTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnModifierTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/RowColumnModifierTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnModifierTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/RowColumnModifierTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SpacerTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/SpacerTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SpacerTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/SpacerTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/TestActivity.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/TestActivity.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/TestActivity.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/TestActivity.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsControllerTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsControllerTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsControllerTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsControllerTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsIgnoringVisibilityTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsIgnoringVisibilityTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsIgnoringVisibilityTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsIgnoringVisibilityTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt
rename to compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt
diff --git a/compose/foundation/foundation-layout/src/test/kotlin/androidx/compose/foundation/layout/WindowInsetsTest.kt b/compose/foundation/foundation-layout/src/androidUnitTest/kotlin/androidx/compose/foundation/layout/WindowInsetsTest.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/test/kotlin/androidx/compose/foundation/layout/WindowInsetsTest.kt
rename to compose/foundation/foundation-layout/src/androidUnitTest/kotlin/androidx/compose/foundation/layout/WindowInsetsTest.kt
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 284cdd3..d0804c6 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -2,13 +2,13 @@
 package androidx.compose.foundation {
 
   public final class BackgroundKt {
-    method public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Brush brush, optional androidx.compose.ui.graphics.Shape shape, optional @FloatRange(from=0.0, to=1.0) float alpha);
-    method public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, long color, optional androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Brush brush, optional androidx.compose.ui.graphics.Shape shape, optional @FloatRange(from=0.0, to=1.0) float alpha);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, long color, optional androidx.compose.ui.graphics.Shape shape);
   }
 
   public final class BasicMarqueeKt {
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.MarqueeSpacing MarqueeSpacing(float spacing);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier basicMarquee(androidx.compose.ui.Modifier, optional int iterations, optional int animationMode, optional int delayMillis, optional int initialDelayMillis, optional androidx.compose.foundation.MarqueeSpacing spacing, optional float velocity);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier basicMarquee(androidx.compose.ui.Modifier, optional int iterations, optional int animationMode, optional int delayMillis, optional int initialDelayMillis, optional androidx.compose.foundation.MarqueeSpacing spacing, optional float velocity);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static int getDefaultMarqueeDelayMillis();
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static int getDefaultMarqueeIterations();
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.MarqueeSpacing getDefaultMarqueeSpacing();
@@ -28,7 +28,7 @@
 
   public final class BasicTooltipKt {
     method @androidx.compose.runtime.Composable public static void BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, androidx.compose.foundation.BasicTooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method public static androidx.compose.foundation.BasicTooltipState BasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
+    method @androidx.compose.runtime.Stable public static androidx.compose.foundation.BasicTooltipState BasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
     method @androidx.compose.runtime.Composable public static androidx.compose.foundation.BasicTooltipState rememberBasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
   }
 
@@ -47,9 +47,9 @@
   }
 
   public final class BorderKt {
-    method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, optional androidx.compose.ui.graphics.Shape shape);
-    method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
-    method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, long color, optional androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, optional androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, long color, optional androidx.compose.ui.graphics.Shape shape);
   }
 
   @androidx.compose.runtime.Immutable public final class BorderStroke {
@@ -83,7 +83,7 @@
   }
 
   public final class ClipScrollableContainerKt {
-    method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface CombinedClickableNode extends androidx.compose.ui.node.PointerInputModifierNode {
@@ -103,7 +103,7 @@
   }
 
   public final class FocusableKt {
-    method public static androidx.compose.ui.Modifier focusGroup(androidx.compose.ui.Modifier);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier focusGroup(androidx.compose.ui.Modifier);
     method public static androidx.compose.ui.Modifier focusable(androidx.compose.ui.Modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
@@ -111,6 +111,30 @@
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier onFocusedBoundsChanged(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,kotlin.Unit> onPositioned);
   }
 
+  public final class GraphicsSurfaceKt {
+    method @androidx.compose.runtime.Composable public static void EmbeddedGraphicsSurface(optional androidx.compose.ui.Modifier modifier, optional boolean isOpaque, optional long surfaceSize, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.GraphicsSurfaceScope,kotlin.Unit> onInit);
+    method @androidx.compose.runtime.Composable public static void GraphicsSurface(optional androidx.compose.ui.Modifier modifier, optional boolean isOpaque, optional int zOrder, optional long surfaceSize, optional boolean isSecure, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.GraphicsSurfaceScope,kotlin.Unit> onInit);
+  }
+
+  public interface GraphicsSurfaceScope {
+    method public void onSurface(kotlin.jvm.functions.Function5<? super androidx.compose.foundation.SurfaceCoroutineScope,? super android.view.Surface,? super java.lang.Integer,? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onSurface);
+  }
+
+  @kotlin.jvm.JvmInline public final value class GraphicsSurfaceZOrder {
+    method public int getZOrder();
+    property public final int zOrder;
+    field public static final androidx.compose.foundation.GraphicsSurfaceZOrder.Companion Companion;
+  }
+
+  public static final class GraphicsSurfaceZOrder.Companion {
+    method public int getBehind();
+    method public int getMediaOverlay();
+    method public int getOnTop();
+    property public final int Behind;
+    property public final int MediaOverlay;
+    property public final int OnTop;
+  }
+
   public final class HoverableKt {
     method public static androidx.compose.ui.Modifier hoverable(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean enabled);
   }
@@ -140,21 +164,7 @@
   }
 
   public final class MagnifierKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier magnifier(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.geometry.Offset> sourceCenter, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.geometry.Offset> magnifierCenter, optional float zoom, optional androidx.compose.foundation.MagnifierStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.DpSize,kotlin.Unit>? onSizeChanged);
-  }
-
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public final class MagnifierStyle {
-    ctor @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public MagnifierStyle(optional long size, optional float cornerRadius, optional float elevation, optional boolean clippingEnabled, optional boolean fishEyeEnabled);
-    method public boolean isSupported();
-    property public final boolean isSupported;
-    field public static final androidx.compose.foundation.MagnifierStyle.Companion Companion;
-  }
-
-  public static final class MagnifierStyle.Companion {
-    method public androidx.compose.foundation.MagnifierStyle getDefault();
-    method public androidx.compose.foundation.MagnifierStyle getTextDefault();
-    property public final androidx.compose.foundation.MagnifierStyle Default;
-    property public final androidx.compose.foundation.MagnifierStyle TextDefault;
+    method public static androidx.compose.ui.Modifier magnifier(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.geometry.Offset> sourceCenter, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.geometry.Offset> magnifierCenter, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.DpSize,kotlin.Unit>? onSizeChanged, optional float zoom, optional long size, optional float cornerRadius, optional float elevation, optional boolean clippingEnabled);
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @kotlin.jvm.JvmInline public final value class MarqueeAnimationMode {
@@ -260,6 +270,14 @@
     property public final androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.ScrollState,?> Saver;
   }
 
+  public interface SurfaceCoroutineScope extends androidx.compose.foundation.SurfaceScope kotlinx.coroutines.CoroutineScope {
+  }
+
+  public interface SurfaceScope {
+    method public void onChanged(android.view.Surface, kotlin.jvm.functions.Function3<? super android.view.Surface,? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> onChanged);
+    method public void onDestroyed(android.view.Surface, kotlin.jvm.functions.Function1<? super android.view.Surface,kotlin.Unit> onDestroyed);
+  }
+
   public final class SystemGestureExclusionKt {
     method public static androidx.compose.ui.Modifier systemGestureExclusion(androidx.compose.ui.Modifier);
     method public static androidx.compose.ui.Modifier systemGestureExclusion(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,androidx.compose.ui.geometry.Rect> exclusion);
@@ -416,8 +434,8 @@
   }
 
   public final class ScrollableKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
-    method public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface ScrollableState {
@@ -494,7 +512,7 @@
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final class SnapFlingBehavior implements androidx.compose.foundation.gestures.FlingBehavior {
-    ctor public SnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider, androidx.compose.animation.core.AnimationSpec<java.lang.Float> lowVelocityAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> highVelocityAnimationSpec, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.ui.unit.Density density, optional float shortSnapVelocityThreshold);
+    ctor public SnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider, androidx.compose.animation.core.AnimationSpec<java.lang.Float> lowVelocityAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> highVelocityAnimationSpec, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, float shortSnapVelocityThreshold);
     method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float>);
     method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onSettlingDistanceUpdated, kotlin.coroutines.Continuation<? super java.lang.Float>);
   }
@@ -504,13 +522,12 @@
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface SnapLayoutInfoProvider {
-    method public float calculateApproachOffset(androidx.compose.ui.unit.Density, float initialVelocity);
-    method public float calculateSnapStepSize(androidx.compose.ui.unit.Density);
-    method public float calculateSnappingOffset(androidx.compose.ui.unit.Density, float currentVelocity);
+    method public float calculateApproachOffset(float initialVelocity);
+    method public float calculateSnappingOffset(float currentVelocity);
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public fun interface SnapPositionInLayout {
-    method public int position(androidx.compose.ui.unit.Density, int layoutSize, int itemSize, int itemIndex);
+    method public int position(int layoutSize, int itemSize, int itemIndex);
     field public static final androidx.compose.foundation.gestures.snapping.SnapPositionInLayout.Companion Companion;
   }
 
@@ -1181,6 +1198,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public final suspend Object? scrollToPage(int page, optional float pageOffsetFraction, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final void updateCurrentPage(androidx.compose.foundation.gestures.ScrollScope, int page, optional float pageOffsetFraction);
+    method public final void updateTargetPage(androidx.compose.foundation.gestures.ScrollScope, int targetPage);
     property public final boolean canScrollBackward;
     property public final boolean canScrollForward;
     property public final int currentPage;
@@ -1226,7 +1245,7 @@
 package androidx.compose.foundation.selection {
 
   public final class SelectableGroupKt {
-    method public static androidx.compose.ui.Modifier selectableGroup(androidx.compose.ui.Modifier);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier selectableGroup(androidx.compose.ui.Modifier);
   }
 
   public final class SelectableKt {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 1ab4f695..d5052df 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -2,13 +2,13 @@
 package androidx.compose.foundation {
 
   public final class BackgroundKt {
-    method public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Brush brush, optional androidx.compose.ui.graphics.Shape shape, optional @FloatRange(from=0.0, to=1.0) float alpha);
-    method public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, long color, optional androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Brush brush, optional androidx.compose.ui.graphics.Shape shape, optional @FloatRange(from=0.0, to=1.0) float alpha);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, long color, optional androidx.compose.ui.graphics.Shape shape);
   }
 
   public final class BasicMarqueeKt {
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.MarqueeSpacing MarqueeSpacing(float spacing);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier basicMarquee(androidx.compose.ui.Modifier, optional int iterations, optional int animationMode, optional int delayMillis, optional int initialDelayMillis, optional androidx.compose.foundation.MarqueeSpacing spacing, optional float velocity);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier basicMarquee(androidx.compose.ui.Modifier, optional int iterations, optional int animationMode, optional int delayMillis, optional int initialDelayMillis, optional androidx.compose.foundation.MarqueeSpacing spacing, optional float velocity);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static int getDefaultMarqueeDelayMillis();
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static int getDefaultMarqueeIterations();
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.MarqueeSpacing getDefaultMarqueeSpacing();
@@ -28,7 +28,7 @@
 
   public final class BasicTooltipKt {
     method @androidx.compose.runtime.Composable public static void BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, androidx.compose.foundation.BasicTooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method public static androidx.compose.foundation.BasicTooltipState BasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
+    method @androidx.compose.runtime.Stable public static androidx.compose.foundation.BasicTooltipState BasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
     method @androidx.compose.runtime.Composable public static androidx.compose.foundation.BasicTooltipState rememberBasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
   }
 
@@ -47,9 +47,9 @@
   }
 
   public final class BorderKt {
-    method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, optional androidx.compose.ui.graphics.Shape shape);
-    method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
-    method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, long color, optional androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, optional androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, long color, optional androidx.compose.ui.graphics.Shape shape);
   }
 
   @androidx.compose.runtime.Immutable public final class BorderStroke {
@@ -83,7 +83,7 @@
   }
 
   public final class ClipScrollableContainerKt {
-    method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface CombinedClickableNode extends androidx.compose.ui.node.PointerInputModifierNode {
@@ -103,7 +103,7 @@
   }
 
   public final class FocusableKt {
-    method public static androidx.compose.ui.Modifier focusGroup(androidx.compose.ui.Modifier);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier focusGroup(androidx.compose.ui.Modifier);
     method public static androidx.compose.ui.Modifier focusable(androidx.compose.ui.Modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
@@ -111,6 +111,30 @@
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier onFocusedBoundsChanged(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,kotlin.Unit> onPositioned);
   }
 
+  public final class GraphicsSurfaceKt {
+    method @androidx.compose.runtime.Composable public static void EmbeddedGraphicsSurface(optional androidx.compose.ui.Modifier modifier, optional boolean isOpaque, optional long surfaceSize, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.GraphicsSurfaceScope,kotlin.Unit> onInit);
+    method @androidx.compose.runtime.Composable public static void GraphicsSurface(optional androidx.compose.ui.Modifier modifier, optional boolean isOpaque, optional int zOrder, optional long surfaceSize, optional boolean isSecure, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.GraphicsSurfaceScope,kotlin.Unit> onInit);
+  }
+
+  public interface GraphicsSurfaceScope {
+    method public void onSurface(kotlin.jvm.functions.Function5<? super androidx.compose.foundation.SurfaceCoroutineScope,? super android.view.Surface,? super java.lang.Integer,? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onSurface);
+  }
+
+  @kotlin.jvm.JvmInline public final value class GraphicsSurfaceZOrder {
+    method public int getZOrder();
+    property public final int zOrder;
+    field public static final androidx.compose.foundation.GraphicsSurfaceZOrder.Companion Companion;
+  }
+
+  public static final class GraphicsSurfaceZOrder.Companion {
+    method public int getBehind();
+    method public int getMediaOverlay();
+    method public int getOnTop();
+    property public final int Behind;
+    property public final int MediaOverlay;
+    property public final int OnTop;
+  }
+
   public final class HoverableKt {
     method public static androidx.compose.ui.Modifier hoverable(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean enabled);
   }
@@ -140,21 +164,7 @@
   }
 
   public final class MagnifierKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier magnifier(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.geometry.Offset> sourceCenter, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.geometry.Offset> magnifierCenter, optional float zoom, optional androidx.compose.foundation.MagnifierStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.DpSize,kotlin.Unit>? onSizeChanged);
-  }
-
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public final class MagnifierStyle {
-    ctor @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public MagnifierStyle(optional long size, optional float cornerRadius, optional float elevation, optional boolean clippingEnabled, optional boolean fishEyeEnabled);
-    method public boolean isSupported();
-    property public final boolean isSupported;
-    field public static final androidx.compose.foundation.MagnifierStyle.Companion Companion;
-  }
-
-  public static final class MagnifierStyle.Companion {
-    method public androidx.compose.foundation.MagnifierStyle getDefault();
-    method public androidx.compose.foundation.MagnifierStyle getTextDefault();
-    property public final androidx.compose.foundation.MagnifierStyle Default;
-    property public final androidx.compose.foundation.MagnifierStyle TextDefault;
+    method public static androidx.compose.ui.Modifier magnifier(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.geometry.Offset> sourceCenter, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.geometry.Offset> magnifierCenter, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.DpSize,kotlin.Unit>? onSizeChanged, optional float zoom, optional long size, optional float cornerRadius, optional float elevation, optional boolean clippingEnabled);
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @kotlin.jvm.JvmInline public final value class MarqueeAnimationMode {
@@ -262,6 +272,14 @@
     property public final androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.ScrollState,?> Saver;
   }
 
+  public interface SurfaceCoroutineScope extends androidx.compose.foundation.SurfaceScope kotlinx.coroutines.CoroutineScope {
+  }
+
+  public interface SurfaceScope {
+    method public void onChanged(android.view.Surface, kotlin.jvm.functions.Function3<? super android.view.Surface,? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> onChanged);
+    method public void onDestroyed(android.view.Surface, kotlin.jvm.functions.Function1<? super android.view.Surface,kotlin.Unit> onDestroyed);
+  }
+
   public final class SystemGestureExclusionKt {
     method public static androidx.compose.ui.Modifier systemGestureExclusion(androidx.compose.ui.Modifier);
     method public static androidx.compose.ui.Modifier systemGestureExclusion(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,androidx.compose.ui.geometry.Rect> exclusion);
@@ -418,8 +436,8 @@
   }
 
   public final class ScrollableKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
-    method public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface ScrollableState {
@@ -496,7 +514,7 @@
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final class SnapFlingBehavior implements androidx.compose.foundation.gestures.FlingBehavior {
-    ctor public SnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider, androidx.compose.animation.core.AnimationSpec<java.lang.Float> lowVelocityAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> highVelocityAnimationSpec, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.ui.unit.Density density, optional float shortSnapVelocityThreshold);
+    ctor public SnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider, androidx.compose.animation.core.AnimationSpec<java.lang.Float> lowVelocityAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> highVelocityAnimationSpec, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, float shortSnapVelocityThreshold);
     method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float>);
     method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onSettlingDistanceUpdated, kotlin.coroutines.Continuation<? super java.lang.Float>);
   }
@@ -506,13 +524,12 @@
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface SnapLayoutInfoProvider {
-    method public float calculateApproachOffset(androidx.compose.ui.unit.Density, float initialVelocity);
-    method public float calculateSnapStepSize(androidx.compose.ui.unit.Density);
-    method public float calculateSnappingOffset(androidx.compose.ui.unit.Density, float currentVelocity);
+    method public float calculateApproachOffset(float initialVelocity);
+    method public float calculateSnappingOffset(float currentVelocity);
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public fun interface SnapPositionInLayout {
-    method public int position(androidx.compose.ui.unit.Density, int layoutSize, int itemSize, int itemIndex);
+    method public int position(int layoutSize, int itemSize, int itemIndex);
     field public static final androidx.compose.foundation.gestures.snapping.SnapPositionInLayout.Companion Companion;
   }
 
@@ -1183,6 +1200,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public final suspend Object? scrollToPage(int page, optional float pageOffsetFraction, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final void updateCurrentPage(androidx.compose.foundation.gestures.ScrollScope, int page, optional float pageOffsetFraction);
+    method public final void updateTargetPage(androidx.compose.foundation.gestures.ScrollScope, int targetPage);
     property public final boolean canScrollBackward;
     property public final boolean canScrollForward;
     property public final int currentPage;
@@ -1228,7 +1247,7 @@
 package androidx.compose.foundation.selection {
 
   public final class SelectableGroupKt {
-    method public static androidx.compose.ui.Modifier selectableGroup(androidx.compose.ui.Modifier);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier selectableGroup(androidx.compose.ui.Modifier);
   }
 
   public final class SelectableKt {
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 60bba5d..a6d0d8f 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -88,7 +88,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:test-utils"))
@@ -116,7 +116,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
@@ -124,6 +124,8 @@
                 implementation(libs.junit)
                 implementation(libs.truth)
                 implementation(libs.kotlinReflect)
+                implementation(libs.mockitoCore)
+                implementation(libs.mockitoKotlin)
                 implementation(project(":constraintlayout:constraintlayout-compose"))
             }
         }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/lint-baseline.xml b/compose/foundation/foundation/integration-tests/foundation-demos/lint-baseline.xml
index 4944215..652b3f1 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/lint-baseline.xml
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="NewApi"
@@ -102,69 +102,6 @@
 
     <issue
         id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method SnapLayoutInfoProvider has parameter &apos;itemSize&apos; with type Function1&lt;? super Density, Float>."
-        errorLine1="    itemSize: Density.() -> Float,"
-        errorLine2="              ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method SnapLayoutInfoProvider has parameter &apos;layoutSize&apos; with type Function1&lt;? super Density, Float>."
-        errorLine1="    layoutSize: Density.() -> Float"
-        errorLine2="                ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in variable &apos;isValidDistance&apos; with type Function1&lt;? super Float, ? extends Boolean>."
-        errorLine1="    fun Float.isValidDistance(): Boolean {"
-        errorLine2="    ^">
-        <location
-            file="src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method RowSnappingMainLayout has parameter &apos;onLayoutSizeChanged&apos; with type Function1&lt;? super IntSize, Unit>."
-        errorLine1="    onLayoutSizeChanged: (IntSize) -> Unit"
-        errorLine2="                         ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method rememberRowSnapLayoutInfoProvider has parameter &apos;layoutSize&apos; with type Function1&lt;? super Density, Float>."
-        errorLine1="    layoutSize: Density.() -> Float"
-        errorLine2="                ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method rememberMultiPageRowSnapLayoutInfoProvider has parameter &apos;layoutSize&apos; with type Function1&lt;? super Density, Float>."
-        errorLine1="    layoutSize: Density.() -> Float"
-        errorLine2="                ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method rememberViewPortRowSnapLayoutInfoProvider has parameter &apos;layoutSize&apos; with type Function1&lt;? super Density, Float>."
-        errorLine1="    layoutSize: Density.() -> Float"
-        errorLine2="                ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method ResizeHandle has parameter &apos;onDrag&apos; with type Function1&lt;? super Float, Unit>."
         errorLine1="private fun ResizeHandle(orientation: Orientation, onDrag: (Float) -> Unit) {"
         errorLine2="                                                           ~~~~~~~~~~~~~~~">
@@ -183,24 +120,6 @@
 
     <issue
         id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in constructor ViewPortBasedSnappingLayoutInfoProvider has parameter &apos;viewPortStep&apos; with type Function0&lt;Float>."
-        errorLine1="    private val viewPortStep: () -> Float"
-        errorLine2="                              ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method SnappingDemoMainLayout has parameter &apos;content&apos; with type Function1&lt;? super Integer, Unit>."
-        errorLine1="    content: @Composable (Int) -> Unit"
-        errorLine2="             ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in variable &apos;setDemoIndex&apos; with type Function1&lt;? super Integer, ? extends Unit>."
         errorLine1="    val (currentDemoIndex, setDemoIndex) = rememberSaveable { mutableIntStateOf(-1) }"
         errorLine2="                           ~~~~~~~~~~~~">
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
index 971cc2b..4a07721 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
@@ -51,6 +51,12 @@
     ComposableDemo("Draggable, Scrollable, Zoomable, Focusable") { HighLevelGesturesDemo() }
 )
 
+private val NestedScrollDemos = listOf(
+    ComposableDemo("Nested Scroll") { NestedScrollDemo() },
+    ComposableDemo("Nested Scroll Connection") { NestedScrollConnectionSample() },
+    ComposableDemo("Nested Scroll Simple Column") { SimpleColumnNestedScrollSample() },
+)
+
 val FoundationDemos = DemoCategory(
     "Foundation",
     listOf(
@@ -60,14 +66,14 @@
         ComposableDemo("Vertical scroll") { VerticalScrollExample() },
         ComposableDemo("Controlled Scrollable Row") { ControlledScrollableRowSample() },
         ComposableDemo("Draw Modifiers") { DrawModifiersDemo() },
+        ComposableDemo("Graphics Surfaces") { GraphicsSurfaceDemo() },
         DemoCategory("Lazy lists", LazyListDemos),
         DemoCategory("Snapping", SnappingDemos),
         DemoCategory("Pagers", PagerDemos),
         ComposableDemo("Simple InteractionSource") { SimpleInteractionSourceSample() },
         ComposableDemo("Flow InteractionSource") { InteractionSourceFlowSample() },
         DemoCategory("Suspending Gesture Detectors", CoroutineGestureDemos),
-        ComposableDemo("Nested Scroll") { NestedScrollDemo() },
-        ComposableDemo("Nested Scroll Connection") { NestedScrollConnectionSample() },
+        DemoCategory("Nested Scroll", NestedScrollDemos),
         DemoCategory("Relocation Demos", RelocationDemos),
         DemoCategory("Focus Demos", FocusDemos),
         DemoCategory("Magnifier Demos", MagnifierDemos),
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/GraphicsSurfaceDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/GraphicsSurfaceDemo.kt
new file mode 100644
index 0000000..4cc8c77
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/GraphicsSurfaceDemo.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2019 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.demos
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.samples.EmbeddedGraphicsSurfaceColors
+import androidx.compose.foundation.samples.GraphicsSurfaceColors
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun GraphicsSurfaceDemo() {
+    Column(Modifier.verticalScroll(rememberScrollState())) {
+        Text("GraphicsSurface:")
+        GraphicsSurfaceColors()
+        Spacer(Modifier.height(50.dp))
+        Text("EmbeddedGraphicsSurface:")
+        EmbeddedGraphicsSurfaceColors()
+    }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/MagnifierDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/MagnifierDemos.kt
index 0121df1..3a98a7e 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/MagnifierDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/MagnifierDemos.kt
@@ -16,12 +16,11 @@
 
 package androidx.compose.foundation.demos
 
+import android.os.Build
 import androidx.compose.animation.animateColor
 import androidx.compose.animation.core.infiniteRepeatable
 import androidx.compose.animation.core.rememberInfiniteTransition
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.MagnifierStyle
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
@@ -59,14 +58,7 @@
     ComposableDemo("Multitouch Custom Magnifier") { MultitouchCustomMagnifierDemo() },
 )
 
-@OptIn(ExperimentalFoundationApi::class)
-private val DemoMagnifierStyle = MagnifierStyle(
-    size = DpSize(100.dp, 100.dp),
-    cornerRadius = 50.dp,
-)
-
 @Preview
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 fun MultitouchCustomMagnifierDemo() {
     // Track the offset for every pointer ID that is currently "down".
@@ -86,7 +78,7 @@
             style = TextStyle(textAlign = TextAlign.Center),
             modifier = Modifier.fillMaxWidth()
         )
-        if (!DemoMagnifierStyle.isSupported) {
+        if (Build.VERSION.SDK_INT < 28) {
             Text(
                 "Magnifier not supported on this platform.",
                 color = Color.Red,
@@ -150,7 +142,8 @@
                                     ?: Offset.Zero
                             },
                             zoom = 3f,
-                            style = DemoMagnifierStyle
+                            size = DpSize(100.dp, 100.dp),
+                            cornerRadius = 50.dp,
                         )
                     )
                 }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/NestedScrollDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/NestedScrollDemos.kt
index fe39345..f9900e2 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/NestedScrollDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/NestedScrollDemos.kt
@@ -18,6 +18,8 @@
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
@@ -106,6 +108,7 @@
                 contentAlignment = Alignment.Center
             ) {
                 Text(
+                    modifier = Modifier.focusable(),
                     text = "$outerOuterIndex : $outerIndex : $innerIndex",
                     fontSize = 24.sp
                 )
@@ -145,9 +148,55 @@
                 items(100) { index ->
                     Text("I'm item $index", modifier = Modifier
                         .fillMaxWidth()
+                        .focusable()
                         .padding(16.dp))
                 }
             }
         }
     }
 }
+
+@Composable
+fun SimpleColumnNestedScrollSample() {
+    val scrollState = rememberScrollState()
+
+    Column(
+        Modifier
+            .fillMaxWidth()
+            .background(Color.Red)
+            .verticalScroll(scrollState),
+        verticalArrangement = Arrangement.spacedBy(20.dp)
+    ) {
+        Text(
+            modifier = Modifier.fillMaxWidth(),
+            text = "Outer Scrollable Column"
+        )
+
+        for (i in 0 until 4) {
+            SimpleColumn("Inner Scrollable Column: $i")
+        }
+    }
+}
+
+@Composable
+fun SimpleColumn(label: String) {
+    Column(
+        Modifier
+            .fillMaxWidth()
+            .height(200.dp)
+            .background(Color.Green)
+            .verticalScroll(rememberScrollState())
+    ) {
+        Text(
+            modifier = Modifier.fillMaxWidth(),
+            text = "$label INNER, scrollable only"
+        )
+
+        for (i in 0 until 20) {
+            Text(
+                modifier = Modifier.fillMaxWidth().focusable(),
+                text = "Text $i",
+            )
+        }
+    }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyGridSnappingDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyGridSnappingDemos.kt
index 86151d9..3f84793 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyGridSnappingDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyGridSnappingDemos.kt
@@ -42,12 +42,16 @@
 import androidx.compose.ui.unit.sp
 
 val LazyGridSnappingDemos = listOf(
-    ComposableDemo("Single Page - Same Size Pages") { GridSinglePageSnapping() },
+    ComposableDemo("Single Item - Same Size Items") { GridSingleItemSnapping() },
 )
 
+/**
+ * Snapping happens to the next item and items have the same size. We use the top line in the grid
+ * as a reference point.
+ */
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun GridSinglePageSnapping() {
+private fun GridSingleItemSnapping() {
     val lazyGridState = rememberLazyGridState()
     val snappingLayout = remember(lazyGridState) { SnapLayoutInfoProvider(lazyGridState) }
     val flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider = snappingLayout)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyListSnappingDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyListSnappingDemos.kt
index 962eaf7..b35a1e1 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyListSnappingDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyListSnappingDemos.kt
@@ -27,23 +27,26 @@
 import androidx.compose.integration.demos.common.ComposableDemo
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
-import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastSumBy
 
 val LazyListSnappingDemos = listOf(
-    ComposableDemo("Single Page - Same Size Pages") { SamePageSizeDemo() },
-    ComposableDemo("Single Page - Multi-Size Pages") { MultiSizePageDemo() },
-    ComposableDemo("Single Page - Large Pages") { LargePageSizeDemo() },
-    ComposableDemo("Single Page - List with Content padding") { DifferentContentPaddingDemo() },
-    ComposableDemo("Multi Page - Animation Based Offset") { MultiPageSnappingDemo() },
-    ComposableDemo("Multi Page - View Port Based Offset") { ViewPortBasedSnappingDemo() },
+    ComposableDemo("Single Item - Same Size Items") { SameItemSizeDemo() },
+    ComposableDemo("Single Item - Different Size Item") { DifferentItemSizeDemo() },
+    ComposableDemo("Single Item - Large Items") { LargeItemSizeDemo() },
+    ComposableDemo("Single Item - List with Content padding") { DifferentContentPaddingDemo() },
+    ComposableDemo("Multi Item - Decayed Snapping") { DecayedSnappingDemo() },
+    ComposableDemo("Multi Item - View Port Based Offset") { ViewPortBasedSnappingDemo() },
 )
 
+/**
+ * Snapping happens to the next item and items have the same size
+ */
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun SamePageSizeDemo() {
+private fun SameItemSizeDemo() {
     val lazyListState = rememberLazyListState()
-    val layoutInfoProvider = rememberNextPageSnappingLayoutInfoProvider(lazyListState)
+    val layoutInfoProvider = rememberNextItemSnappingLayoutInfoProvider(lazyListState)
     val flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
 
     SnappingDemoMainLayout(
@@ -54,11 +57,14 @@
     }
 }
 
+/**
+ * Snapping happens to the next item and items have the different sizes
+ */
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun MultiSizePageDemo() {
+private fun DifferentItemSizeDemo() {
     val lazyListState = rememberLazyListState()
-    val layoutInfoProvider = rememberNextPageSnappingLayoutInfoProvider(lazyListState)
+    val layoutInfoProvider = rememberNextItemSnappingLayoutInfoProvider(lazyListState)
     val flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
 
     SnappingDemoMainLayout(lazyListState = lazyListState, flingBehavior = flingBehavior) {
@@ -70,11 +76,14 @@
     }
 }
 
+/**
+ * Snapping happens to the next item and items are larger than the view port
+ */
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun LargePageSizeDemo() {
+private fun LargeItemSizeDemo() {
     val lazyListState = rememberLazyListState()
-    val layoutInfoProvider = rememberNextPageSnappingLayoutInfoProvider(lazyListState)
+    val layoutInfoProvider = rememberNextItemSnappingLayoutInfoProvider(lazyListState)
     val flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
 
     SnappingDemoMainLayout(lazyListState = lazyListState, flingBehavior = flingBehavior) {
@@ -86,6 +95,9 @@
     }
 }
 
+/**
+ * Snapping happens to the next item and list has content paddings
+ */
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun DifferentContentPaddingDemo() {
@@ -103,9 +115,12 @@
     }
 }
 
+/**
+ * Snapping happens after a decay animation and items have the same size
+ */
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun MultiPageSnappingDemo() {
+private fun DecayedSnappingDemo() {
     val lazyListState = rememberLazyListState()
     val flingBehavior = rememberSnapFlingBehavior(lazyListState)
     SnappingDemoMainLayout(lazyListState = lazyListState, flingBehavior = flingBehavior) {
@@ -113,6 +128,9 @@
     }
 }
 
+/**
+ * Snapping happens to at max one view port item's worth distance.
+ */
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun ViewPortBasedSnappingDemo() {
@@ -127,13 +145,13 @@
 
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun rememberNextPageSnappingLayoutInfoProvider(
+private fun rememberNextItemSnappingLayoutInfoProvider(
     state: LazyListState
 ): SnapLayoutInfoProvider {
     return remember(state) {
         val basedSnappingLayoutInfoProvider = SnapLayoutInfoProvider(lazyListState = state)
         object : SnapLayoutInfoProvider by basedSnappingLayoutInfoProvider {
-            override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
+            override fun calculateApproachOffset(initialVelocity: Float): Float {
                 return 0f
             }
         }
@@ -150,6 +168,10 @@
         ViewPortBasedSnappingLayoutInfoProvider(
             SnapLayoutInfoProvider(lazyListState = state),
             decayAnimationSpec,
+            {
+                val visibleItemsSum = state.layoutInfo.visibleItemsInfo.fastSumBy { it.size }
+                visibleItemsSum / state.layoutInfo.visibleItemsInfo.size.toFloat()
+            }
         ) { state.layoutInfo.viewportSize.width.toFloat() }
     }
 }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/NonItemBasedSnapping.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/NonItemBasedSnapping.kt
new file mode 100644
index 0000000..f982d18
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/NonItemBasedSnapping.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.foundation.demos.snapping
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
+import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+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.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import kotlin.math.abs
+
+/**
+ * A provider that doesn't use the concept of items for snapping.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+class NonItemBasedSnappingLayoutInfoProvider(
+    private val currentOffset: Int,
+    layoutSize: Int,
+    thumbSize: Int
+) : SnapLayoutInfoProvider {
+
+    // start, middle, end of the layout
+    private val offsetList = listOf(0, layoutSize / 2 - thumbSize / 2, (layoutSize - thumbSize))
+
+    // do not approach, our snapping positions are discrete.
+    override fun calculateApproachOffset(initialVelocity: Float): Float = 0f
+
+    override fun calculateSnappingOffset(currentVelocity: Float): Float {
+        val targetOffset = if (currentVelocity == 0.0f) {
+            // snap to closest offset
+            var closestOffset = 0
+            var prevMinAbs = Int.MAX_VALUE
+            offsetList.forEach {
+                val absDistance = abs(currentOffset - it)
+                if (absDistance < prevMinAbs) {
+                    prevMinAbs = absDistance
+                    closestOffset = it
+                }
+            }
+            (closestOffset).toFloat()
+        } else if (currentVelocity > 0) {
+            // snap to the next offset
+            val offset = offsetList.firstOrNull { it > currentOffset }
+            (offset ?: 0).toFloat() // if offset is found, move there, if not, don't move
+        } else {
+            // snap to the previous offset
+            val offset = offsetList.reversed().firstOrNull { it < currentOffset }
+            (offset ?: 0).toFloat() // if offset is found, move there, if not, don't move
+        }
+        return targetOffset - currentOffset // distance that needs to be consumed to reach target
+    }
+}
+
+private val ThumbSize = 60.dp
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun NonItemBasedLayout() {
+    var thumbOffset by remember { mutableStateOf(IntOffset.Zero) }
+    var layoutSize by remember { mutableIntStateOf(0) }
+
+    val thumbSize = with(LocalDensity.current) { ThumbSize.roundToPx() }
+    val maxPosition = with(LocalDensity.current) {
+        layoutSize - ThumbSize.roundToPx()
+    }
+
+    val snapLayoutInfoProvider = remember(thumbOffset, layoutSize, thumbSize) {
+        NonItemBasedSnappingLayoutInfoProvider(thumbOffset.x, layoutSize, thumbSize)
+    }
+
+    val fling = rememberSnapFlingBehavior(snapLayoutInfoProvider = snapLayoutInfoProvider)
+    val scrollableState = rememberScrollableState {
+        val previousPosition = thumbOffset.x
+        val newPosition = (previousPosition + it).coerceIn(0.0f, maxPosition.toFloat()).toInt()
+        thumbOffset = thumbOffset.copy(x = newPosition)
+        it // need to return correct consumption
+    }
+    Box(
+        modifier = Modifier
+            .requiredHeight(ThumbSize)
+            .fillMaxWidth()
+            .background(Color.LightGray)
+            .scrollable(
+                scrollableState,
+                orientation = Orientation.Horizontal,
+                flingBehavior = fling
+            )
+            .onSizeChanged {
+                layoutSize = it.width
+            }
+    ) {
+        Box(modifier = Modifier
+            .offset { thumbOffset }
+            .requiredSize(ThumbSize)
+            .background(Color.Red))
+    }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt
index f2e5bae..375e80a 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt
@@ -19,47 +19,44 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
-import androidx.compose.ui.unit.Density
 import kotlin.math.abs
 import kotlin.math.ceil
 import kotlin.math.floor
 import kotlin.math.roundToInt
 import kotlin.math.sign
 
+@Suppress("PrimitiveInLambda")
 @OptIn(ExperimentalFoundationApi::class)
 fun SnapLayoutInfoProvider(
     scrollState: ScrollState,
-    itemSize: Density.() -> Float,
-    layoutSize: Density.() -> Float
+    itemSize: () -> Float,
+    layoutSize: () -> Float
 ) = object : SnapLayoutInfoProvider {
 
-    fun Density.nextFullItemCenter(layoutCenter: Float): Float {
+    fun nextFullItemCenter(layoutCenter: Float): Float {
         val intItemSize = itemSize().roundToInt()
-        return floor((layoutCenter + calculateSnapStepSize()) / itemSize().roundToInt()) *
+        return floor((layoutCenter + itemSize()) / itemSize().roundToInt()) *
             intItemSize
     }
 
-    fun Density.previousFullItemCenter(layoutCenter: Float): Float {
+    fun previousFullItemCenter(layoutCenter: Float): Float {
         val intItemSize = itemSize().roundToInt()
-        return ceil((layoutCenter - calculateSnapStepSize()) / itemSize().roundToInt()) *
+        return ceil((layoutCenter - itemSize()) / itemSize().roundToInt()) *
             intItemSize
     }
 
-    override fun Density.calculateSnappingOffset(currentVelocity: Float): Float {
-        val layoutCenter = layoutSize() / 2f + scrollState.value + calculateSnapStepSize() / 2f
+    override fun calculateSnappingOffset(currentVelocity: Float): Float {
+        val layoutCenter = layoutSize() / 2f + scrollState.value + itemSize() / 2f
         val lowerBound = nextFullItemCenter(layoutCenter) - layoutCenter
         val upperBound = previousFullItemCenter(layoutCenter) - layoutCenter
 
         return calculateFinalOffset(currentVelocity, upperBound, lowerBound)
     }
 
-    override fun Density.calculateSnapStepSize(): Float {
-        return itemSize()
-    }
-
-    override fun Density.calculateApproachOffset(initialVelocity: Float): Float = 0f
+    override fun calculateApproachOffset(initialVelocity: Float): Float = 0f
 }
 
+@Suppress("PrimitiveInLambda")
 internal fun calculateFinalOffset(velocity: Float, lowerBound: Float, upperBound: Float): Float {
 
     fun Float.isValidDistance(): Boolean {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt
index e28f6a6..aace1fa 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation.demos.snapping
 
 import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.calculateTargetValue
 import androidx.compose.animation.rememberSplineBasedDecay
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.ScrollState
@@ -44,17 +45,21 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import kotlin.math.absoluteValue
+import kotlin.math.sign
 
 val RowSnappingDemos = listOf(
-    ComposableDemo("Single Page - Same Size Pages") { SinglePageSnapping() },
-    ComposableDemo("Multi Page - Animation Based Offset") { MultiPageSnappingDemo() },
-    ComposableDemo("Multi Page - View Port Based Offset") { ViewPortBasedSnappingDemo() },
+    ComposableDemo("Single Item - Same Size Items") { SinglePageSnapping() },
+    ComposableDemo("Multi Item - Decayed Snapping") { DecayedSnappingDemo() },
+    ComposableDemo("Multi Item - View Port Based Offset") { ViewPortBasedSnappingDemo() },
 )
 
+/**
+ * Snapping happens to the next item and items have the same size
+ */
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun SinglePageSnapping() {
@@ -67,30 +72,37 @@
     RowSnappingMainLayout(snapFlingBehavior, scrollState) { layoutSizeState.value = it }
 }
 
+/**
+ * Snapping happens after a decay animation. Items have the same size.
+ */
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun MultiPageSnappingDemo() {
+private fun DecayedSnappingDemo() {
     val scrollState = rememberScrollState()
     val layoutSizeState = remember { mutableStateOf(IntSize.Zero) }
-    val layoutInfoProvider = rememberMultiPageRowSnapLayoutInfoProvider(scrollState) {
+    val layoutInfoProvider = rememberDecayedSnappingLayoutInfoProvider(scrollState) {
         layoutSizeState.value.width.toFloat()
     }
     val snapFlingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider = layoutInfoProvider)
     RowSnappingMainLayout(snapFlingBehavior, scrollState) { layoutSizeState.value = it }
 }
 
+/**
+ * Snapping happens to at max one view port item's worth distance.
+ */
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun ViewPortBasedSnappingDemo() {
     val scrollState = rememberScrollState()
     val layoutSizeState = remember { mutableStateOf(IntSize.Zero) }
-    val layoutInfoProvider = rememberViewPortRowSnapLayoutInfoProvider(scrollState) {
+    val layoutInfoProvider = rememberViewPortSnapLayoutInfoProvider(scrollState) {
         layoutSizeState.value.width.toFloat()
     }
     val snapFlingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider = layoutInfoProvider)
     RowSnappingMainLayout(snapFlingBehavior, scrollState) { layoutSizeState.value = it }
 }
 
+@Suppress("PrimitiveInLambda")
 @Composable
 private fun RowSnappingMainLayout(
     snapFlingBehavior: FlingBehavior,
@@ -137,57 +149,97 @@
     }
 }
 
+@Suppress("PrimitiveInLambda")
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun rememberRowSnapLayoutInfoProvider(
     scrollState: ScrollState,
-    layoutSize: Density.() -> Float
+    layoutSize: () -> Float
 ): SnapLayoutInfoProvider {
+    val density = LocalDensity.current
     return remember(scrollState, layoutSize) {
         SnapLayoutInfoProvider(
             scrollState = scrollState,
-            itemSize = { RowItemSize.toPx() },
+            itemSize = { with(density) { RowItemSize.toPx() } },
             layoutSize = layoutSize
         )
     }
 }
 
+@Suppress("PrimitiveInLambda")
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun rememberMultiPageRowSnapLayoutInfoProvider(
+private fun rememberDecayedSnappingLayoutInfoProvider(
     scrollState: ScrollState,
-    layoutSize: Density.() -> Float
+    layoutSize: () -> Float
 ): SnapLayoutInfoProvider {
     val animation: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
+    val density = LocalDensity.current
+    val scrollStateLayoutInfoProvider = SnapLayoutInfoProvider(
+        scrollState = scrollState,
+        itemSize = { with(density) { RowItemSize.toPx() } },
+        layoutSize = layoutSize
+    )
     return remember(scrollState, layoutSize) {
-        MultiPageSnappingLayoutInfoProvider(
-            SnapLayoutInfoProvider(
-                scrollState = scrollState,
-                itemSize = { RowItemSize.toPx() },
-                layoutSize = layoutSize
-            ),
-            animation
+        DecayedSnappingLayoutInfoProvider(
+            scrollStateLayoutInfoProvider,
+            animation,
+        ) { with(density) { RowItemSize.toPx() } }
+    }
+}
+
+@Suppress("PrimitiveInLambda")
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun rememberViewPortSnapLayoutInfoProvider(
+    scrollState: ScrollState,
+    layoutSize: () -> Float
+): SnapLayoutInfoProvider {
+    val density = LocalDensity.current
+    val decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
+    val baseSnapLayoutInfoProvider = rememberScrollStateLayoutInfoProvider(
+        scrollState = scrollState,
+        layoutSize = layoutSize
+    )
+
+    return remember(baseSnapLayoutInfoProvider, density, layoutSize) {
+        ViewPortBasedSnappingLayoutInfoProvider(
+            baseSnapLayoutInfoProvider,
+            decayAnimationSpec,
+            viewPortStep = layoutSize,
+            itemSize = { with(density) { RowItemSize.toPx() } }
         )
     }
 }
 
+@Suppress("PrimitiveInLambda")
+@OptIn(ExperimentalFoundationApi::class)
+internal class DecayedSnappingLayoutInfoProvider(
+    private val baseSnapLayoutInfoProvider: SnapLayoutInfoProvider,
+    private val decayAnimationSpec: DecayAnimationSpec<Float>,
+    private val itemSize: () -> Float
+) : SnapLayoutInfoProvider by baseSnapLayoutInfoProvider {
+    override fun calculateApproachOffset(initialVelocity: Float): Float {
+        val offset = decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
+        val finalDecayedOffset = (offset.absoluteValue - itemSize()).coerceAtLeast(0f)
+        return finalDecayedOffset * initialVelocity.sign
+    }
+}
+
+@Suppress("PrimitiveInLambda")
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun rememberViewPortRowSnapLayoutInfoProvider(
+private fun rememberScrollStateLayoutInfoProvider(
     scrollState: ScrollState,
-    layoutSize: Density.() -> Float
+    layoutSize: () -> Float
 ): SnapLayoutInfoProvider {
     val density = LocalDensity.current
-    val decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
-    return remember(scrollState, density, layoutSize) {
-        ViewPortBasedSnappingLayoutInfoProvider(
-            SnapLayoutInfoProvider(
-                scrollState = scrollState,
-                itemSize = { RowItemSize.toPx() },
-                layoutSize = layoutSize
-            ),
-            decayAnimationSpec
-        ) { with(density, layoutSize) }
+    return remember(scrollState, layoutSize, density) {
+        SnapLayoutInfoProvider(
+            scrollState = scrollState,
+            itemSize = { with(density) { RowItemSize.toPx() } },
+            layoutSize = layoutSize
+        )
     }
 }
 
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt
index f4e5e58..0605ba4 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt
@@ -16,12 +16,8 @@
 
 package androidx.compose.foundation.demos.snapping
 
-import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.core.calculateTargetValue
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.calculateEndPadding
@@ -32,6 +28,7 @@
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.integration.demos.common.ComposableDemo
 import androidx.compose.integration.demos.common.DemoCategory
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
@@ -42,45 +39,21 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import kotlin.math.absoluteValue
-import kotlin.math.sign
 
 val SnappingDemos = listOf(
     DemoCategory("Lazy List Snapping", LazyListSnappingDemos),
     DemoCategory("Scrollable Row Snapping", RowSnappingDemos),
     DemoCategory("Lazy Grid Snapping", LazyGridSnappingDemos),
+    ComposableDemo("Non Item based Snapping") {
+        NonItemBasedLayout()
+    },
 )
 
-@OptIn(ExperimentalFoundationApi::class)
-internal class MultiPageSnappingLayoutInfoProvider(
-    private val baseSnapLayoutInfoProvider: SnapLayoutInfoProvider,
-    private val decayAnimationSpec: DecayAnimationSpec<Float>
-) : SnapLayoutInfoProvider by baseSnapLayoutInfoProvider {
-    override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
-        val offset = decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
-        val finalDecayedOffset = (offset.absoluteValue - calculateSnapStepSize()).coerceAtLeast(0f)
-        return finalDecayedOffset * initialVelocity.sign
-    }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-internal class ViewPortBasedSnappingLayoutInfoProvider(
-    private val baseSnapLayoutInfoProvider: SnapLayoutInfoProvider,
-    private val decayAnimationSpec: DecayAnimationSpec<Float>,
-    private val viewPortStep: () -> Float
-) : SnapLayoutInfoProvider by baseSnapLayoutInfoProvider {
-    override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
-        val offset = decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
-        val viewPortOffset = viewPortStep()
-        return offset.coerceIn(-viewPortOffset, viewPortOffset)
-    }
-}
-
+@Suppress("PrimitiveInLambda")
 @Composable
 internal fun SnappingDemoMainLayout(
     lazyListState: LazyListState,
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemosCommon.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemosCommon.kt
new file mode 100644
index 0000000..34f8703
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemosCommon.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.foundation.demos.snapping
+
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
+import kotlin.math.absoluteValue
+import kotlin.math.sign
+
+@Suppress("PrimitiveInLambda")
+@OptIn(ExperimentalFoundationApi::class)
+internal class ViewPortBasedSnappingLayoutInfoProvider(
+    private val baseSnapLayoutInfoProvider: SnapLayoutInfoProvider,
+    private val decayAnimationSpec: DecayAnimationSpec<Float>,
+    private val viewPortStep: () -> Float,
+    private val itemSize: () -> Float
+) : SnapLayoutInfoProvider by baseSnapLayoutInfoProvider {
+    override fun calculateApproachOffset(initialVelocity: Float): Float {
+        val offset = decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
+        val finalOffset = (offset.absoluteValue - itemSize()).coerceAtLeast(0.0f) * offset.sign
+        val viewPortOffset = viewPortStep()
+        return finalOffset.coerceIn(-viewPortOffset, viewPortOffset)
+    }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionScrollable.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionScrollable.kt
new file mode 100644
index 0000000..d46aded
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionScrollable.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2020 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.demos.text
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.RadioButton
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@SuppressLint("PrimitiveInLambda")
+@Preview
+@Composable
+fun TextScrollableColumnSelectionDemo() {
+    val spacing = 16.dp
+    Column(
+        modifier = Modifier.padding(spacing),
+        verticalArrangement = spacedBy(spacing)
+    ) {
+        Text(
+            text = "We expect that selection works, " +
+                "regardless of how many times each text goes in or out of view. " +
+                "The selection handles and text toolbar also should follow the selection " +
+                "when it is scrolled.",
+            style = MaterialTheme.typography.body1.merge(),
+        )
+        val (selectedOption, onOptionSelected) = remember {
+            mutableStateOf(Options.LongScrollableText)
+        }
+        Column(Modifier.selectableGroup()) {
+            Options.values().forEach { option ->
+                Row(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .selectable(
+                            selected = option == selectedOption,
+                            onClick = { onOptionSelected(option) }
+                        ),
+                    verticalAlignment = Alignment.CenterVertically,
+                ) {
+                    RadioButton(
+                        selected = option == selectedOption,
+                        onClick = { onOptionSelected(option) }
+                    )
+                    Text(
+                        text = option.displayText,
+                        style = MaterialTheme.typography.body1.merge(),
+                    )
+                }
+            }
+        }
+        selectedOption.Content()
+    }
+}
+
+@Suppress("unused") // enum values used in .values()
+private enum class Options(val displayText: String, val content: @Composable () -> Unit) {
+    LongScrollableText("Long Scrollable Single Text", {
+        MyText(
+            modifier = Modifier.verticalScroll(rememberScrollState()),
+            text = (0..100).joinToString(separator = "\n") { it.toString() },
+        )
+    }),
+    LongTextScrollableColumn("Long Single Text in Scrollable Column", {
+        Column(Modifier.verticalScroll(rememberScrollState())) {
+            MyText((0..100).joinToString(separator = "\n") { it.toString() })
+        }
+    }),
+    MultiTextScrollableColumn("Multiple Texts in Scrollable Column", {
+        Column(Modifier.verticalScroll(rememberScrollState())) {
+            repeat(100) { MyText(it.toString()) }
+        }
+    }),
+    MultiTextLazyColumn("Multiple Texts in LazyColumn", {
+        LazyColumn {
+            items(100) { MyText(it.toString()) }
+        }
+    });
+
+    @Composable
+    fun Content() {
+        SelectionContainer(content = content)
+    }
+}
+
+@Composable
+private fun MyText(text: String, modifier: Modifier = Modifier) {
+    Text(
+        text = text,
+        style = TextStyle(fontSize = fontSize8, textAlign = TextAlign.Center),
+        modifier = modifier.fillMaxWidth()
+    )
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 8b575e1..ef5b5ac 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -152,6 +152,9 @@
                 ComposableDemo("Text selection") { TextSelectionDemo() },
                 ComposableDemo("Text selection sample") { TextSelectionSample() },
                 ComposableDemo("Overflowed Selection") { TextOverflowedSelectionDemo() },
+                ComposableDemo("Scrollable Column Text Selection") {
+                    TextScrollableColumnSelectionDemo()
+                },
             )
         ),
         DemoCategory(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicSecureTextFieldDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicSecureTextFieldDemos.kt
index c8d01b0..37ecac6 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicSecureTextFieldDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicSecureTextFieldDemos.kt
@@ -30,8 +30,10 @@
 import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.foundation.text2.input.TextObfuscationMode
 import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Button
 import androidx.compose.material.Icon
 import androidx.compose.material.IconToggleButton
+import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Info
 import androidx.compose.material.icons.filled.Warning
@@ -42,6 +44,8 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.unit.dp
@@ -55,6 +59,11 @@
             .imePadding()
             .verticalScroll(rememberScrollState())
     ) {
+        val clipboardManager = LocalClipboardManager.current
+        Button(onClick = { clipboardManager.setText(AnnotatedString("\uD801\uDC37")) }) {
+            Text("Copy surrogate pair \"\uD801\uDC37\"")
+        }
+
         TagLine(tag = "Visible")
         BasicSecureTextFieldDemo(TextObfuscationMode.Visible)
 
diff --git a/compose/foundation/foundation/lint-baseline.xml b/compose/foundation/foundation/lint-baseline.xml
index 21f098b..40e6813 100644
--- a/compose/foundation/foundation/lint-baseline.xml
+++ b/compose/foundation/foundation/lint-baseline.xml
@@ -1,14 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="cli" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 28 (current min is 21): `PlatformMagnifierImpl`"
-        errorLine1="                    ) as PlatformMagnifierFactoryApi28Impl.PlatformMagnifierImpl"
-        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/foundation/PlatformMagnifierTest.kt"/>
-    </issue>
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="BanSuppressTag"
@@ -34,7 +25,7 @@
         errorLine1="            Thread.sleep(5)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt"/>
     </issue>
 
     <issue
@@ -43,7 +34,7 @@
         errorLine1="            Thread.sleep(5)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt"/>
     </issue>
 
     <issue
@@ -52,25 +43,7 @@
         errorLine1="            Thread.sleep(5)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt"/>
-    </issue>
-
-    <issue
-        id="ExperimentalPropertyAnnotation"
-        message="This property does not have all required annotations to correctly mark it as experimental."
-        errorLine1="        @ExperimentalFoundationApi"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/foundation/Magnifier.kt"/>
-    </issue>
-
-    <issue
-        id="ExperimentalPropertyAnnotation"
-        message="This property does not have all required annotations to correctly mark it as experimental."
-        errorLine1="        @ExperimentalFoundationApi"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/foundation/Magnifier.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt"/>
     </issue>
 
     <issue
@@ -156,24 +129,6 @@
 
     <issue
         id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method BasicSecureTextField has parameter &apos;onSubmit&apos; with type Function1&lt;? super ImeAction, Boolean>."
-        errorLine1="    onSubmit: ((ImeAction) -> Boolean)? = null,"
-        errorLine2="              ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/foundation/text2/BasicSecureTextField.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method KeyboardActions has parameter &apos;onSubmit&apos; with type Function1&lt;? super ImeAction, Boolean>."
-        errorLine1="private fun KeyboardActions(onSubmit: (ImeAction) -> Boolean) = KeyboardActions("
-        errorLine2="                                      ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/foundation/text2/BasicSecureTextField.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method ClickableText has parameter &apos;onClick&apos; with type Function1&lt;? super Integer, Unit>."
         errorLine1="    onClick: (Int) -> Unit"
         errorLine2="             ~~~~~~~~~~~~~">
@@ -1264,7 +1219,7 @@
     <issue
         id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method magnifier has parameter &apos;onSizeChanged&apos; with type Function1&lt;? super DpSize, Unit>."
-        errorLine1="    onSizeChanged: ((DpSize) -> Unit)? = null"
+        errorLine1="    onSizeChanged: ((DpSize) -> Unit)? = null,"
         errorLine2="                   ~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/androidMain/kotlin/androidx/compose/foundation/Magnifier.kt"/>
@@ -1470,42 +1425,6 @@
 
     <issue
         id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method HorizontalPager has parameter &apos;key&apos; with type Function1&lt;? super Integer, ? extends Object>."
-        errorLine1="    key: ((index: Int) -> Any)? = null,"
-        errorLine2="         ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method HorizontalPager has parameter &apos;pageContent&apos; with type Function2&lt;? super PagerScope, ? super Integer, Unit>."
-        errorLine1="    pageContent: @Composable PagerScope.(page: Int) -> Unit"
-        errorLine2="                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method VerticalPager has parameter &apos;key&apos; with type Function1&lt;? super Integer, ? extends Object>."
-        errorLine1="    key: ((index: Int) -> Any)? = null,"
-        errorLine2="         ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method VerticalPager has parameter &apos;pageContent&apos; with type Function2&lt;? super PagerScope, ? super Integer, Unit>."
-        errorLine1="    pageContent: @Composable PagerScope.(page: Int) -> Unit"
-        errorLine2="                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method VerticalPager has parameter &apos;key&apos; with type Function1&lt;? super Integer, ? extends Object>."
         errorLine1="    key: ((index: Int) -> Any)? = null,"
         errorLine2="         ~~~~~~~~~~~~~~~~~~~~~~">
@@ -1686,15 +1605,6 @@
 
     <issue
         id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method adjustByBoundary has parameter &apos;boundaryFun&apos; with type Function1&lt;? super Integer, TextRange>."
-        errorLine1="            boundaryFun: (Int) -> TextRange"
-        errorLine2="                         ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustment.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method animatedSelectionMagnifier has parameter &apos;magnifierCenter&apos; with type Function0&lt;Offset>."
         errorLine1="    magnifierCenter: () -> Offset,"
         errorLine2="                     ~~~~~~~~~~~~">
@@ -1722,15 +1632,6 @@
 
     <issue
         id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in variable &apos;getMagnifierCenter&apos; with type Function2&lt;? super AnchorInfo, ? super Boolean, ? extends Offset>."
-        errorLine1="    fun getMagnifierCenter(anchor: AnchorInfo, isStartHandle: Boolean): Offset {"
-        errorLine2="    ^">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method setOnPositionChangeCallback$lint_module has parameter &apos;&lt;set-?>&apos; with type Function1&lt;? super Long, Unit>."
         errorLine1="    /**"
         errorLine2="    ^">
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/GraphicsSurfaceSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/GraphicsSurfaceSamples.kt
new file mode 100644
index 0000000..dc63c5f
--- /dev/null
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/GraphicsSurfaceSamples.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.foundation.samples
+
+import android.graphics.Rect
+import androidx.annotation.Sampled
+import androidx.compose.foundation.EmbeddedGraphicsSurface
+import androidx.compose.foundation.GraphicsSurface
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.unit.dp
+import kotlin.math.sin
+
+@Sampled
+@Composable
+fun GraphicsSurfaceColors() {
+    GraphicsSurface(
+        modifier = Modifier.fillMaxWidth().height(400.dp)
+    ) {
+        // Resources can be initialized/cached here
+
+        // A surface is available, we can start rendering
+        onSurface { surface, width, height ->
+            var w = width
+            var h = height
+
+            // Initial draw to avoid a black frame
+            surface.lockCanvas(Rect(0, 0, w, h)).apply {
+                drawColor(Color.Blue.toArgb())
+                surface.unlockCanvasAndPost(this)
+            }
+
+            // React to surface dimension changes
+            surface.onChanged { newWidth, newHeight ->
+                w = newWidth
+                h = newHeight
+            }
+
+            // Cleanup if needed
+            surface.onDestroyed {
+            }
+
+            // Render loop, automatically cancelled by GraphicsSurface
+            // on surface destruction
+            while (true) {
+                withFrameNanos { time ->
+                    surface.lockCanvas(Rect(0, 0, w, h)).apply {
+                        val timeMs = time / 1_000_000L
+                        val t = 0.5f + 0.5f * sin(timeMs / 1_000.0f)
+                        drawColor(lerp(Color.Blue, Color.Green, t).toArgb())
+                        surface.unlockCanvasAndPost(this)
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+@Composable
+fun EmbeddedGraphicsSurfaceColors() {
+    EmbeddedGraphicsSurface(
+        modifier = Modifier.fillMaxWidth().height(400.dp)
+    ) {
+        // Resources can be initialized/cached here
+
+        // A surface is available, we can start rendering
+        onSurface { surface, width, height ->
+            var w = width
+            var h = height
+
+            // Initial draw to avoid a black frame
+            surface.lockCanvas(Rect(0, 0, w, h)).apply {
+                drawColor(Color.Yellow.toArgb())
+                surface.unlockCanvasAndPost(this)
+            }
+
+            // React to surface dimension changes
+            surface.onChanged { newWidth, newHeight ->
+                w = newWidth
+                h = newHeight
+            }
+
+            // Cleanup if needed
+            surface.onDestroyed {
+            }
+
+            // Render loop, automatically cancelled by EmbeddedGraphicsSurface
+            // on surface destruction
+            while (true) {
+                withFrameNanos { time ->
+                    surface.lockCanvas(Rect(0, 0, w, h)).apply {
+                        val timeMs = time / 1_000_000L
+                        val t = 0.5f + 0.5f * sin(timeMs / 1_000.0f)
+                        drawColor(lerp(Color.Yellow, Color.Red, t).toArgb())
+                        surface.unlockCanvasAndPost(this)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/MagnifierSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/MagnifierSamples.kt
index fa88954..d928f08 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/MagnifierSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/MagnifierSamples.kt
@@ -16,9 +16,8 @@
 
 package androidx.compose.foundation.samples
 
+import android.os.Build
 import androidx.annotation.Sampled
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.MagnifierStyle
 import androidx.compose.foundation.gestures.detectDragGestures
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.magnifier
@@ -35,7 +34,6 @@
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.input.pointer.pointerInput
 
-@OptIn(ExperimentalFoundationApi::class)
 @Sampled
 @Composable
 fun MagnifierSample() {
@@ -43,7 +41,7 @@
     // Hide the magnifier until a drag starts.
     var magnifierCenter by remember { mutableStateOf(Offset.Unspecified) }
 
-    if (!MagnifierStyle.Default.isSupported) {
+    if (Build.VERSION.SDK_INT < 28) {
         Text("Magnifier is not supported on this platform.")
     } else {
         Box(
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/PagerSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/PagerSamples.kt
index 979c731..b845444 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/PagerSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/PagerSamples.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation.samples
 
 import androidx.annotation.Sampled
+import androidx.compose.animation.core.animate
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
@@ -30,6 +31,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.pager.HorizontalPager
 import androidx.compose.foundation.pager.PageSize
+import androidx.compose.foundation.pager.PagerState
 import androidx.compose.foundation.pager.VerticalPager
 import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.foundation.rememberScrollState
@@ -130,6 +132,66 @@
 @OptIn(ExperimentalFoundationApi::class)
 @Sampled
 @Composable
+fun PagerCustomAnimateScrollToPage() {
+    suspend fun PagerState.customAnimateScrollToPage(page: Int) {
+        val preJumpPosition = if (page > currentPage) {
+            (page - 1).coerceAtLeast(0)
+        } else {
+            (page + 1).coerceAtMost(pageCount)
+        }
+        scroll {
+            // Update the target page
+            updateTargetPage(page)
+
+            // pre-jump to 1 page before our target page
+            updateCurrentPage(preJumpPosition, 0.0f)
+            val targetPageDiff = page - currentPage
+            val distance = targetPageDiff * layoutInfo.pageSize.toFloat()
+            var previousValue = 0.0f
+            animate(
+                0f,
+                distance,
+            ) { currentValue, _ ->
+                previousValue += scrollBy(currentValue - previousValue)
+            }
+        }
+    }
+
+    val state = rememberPagerState(initialPage = 5) { 10 }
+    val scope = rememberCoroutineScope()
+
+    Column {
+        HorizontalPager(
+            modifier = Modifier
+                .fillMaxSize()
+                .weight(0.9f),
+            state = state
+        ) { page ->
+            Box(
+                modifier = Modifier
+                    .padding(10.dp)
+                    .background(Color.Blue)
+                    .fillMaxWidth()
+                    .aspectRatio(1f),
+                contentAlignment = Alignment.Center
+            ) {
+                Text(text = page.toString(), fontSize = 32.sp)
+            }
+        }
+
+        Button(onClick = {
+            scope.launch {
+                state.customAnimateScrollToPage(1)
+            }
+        }) {
+            Text(text = "Jump to Page 1")
+        }
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Sampled
+@Composable
 fun CustomPageSizeSample() {
 
     // [PageSize] should be defined as a top level constant in order to avoid unnecessary re-
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/MagnifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/MagnifierTest.kt
deleted file mode 100644
index 89438cc..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/MagnifierTest.kt
+++ /dev/null
@@ -1,891 +0,0 @@
-/*
- * Copyright 2021 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
-
-import android.annotation.SuppressLint
-import android.view.View
-import androidx.compose.foundation.MagnifierStyle.Companion.isStyleSupported
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.offset
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.InspectableModifier
-import androidx.compose.ui.platform.InspectableValue
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.ValueElement
-import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
-import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.toSize
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalFoundationApi::class)
-@SuppressLint("NewApi")
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class MagnifierTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Before
-    fun setUp() {
-        isDebugInspectorInfoEnabled = true
-    }
-
-    @After
-    fun tearDown() {
-        isDebugInspectorInfoEnabled = false
-    }
-
-    @Test
-    fun magnifierStyle_equal() {
-        val configuration1 = MagnifierStyle(
-            size = DpSize(1.dp, 1.dp),
-            cornerRadius = 1.dp,
-            elevation = 1.dp,
-            clippingEnabled = true,
-            fishEyeEnabled = true
-        )
-        val configuration2 = MagnifierStyle(
-            size = DpSize(1.dp, 1.dp),
-            cornerRadius = 1.dp,
-            elevation = 1.dp,
-            clippingEnabled = true,
-            fishEyeEnabled = true
-        )
-
-        assertThat(configuration1).isEqualTo(configuration2)
-    }
-
-    @Test
-    fun magnifierStyle_notEqualSize() {
-        val configuration1 = MagnifierStyle(
-            size = DpSize(1.dp, 1.dp),
-            cornerRadius = 1.dp,
-            elevation = 1.dp,
-            clippingEnabled = true,
-            fishEyeEnabled = true
-        )
-        val configuration2 = MagnifierStyle(
-            size = DpSize(1.dp, 2.dp),
-            cornerRadius = 1.dp,
-            elevation = 1.dp,
-            clippingEnabled = true,
-            fishEyeEnabled = true
-        )
-
-        assertThat(configuration1).isNotEqualTo(configuration2)
-    }
-
-    @Test
-    fun magnifierStyle_hashCodeEqual_whenEqual() {
-        val configuration1 = MagnifierStyle(
-            size = DpSize(1.dp, 1.dp),
-            cornerRadius = 1.dp,
-            elevation = 1.dp,
-            clippingEnabled = true,
-            fishEyeEnabled = true
-        )
-        val configuration2 = MagnifierStyle(
-            size = DpSize(1.dp, 1.dp),
-            cornerRadius = 1.dp,
-            elevation = 1.dp,
-            clippingEnabled = true,
-            fishEyeEnabled = true
-        )
-
-        assertThat(configuration1.hashCode()).isEqualTo(configuration2.hashCode())
-    }
-
-    @Test
-    fun magnifierStyle_hashCodeNotEqual_whenNotEqual() {
-        val configuration1 = MagnifierStyle(
-            size = DpSize(1.dp, 1.dp),
-            cornerRadius = 1.dp,
-            elevation = 1.dp,
-            clippingEnabled = true,
-            fishEyeEnabled = true
-        )
-        val configuration2 = MagnifierStyle(
-            size = DpSize(1.dp, 2.dp),
-            cornerRadius = 1.dp,
-            elevation = 1.dp,
-            clippingEnabled = true,
-            fishEyeEnabled = true
-        )
-
-        assertThat(configuration1.hashCode()).isNotEqualTo(configuration2.hashCode())
-    }
-
-    @Test
-    fun magnifierStyle_toString_whenNotTextDefault() {
-        assertThat(MagnifierStyle.Default.toString()).isEqualTo(
-            "MagnifierStyle(" +
-                "size=DpSize.Unspecified, " +
-                "cornerRadius=Dp.Unspecified, " +
-                "elevation=Dp.Unspecified, " +
-                "clippingEnabled=true, " +
-                "fishEyeEnabled=false)"
-        )
-    }
-
-    @Test
-    fun magnifierStyle_toString_whenTextDefault() {
-        assertThat(MagnifierStyle.TextDefault.toString()).isEqualTo("MagnifierStyle.TextDefault")
-    }
-
-    @Test
-    fun magnifierStyle_isSupported() {
-        // Never supported on old versions.
-        assertThat(isStyleSupported(MagnifierStyle.Default, sdkVersion = 21)).isFalse()
-        assertThat(isStyleSupported(MagnifierStyle.Default, sdkVersion = 27)).isFalse()
-        assertThat(isStyleSupported(MagnifierStyle.TextDefault, sdkVersion = 27)).isFalse()
-
-        // Defaults supported on lowest supported version.
-        assertThat(isStyleSupported(MagnifierStyle.Default, sdkVersion = 28)).isTrue()
-        assertThat(isStyleSupported(MagnifierStyle.TextDefault, sdkVersion = 28)).isTrue()
-        assertThat(isStyleSupported(MagnifierStyle(), sdkVersion = 28)).isTrue()
-
-        // Custom styles only available after 28.
-        assertThat(
-            isStyleSupported(
-                MagnifierStyle(cornerRadius = 42.dp),
-                sdkVersion = 28
-            )
-        ).isFalse()
-        assertThat(isStyleSupported(MagnifierStyle(cornerRadius = 42.dp), sdkVersion = 29)).isTrue()
-
-        // Fisheye is never supported (yet, see b/202451044).
-        assertThat(
-            isStyleSupported(
-                MagnifierStyle(fishEyeEnabled = true),
-                sdkVersion = 9999
-            )
-        ).isFalse()
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun magnifier_inspectorValue_whenSupported() {
-        val sourceCenterLambda: Density.() -> Offset = { Offset(42f, 42f) }
-        val magnifierCenterLambda: Density.() -> Offset = { Offset(42f, 42f) }
-        val modifier = Modifier.magnifier(
-            sourceCenter = sourceCenterLambda,
-            magnifierCenter = magnifierCenterLambda
-        ).findInspectableValue()!!
-        assertThat(modifier.nameFallback).isEqualTo("magnifier")
-        assertThat(modifier.valueOverride).isNull()
-        assertThat(modifier.inspectableElements.toList()).containsExactly(
-            ValueElement("sourceCenter", sourceCenterLambda),
-            ValueElement("magnifierCenter", magnifierCenterLambda),
-            ValueElement("zoom", Float.NaN),
-            ValueElement("style", MagnifierStyle.Default),
-        )
-    }
-
-    @SdkSuppress(maxSdkVersion = 27)
-    @Test
-    fun magnifier_inspectorValue_whenNotSupported() {
-        val sourceCenterLambda: Density.() -> Offset = { Offset(42f, 42f) }
-        val magnifierCenterLambda: Density.() -> Offset = { Offset(42f, 42f) }
-        val modifier = Modifier.magnifier(
-            sourceCenter = sourceCenterLambda,
-            magnifierCenter = magnifierCenterLambda
-        ).findInspectableValue()!!
-        assertThat(modifier.nameFallback).isEqualTo("magnifier (not supported)")
-        assertThat(modifier.valueOverride).isNull()
-        assertThat(modifier.inspectableElements.toList()).containsExactly(
-            ValueElement("sourceCenter", sourceCenterLambda),
-            ValueElement("magnifierCenter", magnifierCenterLambda),
-            ValueElement("zoom", Float.NaN),
-            ValueElement("style", MagnifierStyle.Default),
-        )
-    }
-
-    @SdkSuppress(maxSdkVersion = 27)
-    @Test
-    fun magnifier_returnsEmptyModifier_whenNotSupported() {
-        val modifier = Modifier.magnifier(sourceCenter = { Offset.Zero })
-        val elements: List<Modifier.Element> =
-            modifier.foldIn(emptyList()) { elements, element -> elements + element }
-
-        // Modifier.magnifier doesn't have its own modifier class, so instead of checking for the
-        // absence of the actual modifier we just check that the only modifier returned is the
-        // InspectableValue (which actually has two elements).
-        assertThat(elements).hasSize(2)
-        assertThat(elements.first()).isInstanceOf(InspectableValue::class.java)
-        assertThat(elements.last()).isInstanceOf(InspectableModifier.End::class.java)
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_recreatesMagnifier_whenDensityChanged() {
-        val magnifierFactory = CountingPlatformMagnifierFactory()
-        var density by mutableStateOf(Density(1f))
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides density) {
-                Box(
-                    Modifier.magnifier(
-                        sourceCenter = { Offset.Zero },
-                        magnifierCenter = { Offset.Unspecified },
-                        zoom = Float.NaN,
-                        style = MagnifierStyle.Default,
-                        onSizeChanged = null,
-                        platformMagnifierFactory = magnifierFactory
-                    )
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(magnifierFactory.creationCount).isEqualTo(1)
-        }
-
-        density = Density(density.density * 2)
-
-        rule.runOnIdle {
-            assertThat(magnifierFactory.creationCount).isEqualTo(2)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_recreatesMagnifier_whenConfigurationChanged() {
-        val magnifierFactory = CountingPlatformMagnifierFactory()
-        var configuration by mutableStateOf(MagnifierStyle(elevation = 1.dp))
-        rule.setContent {
-            Box(
-                Modifier.magnifier(
-                    sourceCenter = { Offset.Zero },
-                    magnifierCenter = { Offset.Unspecified },
-                    zoom = Float.NaN,
-                    style = configuration,
-                    onSizeChanged = null,
-                    platformMagnifierFactory = magnifierFactory
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(magnifierFactory.creationCount).isEqualTo(1)
-        }
-
-        configuration = MagnifierStyle(elevation = configuration.elevation * 2)
-
-        rule.runOnIdle {
-            assertThat(magnifierFactory.creationCount).isEqualTo(2)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_recreatesMagnifier_whenConfigurationChangedToText() {
-        val magnifierFactory = CountingPlatformMagnifierFactory()
-        var style: MagnifierStyle by mutableStateOf(MagnifierStyle.Default)
-        rule.setContent {
-            Box(
-                Modifier.magnifier(
-                    sourceCenter = { Offset.Zero },
-                    magnifierCenter = { Offset.Unspecified },
-                    zoom = Float.NaN,
-                    style = style,
-                    onSizeChanged = null,
-                    platformMagnifierFactory = magnifierFactory
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(magnifierFactory.creationCount).isEqualTo(1)
-        }
-
-        style = MagnifierStyle.TextDefault
-
-        rule.runOnIdle {
-            assertThat(magnifierFactory.creationCount).isEqualTo(2)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_recreatesMagnifier_whenCannotUpdateZoom() {
-        val magnifierFactory = CountingPlatformMagnifierFactory()
-        var zoom by mutableStateOf(1f)
-        rule.setContent {
-            Box(
-                Modifier.magnifier(
-                    sourceCenter = { Offset.Zero },
-                    magnifierCenter = { Offset.Unspecified },
-                    zoom = zoom,
-                    style = MagnifierStyle.Default,
-                    onSizeChanged = null,
-                    platformMagnifierFactory = magnifierFactory
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(magnifierFactory.creationCount).isEqualTo(1)
-        }
-
-        zoom += 2
-
-        rule.runOnIdle {
-            assertThat(magnifierFactory.creationCount).isEqualTo(2)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_doesNotRecreateMagnifier_whenCanUpdateZoom() {
-        val magnifierFactory = CountingPlatformMagnifierFactory(canUpdateZoom = true)
-        var zoom by mutableStateOf(1f)
-        rule.setContent {
-            Box(
-                Modifier.magnifier(
-                    sourceCenter = { Offset.Zero },
-                    magnifierCenter = { Offset.Unspecified },
-                    zoom = zoom,
-                    style = MagnifierStyle.Default,
-                    onSizeChanged = null,
-                    platformMagnifierFactory = magnifierFactory
-
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(magnifierFactory.creationCount).isEqualTo(1)
-        }
-
-        zoom += 2
-
-        rule.runOnIdle {
-            assertThat(magnifierFactory.creationCount).isEqualTo(1)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_updatesContent_whenLayerRedrawn() {
-        var drawTrigger by mutableStateOf(0)
-        val platformMagnifier = CountingPlatformMagnifier()
-        rule.setContent {
-            Box(
-                Modifier
-                    // If the node has zero size, it won't draw and this test won't work.
-                    .fillMaxSize()
-                    .drawBehind {
-                        // Read this state to trigger re-draw when it changes.
-                        drawCircle(Color.Black, radius = drawTrigger.toFloat())
-                    }
-                    .magnifier(
-                        sourceCenter = { Offset.Zero },
-                        magnifierCenter = { Offset.Unspecified },
-                        zoom = Float.NaN,
-                        style = MagnifierStyle.Default,
-                        onSizeChanged = null,
-                        platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
-                    )
-            )
-        }
-
-        rule.runOnIdle {
-            // This will always happen once right away, since there's always an immediate draw pass.
-            assertThat(platformMagnifier.contentUpdateCount).isEqualTo(1)
-        }
-
-        drawTrigger++
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.contentUpdateCount).isEqualTo(2)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_doesNotUpdateProperties_whenLayerRedrawn() {
-        var drawTrigger by mutableStateOf(0)
-        val platformMagnifier = CountingPlatformMagnifier()
-        rule.setContent {
-            Box(
-                Modifier
-                    // If the node has zero size, it won't draw and this test won't work.
-                    .fillMaxSize()
-                    .drawBehind {
-                        // Read this state to trigger re-draw when it changes.
-                        drawCircle(Color.Black, radius = drawTrigger.toFloat())
-                    }
-                    .magnifier(
-                        sourceCenter = { Offset.Zero },
-                        magnifierCenter = { Offset.Unspecified },
-                        zoom = Float.NaN,
-                        style = MagnifierStyle.Default,
-                        onSizeChanged = null,
-                        platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
-                    )
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
-        }
-
-        drawTrigger++
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_updatesProperties_whenPlacementChanged() {
-        var layoutOffset by mutableStateOf(IntOffset.Zero)
-        val platformMagnifier = CountingPlatformMagnifier()
-        rule.setContent {
-            Box(
-                Modifier
-                    .offset { layoutOffset }
-                    .magnifier(
-                        sourceCenter = { Offset.Zero },
-                        magnifierCenter = { Offset.Unspecified },
-                        zoom = Float.NaN,
-                        style = MagnifierStyle.Default,
-                        onSizeChanged = null,
-                        platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
-                    )
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
-        }
-
-        layoutOffset += IntOffset(10, 1)
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(2)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_updatesProperties_whenSourceCenterChanged() {
-        var sourceCenter by mutableStateOf(Offset(1f, 1f))
-        val platformMagnifier = CountingPlatformMagnifier()
-        rule.setContent {
-            Box(
-                Modifier.magnifier(
-                    sourceCenter = { sourceCenter },
-                    magnifierCenter = { Offset.Unspecified },
-                    zoom = Float.NaN,
-                    style = MagnifierStyle.Default,
-                    onSizeChanged = null,
-                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
-        }
-
-        sourceCenter += Offset(1f, 1f)
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(2)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_updatesProperties_whenMagnifierCenterChanged() {
-        var magnifierCenter by mutableStateOf(Offset(1f, 1f))
-        val platformMagnifier = CountingPlatformMagnifier()
-        rule.setContent {
-            Box(
-                Modifier.magnifier(
-                    sourceCenter = { Offset.Zero },
-                    magnifierCenter = { magnifierCenter },
-                    zoom = Float.NaN,
-                    style = MagnifierStyle.Default,
-                    onSizeChanged = null,
-                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
-        }
-
-        magnifierCenter += Offset(1f, 1f)
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(2)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @Test
-    fun platformMagnifierModifier_updatesProperties_whenZoomChanged() {
-        var zoom by mutableStateOf(1f)
-        val platformMagnifier = CountingPlatformMagnifier()
-        rule.setContent {
-            Box(
-                Modifier.magnifier(
-                    sourceCenter = { Offset.Zero },
-                    magnifierCenter = { Offset.Unspecified },
-                    zoom = zoom,
-                    style = MagnifierStyle.Default,
-                    onSizeChanged = null,
-                    platformMagnifierFactory = PlatformMagnifierFactory(
-                        platformMagnifier,
-                        canUpdateZoom = true
-                    )
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
-        }
-
-        zoom += 1f
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(2)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_dismissesMagnifier_whenRemovedFromComposition() {
-        var showMagnifier by mutableStateOf(true)
-        val platformMagnifier = CountingPlatformMagnifier()
-        rule.setContent {
-            Box(
-                if (showMagnifier) {
-                    Modifier.magnifier(
-                        sourceCenter = { Offset.Zero },
-                        magnifierCenter = { Offset.Unspecified },
-                        zoom = Float.NaN,
-                        style = MagnifierStyle.Default,
-                        onSizeChanged = null,
-                        platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
-                    )
-                } else {
-                    Modifier
-                }
-            )
-        }
-
-        val initialDismissCount = rule.runOnIdle { platformMagnifier.dismissCount }
-
-        showMagnifier = false
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.dismissCount).isEqualTo(initialDismissCount + 1)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_dismissesMagnifier_whenCenterUnspecified() {
-        // Show the magnifier initially and then hide it, to ensure that it's actually dismissed vs
-        // just never shown.
-        var sourceCenter by mutableStateOf(Offset.Zero)
-        val platformMagnifier = CountingPlatformMagnifier()
-        rule.setContent {
-            Box(
-                Modifier.magnifier(
-                    sourceCenter = { sourceCenter },
-                    magnifierCenter = { Offset.Unspecified },
-                    zoom = Float.NaN,
-                    style = MagnifierStyle.Default,
-                    onSizeChanged = null,
-                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
-        }
-        val initialDismissCount = rule.runOnIdle { platformMagnifier.dismissCount }
-
-        // Now update with an unspecified sourceCenter to hide it.
-        sourceCenter = Offset.Unspecified
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
-            assertThat(platformMagnifier.dismissCount).isEqualTo(initialDismissCount + 1)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_dismissesMagnifier_whenMagnifierRecreated() {
-        var configuration by mutableStateOf(MagnifierStyle(elevation = 1.dp))
-        val platformMagnifier = CountingPlatformMagnifier()
-        rule.setContent {
-            Box(
-                Modifier.magnifier(
-                    sourceCenter = { Offset.Zero },
-                    magnifierCenter = { Offset.Unspecified },
-                    zoom = Float.NaN,
-                    style = configuration,
-                    onSizeChanged = null,
-                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
-                )
-            )
-        }
-
-        val initialDismissCount = rule.runOnIdle { platformMagnifier.dismissCount }
-
-        configuration = MagnifierStyle(elevation = configuration.elevation + 1.dp)
-
-        rule.runOnIdle {
-            assertThat(platformMagnifier.dismissCount).isEqualTo(initialDismissCount + 1)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_firesOnSizeChanged_initially() {
-        val magnifierSize = IntSize(10, 11)
-        val sizeEvents = mutableListOf<DpSize>()
-        val platformMagnifier = CountingPlatformMagnifier().apply {
-            size = magnifierSize
-        }
-        rule.setContent {
-            Box(
-                Modifier.magnifier(
-                    sourceCenter = { Offset.Zero },
-                    magnifierCenter = { Offset.Unspecified },
-                    zoom = Float.NaN,
-                    style = MagnifierStyle.Default,
-                    onSizeChanged = { sizeEvents += it },
-                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(sizeEvents).containsExactly(
-                with(rule.density) {
-                    magnifierSize.toSize().toDpSize()
-                }
-            )
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_firesOnSizeChanged_initially_whenSourceCenterUnspecified() {
-        val magnifierSize = IntSize(10, 11)
-        val sizeEvents = mutableListOf<DpSize>()
-        val platformMagnifier = CountingPlatformMagnifier().apply {
-            size = magnifierSize
-        }
-        rule.setContent {
-            Box(
-                Modifier.magnifier(
-                    sourceCenter = { Offset.Unspecified },
-                    magnifierCenter = { Offset.Unspecified },
-                    zoom = Float.NaN,
-                    style = MagnifierStyle.Default,
-                    onSizeChanged = { sizeEvents += it },
-                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(sizeEvents).containsExactly(
-                with(rule.density) {
-                    magnifierSize.toSize().toDpSize()
-                }
-            )
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_firesOnSizeChanged_whenNewSizeRequested() {
-        val size1 = IntSize(10, 11)
-        val size2 = size1 * 2
-        var magnifierSize by mutableStateOf(size1)
-        val magnifierDpSize by derivedStateOf {
-            with(rule.density) {
-                magnifierSize.toSize().toDpSize()
-            }
-        }
-        val sizeEvents = mutableListOf<DpSize>()
-        val platformMagnifier = CountingPlatformMagnifier().apply {
-            size = magnifierSize
-        }
-        rule.setContent {
-            Box(
-                Modifier.magnifier(
-                    sourceCenter = { Offset.Zero },
-                    magnifierCenter = { Offset.Unspecified },
-                    zoom = Float.NaN,
-                    style = MagnifierStyle(size = magnifierDpSize),
-                    onSizeChanged = { sizeEvents += it },
-                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            // Need to update the fake magnifier so it reports the right size when asked…
-            platformMagnifier.size = size2
-            // …and update the mutable state to trigger a recomposition.
-            magnifierSize = size2
-        }
-
-        rule.runOnIdle {
-            assertThat(sizeEvents).containsExactlyElementsIn(
-                listOf(size1, size2).map {
-                    with(rule.density) { it.toSize().toDpSize() }
-                }
-            ).inOrder()
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun platformMagnifierModifier_reportsSemantics() {
-        var magnifierOffset by mutableStateOf(Offset.Zero)
-        rule.setContent {
-            Box(Modifier.magnifier(sourceCenter = { magnifierOffset }))
-        }
-        val getPosition = rule.onNode(keyIsDefined(MagnifierPositionInRoot))
-            .fetchSemanticsNode()
-            .config[MagnifierPositionInRoot]
-
-        rule.runOnIdle {
-            assertThat(getPosition()).isEqualTo(magnifierOffset)
-        }
-
-        // Move the modifier, same function should return new value.
-        magnifierOffset = Offset(42f, 24f)
-
-        rule.runOnIdle {
-            assertThat(getPosition()).isEqualTo(magnifierOffset)
-        }
-    }
-
-    private fun PlatformMagnifierFactory(
-        platformMagnifier: PlatformMagnifier,
-        canUpdateZoom: Boolean = false
-    ) = object : PlatformMagnifierFactory {
-        override val canUpdateZoom: Boolean = canUpdateZoom
-        override fun create(
-            style: MagnifierStyle,
-            view: View,
-            density: Density,
-            initialZoom: Float
-        ): PlatformMagnifier {
-            return platformMagnifier
-        }
-    }
-
-    private fun Modifier.findInspectableValue(): InspectableValue? =
-        foldIn<InspectableValue?>(null) { acc, element -> acc ?: element as? InspectableValue }
-
-    private class CountingPlatformMagnifierFactory(
-        override val canUpdateZoom: Boolean = false
-    ) : PlatformMagnifierFactory {
-        var creationCount = 0
-
-        override fun create(
-            style: MagnifierStyle,
-            view: View,
-            density: Density,
-            initialZoom: Float
-        ): PlatformMagnifier {
-            creationCount++
-            return NoopPlatformMagnifier
-        }
-    }
-
-    private object NoopPlatformMagnifier : PlatformMagnifier {
-        override val size: IntSize = IntSize.Zero
-
-        override fun updateContent() {
-        }
-
-        override fun update(
-            sourceCenter: Offset,
-            magnifierCenter: Offset,
-            zoom: Float
-        ) {
-        }
-
-        override fun dismiss() {
-        }
-    }
-
-    private class CountingPlatformMagnifier : PlatformMagnifier {
-        var contentUpdateCount = 0
-        var propertyUpdateCount = 0
-        var dismissCount = 0
-
-        override var size: IntSize = IntSize.Zero
-
-        override fun updateContent() {
-            contentUpdateCount++
-        }
-
-        override fun update(
-            sourceCenter: Offset,
-            magnifierCenter: Offset,
-            zoom: Float
-        ) {
-            propertyUpdateCount++
-        }
-
-        override fun dismiss() {
-            dismissCount++
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/PlatformMagnifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/PlatformMagnifierTest.kt
deleted file mode 100644
index 4ed6ab0..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/PlatformMagnifierTest.kt
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright 2021 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
-
-import android.graphics.Point
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.center
-import androidx.compose.ui.unit.toOffset
-import androidx.compose.ui.unit.toSize
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalFoundationApi::class)
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class PlatformMagnifierTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @SdkSuppress(minSdkVersion = 29)
-    @Test
-    fun androidPlatformMagnifier_showsMagnifier() {
-        val magnifier = createAndroidPlatformMagnifier()
-        rule.runOnIdle {
-            magnifier.update(
-                sourceCenter = Offset.Zero,
-                magnifierCenter = Offset.Unspecified,
-                zoom = Float.NaN
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(magnifier.magnifier.position).isEqualTo(Point(0, 0))
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @Test
-    fun androidPlatformMagnifier_updatesZoom_whenValid() {
-        val magnifier = createAndroidPlatformMagnifier()
-        rule.runOnIdle {
-            magnifier.update(
-                sourceCenter = Offset.Zero,
-                magnifierCenter = Offset.Unspecified,
-                zoom = 1f
-            )
-
-            assertThat(magnifier.magnifier.zoom).isEqualTo(1f)
-
-            magnifier.update(
-                sourceCenter = Offset.Zero,
-                magnifierCenter = Offset.Unspecified,
-                zoom = 2f
-            )
-
-            assertThat(magnifier.magnifier.zoom).isEqualTo(2f)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @Test
-    fun androidPlatformMagnifier_doesNotUpdateZoom_whenNaN() {
-        val magnifier = createAndroidPlatformMagnifier()
-        rule.runOnIdle {
-            magnifier.update(
-                sourceCenter = Offset.Zero,
-                magnifierCenter = Offset.Unspecified,
-                zoom = 1f
-            )
-
-            assertThat(magnifier.magnifier.zoom).isEqualTo(1f)
-
-            magnifier.update(
-                sourceCenter = Offset.Zero,
-                magnifierCenter = Offset.Unspecified,
-                zoom = Float.NaN
-            )
-
-            assertThat(magnifier.magnifier.zoom).isEqualTo(1f)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @Test
-    fun androidPlatformMagnifier_specifiesMagnifierCenter_whenSpecified() {
-        val magnifier = createAndroidPlatformMagnifier()
-        rule.runOnIdle {
-            magnifier.update(
-                sourceCenter = Offset.Zero,
-                magnifierCenter = VIEW_SIZE.center.toOffset(),
-                zoom = Float.NaN
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(magnifier.magnifier.sourcePosition).isEqualTo(Point(0, 0))
-            // position is the top-left of the magnifier so we need to offset it.
-            assertThat(magnifier.magnifier.position!!.x + magnifier.magnifier.width / 2)
-                .isEqualTo(VIEW_SIZE.center.x)
-            assertThat(magnifier.magnifier.position!!.y + magnifier.magnifier.height / 2)
-                .isEqualTo(VIEW_SIZE.center.y)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @Test
-    fun androidPlatformMagnifier_doesNotSpecifyMagnifierCenter_whenNotSpecified() {
-        // To avoid making this test depend on the actual default offset the framework happens to
-        // use for the magnifier, we just record the first magnifier position after placing the
-        // source, then move the source and check the new position and assert that it moved the
-        // same amount.
-        val magnifierDelta = IntOffset(10, 10)
-        val magnifier = createAndroidPlatformMagnifier()
-        rule.runOnIdle {
-            magnifier.update(
-                sourceCenter = VIEW_SIZE.center.toOffset(),
-                magnifierCenter = Offset.Unspecified,
-                zoom = Float.NaN
-            )
-            val initialMagnifierPosition = magnifier.magnifier.position!!.toIntOffset()
-
-            magnifier.update(
-                sourceCenter = (VIEW_SIZE.center + magnifierDelta).toOffset(),
-                magnifierCenter = Offset.Unspecified,
-                zoom = Float.NaN
-            )
-
-            assertThat(magnifier.magnifier.position!!.toIntOffset())
-                .isEqualTo(initialMagnifierPosition + magnifierDelta)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 28)
-    @Test
-    fun androidPlatformMagnifier_returnsDefaultSize() {
-        val magnifier = createAndroidPlatformMagnifier()
-        assertThat(magnifier.size.width).isGreaterThan(0)
-        assertThat(magnifier.size.height).isGreaterThan(0)
-    }
-
-    // Size is only configurable on 29+
-    @SdkSuppress(minSdkVersion = 29)
-    @Test
-    fun androidPlatformMagnifier_usesRequestedSize() {
-        val magnifierSize = IntSize(10, 11)
-        val magnifier = with(rule.density) {
-            createAndroidPlatformMagnifier(size = magnifierSize.toSize().toDpSize())
-        }
-        assertThat(magnifier.size).isEqualTo(magnifierSize)
-    }
-
-    private fun createAndroidPlatformMagnifier(
-        size: DpSize = DpSize.Unspecified
-    ): PlatformMagnifierFactoryApi28Impl.PlatformMagnifierImpl {
-        lateinit var magnifier: PlatformMagnifierFactoryApi28Impl.PlatformMagnifierImpl
-        rule.setContent {
-            val dpSize = with(LocalDensity.current) { VIEW_SIZE.toSize().toDpSize() }
-            // Force the view to measure to non-zero size to give the magnifier room to show.
-            Box(Modifier.requiredSize(dpSize)) {
-                val currentView = LocalView.current
-                val density = LocalDensity.current
-
-                DisposableEffect(Unit) {
-                    magnifier = PlatformMagnifierFactory.getForCurrentPlatform().create(
-                        view = currentView,
-                        density = density,
-                        initialZoom = Float.NaN,
-                        style = MagnifierStyle(size = size),
-                    ) as PlatformMagnifierFactoryApi28Impl.PlatformMagnifierImpl
-                    onDispose {}
-                }
-            }
-        }
-        return rule.runOnIdle { magnifier }
-    }
-
-    private companion object {
-        val VIEW_SIZE = IntSize(500, 500)
-
-        fun Point.toIntOffset() = IntOffset(x, y)
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
deleted file mode 100644
index b5fe0b2..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ /dev/null
@@ -1,2779 +0,0 @@
-/*
- * Copyright 2020 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
-
-import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.core.keyframes
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.gestures.DefaultFlingBehavior
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.ModifierLocalScrollableContainer
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.gestures.ScrollableDefaults
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.scrollable
-import androidx.compose.foundation.interaction.DragInteraction
-import androidx.compose.foundation.interaction.Interaction
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.currentComposer
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.testutils.first
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.MotionDurationScale
-import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.util.VelocityTracker
-import androidx.compose.ui.input.pointer.util.VelocityTrackerAddPointsFix
-import androidx.compose.ui.materialize
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalReadScope
-import androidx.compose.ui.platform.AbstractComposeView
-import androidx.compose.ui.platform.InspectableValue
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.platform.LocalViewConfiguration
-import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
-import androidx.compose.ui.test.ScrollWheel
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.test.performMouseInput
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.swipe
-import androidx.compose.ui.test.swipeDown
-import androidx.compose.ui.test.swipeLeft
-import androidx.compose.ui.test.swipeRight
-import androidx.compose.ui.test.swipeUp
-import androidx.compose.ui.test.swipeWithVelocity
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastForEach
-import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.action.CoordinatesProvider
-import androidx.test.espresso.action.GeneralLocation
-import androidx.test.espresso.action.GeneralSwipeAction
-import androidx.test.espresso.action.Press
-import androidx.test.espresso.action.Swipe
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlin.math.abs
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
-import org.hamcrest.CoreMatchers.allOf
-import org.hamcrest.CoreMatchers.instanceOf
-import org.junit.After
-import org.junit.Assert
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class ScrollableTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    private val scrollableBoxTag = "scrollableBox"
-
-    private lateinit var scope: CoroutineScope
-
-    private fun ComposeContentTestRule.setContentAndGetScope(content: @Composable () -> Unit) {
-        setContent {
-            val actualScope = rememberCoroutineScope()
-            SideEffect { scope = actualScope }
-            content()
-        }
-    }
-
-    @Before
-    fun before() {
-        isDebugInspectorInfoEnabled = true
-    }
-
-    @After
-    fun after() {
-        isDebugInspectorInfoEnabled = false
-    }
-
-    @Test
-    fun scrollable_horizontalScroll() {
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        setScrollableContent {
-            Modifier.scrollable(
-                state = controller,
-                orientation = Orientation.Horizontal
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x + 100f, this.center.y),
-                durationMillis = 100
-            )
-        }
-
-        val lastTotal = rule.runOnIdle {
-            assertThat(total).isGreaterThan(0)
-            total
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x, this.center.y + 100f),
-                durationMillis = 100
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(total).isEqualTo(lastTotal)
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x - 100f, this.center.y),
-                durationMillis = 100
-            )
-        }
-        rule.runOnIdle {
-            assertThat(total).isLessThan(0.01f)
-        }
-    }
-
-    @OptIn(ExperimentalTestApi::class)
-    @Test
-    fun scrollable_horizontalScroll_mouseWheel() {
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        setScrollableContent {
-            Modifier.scrollable(
-                state = controller,
-                orientation = Orientation.Horizontal
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(-100f, ScrollWheel.Horizontal)
-        }
-
-        val lastTotal = rule.runOnIdle {
-            assertThat(total).isGreaterThan(0)
-            total
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(-100f, ScrollWheel.Vertical)
-        }
-
-        rule.runOnIdle {
-            assertThat(total).isEqualTo(lastTotal)
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(100f, ScrollWheel.Horizontal)
-        }
-        rule.runOnIdle {
-            assertThat(total).isLessThan(0.01f)
-        }
-    }
-
-    @Test
-    fun scrollable_horizontalScroll_reverse() {
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        setScrollableContent {
-            Modifier.scrollable(
-                reverseDirection = true,
-                state = controller,
-                orientation = Orientation.Horizontal
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x + 100f, this.center.y),
-                durationMillis = 100
-            )
-        }
-
-        val lastTotal = rule.runOnIdle {
-            assertThat(total).isLessThan(0)
-            total
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x, this.center.y + 100f),
-                durationMillis = 100
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(total).isEqualTo(lastTotal)
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x - 100f, this.center.y),
-                durationMillis = 100
-            )
-        }
-        rule.runOnIdle {
-            assertThat(total).isLessThan(0.01f)
-        }
-    }
-
-    @OptIn(ExperimentalTestApi::class)
-    @Test
-    fun scrollable_horizontalScroll_reverse_mouseWheel() {
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        setScrollableContent {
-            Modifier.scrollable(
-                reverseDirection = true,
-                state = controller,
-                orientation = Orientation.Horizontal
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(-100f, ScrollWheel.Horizontal)
-        }
-
-        val lastTotal = rule.runOnIdle {
-            assertThat(total).isLessThan(0)
-            total
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(-100f, ScrollWheel.Vertical)
-        }
-
-        rule.runOnIdle {
-            assertThat(total).isEqualTo(lastTotal)
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(100f, ScrollWheel.Horizontal)
-        }
-        rule.runOnIdle {
-            assertThat(total).isLessThan(0.01f)
-        }
-    }
-
-    @Test
-    fun scrollable_verticalScroll() {
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        setScrollableContent {
-            Modifier.scrollable(
-                state = controller,
-                orientation = Orientation.Vertical
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x, this.center.y + 100f),
-                durationMillis = 100
-            )
-        }
-
-        val lastTotal = rule.runOnIdle {
-            assertThat(total).isGreaterThan(0)
-            total
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x + 100f, this.center.y),
-                durationMillis = 100
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(total).isEqualTo(lastTotal)
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x, this.center.y - 100f),
-                durationMillis = 100
-            )
-        }
-        rule.runOnIdle {
-            assertThat(total).isLessThan(0.01f)
-        }
-    }
-
-    @OptIn(ExperimentalTestApi::class)
-    @Test
-    fun scrollable_verticalScroll_mouseWheel() {
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        setScrollableContent {
-            Modifier.scrollable(
-                state = controller,
-                orientation = Orientation.Vertical
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(-100f, ScrollWheel.Vertical)
-        }
-
-        val lastTotal = rule.runOnIdle {
-            assertThat(total).isGreaterThan(0)
-            total
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(-100f, ScrollWheel.Horizontal)
-        }
-
-        rule.runOnIdle {
-            assertThat(total).isEqualTo(lastTotal)
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(100f, ScrollWheel.Vertical)
-        }
-        rule.runOnIdle {
-            assertThat(total).isLessThan(0.01f)
-        }
-    }
-
-    @Test
-    fun scrollable_verticalScroll_reversed() {
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        setScrollableContent {
-            Modifier.scrollable(
-                reverseDirection = true,
-                state = controller,
-                orientation = Orientation.Vertical
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x, this.center.y + 100f),
-                durationMillis = 100
-            )
-        }
-
-        val lastTotal = rule.runOnIdle {
-            assertThat(total).isLessThan(0)
-            total
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x + 100f, this.center.y),
-                durationMillis = 100
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(total).isEqualTo(lastTotal)
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x, this.center.y - 100f),
-                durationMillis = 100
-            )
-        }
-        rule.runOnIdle {
-            assertThat(total).isLessThan(0.01f)
-        }
-    }
-
-    @OptIn(ExperimentalTestApi::class)
-    @Test
-    fun scrollable_verticalScroll_reversed_mouseWheel() {
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        setScrollableContent {
-            Modifier.scrollable(
-                reverseDirection = true,
-                state = controller,
-                orientation = Orientation.Vertical
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(-100f, ScrollWheel.Vertical)
-        }
-
-        val lastTotal = rule.runOnIdle {
-            assertThat(total).isLessThan(0)
-            total
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(-100f, ScrollWheel.Horizontal)
-        }
-
-        rule.runOnIdle {
-            assertThat(total).isEqualTo(lastTotal)
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(100f, ScrollWheel.Vertical)
-        }
-        rule.runOnIdle {
-            assertThat(total).isLessThan(0.01f)
-        }
-    }
-
-    @Test
-    fun scrollable_disabledWontCallLambda() {
-        val enabled = mutableStateOf(true)
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        setScrollableContent {
-            Modifier.scrollable(
-                state = controller,
-                orientation = Orientation.Horizontal,
-                enabled = enabled.value
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x + 100f, this.center.y),
-                durationMillis = 100
-            )
-        }
-        val prevTotal = rule.runOnIdle {
-            assertThat(total).isGreaterThan(0f)
-            enabled.value = false
-            total
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x + 100f, this.center.y),
-                durationMillis = 100
-            )
-        }
-        rule.runOnIdle {
-            assertThat(total).isEqualTo(prevTotal)
-        }
-    }
-
-    @Test
-    fun scrollable_startWithoutSlop_ifFlinging() {
-        rule.mainClock.autoAdvance = false
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        setScrollableContent {
-            Modifier.scrollable(
-                state = controller,
-                orientation = Orientation.Horizontal
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            swipeWithVelocity(
-                start = this.center,
-                end = Offset(this.center.x + 200f, this.center.y),
-                durationMillis = 100,
-                endVelocity = 4000f
-            )
-        }
-        assertThat(total).isGreaterThan(0f)
-        val prev = total
-        // pump frames twice to start fling animation
-        rule.mainClock.advanceTimeByFrame()
-        rule.mainClock.advanceTimeByFrame()
-        val prevAfterSomeFling = total
-        assertThat(prevAfterSomeFling).isGreaterThan(prev)
-        // don't advance main clock anymore since we're in the middle of the fling. Now interrupt
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            down(this.center)
-            moveBy(Offset(115f, 0f))
-            up()
-        }
-        val expected = prevAfterSomeFling + 115
-        assertThat(total).isEqualTo(expected)
-    }
-
-    @Test
-    fun scrollable_blocksDownEvents_ifFlingingCaught() {
-        rule.mainClock.autoAdvance = false
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        rule.setContent {
-            Box {
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier
-                        .size(300.dp)
-                        .scrollable(
-                            orientation = Orientation.Horizontal,
-                            state = controller
-                        )
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .size(300.dp)
-                            .testTag(scrollableBoxTag)
-                            .clickable {
-                                assertWithMessage("Clickable shouldn't click when fling caught")
-                                    .fail()
-                            }
-                    )
-                }
-            }
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            swipeWithVelocity(
-                start = this.center,
-                end = Offset(this.center.x + 200f, this.center.y),
-                durationMillis = 100,
-                endVelocity = 4000f
-            )
-        }
-        assertThat(total).isGreaterThan(0f)
-        val prev = total
-        // pump frames twice to start fling animation
-        rule.mainClock.advanceTimeByFrame()
-        rule.mainClock.advanceTimeByFrame()
-        val prevAfterSomeFling = total
-        assertThat(prevAfterSomeFling).isGreaterThan(prev)
-        // don't advance main clock anymore since we're in the middle of the fling. Now interrupt
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            down(this.center)
-            up()
-        }
-        // shouldn't assert in clickable lambda
-    }
-
-    @Test
-    fun scrollable_snappingScrolling() {
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        setScrollableContent {
-            Modifier.scrollable(
-                orientation = Orientation.Vertical,
-                state = controller
-            )
-        }
-        rule.waitForIdle()
-        assertThat(total).isEqualTo(0f)
-
-        scope.launch {
-            controller.animateScrollBy(1000f)
-        }
-        rule.waitForIdle()
-        assertThat(total).isWithin(0.001f).of(1000f)
-
-        scope.launch {
-            controller.animateScrollBy(-200f)
-        }
-        rule.waitForIdle()
-        assertThat(total).isWithin(0.001f).of(800f)
-    }
-
-    @Test
-    fun scrollable_explicitDisposal() {
-        rule.mainClock.autoAdvance = false
-        val emit = mutableStateOf(true)
-        val expectEmission = mutableStateOf(true)
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                assertWithMessage("Animating after dispose!").that(expectEmission.value).isTrue()
-                total += it
-                it
-            }
-        )
-        setScrollableContent {
-            if (emit.value) {
-                Modifier.scrollable(
-                    orientation = Orientation.Horizontal,
-                    state = controller
-                )
-            } else {
-                Modifier
-            }
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipeWithVelocity(
-                start = this.center,
-                end = Offset(this.center.x + 200f, this.center.y),
-                durationMillis = 100,
-                endVelocity = 4000f
-            )
-        }
-        assertThat(total).isGreaterThan(0f)
-
-        // start the fling for a few frames
-        rule.mainClock.advanceTimeByFrame()
-        rule.mainClock.advanceTimeByFrame()
-        // flip the emission
-        rule.runOnUiThread {
-            emit.value = false
-        }
-        // propagate the emit flip and record the value
-        rule.mainClock.advanceTimeByFrame()
-        val prevTotal = total
-        // make sure we don't receive any deltas
-        rule.runOnUiThread {
-            expectEmission.value = false
-        }
-
-        // pump the clock until idle
-        rule.mainClock.autoAdvance = true
-        rule.waitForIdle()
-
-        // still same and didn't fail in onScrollConsumptionRequested.. lambda
-        assertThat(total).isEqualTo(prevTotal)
-    }
-
-    @Test
-    fun scrollable_nestedDrag() {
-        var innerDrag = 0f
-        var outerDrag = 0f
-        val outerState = ScrollableState(
-            consumeScrollDelta = {
-                outerDrag += it
-                it
-            }
-        )
-        val innerState = ScrollableState(
-            consumeScrollDelta = {
-                innerDrag += it / 2
-                it / 2
-            }
-        )
-
-        rule.setContentAndGetScope {
-            Box {
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier
-                        .size(300.dp)
-                        .scrollable(
-                            state = outerState,
-                            orientation = Orientation.Horizontal
-                        )
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .testTag(scrollableBoxTag)
-                            .size(300.dp)
-                            .scrollable(
-                                state = innerState,
-                                orientation = Orientation.Horizontal
-                            )
-                    )
-                }
-            }
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipeWithVelocity(
-                start = this.center,
-                end = Offset(this.center.x + 200f, this.center.y),
-                durationMillis = 300,
-                endVelocity = 0f
-            )
-        }
-        val lastEqualDrag = rule.runOnIdle {
-            assertThat(innerDrag).isGreaterThan(0f)
-            assertThat(outerDrag).isGreaterThan(0f)
-            // we consumed half delta in child, so exactly half should go to the parent
-            assertThat(outerDrag).isEqualTo(innerDrag)
-            innerDrag
-        }
-        rule.runOnIdle {
-            // values should be the same since no fling
-            assertThat(innerDrag).isEqualTo(lastEqualDrag)
-            assertThat(outerDrag).isEqualTo(lastEqualDrag)
-        }
-    }
-
-    @OptIn(ExperimentalTestApi::class)
-    @Test
-    fun scrollable_nestedScroll_childPartialConsumptionForMouseWheel() {
-        var innerDrag = 0f
-        var outerDrag = 0f
-        val outerState = ScrollableState(
-            consumeScrollDelta = {
-                // Since the child has already consumed half, the parent will consume the rest.
-                outerDrag += it
-                it
-            }
-        )
-        val innerState = ScrollableState(
-            consumeScrollDelta = {
-                // Child consumes half, leaving the rest for the parent to consume.
-                innerDrag += it / 2
-                it / 2
-            }
-        )
-
-        rule.setContentAndGetScope {
-            Box {
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier
-                        .size(300.dp)
-                        .scrollable(
-                            state = outerState,
-                            orientation = Orientation.Horizontal
-                        )
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .testTag(scrollableBoxTag)
-                            .size(300.dp)
-                            .scrollable(
-                                state = innerState,
-                                orientation = Orientation.Horizontal
-                            )
-                    )
-                }
-            }
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
-            this.scroll(-200f, ScrollWheel.Horizontal)
-        }
-        rule.runOnIdle {
-            assertThat(innerDrag).isGreaterThan(0f)
-            assertThat(outerDrag).isGreaterThan(0f)
-            // Since child (inner) consumes half of the scroll, the parent (outer) consumes the
-            // remainder (which is half as well), so they will be equal.
-            assertThat(innerDrag).isEqualTo(outerDrag)
-            innerDrag
-        }
-    }
-
-    @Test
-    fun scrollable_nestedFling() {
-        var innerDrag = 0f
-        var outerDrag = 0f
-        val outerState = ScrollableState(
-            consumeScrollDelta = {
-                outerDrag += it
-                it
-            }
-        )
-        val innerState = ScrollableState(
-            consumeScrollDelta = {
-                innerDrag += it / 2
-                it / 2
-            }
-        )
-
-        rule.setContentAndGetScope {
-            Box {
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier
-                        .size(300.dp)
-                        .scrollable(
-                            state = outerState,
-                            orientation = Orientation.Horizontal
-                        )
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .testTag(scrollableBoxTag)
-                            .size(300.dp)
-                            .scrollable(
-                                state = innerState,
-                                orientation = Orientation.Horizontal
-                            )
-                    )
-                }
-            }
-        }
-
-        // swipe again with velocity
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x + 200f, this.center.y),
-                durationMillis = 300
-            )
-        }
-        assertThat(innerDrag).isGreaterThan(0f)
-        assertThat(outerDrag).isGreaterThan(0f)
-        // we consumed half delta in child, so exactly half should go to the parent
-        assertThat(outerDrag).isEqualTo(innerDrag)
-        val lastEqualDrag = innerDrag
-        rule.runOnIdle {
-            assertThat(innerDrag).isGreaterThan(lastEqualDrag)
-            assertThat(outerDrag).isGreaterThan(lastEqualDrag)
-        }
-    }
-
-    @Test
-    fun scrollable_nestedScrollAbove_respectsPreConsumption() {
-        var value = 0f
-        var lastReceivedPreScrollAvailable = 0f
-        val preConsumeFraction = 0.7f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                val expected = lastReceivedPreScrollAvailable * (1 - preConsumeFraction)
-                assertThat(it - expected).isWithin(0.01f)
-                value += it
-                it
-            }
-        )
-        val preConsumingParent = object : NestedScrollConnection {
-            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
-                lastReceivedPreScrollAvailable = available.x
-                return available * preConsumeFraction
-            }
-
-            override suspend fun onPreFling(available: Velocity): Velocity {
-                // consume all velocity
-                return available
-            }
-        }
-
-        rule.setContentAndGetScope {
-            Box {
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier
-                        .size(300.dp)
-                        .nestedScroll(preConsumingParent)
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .size(300.dp)
-                            .testTag(scrollableBoxTag)
-                            .scrollable(
-                                state = controller,
-                                orientation = Orientation.Horizontal
-                            )
-                    )
-                }
-            }
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipe(
-                start = this.center,
-                end = Offset(this.center.x + 200f, this.center.y),
-                durationMillis = 300
-            )
-        }
-
-        val preFlingValue = rule.runOnIdle { value }
-        rule.runOnIdle {
-            // if scrollable respects pre-fling consumption, it should fling 0px since we
-            // pre-consume all
-            assertThat(preFlingValue).isEqualTo(value)
-        }
-    }
-
-    @Test
-    fun scrollable_nestedScrollAbove_proxiesPostCycles() {
-        var value = 0f
-        var expectedLeft = 0f
-        val velocityFlung = 5000f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                val toConsume = it * 0.345f
-                value += toConsume
-                expectedLeft = it - toConsume
-                toConsume
-            }
-        )
-        val parent = object : NestedScrollConnection {
-            override fun onPostScroll(
-                consumed: Offset,
-                available: Offset,
-                source: NestedScrollSource
-            ): Offset {
-                // we should get in post scroll as much as left in controller callback
-                assertThat(available.x).isEqualTo(expectedLeft)
-                return if (source == NestedScrollSource.Fling) Offset.Zero else available
-            }
-
-            override suspend fun onPostFling(
-                consumed: Velocity,
-                available: Velocity
-            ): Velocity {
-                val expected = velocityFlung - consumed.x
-                assertThat(consumed.x).isLessThan(velocityFlung)
-                assertThat(abs(available.x - expected)).isLessThan(0.1f)
-                return available
-            }
-        }
-
-        rule.setContentAndGetScope {
-            Box {
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier
-                        .size(300.dp)
-                        .nestedScroll(parent)
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .size(300.dp)
-                            .testTag(scrollableBoxTag)
-                            .scrollable(
-                                state = controller,
-                                orientation = Orientation.Horizontal
-                            )
-                    )
-                }
-            }
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipeWithVelocity(
-                start = this.center,
-                end = Offset(this.center.x + 500f, this.center.y),
-                durationMillis = 300,
-                endVelocity = velocityFlung
-            )
-        }
-
-        // all assertions in callback above
-        rule.waitForIdle()
-    }
-
-    @Test
-    fun scrollable_nestedScrollAbove_reversed_proxiesPostCycles() {
-        var value = 0f
-        var expectedLeft = 0f
-        val velocityFlung = 5000f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                val toConsume = it * 0.345f
-                value += toConsume
-                expectedLeft = it - toConsume
-                toConsume
-            }
-        )
-        val parent = object : NestedScrollConnection {
-            override fun onPostScroll(
-                consumed: Offset,
-                available: Offset,
-                source: NestedScrollSource
-            ): Offset {
-                // we should get in post scroll as much as left in controller callback
-                assertThat(available.x).isEqualTo(-expectedLeft)
-                return if (source == NestedScrollSource.Fling) Offset.Zero else available
-            }
-
-            override suspend fun onPostFling(
-                consumed: Velocity,
-                available: Velocity
-            ): Velocity {
-                val expected = velocityFlung - consumed.x
-                assertThat(consumed.x).isLessThan(velocityFlung)
-                assertThat(abs(available.x - expected)).isLessThan(0.1f)
-                return available
-            }
-        }
-
-        rule.setContentAndGetScope {
-            Box {
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier
-                        .size(300.dp)
-                        .nestedScroll(parent)
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .size(300.dp)
-                            .testTag(scrollableBoxTag)
-                            .scrollable(
-                                state = controller,
-                                reverseDirection = true,
-                                orientation = Orientation.Horizontal
-                            )
-                    )
-                }
-            }
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipeWithVelocity(
-                start = this.center,
-                end = Offset(this.center.x + 500f, this.center.y),
-                durationMillis = 300,
-                endVelocity = velocityFlung
-            )
-        }
-
-        // all assertions in callback above
-        rule.waitForIdle()
-    }
-
-    @Test
-    fun scrollable_nestedScrollBelow_listensDispatches() {
-        var value = 0f
-        var expectedConsumed = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                expectedConsumed = it * 0.3f
-                value += expectedConsumed
-                expectedConsumed
-            }
-        )
-        val child = object : NestedScrollConnection {}
-        val dispatcher = NestedScrollDispatcher()
-
-        rule.setContentAndGetScope {
-            Box {
-                Box(
-                    modifier = Modifier
-                        .size(300.dp)
-                        .scrollable(
-                            state = controller,
-                            orientation = Orientation.Horizontal
-                        )
-                ) {
-                    Box(
-                        Modifier
-                            .size(200.dp)
-                            .testTag(scrollableBoxTag)
-                            .nestedScroll(child, dispatcher)
-                    )
-                }
-            }
-        }
-
-        val lastValueBeforeFling = rule.runOnIdle {
-            val preScrollConsumed = dispatcher
-                .dispatchPreScroll(Offset(20f, 20f), NestedScrollSource.Drag)
-            // scrollable is not interested in pre scroll
-            assertThat(preScrollConsumed).isEqualTo(Offset.Zero)
-
-            val consumed = dispatcher.dispatchPostScroll(
-                Offset(20f, 20f),
-                Offset(50f, 50f),
-                NestedScrollSource.Drag
-            )
-            assertThat(consumed.x - expectedConsumed).isWithin(0.001f)
-            value
-        }
-
-        scope.launch {
-            val preFlingConsumed = dispatcher.dispatchPreFling(Velocity(50f, 50f))
-            // scrollable won't participate in the pre fling
-            assertThat(preFlingConsumed).isEqualTo(Velocity.Zero)
-        }
-        rule.waitForIdle()
-
-        scope.launch {
-            dispatcher.dispatchPostFling(
-                Velocity(1000f, 1000f),
-                Velocity(2000f, 2000f)
-            )
-        }
-
-        rule.runOnIdle {
-            // catch that scrollable caught our post fling and flung
-            assertThat(value).isGreaterThan(lastValueBeforeFling)
-        }
-    }
-
-    @Test
-    fun scrollable_nestedScroll_allowParentWhenDisabled() {
-        var childValue = 0f
-        var parentValue = 0f
-        val childController = ScrollableState(
-            consumeScrollDelta = {
-                childValue += it
-                it
-            }
-        )
-        val parentController = ScrollableState(
-            consumeScrollDelta = {
-                parentValue += it
-                it
-            }
-        )
-
-        rule.setContentAndGetScope {
-            Box {
-                Box(
-                    modifier = Modifier
-                        .size(300.dp)
-                        .scrollable(
-                            state = parentController,
-                            orientation = Orientation.Horizontal
-                        )
-                ) {
-                    Box(
-                        Modifier
-                            .size(200.dp)
-                            .testTag(scrollableBoxTag)
-                            .scrollable(
-                                enabled = false,
-                                orientation = Orientation.Horizontal,
-                                state = childController
-                            )
-                    )
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(parentValue).isEqualTo(0f)
-            assertThat(childValue).isEqualTo(0f)
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag)
-            .performTouchInput {
-                swipe(center, center.copy(x = center.x + 100f))
-            }
-
-        rule.runOnIdle {
-            assertThat(childValue).isEqualTo(0f)
-            assertThat(parentValue).isGreaterThan(0f)
-        }
-    }
-
-    @Test
-    fun scrollable_nestedScroll_disabledConnectionNoOp() {
-        var childValue = 0f
-        var parentValue = 0f
-        var selfValue = 0f
-        val childController = ScrollableState(
-            consumeScrollDelta = {
-                childValue += it / 2
-                it / 2
-            }
-        )
-        val middleController = ScrollableState(
-            consumeScrollDelta = {
-                selfValue += it / 2
-                it / 2
-            }
-        )
-        val parentController = ScrollableState(
-            consumeScrollDelta = {
-                parentValue += it / 2
-                it / 2
-            }
-        )
-
-        rule.setContentAndGetScope {
-            Box {
-                Box(
-                    modifier = Modifier
-                        .size(300.dp)
-                        .scrollable(
-                            state = parentController,
-                            orientation = Orientation.Horizontal
-                        )
-                ) {
-                    Box(
-                        Modifier
-                            .size(200.dp)
-                            .scrollable(
-                                enabled = false,
-                                orientation = Orientation.Horizontal,
-                                state = middleController
-                            )
-                    ) {
-                        Box(
-                            Modifier
-                                .size(200.dp)
-                                .testTag(scrollableBoxTag)
-                                .scrollable(
-                                    orientation = Orientation.Horizontal,
-                                    state = childController
-                                )
-                        )
-                    }
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(parentValue).isEqualTo(0f)
-            assertThat(selfValue).isEqualTo(0f)
-            assertThat(childValue).isEqualTo(0f)
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag)
-            .performTouchInput {
-                swipe(center, center.copy(x = center.x + 100f))
-            }
-
-        rule.runOnIdle {
-            assertThat(childValue).isGreaterThan(0f)
-            // disabled middle node doesn't consume
-            assertThat(selfValue).isEqualTo(0f)
-            // but allow nested scroll to propagate up correctly
-            assertThat(parentValue).isGreaterThan(0f)
-        }
-    }
-
-    @Test
-    fun scrollable_nestedFlingCancellation_shouldPreventDeltasFromPropagating() {
-        var childDeltas = 0f
-        var touchSlop = 0f
-        val childController = ScrollableState {
-            childDeltas += it
-            it
-        }
-        val flingCancellationParent = object : NestedScrollConnection {
-            override fun onPostScroll(
-                consumed: Offset,
-                available: Offset,
-                source: NestedScrollSource
-            ): Offset {
-                if (source == NestedScrollSource.Fling && available != Offset.Zero) {
-                    throw CancellationException()
-                }
-                return Offset.Zero
-            }
-        }
-
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            Box(modifier = Modifier.nestedScroll(flingCancellationParent)) {
-                Box(
-                    modifier = Modifier
-                        .size(600.dp)
-                        .testTag("childScrollable")
-                        .scrollable(childController, Orientation.Horizontal)
-                )
-            }
-        }
-
-        // First drag, this won't trigger the cancellation flow.
-        rule.onNodeWithTag("childScrollable").performTouchInput {
-            down(centerLeft)
-            moveBy(Offset(100f, 0f))
-            up()
-        }
-
-        rule.runOnIdle {
-            assertThat(childDeltas).isEqualTo(100f - touchSlop)
-        }
-
-        childDeltas = 0f
-        var dragged = 0f
-        rule.onNodeWithTag("childScrollable").performTouchInput {
-            swipeWithVelocity(centerLeft, centerRight, 2000f)
-            dragged = centerRight.x - centerLeft.x
-        }
-
-        // child didn't receive more deltas after drag, because fling was cancelled by the parent
-        assertThat(childDeltas).isEqualTo(dragged - touchSlop)
-    }
-
-    @Test
-    fun scrollable_bothOrientations_proxiesPostFling() {
-        val velocityFlung = 5000f
-        val outerState = ScrollableState(consumeScrollDelta = { 0f })
-        val innerState = ScrollableState(consumeScrollDelta = { 0f })
-        val innerFlingBehavior = object : FlingBehavior {
-            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
-                return initialVelocity
-            }
-        }
-        val parent = object : NestedScrollConnection {
-            override suspend fun onPostFling(
-                consumed: Velocity,
-                available: Velocity
-            ): Velocity {
-                assertThat(consumed.x).isEqualTo(0f)
-                assertThat(available.x).isWithin(0.1f).of(velocityFlung)
-                return available
-            }
-        }
-
-        rule.setContentAndGetScope {
-            Box {
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier
-                        .size(300.dp)
-                        .nestedScroll(parent)
-                        .scrollable(
-                            state = outerState,
-                            orientation = Orientation.Vertical
-                        )
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .size(300.dp)
-                            .testTag(scrollableBoxTag)
-                            .scrollable(
-                                state = innerState,
-                                flingBehavior = innerFlingBehavior,
-                                orientation = Orientation.Horizontal
-                            )
-                    )
-                }
-            }
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            this.swipeWithVelocity(
-                start = this.center,
-                end = Offset(this.center.x + 500f, this.center.y),
-                durationMillis = 300,
-                endVelocity = velocityFlung
-            )
-        }
-
-        // all assertions in callback above
-        rule.waitForIdle()
-    }
-
-    @Test
-    fun scrollable_interactionSource() {
-        val interactionSource = MutableInteractionSource()
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-
-        setScrollableContent {
-            Modifier.scrollable(
-                interactionSource = interactionSource,
-                orientation = Orientation.Horizontal,
-                state = controller
-            )
-        }
-
-        val interactions = mutableListOf<Interaction>()
-
-        scope.launch {
-            interactionSource.interactions.collect { interactions.add(it) }
-        }
-
-        rule.runOnIdle {
-            assertThat(interactions).isEmpty()
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag)
-            .performTouchInput {
-                down(Offset(visibleSize.width / 4f, visibleSize.height / 2f))
-                moveBy(Offset(visibleSize.width / 2f, 0f))
-            }
-
-        rule.runOnIdle {
-            assertThat(interactions).hasSize(1)
-            assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag)
-            .performTouchInput {
-                up()
-            }
-
-        rule.runOnIdle {
-            assertThat(interactions).hasSize(2)
-            assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
-            assertThat(interactions[1]).isInstanceOf(DragInteraction.Stop::class.java)
-            assertThat((interactions[1] as DragInteraction.Stop).start)
-                .isEqualTo(interactions[0])
-        }
-    }
-
-    @Test
-    fun scrollable_interactionSource_resetWhenDisposed() {
-        val interactionSource = MutableInteractionSource()
-        var emitScrollableBox by mutableStateOf(true)
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-
-        rule.setContentAndGetScope {
-            Box {
-                if (emitScrollableBox) {
-                    Box(
-                        modifier = Modifier
-                            .testTag(scrollableBoxTag)
-                            .size(100.dp)
-                            .scrollable(
-                                interactionSource = interactionSource,
-                                orientation = Orientation.Horizontal,
-                                state = controller
-                            )
-                    )
-                }
-            }
-        }
-
-        val interactions = mutableListOf<Interaction>()
-
-        scope.launch {
-            interactionSource.interactions.collect { interactions.add(it) }
-        }
-
-        rule.runOnIdle {
-            assertThat(interactions).isEmpty()
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag)
-            .performTouchInput {
-                down(Offset(visibleSize.width / 4f, visibleSize.height / 2f))
-                moveBy(Offset(visibleSize.width / 2f, 0f))
-            }
-
-        rule.runOnIdle {
-            assertThat(interactions).hasSize(1)
-            assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
-        }
-
-        // Dispose scrollable
-        rule.runOnIdle {
-            emitScrollableBox = false
-        }
-
-        rule.runOnIdle {
-            assertThat(interactions).hasSize(2)
-            assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
-            assertThat(interactions[1]).isInstanceOf(DragInteraction.Cancel::class.java)
-            assertThat((interactions[1] as DragInteraction.Cancel).start)
-                .isEqualTo(interactions[0])
-        }
-    }
-
-    @Test
-    fun scrollable_flingBehaviourCalled_whenVelocity0() {
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        var flingCalled = 0
-        var flingVelocity: Float = Float.MAX_VALUE
-        val flingBehaviour = object : FlingBehavior {
-            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
-                flingCalled++
-                flingVelocity = initialVelocity
-                return 0f
-            }
-        }
-        setScrollableContent {
-            Modifier.scrollable(
-                state = controller,
-                flingBehavior = flingBehaviour,
-                orientation = Orientation.Horizontal
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            down(this.center)
-            moveBy(Offset(115f, 0f))
-            up()
-        }
-        assertThat(flingCalled).isEqualTo(1)
-        assertThat(flingVelocity).isLessThan(0.01f)
-        assertThat(flingVelocity).isGreaterThan(-0.01f)
-    }
-
-    @Test
-    fun scrollable_flingBehaviourCalled() {
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        var flingCalled = 0
-        var flingVelocity: Float = Float.MAX_VALUE
-        val flingBehaviour = object : FlingBehavior {
-            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
-                flingCalled++
-                flingVelocity = initialVelocity
-                return 0f
-            }
-        }
-        setScrollableContent {
-            Modifier.scrollable(
-                state = controller,
-                flingBehavior = flingBehaviour,
-                orientation = Orientation.Horizontal
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            swipeWithVelocity(
-                this.center,
-                this.center + Offset(115f, 0f),
-                endVelocity = 1000f
-            )
-        }
-        assertThat(flingCalled).isEqualTo(1)
-        assertThat(flingVelocity).isWithin(5f).of(1000f)
-    }
-
-    @Test
-    fun scrollable_flingBehaviourCalled_reversed() {
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        var flingCalled = 0
-        var flingVelocity: Float = Float.MAX_VALUE
-        val flingBehaviour = object : FlingBehavior {
-            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
-                flingCalled++
-                flingVelocity = initialVelocity
-                return 0f
-            }
-        }
-        setScrollableContent {
-            Modifier.scrollable(
-                state = controller,
-                reverseDirection = true,
-                flingBehavior = flingBehaviour,
-                orientation = Orientation.Horizontal
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            swipeWithVelocity(
-                this.center,
-                this.center + Offset(115f, 0f),
-                endVelocity = 1000f
-            )
-        }
-        assertThat(flingCalled).isEqualTo(1)
-        assertThat(flingVelocity).isWithin(5f).of(-1000f)
-    }
-
-    @Test
-    fun scrollable_flingBehaviourCalled_correctScope() {
-        var total = 0f
-        var returned = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        val flingBehaviour = object : FlingBehavior {
-            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
-                returned = scrollBy(123f)
-                return 0f
-            }
-        }
-        setScrollableContent {
-            Modifier.scrollable(
-                state = controller,
-                flingBehavior = flingBehaviour,
-                orientation = Orientation.Horizontal
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            down(center)
-            moveBy(Offset(x = 100f, y = 0f))
-        }
-
-        val prevTotal = rule.runOnIdle {
-            assertThat(total).isGreaterThan(0f)
-            total
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            up()
-        }
-
-        rule.runOnIdle {
-            assertThat(total).isEqualTo(prevTotal + 123)
-            assertThat(returned).isEqualTo(123f)
-        }
-    }
-
-    @Test
-    fun scrollable_flingBehaviourCalled_reversed_correctScope() {
-        var total = 0f
-        var returned = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        val flingBehaviour = object : FlingBehavior {
-            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
-                returned = scrollBy(123f)
-                return 0f
-            }
-        }
-        setScrollableContent {
-            Modifier.scrollable(
-                state = controller,
-                reverseDirection = true,
-                flingBehavior = flingBehaviour,
-                orientation = Orientation.Horizontal
-            )
-        }
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            down(center)
-            moveBy(Offset(x = 100f, y = 0f))
-        }
-
-        val prevTotal = rule.runOnIdle {
-            assertThat(total).isLessThan(0f)
-            total
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            up()
-        }
-
-        rule.runOnIdle {
-            assertThat(total).isEqualTo(prevTotal + 123)
-            assertThat(returned).isEqualTo(123f)
-        }
-    }
-
-    @Test
-    fun scrollable_setsModifierLocalScrollableContainer() {
-        val controller = ScrollableState { it }
-
-        var isOuterInScrollableContainer: Boolean? = null
-        var isInnerInScrollableContainer: Boolean? = null
-        rule.setContent {
-            Box {
-                Box(
-                    modifier = Modifier
-                        .testTag(scrollableBoxTag)
-                        .size(100.dp)
-                        .then(
-                            object : ModifierLocalConsumer {
-                                override fun onModifierLocalsUpdated(
-                                    scope: ModifierLocalReadScope
-                                ) {
-                                    with(scope) {
-                                        isOuterInScrollableContainer =
-                                            ModifierLocalScrollableContainer.current
-                                    }
-                                }
-                            }
-                        )
-                        .scrollable(
-                            state = controller,
-                            orientation = Orientation.Horizontal
-                        )
-                        .then(
-                            object : ModifierLocalConsumer {
-                                override fun onModifierLocalsUpdated(
-                                    scope: ModifierLocalReadScope
-                                ) {
-                                    with(scope) {
-                                        isInnerInScrollableContainer =
-                                            ModifierLocalScrollableContainer.current
-                                    }
-                                }
-                            }
-                        )
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(isOuterInScrollableContainer).isFalse()
-            assertThat(isInnerInScrollableContainer).isTrue()
-        }
-    }
-
-    @Test
-    fun scrollable_scrollByWorksWithRepeatableAnimations() {
-        rule.mainClock.autoAdvance = false
-
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        rule.setContentAndGetScope {
-            Box(
-                modifier = Modifier
-                    .size(100.dp)
-                    .scrollable(
-                        state = controller,
-                        orientation = Orientation.Horizontal
-                    )
-            )
-        }
-
-        rule.runOnIdle {
-            scope.launch {
-                controller.animateScrollBy(
-                    100f,
-                    keyframes {
-                        durationMillis = 2500
-                        // emulate a repeatable animation:
-                        0f at 0
-                        100f at 500
-                        100f at 1000
-                        0f at 1500
-                        0f at 2000
-                        100f at 2500
-                    }
-                )
-            }
-        }
-
-        rule.mainClock.advanceTimeBy(250)
-        rule.runOnIdle {
-            // in the middle of the first animation
-            assertThat(total).isGreaterThan(0f)
-            assertThat(total).isLessThan(100f)
-        }
-
-        rule.mainClock.advanceTimeBy(500) // 750 ms
-        rule.runOnIdle {
-            // first animation finished
-            assertThat(total).isEqualTo(100)
-        }
-
-        rule.mainClock.advanceTimeBy(250) // 1250 ms
-        rule.runOnIdle {
-            // in the middle of the second animation
-            assertThat(total).isGreaterThan(0f)
-            assertThat(total).isLessThan(100f)
-        }
-
-        rule.mainClock.advanceTimeBy(500) // 1750 ms
-        rule.runOnIdle {
-            // second animation finished
-            assertThat(total).isEqualTo(0)
-        }
-
-        rule.mainClock.advanceTimeBy(500) // 2250 ms
-        rule.runOnIdle {
-            // in the middle of the third animation
-            assertThat(total).isGreaterThan(0f)
-            assertThat(total).isLessThan(100f)
-        }
-
-        rule.mainClock.advanceTimeBy(500) // 2750 ms
-        rule.runOnIdle {
-            // third animation finished
-            assertThat(total).isEqualTo(100)
-        }
-    }
-
-    @Test
-    fun scrollable_cancellingAnimateScrollUpdatesIsScrollInProgress() {
-        rule.mainClock.autoAdvance = false
-
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        rule.setContentAndGetScope {
-            Box(
-                modifier = Modifier
-                    .size(100.dp)
-                    .scrollable(
-                        state = controller,
-                        orientation = Orientation.Horizontal
-                    )
-            )
-        }
-
-        lateinit var animateJob: Job
-
-        rule.runOnIdle {
-            animateJob = scope.launch {
-                controller.animateScrollBy(
-                    100f,
-                    tween(1000)
-                )
-            }
-        }
-
-        rule.mainClock.advanceTimeBy(500)
-        rule.runOnIdle {
-            assertThat(controller.isScrollInProgress).isTrue()
-        }
-
-        // Stop halfway through the animation
-        animateJob.cancel()
-
-        rule.runOnIdle {
-            assertThat(controller.isScrollInProgress).isFalse()
-        }
-    }
-
-    @Test
-    fun scrollable_preemptingAnimateScrollUpdatesIsScrollInProgress() {
-        rule.mainClock.autoAdvance = false
-
-        var total = 0f
-        val controller = ScrollableState(
-            consumeScrollDelta = {
-                total += it
-                it
-            }
-        )
-        rule.setContentAndGetScope {
-            Box(
-                modifier = Modifier
-                    .size(100.dp)
-                    .scrollable(
-                        state = controller,
-                        orientation = Orientation.Horizontal
-                    )
-            )
-        }
-
-        rule.runOnIdle {
-            scope.launch {
-                controller.animateScrollBy(
-                    100f,
-                    tween(1000)
-                )
-            }
-        }
-
-        rule.mainClock.advanceTimeBy(500)
-        rule.runOnIdle {
-            assertThat(total).isGreaterThan(0f)
-            assertThat(total).isLessThan(100f)
-            assertThat(controller.isScrollInProgress).isTrue()
-            scope.launch {
-                controller.animateScrollBy(
-                    -100f,
-                    tween(1000)
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(controller.isScrollInProgress).isTrue()
-        }
-
-        rule.mainClock.advanceTimeBy(1000)
-        rule.mainClock.advanceTimeByFrame()
-
-        rule.runOnIdle {
-            assertThat(total).isGreaterThan(-75f)
-            assertThat(total).isLessThan(0f)
-            assertThat(controller.isScrollInProgress).isFalse()
-        }
-    }
-
-    @Test
-    fun scrollable_multiDirectionsShouldPropagateOrthogonalAxisToNextParentWithSameDirection() {
-        var innerDelta = 0f
-        var middleDelta = 0f
-        var outerDelta = 0f
-
-        val outerStateController = ScrollableState {
-            outerDelta += it
-            it
-        }
-
-        val middleController = ScrollableState {
-            middleDelta += it
-            it / 2
-        }
-
-        val innerController = ScrollableState {
-            innerDelta += it
-            it / 2
-        }
-
-        rule.setContentAndGetScope {
-            Box(
-                modifier = Modifier
-                    .testTag("outerScrollable")
-                    .size(300.dp)
-                    .scrollable(
-                        outerStateController,
-                        orientation = Orientation.Horizontal
-                    )
-
-            ) {
-                Box(
-                    modifier = Modifier
-                        .testTag("middleScrollable")
-                        .size(300.dp)
-                        .scrollable(
-                            middleController,
-                            orientation = Orientation.Vertical
-                        )
-
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .testTag("innerScrollable")
-                            .size(300.dp)
-                            .scrollable(
-                                innerController,
-                                orientation = Orientation.Horizontal
-                            )
-                    )
-                }
-            }
-        }
-
-        rule.onNodeWithTag("innerScrollable").performTouchInput {
-            down(center)
-            moveBy(Offset(100f, 0f))
-            up()
-        }
-
-        rule.runOnIdle {
-            assertThat(innerDelta).isGreaterThan(0)
-            assertThat(middleDelta).isEqualTo(0)
-            assertThat(outerDelta).isEqualTo(innerDelta / 2f)
-        }
-    }
-
-    @Test
-    fun nestedScrollable_shouldImmediateScrollIfChildIsFlinging() {
-        var innerDelta = 0f
-        var middleDelta = 0f
-        var outerDelta = 0f
-        var touchSlop = 0f
-
-        val outerStateController = ScrollableState {
-            outerDelta += it
-            0f
-        }
-
-        val middleController = ScrollableState {
-            middleDelta += it
-            0f
-        }
-
-        val innerController = ScrollableState {
-            innerDelta += it
-            it / 2f
-        }
-
-        rule.setContentAndGetScope {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            Box(
-                modifier = Modifier
-                    .testTag("outerScrollable")
-                    .size(600.dp)
-                    .background(Color.Red)
-                    .scrollable(
-                        outerStateController,
-                        orientation = Orientation.Vertical
-                    ),
-                contentAlignment = Alignment.BottomStart
-            ) {
-                Box(
-                    modifier = Modifier
-                        .testTag("middleScrollable")
-                        .size(300.dp)
-                        .background(Color.Blue)
-                        .scrollable(
-                            middleController,
-                            orientation = Orientation.Vertical
-                        ),
-                    contentAlignment = Alignment.BottomStart
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .testTag("innerScrollable")
-                            .size(50.dp)
-                            .background(Color.Yellow)
-                            .scrollable(
-                                innerController,
-                                orientation = Orientation.Vertical
-                            )
-                    )
-                }
-            }
-        }
-
-        rule.mainClock.autoAdvance = false
-        rule.onNodeWithTag("innerScrollable").performTouchInput {
-            swipeUp()
-        }
-
-        rule.mainClock.advanceTimeByFrame()
-        rule.mainClock.advanceTimeByFrame()
-
-        val previousOuter = outerDelta
-
-        rule.onNodeWithTag("outerScrollable").performTouchInput {
-            down(topCenter)
-            // Move less than touch slop, should start immediately
-            moveBy(Offset(0f, touchSlop / 2))
-        }
-
-        rule.mainClock.autoAdvance = true
-
-        rule.runOnIdle {
-            assertThat(outerDelta).isEqualTo(previousOuter + touchSlop / 2)
-        }
-    }
-
-    // b/179417109 Double checks that in a nested scroll cycle, the parent post scroll
-    // consumption is taken into consideration.
-    @Test
-    fun dispatchScroll_shouldReturnConsumedDeltaInNestedScrollChain() {
-        var consumedInner = 0f
-        var consumedOuter = 0f
-        var touchSlop = 0f
-
-        var preScrollAvailable = Offset.Zero
-        var consumedPostScroll = Offset.Zero
-        var postScrollAvailable = Offset.Zero
-
-        val outerStateController = ScrollableState {
-            consumedOuter += it
-            it
-        }
-
-        val innerController = ScrollableState {
-            consumedInner += it / 2
-            it / 2
-        }
-
-        val connection = object : NestedScrollConnection {
-            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
-                preScrollAvailable += available
-                return Offset.Zero
-            }
-
-            override fun onPostScroll(
-                consumed: Offset,
-                available: Offset,
-                source: NestedScrollSource
-            ): Offset {
-                consumedPostScroll += consumed
-                postScrollAvailable += available
-                return Offset.Zero
-            }
-        }
-
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            Box(modifier = Modifier.nestedScroll(connection)) {
-                Box(
-                    modifier = Modifier
-                        .testTag("outerScrollable")
-                        .size(300.dp)
-                        .scrollable(
-                            outerStateController,
-                            orientation = Orientation.Horizontal
-                        )
-
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .testTag("innerScrollable")
-                            .size(300.dp)
-                            .scrollable(
-                                innerController,
-                                orientation = Orientation.Horizontal
-                            )
-                    )
-                }
-            }
-        }
-
-        val scrollDelta = 200f
-
-        rule.onRoot().performTouchInput {
-            down(center)
-            moveBy(Offset(scrollDelta, 0f))
-            up()
-        }
-
-        rule.runOnIdle {
-            assertThat(consumedInner).isGreaterThan(0)
-            assertThat(consumedOuter).isGreaterThan(0)
-            assertThat(touchSlop).isGreaterThan(0)
-            assertThat(postScrollAvailable.x).isEqualTo(0f)
-            assertThat(consumedPostScroll.x).isEqualTo(scrollDelta - touchSlop)
-            assertThat(preScrollAvailable.x).isEqualTo(scrollDelta - touchSlop)
-            assertThat(scrollDelta).isEqualTo(consumedInner + consumedOuter + touchSlop)
-        }
-    }
-
-    @Test
-    fun testInspectorValue() {
-        val controller = ScrollableState(
-            consumeScrollDelta = { it }
-        )
-        rule.setContentAndGetScope {
-            val modifier =
-                Modifier.scrollable(controller, Orientation.Vertical).first() as InspectableValue
-            assertThat(modifier.nameFallback).isEqualTo("scrollable")
-            assertThat(modifier.valueOverride).isNull()
-            assertThat(modifier.inspectableElements.map { it.name }.asIterable()).containsExactly(
-                "orientation",
-                "state",
-                "overscrollEffect",
-                "enabled",
-                "reverseDirection",
-                "flingBehavior",
-                "interactionSource",
-                "scrollableBringIntoViewConfig",
-            )
-        }
-    }
-
-    @OptIn(ExperimentalFoundationApi::class)
-    @Test
-    fun producingEqualMaterializedModifierAfterRecomposition() {
-        val state = ScrollableState { it }
-        val counter = mutableStateOf(0)
-        var materialized: Modifier? = null
-
-        rule.setContent {
-            counter.value // just to trigger recomposition
-            materialized = currentComposer.materialize(
-                Modifier.scrollable(
-                    state,
-                    Orientation.Vertical,
-                    NoOpOverscrollEffect
-                )
-            )
-        }
-
-        lateinit var first: Modifier
-        rule.runOnIdle {
-            first = requireNotNull(materialized)
-            materialized = null
-            counter.value++
-        }
-
-        rule.runOnIdle {
-            val second = requireNotNull(materialized)
-            assertThat(first).isEqualTo(second)
-        }
-    }
-
-    @Test
-    fun focusStaysInScrollableEvenThoughThereIsACloserItemOutside() {
-        lateinit var focusManager: FocusManager
-        val initialFocus = FocusRequester()
-        var nextItemIsFocused = false
-        rule.setContent {
-            focusManager = LocalFocusManager.current
-            Column {
-                Column(
-                    Modifier
-                        .size(10.dp)
-                        .verticalScroll(rememberScrollState())
-                ) {
-                    Box(
-                        Modifier
-                            .size(10.dp)
-                            .focusRequester(initialFocus)
-                            .focusable()
-                    )
-                    Box(Modifier.size(10.dp))
-                    Box(
-                        Modifier
-                            .size(10.dp)
-                            .onFocusChanged { nextItemIsFocused = it.isFocused }
-                            .focusable())
-                }
-                Box(
-                    Modifier
-                        .size(10.dp)
-                        .focusable()
-                )
-            }
-        }
-
-        rule.runOnIdle { initialFocus.requestFocus() }
-        rule.runOnIdle { focusManager.moveFocus(FocusDirection.Down) }
-
-        rule.runOnIdle { assertThat(nextItemIsFocused).isTrue() }
-    }
-
-    @Test
-    fun verticalScrollable_assertVelocityCalculationIsSimilarInsideOutsideVelocityTracker() {
-        // arrange
-        val tracker = VelocityTracker()
-        var velocity = Velocity.Zero
-        val capturingScrollConnection = object : NestedScrollConnection {
-            override suspend fun onPreFling(available: Velocity): Velocity {
-                velocity += available
-                return Velocity.Zero
-            }
-        }
-        val controller = ScrollableState { _ -> 0f }
-
-        setScrollableContent {
-            Modifier
-                .pointerInput(Unit) {
-                    savePointerInputEvents(tracker, this)
-                }
-                .nestedScroll(capturingScrollConnection)
-                .scrollable(controller, Orientation.Vertical)
-        }
-
-        // act
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            swipeUp()
-        }
-
-        // assert
-        rule.runOnIdle {
-            val diff = abs((velocity - tracker.calculateVelocity()).y)
-            assertThat(diff).isLessThan(VelocityTrackerCalculationThreshold)
-        }
-        tracker.resetTracking()
-        velocity = Velocity.Zero
-
-        // act
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            swipeDown()
-        }
-
-        // assert
-        rule.runOnIdle {
-            val diff = abs((velocity - tracker.calculateVelocity()).y)
-            assertThat(diff).isLessThan(VelocityTrackerCalculationThreshold)
-        }
-    }
-
-    @Test
-    fun horizontalScrollable_assertVelocityCalculationIsSimilarInsideOutsideVelocityTracker() {
-        // arrange
-        val tracker = VelocityTracker()
-        var velocity = Velocity.Zero
-        val capturingScrollConnection = object : NestedScrollConnection {
-            override suspend fun onPreFling(available: Velocity): Velocity {
-                velocity += available
-                return Velocity.Zero
-            }
-        }
-        val controller = ScrollableState { _ -> 0f }
-
-        setScrollableContent {
-            Modifier
-                .pointerInput(Unit) {
-                    savePointerInputEvents(tracker, this)
-                }
-                .nestedScroll(capturingScrollConnection)
-                .scrollable(controller, Orientation.Horizontal)
-        }
-
-        // act
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            swipeLeft()
-        }
-
-        // assert
-        rule.runOnIdle {
-            val diff = abs((velocity - tracker.calculateVelocity()).x)
-            assertThat(diff).isLessThan(VelocityTrackerCalculationThreshold)
-        }
-        tracker.resetTracking()
-        velocity = Velocity.Zero
-
-        // act
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            swipeRight()
-        }
-
-        // assert
-        rule.runOnIdle {
-            val diff = abs((velocity - tracker.calculateVelocity()).x)
-            assertThat(diff).isLessThan(VelocityTrackerCalculationThreshold)
-        }
-    }
-
-    @Test
-    fun offsetsScrollable_velocityCalculationShouldConsiderLocalPositions() {
-        // arrange
-        var velocity = Velocity.Zero
-        val fullScreen = mutableStateOf(false)
-        lateinit var scrollState: LazyListState
-        val capturingScrollConnection = object : NestedScrollConnection {
-            override suspend fun onPreFling(available: Velocity): Velocity {
-                velocity += available
-                return Velocity.Zero
-            }
-        }
-        rule.setContent {
-            scrollState = rememberLazyListState()
-            Column(modifier = Modifier.nestedScroll(capturingScrollConnection)) {
-                if (!fullScreen.value) {
-                    Box(
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .background(Color.Black)
-                            .height(400.dp)
-                    )
-                }
-
-                LazyColumn(state = scrollState) {
-                    items(100) {
-                        Box(
-                            modifier = Modifier
-                                .padding(10.dp)
-                                .background(Color.Red)
-                                .fillMaxWidth()
-                                .height(50.dp)
-                        )
-                    }
-                }
-            }
-        }
-        // act
-        // Register generated velocity with offset
-        composeViewSwipeUp()
-        rule.waitForIdle()
-        val previousVelocity = velocity
-        velocity = Velocity.Zero
-        // Remove offset and restart scroll
-        fullScreen.value = true
-        rule.runOnIdle {
-            runBlocking {
-                scrollState.scrollToItem(0)
-            }
-        }
-        rule.waitForIdle()
-        // Register generated velocity without offset, should be larger as there was more
-        // screen to cover.
-        composeViewSwipeUp()
-
-        // assert
-        rule.runOnIdle {
-            assertThat(abs(previousVelocity.y)).isNotEqualTo(abs(velocity.y))
-        }
-    }
-
-    @Test
-    fun disableSystemAnimations_defaultFlingBehaviorShouldContinueToWork() {
-
-        val controller = ScrollableState { 0f }
-        var defaultFlingBehavior: DefaultFlingBehavior? = null
-        setScrollableContent {
-            defaultFlingBehavior = ScrollableDefaults.flingBehavior() as? DefaultFlingBehavior
-            Modifier.scrollable(
-                state = controller,
-                orientation = Orientation.Horizontal,
-                flingBehavior = defaultFlingBehavior
-            )
-        }
-
-        scope.launch {
-            controller.scroll {
-                defaultFlingBehavior?.let {
-                    with(it) { performFling(1000f) }
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(defaultFlingBehavior?.lastAnimationCycleCount).isGreaterThan(1)
-        }
-
-        // Simulate turning of animation
-        scope.launch {
-            controller.scroll {
-                withContext(TestScrollMotionDurationScale(0f)) {
-                    defaultFlingBehavior?.let {
-                        with(it) { performFling(1000f) }
-                    }
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(defaultFlingBehavior?.lastAnimationCycleCount).isGreaterThan(1)
-        }
-    }
-
-    @Test
-    fun defaultFlingBehavior_useScrollMotionDurationScale() {
-
-        val controller = ScrollableState { 0f }
-        var defaultFlingBehavior: DefaultFlingBehavior? = null
-        var switchMotionDurationScale by mutableStateOf(true)
-
-        rule.setContentAndGetScope {
-            val flingSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
-            if (switchMotionDurationScale) {
-                defaultFlingBehavior =
-                    DefaultFlingBehavior(flingSpec, TestScrollMotionDurationScale(1f))
-                Box(
-                    modifier = Modifier
-                        .testTag(scrollableBoxTag)
-                        .size(100.dp)
-                        .scrollable(
-                            state = controller,
-                            orientation = Orientation.Horizontal,
-                            flingBehavior = defaultFlingBehavior
-                        )
-                )
-            } else {
-                defaultFlingBehavior =
-                    DefaultFlingBehavior(flingSpec, TestScrollMotionDurationScale(0f))
-                Box(
-                    modifier = Modifier
-                        .testTag(scrollableBoxTag)
-                        .size(100.dp)
-                        .scrollable(
-                            state = controller,
-                            orientation = Orientation.Horizontal,
-                            flingBehavior = defaultFlingBehavior
-                        )
-                )
-            }
-        }
-
-        scope.launch {
-            controller.scroll {
-                defaultFlingBehavior?.let {
-                    with(it) { performFling(1000f) }
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(defaultFlingBehavior?.lastAnimationCycleCount).isGreaterThan(1)
-        }
-
-        switchMotionDurationScale = false
-        rule.waitForIdle()
-
-        scope.launch {
-            controller.scroll {
-                defaultFlingBehavior?.let {
-                    with(it) { performFling(1000f) }
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(defaultFlingBehavior?.lastAnimationCycleCount).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun scrollable_noMomentum_shouldChangeScrollStateAfterRelease() {
-        val scrollState = ScrollState(0)
-        val delta = 10f
-        var touchSlop = 0f
-        setScrollableContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            Modifier.scrollable(scrollState, Orientation.Vertical)
-        }
-        var previousScrollValue = 0
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            down(center)
-            // generate various move events
-            repeat(30) {
-                moveBy(Offset(0f, delta), delayMillis = 8L)
-                previousScrollValue += delta.toInt()
-            }
-            // stop for a moment
-            advanceEventTime(3000L)
-            up()
-        }
-
-        rule.runOnIdle {
-            Assert.assertEquals((previousScrollValue - touchSlop).toInt(), scrollState.value)
-        }
-    }
-
-    @Test
-    fun defaultScrollableState_scrollByWithNan_shouldFilterOutNan() {
-        val controller = ScrollableState {
-            assertThat(it).isNotNaN()
-            0f
-        }
-
-        val nanGenerator = object : FlingBehavior {
-            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
-                return scrollBy(Float.NaN)
-            }
-        }
-
-        setScrollableContent {
-            Modifier.scrollable(
-                state = controller,
-                orientation = Orientation.Horizontal,
-                flingBehavior = nanGenerator
-            )
-        }
-
-        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
-            swipeLeft()
-        }
-    }
-
-    private fun setScrollableContent(scrollableModifierFactory: @Composable () -> Modifier) {
-        rule.setContentAndGetScope {
-            Box {
-                val scrollable = scrollableModifierFactory()
-                Box(
-                    modifier = Modifier
-                        .testTag(scrollableBoxTag)
-                        .size(100.dp)
-                        .then(scrollable)
-                )
-            }
-        }
-    }
-}
-
-// Very low tolerance on the difference
-internal val VelocityTrackerCalculationThreshold = 1
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal suspend fun savePointerInputEvents(
-    tracker: VelocityTracker,
-    pointerInputScope: PointerInputScope
-) {
-    if (VelocityTrackerAddPointsFix) {
-        savePointerInputEventsWithFix(tracker, pointerInputScope)
-    } else {
-        savePointerInputEventsLegacy(tracker, pointerInputScope)
-    }
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal suspend fun savePointerInputEventsWithFix(
-    tracker: VelocityTracker,
-    pointerInputScope: PointerInputScope
-) {
-    with(pointerInputScope) {
-        coroutineScope {
-            awaitPointerEventScope {
-                while (true) {
-                    var event: PointerInputChange? = awaitFirstDown()
-                    while (event != null && !event.changedToUpIgnoreConsumed()) {
-                        val currentEvent = awaitPointerEvent().changes
-                            .firstOrNull()
-
-                        if (currentEvent != null && !currentEvent.changedToUpIgnoreConsumed()) {
-                            if (currentEvent.historical.isEmpty()) {
-                                tracker.addPosition(
-                                    currentEvent.uptimeMillis,
-                                    currentEvent.position
-                                )
-                            } else {
-                                currentEvent.historical.fastForEach {
-                                    tracker.addPosition(it.uptimeMillis, it.position)
-                                }
-                            }
-                        }
-
-                        event = currentEvent
-                    }
-                }
-            }
-        }
-    }
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal suspend fun savePointerInputEventsLegacy(
-    tracker: VelocityTracker,
-    pointerInputScope: PointerInputScope
-) {
-    with(pointerInputScope) {
-        coroutineScope {
-            awaitPointerEventScope {
-                while (true) {
-                    var event = awaitFirstDown()
-                    tracker.addPosition(event.uptimeMillis, event.position)
-                    while (!event.changedToUpIgnoreConsumed()) {
-                        val currentEvent = awaitPointerEvent().changes
-                            .firstOrNull()
-
-                        if (currentEvent != null) {
-                            currentEvent.historical.fastForEach {
-                                tracker.addPosition(it.uptimeMillis, it.position)
-                            }
-                            tracker.addPosition(
-                                currentEvent.uptimeMillis,
-                                currentEvent.position
-                            )
-                            event = currentEvent
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
-
-internal fun composeViewSwipeUp() {
-    onView(allOf(instanceOf(AbstractComposeView::class.java)))
-        .perform(
-            espressoSwipe(
-                GeneralLocation.CENTER,
-                GeneralLocation.TOP_CENTER
-            )
-        )
-}
-
-internal fun composeViewSwipeDown() {
-    onView(allOf(instanceOf(AbstractComposeView::class.java)))
-        .perform(
-            espressoSwipe(
-                GeneralLocation.CENTER,
-                GeneralLocation.BOTTOM_CENTER
-            )
-        )
-}
-
-internal fun composeViewSwipeLeft() {
-    onView(allOf(instanceOf(AbstractComposeView::class.java)))
-        .perform(
-            espressoSwipe(
-                GeneralLocation.CENTER,
-                GeneralLocation.CENTER_LEFT
-            )
-        )
-}
-
-internal fun composeViewSwipeRight() {
-    onView(allOf(instanceOf(AbstractComposeView::class.java)))
-        .perform(
-            espressoSwipe(
-                GeneralLocation.CENTER,
-                GeneralLocation.CENTER_RIGHT
-            )
-        )
-}
-
-private fun espressoSwipe(
-    start: CoordinatesProvider,
-    end: CoordinatesProvider
-): GeneralSwipeAction {
-    return GeneralSwipeAction(
-        Swipe.FAST, start, end,
-        Press.FINGER
-    )
-}
-
-internal class TestScrollMotionDurationScale(override val scaleFactor: Float) : MotionDurationScale
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyGridSnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyGridSnapFlingBehaviorTest.kt
deleted file mode 100644
index b756f09..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyGridSnapFlingBehaviorTest.kt
+++ /dev/null
@@ -1,525 +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.compose.foundation.gesture.snapping
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
-import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
-import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout.Companion.CenterToCenter
-import androidx.compose.foundation.gestures.snapping.calculateDistanceToDesiredSnapPosition
-import androidx.compose.foundation.gestures.snapping.offsetOnMainAxis
-import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
-import androidx.compose.foundation.gestures.snapping.singleAxisViewportSize
-import androidx.compose.foundation.gestures.snapping.sizeOnMainAxis
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.grid.BaseLazyGridTestWithOrientation
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.LazyGridState
-import androidx.compose.foundation.lazy.grid.rememberLazyGridState
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.TouchInjectionScope
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.swipeLeft
-import androidx.compose.ui.test.swipeUp
-import androidx.compose.ui.test.swipeWithVelocity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.LargeTest
-import com.google.common.truth.Truth
-import kotlin.math.abs
-import kotlin.math.absoluteValue
-import kotlin.test.assertEquals
-import kotlin.test.assertNotEquals
-import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-@OptIn(ExperimentalFoundationApi::class)
-class LazyGridSnapFlingBehaviorTest(private val orientation: Orientation) :
-    BaseLazyGridTestWithOrientation(orientation) {
-
-    private val density: Density
-        get() = rule.density
-
-    private lateinit var snapLayoutInfoProvider: SnapLayoutInfoProvider
-    private lateinit var snapFlingBehavior: FlingBehavior
-
-    @Test
-    fun belowThresholdVelocity_lessThanAnItemScroll_shouldStayInSamePage() {
-        var lazyGridState: LazyGridState? = null
-        var stepSize = 0f
-        var velocityThreshold = 0f
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyGridState().also { lazyGridState = it }
-            stepSize = with(density) { ItemSize.toPx() }
-            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
-            MainLayout(state = state)
-        }
-
-        // Scroll a bit
-        onMainList().swipeOnMainAxis()
-        rule.waitForIdle()
-        val currentItem = density.getCurrentSnappedItem(lazyGridState)
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(stepSize / 2, velocityThreshold / 2)
-        }
-
-        // assert
-        rule.runOnIdle {
-            val nextItem = density.getCurrentSnappedItem(lazyGridState)
-            assertEquals(currentItem, nextItem)
-        }
-    }
-
-    @Test
-    fun belowThresholdVelocity_moreThanAnItemScroll_shouldGoToNextPage() {
-        var lazyGridState: LazyGridState? = null
-        var stepSize = 0f
-        var velocityThreshold = 0f
-
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyGridState().also { lazyGridState = it }
-            stepSize = with(density) { ItemSize.toPx() }
-            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
-            MainLayout(state = state)
-        }
-
-        // Scroll a bit
-        onMainList().swipeOnMainAxis()
-        rule.waitForIdle()
-        val currentItem = density.getCurrentSnappedItem(lazyGridState)
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                stepSize,
-                velocityThreshold / 2
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            val nextItem = density.getCurrentSnappedItem(lazyGridState)
-            assertEquals(nextItem, currentItem + (lazyGridState?.maxCells() ?: 0))
-        }
-    }
-
-    @Test
-    fun aboveThresholdVelocityForward_notLargeEnoughScroll_shouldGoToNextPage() {
-        var lazyGridState: LazyGridState? = null
-        var stepSize = 0f
-        var velocityThreshold = 0f
-
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyGridState().also { lazyGridState = it }
-            stepSize = with(density) { ItemSize.toPx() }
-            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
-            MainLayout(state = state)
-        }
-
-        // Scroll a bit
-        onMainList().swipeOnMainAxis()
-        rule.waitForIdle()
-        val currentItem = density.getCurrentSnappedItem(lazyGridState)
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                stepSize / 2,
-                velocityThreshold * 2
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            val nextItem = density.getCurrentSnappedItem(lazyGridState)
-            assertEquals(nextItem, currentItem + (lazyGridState?.maxCells() ?: 0))
-        }
-    }
-
-    @Ignore // b/293513475
-    @Test
-    fun aboveThresholdVelocityBackward_notLargeEnoughScroll_shouldGoToPreviousPage() {
-        var lazyGridState: LazyGridState? = null
-        var stepSize = 0f
-        var velocityThreshold = 0f
-
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyGridState().also { lazyGridState = it }
-            stepSize = with(density) { ItemSize.toPx() }
-            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
-            MainLayout(state = state)
-        }
-
-        // Scroll a bit
-        onMainList().swipeOnMainAxis()
-        rule.waitForIdle()
-        val currentItem = density.getCurrentSnappedItem(lazyGridState)
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                stepSize / 2,
-                velocityThreshold * 2,
-                true
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            val nextItem = density.getCurrentSnappedItem(lazyGridState)
-            assertEquals(nextItem, currentItem - (lazyGridState?.maxCells() ?: 0))
-        }
-    }
-
-    @Test
-    fun aboveThresholdVelocity_largeEnoughScroll_shouldGoToNextNextPage() {
-        var lazyGridState: LazyGridState? = null
-        var stepSize = 0f
-        var velocityThreshold = 0f
-
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyGridState().also { lazyGridState = it }
-            stepSize = with(density) { ItemSize.toPx() }
-            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
-            MainLayout(state = state)
-        }
-
-        // Scroll a bit
-        onMainList().swipeOnMainAxis()
-        rule.waitForIdle()
-        val currentItem = density.getCurrentSnappedItem(lazyGridState)
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                1.5f * stepSize,
-                velocityThreshold * 3
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            val nextItem = density.getCurrentSnappedItem(lazyGridState)
-            assertEquals(nextItem, currentItem + 2 * (lazyGridState?.maxCells() ?: 0))
-        }
-    }
-
-    @Test
-    fun performFling_shouldPropagateVelocityIfHitEdges() {
-        var stepSize = 0f
-        var latestAvailableVelocity = Velocity.Zero
-        lateinit var lazyGridState: LazyGridState
-        val inspectingNestedScrollConnection = object : NestedScrollConnection {
-            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
-                latestAvailableVelocity = available
-                return Velocity.Zero
-            }
-        }
-
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            lazyGridState = rememberLazyGridState(180) // almost at the end
-            stepSize = with(density) { ItemSize.toPx() }
-            Box(
-                modifier = Modifier
-                    .fillMaxSize()
-                    .nestedScroll(inspectingNestedScrollConnection)
-            ) {
-                MainLayout(state = lazyGridState)
-            }
-        }
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                1.5f * stepSize,
-                30000f
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            assertNotEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
-        }
-
-        // arrange
-        rule.runOnIdle {
-            runBlocking {
-                lazyGridState.scrollToItem(20) // almost at the start
-            }
-        }
-
-        latestAvailableVelocity = Velocity.Zero
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                -1.5f * stepSize,
-                30000f
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            assertNotEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
-        }
-    }
-
-    @Test
-    fun performFling_shouldConsumeAllVelocityIfInTheMiddleOfTheList() {
-        var stepSize = 0f
-        var latestAvailableVelocity = Velocity.Zero
-        lateinit var lazyGridState: LazyGridState
-        val inspectingNestedScrollConnection = object : NestedScrollConnection {
-            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
-                latestAvailableVelocity = available
-                return Velocity.Zero
-            }
-        }
-
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            lazyGridState = rememberLazyGridState(100) // middle of the grid
-            stepSize = with(density) { ItemSize.toPx() }
-            Box(
-                modifier = Modifier
-                    .fillMaxSize()
-                    .nestedScroll(inspectingNestedScrollConnection)
-            ) {
-                MainLayout(state = lazyGridState)
-            }
-        }
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                1.5f * stepSize,
-                10000f // use a not so high velocity
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            assertEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
-        }
-
-        // arrange
-        rule.runOnIdle {
-            runBlocking {
-                lazyGridState.scrollToItem(100) // return to the middle
-            }
-        }
-
-        latestAvailableVelocity = Velocity.Zero
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                -1.5f * stepSize,
-                10000f // use a not so high velocity
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            assertEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
-        }
-    }
-
-    @Test
-    fun remainingScrollOffset_shouldFollowAnimationOffsets() {
-        var stepSize = 0f
-        var velocityThreshold = 0f
-        val scrollOffset = mutableListOf<Float>()
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyGridState()
-            stepSize = with(density) { ItemSize.toPx() }
-            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
-            MainLayout(state = state, scrollOffset)
-        }
-
-        rule.mainClock.autoAdvance = false
-        // act
-        val velocity = velocityThreshold * 3
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                1.5f * stepSize,
-                velocity
-            )
-        }
-        rule.mainClock.advanceTimeByFrame()
-
-        // assert
-        val initialTargetOffset =
-            with(snapLayoutInfoProvider) { density.calculateApproachOffset(velocity) }
-        Truth.assertThat(scrollOffset.first { it != 0f }).isWithin(0.5f)
-            .of(initialTargetOffset)
-
-        // act: wait for remaining offset to grow instead of decay, this indicates the last
-        // snap step will start
-        rule.mainClock.advanceTimeUntil {
-            scrollOffset.size > 2 &&
-                scrollOffset.last() > scrollOffset[scrollOffset.lastIndex - 1]
-        }
-
-        // assert: next calculated offset is the first value emitted by remainingScrollOffset
-        val finalRemainingOffset = with(snapLayoutInfoProvider) {
-            density.calculateSnappingOffset(10000f)
-        }
-        Truth.assertThat(scrollOffset.last()).isWithin(0.5f)
-            .of(finalRemainingOffset)
-        rule.mainClock.autoAdvance = true
-
-        // assert: value settles back to zero
-        rule.runOnIdle {
-            Truth.assertThat(scrollOffset.last()).isEqualTo(0f)
-        }
-    }
-
-    private fun onMainList() = rule.onNodeWithTag(TestTag)
-
-    @Composable
-    fun MainLayout(state: LazyGridState, scrollOffset: MutableList<Float> = mutableListOf()) {
-        snapLayoutInfoProvider = remember(state) { SnapLayoutInfoProvider(state) }
-        val innerFlingBehavior =
-            rememberSnapFlingBehavior(snapLayoutInfoProvider = snapLayoutInfoProvider)
-        snapFlingBehavior = remember(innerFlingBehavior) {
-            QuerySnapFlingBehavior(innerFlingBehavior) { scrollOffset.add(it) }
-        }
-        LazyGrid(
-            cells = GridCells.FixedSize(ItemSize),
-            state = state,
-            modifier = Modifier.testTag(TestTag),
-            flingBehavior = snapFlingBehavior
-        ) {
-            items(200) {
-                Box(modifier = Modifier
-                    .size(ItemSize)
-                    .background(Color.Yellow)) {
-                    BasicText(text = it.toString())
-                }
-            }
-        }
-    }
-
-    private fun LazyGridState.maxCells() =
-        if (layoutInfo.orientation == Orientation.Vertical) {
-            layoutInfo.visibleItemsInfo.maxOf { it.column }
-        } else {
-            layoutInfo.visibleItemsInfo.maxOf { it.row }
-        } + 1
-
-    private fun SemanticsNodeInteraction.swipeOnMainAxis() {
-        performTouchInput {
-            if (orientation == Orientation.Vertical) {
-                swipeUp()
-            } else {
-                swipeLeft()
-            }
-        }
-    }
-
-    private fun Density.getCurrentSnappedItem(state: LazyGridState?): Int {
-        var itemIndex = -1
-        if (state == null) return -1
-        var minDistance = Float.POSITIVE_INFINITY
-        val layoutInfo = state.layoutInfo
-        (state.layoutInfo.visibleItemsInfo).forEach {
-            val distance = calculateDistanceToDesiredSnapPosition(
-                mainAxisViewPortSize = layoutInfo.singleAxisViewportSize,
-                beforeContentPadding = layoutInfo.beforeContentPadding,
-                afterContentPadding = layoutInfo.afterContentPadding,
-                itemSize = it.sizeOnMainAxis(orientation = layoutInfo.orientation),
-                itemOffset = it.offsetOnMainAxis(orientation = layoutInfo.orientation),
-                itemIndex = it.index,
-                snapPositionInLayout = CenterToCenter
-            )
-            if (abs(distance) < minDistance) {
-                minDistance = abs(distance)
-                itemIndex = it.index
-            }
-        }
-        return itemIndex
-    }
-
-    private fun TouchInjectionScope.swipeMainAxisWithVelocity(
-        scrollSize: Float,
-        endVelocity: Float,
-        reversed: Boolean = false
-    ) {
-        val (start, end) = if (orientation == Orientation.Vertical) {
-            bottomCenter to bottomCenter.copy(y = bottomCenter.y - scrollSize)
-        } else {
-            centerRight to centerRight.copy(x = centerRight.x - scrollSize)
-        }
-        swipeWithVelocity(
-            if (reversed) end else start,
-            if (reversed) start else end,
-            endVelocity
-        )
-    }
-
-    private fun Velocity.toAbsoluteFloat(): Float {
-        return (if (orientation == Orientation.Vertical) y else x).absoluteValue
-    }
-
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "{0}")
-        fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
-
-        val ItemSize = 200.dp
-        const val TestTag = "MainList"
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyGridSnapLayoutInfoProviderTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyGridSnapLayoutInfoProviderTest.kt
deleted file mode 100644
index b828a84..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyGridSnapLayoutInfoProviderTest.kt
+++ /dev/null
@@ -1,260 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.gesture.snapping
-
-import androidx.compose.animation.core.calculateTargetValue
-import androidx.compose.animation.splineBasedDecay
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
-import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.grid.BaseLazyGridTestWithOrientation
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.LazyGridState
-import androidx.compose.foundation.lazy.grid.rememberLazyGridState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.LargeTest
-import kotlin.math.absoluteValue
-import kotlin.math.round
-import kotlin.math.sign
-import kotlin.test.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@OptIn(ExperimentalFoundationApi::class)
-@LargeTest
-@RunWith(Parameterized::class)
-class LazyGridSnapLayoutInfoProviderTest(orientation: Orientation) :
-    BaseLazyGridTestWithOrientation(orientation) {
-
-    private val density: Density get() = rule.density
-
-    @Test
-    fun snapStepSize_sameSizeItems_shouldBeAverageItemSize() {
-        var expectedItemSize = 0f
-        var actualItemSize = 0f
-
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyGridState()
-            val layoutInfoProvider = remember(state) { createLayoutInfo(state) }.also {
-                actualItemSize = with(it) { density.calculateSnapStepSize() }
-            }
-            expectedItemSize = with(density) { FixedItemSize.toPx() }
-            MainLayout(
-                state = state,
-                layoutInfo = layoutInfoProvider,
-                items = 200,
-                itemSizeProvider = { FixedItemSize }
-            )
-        }
-
-        rule.runOnIdle {
-            assertEquals(round(expectedItemSize), round(actualItemSize))
-        }
-    }
-
-    @Test
-    fun snapStepSize_differentSizeItems_shouldBeAverageItemSizeOnReferenceIndex() {
-        var actualItemSize = 0f
-        var expectedItemSize = 0f
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyGridState()
-            val layoutInfoProvider = remember(state) { createLayoutInfo(state) }.also {
-                actualItemSize = with(it) { density.calculateSnapStepSize() }
-            }
-            expectedItemSize = state.layoutInfo.visibleItemsInfo.filter {
-                if (vertical) {
-                    it.column == 0
-                } else {
-                    it.row == 0
-                }
-            }.map {
-                if (vertical) it.size.height else it.size.width
-            }.average().toFloat()
-
-            MainLayout(state, layoutInfoProvider, DynamicItemSizes.size, { DynamicItemSizes[it] })
-        }
-
-        rule.runOnIdle {
-            assertEquals(round(expectedItemSize), round(actualItemSize))
-        }
-    }
-
-    @Test
-    fun snapStepSize_withSpacers_shouldBeAverageItemSize() {
-        var snapStepSize = 0f
-        var actualItemSize = 0f
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyGridState()
-            val layoutInfoProvider = remember(state) { createLayoutInfo(state) }.also {
-                snapStepSize = with(it) { density.calculateSnapStepSize() }
-            }
-
-            actualItemSize = with(density) { (FixedItemSize + FixedItemSize / 2).toPx() }
-
-            MainLayout(
-                state = state,
-                layoutInfo = layoutInfoProvider,
-                items = 200,
-                itemSizeProvider = { FixedItemSize }) {
-                if (vertical) {
-                    Column {
-                        Box(
-                            modifier = Modifier
-                                .size(FixedItemSize)
-                                .background(Color.Red)
-                        )
-                        Spacer(
-                            modifier = Modifier
-                                .size(FixedItemSize / 2)
-                                .background(Color.Yellow)
-                        )
-                    }
-                } else {
-                    Row {
-                        Box(
-                            modifier = Modifier
-                                .size(FixedItemSize)
-                                .background(Color.Red)
-                        )
-                        Spacer(
-                            modifier = Modifier
-                                .size(FixedItemSize / 2)
-                                .background(Color.Yellow)
-                        )
-                    }
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertEquals(round(actualItemSize), round(snapStepSize))
-        }
-    }
-
-    @Test
-    fun calculateApproachOffset_highVelocity_approachOffsetIsEqualToDecayMinusItemSize() {
-        lateinit var layoutInfoProvider: SnapLayoutInfoProvider
-        val decay = splineBasedDecay<Float>(rule.density)
-        fun calculateTargetOffset(velocity: Float): Float {
-            val offset = decay.calculateTargetValue(0f, velocity).absoluteValue
-            return (offset - with(density) { 200.dp.toPx() }).coerceAtLeast(0f) * velocity.sign
-        }
-        rule.setContent {
-            val state = rememberLazyGridState()
-            layoutInfoProvider = remember(state) { createLayoutInfo(state) }
-            LazyGrid(
-                cells = GridCells.Fixed(3),
-                state = state,
-                flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
-            ) {
-                items(200) {
-                    Box(modifier = Modifier.size(200.dp))
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertEquals(
-                with(layoutInfoProvider) { density.calculateApproachOffset(10000f) },
-                calculateTargetOffset(10000f)
-            )
-            assertEquals(
-                with(layoutInfoProvider) { density.calculateApproachOffset(-10000f) },
-                calculateTargetOffset(-10000f)
-            )
-        }
-    }
-
-    @Test
-    fun calculateApproachOffset_lowVelocity_approachOffsetIsEqualToZero() {
-        lateinit var layoutInfoProvider: SnapLayoutInfoProvider
-        rule.setContent {
-            val state = rememberLazyGridState()
-            layoutInfoProvider = remember(state) { createLayoutInfo(state) }
-            LazyGrid(
-                cells = GridCells.Fixed(3),
-                state = state,
-                flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
-            ) {
-                items(200) {
-                    Box(modifier = Modifier.size(200.dp))
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertEquals(
-                with(layoutInfoProvider) { density.calculateApproachOffset(1000f) },
-                0f
-            )
-            assertEquals(
-                with(layoutInfoProvider) { density.calculateApproachOffset(-1000f) },
-                0f
-            )
-        }
-    }
-
-    @Composable
-    private fun MainLayout(
-        state: LazyGridState,
-        layoutInfo: SnapLayoutInfoProvider,
-        items: Int,
-        itemSizeProvider: (Int) -> Dp,
-        gridItem: @Composable (Int) -> Unit = { Box(Modifier.size(itemSizeProvider(it))) }
-    ) {
-        LazyGrid(
-            cells = GridCells.Fixed(3),
-            state = state,
-            flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider = layoutInfo)
-        ) {
-            items(items) { gridItem(it) }
-        }
-    }
-
-    private fun createLayoutInfo(
-        state: LazyGridState,
-    ): SnapLayoutInfoProvider {
-        return SnapLayoutInfoProvider(state)
-    }
-
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "{0}")
-        fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
-
-        val FixedItemSize = 200.dp
-        val DynamicItemSizes = (200..500).map { it.dp }
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
deleted file mode 100644
index e33c7cc..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
+++ /dev/null
@@ -1,521 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.gesture.snapping
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
-import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
-import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
-import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout.Companion.CenterToCenter
-import androidx.compose.foundation.gestures.snapping.calculateDistanceToDesiredSnapPosition
-import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
-import androidx.compose.foundation.gestures.snapping.singleAxisViewportSize
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.list.BaseLazyListTestWithOrientation
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.TouchInjectionScope
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.swipeLeft
-import androidx.compose.ui.test.swipeUp
-import androidx.compose.ui.test.swipeWithVelocity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.LargeTest
-import com.google.common.truth.Truth
-import kotlin.math.abs
-import kotlin.math.absoluteValue
-import kotlin.test.assertEquals
-import kotlin.test.assertNotEquals
-import kotlinx.coroutines.runBlocking
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-@OptIn(ExperimentalFoundationApi::class)
-class LazyListSnapFlingBehaviorTest(private val orientation: Orientation) :
-    BaseLazyListTestWithOrientation(orientation) {
-
-    private val density: Density
-        get() = rule.density
-
-    private lateinit var snapLayoutInfoProvider: SnapLayoutInfoProvider
-    private lateinit var snapFlingBehavior: FlingBehavior
-
-    @Test
-    fun belowThresholdVelocity_lessThanAnItemScroll_shouldStayInSamePage() {
-        var lazyListState: LazyListState? = null
-        var stepSize = 0f
-        var velocityThreshold = 0f
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyListState().also { lazyListState = it }
-            stepSize = with(density) { ItemSize.toPx() }
-            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
-            MainLayout(state = state)
-        }
-
-        // Scroll a bit
-        onMainList().swipeOnMainAxis()
-        rule.waitForIdle()
-        val currentItem = density.getCurrentSnappedItem(lazyListState)
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(stepSize / 2, velocityThreshold / 2)
-        }
-
-        // assert
-        rule.runOnIdle {
-            val nextItem = density.getCurrentSnappedItem(lazyListState)
-            assertEquals(currentItem, nextItem)
-        }
-    }
-
-    @Test
-    fun belowThresholdVelocity_moreThanAnItemScroll_shouldGoToNextPage() {
-        var lazyListState: LazyListState? = null
-        var stepSize = 0f
-        var velocityThreshold = 0f
-
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyListState().also { lazyListState = it }
-            stepSize = with(density) { ItemSize.toPx() }
-            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
-            MainLayout(state = state)
-        }
-
-        // Scroll a bit
-        onMainList().swipeOnMainAxis()
-        rule.waitForIdle()
-        val currentItem = density.getCurrentSnappedItem(lazyListState)
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                stepSize,
-                velocityThreshold / 2
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            val nextItem = density.getCurrentSnappedItem(lazyListState)
-            assertEquals(currentItem + 1, nextItem)
-        }
-    }
-
-    @Test
-    fun aboveThresholdVelocityForward_notLargeEnoughScroll_shouldGoToNextPage() {
-        var lazyListState: LazyListState? = null
-        var stepSize = 0f
-        var velocityThreshold = 0f
-
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyListState().also { lazyListState = it }
-            stepSize = with(density) { ItemSize.toPx() }
-            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
-            MainLayout(state = state)
-        }
-
-        // Scroll a bit
-        onMainList().swipeOnMainAxis()
-        rule.waitForIdle()
-        val currentItem = density.getCurrentSnappedItem(lazyListState)
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                stepSize / 2,
-                velocityThreshold * 2
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            val nextItem = density.getCurrentSnappedItem(lazyListState)
-            assertEquals(currentItem + 1, nextItem)
-        }
-    }
-
-    @Test
-    fun aboveThresholdVelocityBackward_notLargeEnoughScroll_shouldGoToPreviousPage() {
-        var lazyListState: LazyListState? = null
-        var stepSize = 0f
-        var velocityThreshold = 0f
-
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyListState().also { lazyListState = it }
-            stepSize = with(density) { ItemSize.toPx() }
-            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
-            MainLayout(state = state)
-        }
-
-        // Scroll a bit
-        onMainList().swipeOnMainAxis()
-        rule.waitForIdle()
-        val currentItem = density.getCurrentSnappedItem(lazyListState)
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                stepSize / 2,
-                velocityThreshold * 2,
-                true
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            val nextItem = density.getCurrentSnappedItem(lazyListState)
-            assertEquals(currentItem - 1, nextItem)
-        }
-    }
-
-    @Test
-    fun aboveThresholdVelocity_largeEnoughScroll_shouldGoToNextNextPage() {
-        var lazyListState: LazyListState? = null
-        var stepSize = 0f
-        var velocityThreshold = 0f
-
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyListState().also { lazyListState = it }
-            stepSize = with(density) { ItemSize.toPx() }
-            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
-            MainLayout(state = state)
-        }
-
-        // Scroll a bit
-        onMainList().swipeOnMainAxis()
-        rule.waitForIdle()
-        val currentItem = density.getCurrentSnappedItem(lazyListState)
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                1.5f * stepSize,
-                velocityThreshold * 3
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            val nextItem = density.getCurrentSnappedItem(lazyListState)
-            assertEquals(currentItem + 2, nextItem)
-        }
-    }
-
-    @Test
-    fun performFling_shouldPropagateVelocityIfHitEdges() {
-        var stepSize = 0f
-        var latestAvailableVelocity = Velocity.Zero
-        lateinit var lazyListState: LazyListState
-        val inspectingNestedScrollConnection = object : NestedScrollConnection {
-            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
-                latestAvailableVelocity = available
-                return Velocity.Zero
-            }
-        }
-
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            lazyListState = rememberLazyListState(180) // almost at the end
-            stepSize = with(density) { ItemSize.toPx() }
-            Box(
-                modifier = Modifier
-                    .fillMaxSize()
-                    .nestedScroll(inspectingNestedScrollConnection)
-            ) {
-                MainLayout(state = lazyListState)
-            }
-        }
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                1.5f * stepSize,
-                30000f
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            assertNotEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
-        }
-
-        // arrange
-        rule.runOnIdle {
-            runBlocking {
-                lazyListState.scrollToItem(20) // almost at the start
-            }
-        }
-
-        latestAvailableVelocity = Velocity.Zero
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                -1.5f * stepSize,
-                30000f
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            assertNotEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
-        }
-    }
-
-    @Test
-    fun performFling_shouldConsumeAllVelocityIfInTheMiddleOfTheList() {
-        var stepSize = 0f
-        var latestAvailableVelocity = Velocity.Zero
-        lateinit var lazyListState: LazyListState
-        val inspectingNestedScrollConnection = object : NestedScrollConnection {
-            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
-                latestAvailableVelocity = available
-                return Velocity.Zero
-            }
-        }
-
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            lazyListState = rememberLazyListState(100) // middle of the list
-            stepSize = with(density) { ItemSize.toPx() }
-            Box(
-                modifier = Modifier
-                    .fillMaxSize()
-                    .nestedScroll(inspectingNestedScrollConnection)
-            ) {
-                MainLayout(state = lazyListState)
-            }
-        }
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                1.5f * stepSize,
-                10000f // use a not so high velocity
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            assertEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
-        }
-
-        // arrange
-        rule.runOnIdle {
-            runBlocking {
-                lazyListState.scrollToItem(100) // return to the middle
-            }
-        }
-
-        latestAvailableVelocity = Velocity.Zero
-
-        // act
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                -1.5f * stepSize,
-                10000f // use a not so high velocity
-            )
-        }
-
-        // assert
-        rule.runOnIdle {
-            assertEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
-        }
-    }
-
-    @Test
-    fun remainingScrollOffset_shouldFollowAnimationOffsets() {
-        var stepSize = 0f
-        var velocityThreshold = 0f
-        val scrollOffset = mutableListOf<Float>()
-        // arrange
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyListState()
-            stepSize = with(density) { ItemSize.toPx() }
-            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
-            MainLayout(state = state, scrollOffset)
-        }
-
-        rule.mainClock.autoAdvance = false
-        // act
-        val velocity = velocityThreshold * 3
-        onMainList().performTouchInput {
-            swipeMainAxisWithVelocity(
-                1.5f * stepSize,
-                velocity
-            )
-        }
-        rule.mainClock.advanceTimeByFrame()
-
-        // assert
-        val initialTargetOffset =
-            with(snapLayoutInfoProvider) { density.calculateApproachOffset(velocity) }
-        Truth.assertThat(scrollOffset.first { it != 0f }).isWithin(0.5f)
-            .of(initialTargetOffset)
-
-        // act: wait for remaining offset to grow instead of decay, this indicates the last
-        // snap step will start
-        rule.mainClock.advanceTimeUntil {
-            scrollOffset.size > 2 &&
-                scrollOffset.last() > scrollOffset[scrollOffset.lastIndex - 1]
-        }
-
-        // assert: next calculated offset is the first value emitted by remainingScrollOffset
-        val finalRemainingOffset = with(snapLayoutInfoProvider) {
-            density.calculateSnappingOffset(10000f)
-        }
-        Truth.assertThat(scrollOffset.last()).isWithin(0.5f)
-            .of(finalRemainingOffset)
-        rule.mainClock.autoAdvance = true
-
-        // assert: value settles back to zero
-        rule.runOnIdle {
-            Truth.assertThat(scrollOffset.last()).isEqualTo(0f)
-        }
-    }
-
-    private fun onMainList() = rule.onNodeWithTag(TestTag)
-
-    @Composable
-    fun MainLayout(state: LazyListState, scrollOffset: MutableList<Float> = mutableListOf()) {
-        snapLayoutInfoProvider = remember(state) { SnapLayoutInfoProvider(state) }
-        val innerFlingBehavior =
-            rememberSnapFlingBehavior(snapLayoutInfoProvider = snapLayoutInfoProvider)
-        snapFlingBehavior = remember(innerFlingBehavior) {
-            QuerySnapFlingBehavior(innerFlingBehavior) {
-                scrollOffset.add(it)
-            }
-        }
-        LazyColumnOrRow(
-            state = state,
-            modifier = Modifier.testTag(TestTag),
-            flingBehavior = snapFlingBehavior
-        ) {
-            items(200) {
-                Box(modifier = Modifier.size(ItemSize))
-            }
-        }
-    }
-
-    private fun SemanticsNodeInteraction.swipeOnMainAxis() {
-        performTouchInput {
-            if (orientation == Orientation.Vertical) {
-                swipeUp()
-            } else {
-                swipeLeft()
-            }
-        }
-    }
-
-    private fun Density.getCurrentSnappedItem(state: LazyListState?): Int {
-        var itemIndex = -1
-        if (state == null) return -1
-        var minDistance = Float.POSITIVE_INFINITY
-        val layoutInfo = state.layoutInfo
-        (state.layoutInfo.visibleItemsInfo).forEach {
-            val distance = calculateDistanceToDesiredSnapPosition(
-                mainAxisViewPortSize = layoutInfo.singleAxisViewportSize,
-                beforeContentPadding = layoutInfo.beforeContentPadding,
-                afterContentPadding = layoutInfo.afterContentPadding,
-                itemSize = it.size,
-                itemOffset = it.offset,
-                itemIndex = it.index,
-                snapPositionInLayout = CenterToCenter
-            )
-            if (abs(distance) < minDistance) {
-                minDistance = abs(distance)
-                itemIndex = it.index
-            }
-        }
-        return itemIndex
-    }
-
-    private fun TouchInjectionScope.swipeMainAxisWithVelocity(
-        scrollSize: Float,
-        endVelocity: Float,
-        reversed: Boolean = false
-    ) {
-        val (start, end) = if (orientation == Orientation.Vertical) {
-            bottomCenter to bottomCenter.copy(y = bottomCenter.y - scrollSize)
-        } else {
-            centerRight to centerRight.copy(x = centerRight.x - scrollSize)
-        }
-        swipeWithVelocity(
-            if (reversed) end else start,
-            if (reversed) start else end,
-            endVelocity
-        )
-    }
-
-    private fun Velocity.toAbsoluteFloat(): Float {
-        return (if (orientation == Orientation.Vertical) y else x).absoluteValue
-    }
-
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "{0}")
-        fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
-
-        val ItemSize = 200.dp
-        const val TestTag = "MainList"
-    }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-internal class QuerySnapFlingBehavior(
-    val snapFlingBehavior: SnapFlingBehavior,
-    val onAnimationStep: (Float) -> Unit
-) : FlingBehavior {
-    override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
-        return with(snapFlingBehavior) {
-            performFling(initialVelocity, onAnimationStep)
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt
deleted file mode 100644
index 259f657..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.gesture.snapping
-
-import androidx.compose.animation.core.calculateTargetValue
-import androidx.compose.animation.splineBasedDecay
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
-import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.list.BaseLazyListTestWithOrientation
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.LargeTest
-import kotlin.math.absoluteValue
-import kotlin.math.round
-import kotlin.math.sign
-import kotlin.test.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@OptIn(ExperimentalFoundationApi::class)
-@LargeTest
-@RunWith(Parameterized::class)
-class LazyListSnapLayoutInfoProviderTest(orientation: Orientation) :
-    BaseLazyListTestWithOrientation(orientation) {
-
-    private val density: Density
-        get() = rule.density
-
-    @Test
-    fun snapStepSize_sameSizeItems_shouldBeAverageItemSize() {
-        var expectedItemSize = 0f
-        var actualItemSize = 0f
-
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyListState()
-            val layoutInfoProvider = remember(state) { createLayoutInfo(state) }.also {
-                actualItemSize = with(it) { density.calculateSnapStepSize() }
-            }
-            expectedItemSize = with(density) { FixedItemSize.toPx() }
-            MainLayout(
-                state = state,
-                layoutInfo = layoutInfoProvider,
-                items = 200,
-                itemSizeProvider = { FixedItemSize }
-            )
-        }
-
-        rule.runOnIdle {
-            assertEquals(round(expectedItemSize), round(actualItemSize))
-        }
-    }
-
-    @Test
-    fun snapStepSize_differentSizeItems_shouldBeAverageItemSize() {
-        var actualItemSize = 0f
-        var expectedItemSize = 0f
-
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyListState()
-            val layoutInfoProvider = remember(state) { createLayoutInfo(state) }.also {
-                actualItemSize = with(it) { density.calculateSnapStepSize() }
-            }
-            expectedItemSize = state.layoutInfo.visibleItemsInfo.map { it.size }.average().toFloat()
-
-            MainLayout(state, layoutInfoProvider, DynamicItemSizes.size, { DynamicItemSizes[it] })
-        }
-
-        rule.runOnIdle {
-            assertEquals(round(expectedItemSize), round(actualItemSize))
-        }
-    }
-
-    @Test
-    fun snapStepSize_withSpacers_shouldBeAverageItemSize() {
-        var snapStepSize = 0f
-        var actualItemSize = 0f
-        rule.setContent {
-            val density = LocalDensity.current
-            val state = rememberLazyListState()
-            val layoutInfoProvider = remember(state) { createLayoutInfo(state) }.also {
-                snapStepSize = with(it) { density.calculateSnapStepSize() }
-            }
-
-            actualItemSize = with(density) { (FixedItemSize + FixedItemSize / 2).toPx() }
-
-            MainLayout(
-                state = state,
-                layoutInfo = layoutInfoProvider,
-                items = 200,
-                itemSizeProvider = { FixedItemSize }) {
-                Box(modifier = Modifier.size(FixedItemSize))
-                Spacer(modifier = Modifier.size(FixedItemSize / 2))
-            }
-        }
-
-        rule.runOnIdle {
-            assertEquals(round(actualItemSize), round(snapStepSize))
-        }
-    }
-
-    @Test
-    fun calculateApproachOffset_highVelocity_approachOffsetIsEqualToDecayMinusItemSize() {
-        lateinit var layoutInfoProvider: SnapLayoutInfoProvider
-        val decay = splineBasedDecay<Float>(rule.density)
-        fun calculateTargetOffset(velocity: Float): Float {
-            val offset = decay.calculateTargetValue(0f, velocity).absoluteValue
-            return (offset - with(density) { 200.dp.toPx() }).coerceAtLeast(0f) * velocity.sign
-        }
-        rule.setContent {
-            val state = rememberLazyListState()
-            layoutInfoProvider = remember(state) { createLayoutInfo(state) }
-            LazyColumnOrRow(
-                state = state,
-                flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
-            ) {
-                items(200) {
-                    Box(modifier = Modifier.size(200.dp))
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertEquals(
-                with(layoutInfoProvider) { density.calculateApproachOffset(10000f) },
-                calculateTargetOffset(10000f)
-            )
-            assertEquals(
-                with(layoutInfoProvider) { density.calculateApproachOffset(-10000f) },
-                calculateTargetOffset(-10000f)
-            )
-        }
-    }
-
-    @Test
-    fun calculateApproachOffset_lowVelocity_approachOffsetIsEqualToZero() {
-        lateinit var layoutInfoProvider: SnapLayoutInfoProvider
-        rule.setContent {
-            val state = rememberLazyListState()
-            layoutInfoProvider = remember(state) { createLayoutInfo(state) }
-            LazyColumnOrRow(
-                state = state,
-                flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
-            ) {
-                items(200) {
-                    Box(modifier = Modifier.size(200.dp))
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertEquals(
-                with(layoutInfoProvider) { density.calculateApproachOffset(1000f) },
-                0f
-            )
-            assertEquals(
-                with(layoutInfoProvider) { density.calculateApproachOffset(-1000f) },
-                0f
-            )
-        }
-    }
-
-    @Composable
-    private fun MainLayout(
-        state: LazyListState,
-        layoutInfo: SnapLayoutInfoProvider,
-        items: Int,
-        itemSizeProvider: (Int) -> Dp,
-        listItem: @Composable (Int) -> Unit = { Box(Modifier.size(itemSizeProvider(it))) }
-    ) {
-        LazyColumnOrRow(
-            state = state,
-            flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider = layoutInfo)
-        ) {
-            items(items) { listItem(it) }
-        }
-    }
-
-    private fun createLayoutInfo(state: LazyListState): SnapLayoutInfoProvider {
-        return SnapLayoutInfoProvider(state)
-    }
-
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "{0}")
-        fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
-
-        val FixedItemSize = 200.dp
-        val DynamicItemSizes = (200..500).map { it.dp }
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
deleted file mode 100644
index cc10f6f..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
+++ /dev/null
@@ -1,571 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.gesture.snapping
-
-import androidx.compose.animation.SplineBasedFloatDecayAnimationSpec
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.AnimationVector
-import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.core.FloatDecayAnimationSpec
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.TwoWayConverter
-import androidx.compose.animation.core.VectorizedAnimationSpec
-import androidx.compose.animation.core.generateDecayAnimationSpec
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.TestScrollMotionDurationScale
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.rememberScrollableState
-import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
-import androidx.compose.foundation.gestures.snapping.NoVelocity
-import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
-import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
-import androidx.compose.foundation.gestures.snapping.calculateFinalOffset
-import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.text.BasicText
-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.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.swipeWithVelocity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth
-import kotlin.test.assertEquals
-import kotlin.test.assertNotEquals
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalFoundationApi::class)
-class SnapFlingBehaviorTest {
-    @get:Rule
-    val rule = createComposeRule()
-
-    private val inspectSpringAnimationSpec = InspectSpringAnimationSpec(spring())
-    private val inspectTweenAnimationSpec = InspectSpringAnimationSpec(tween(easing = LinearEasing))
-
-    private val density: Density
-        get() = rule.density
-
-    @Test
-    fun performFling_whenVelocityIsBelowThreshold_shouldShortSnap() {
-        val testLayoutInfoProvider = TestLayoutInfoProvider()
-        rule.setContent {
-            val testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
-            VelocityEffect(testFlingBehavior, calculateVelocityThreshold() - 1)
-        }
-
-        rule.runOnIdle {
-            assertEquals(0, testLayoutInfoProvider.calculateApproachOffsetCount)
-        }
-    }
-
-    @Test
-    fun performFling_whenVelocityIsAboveThreshold_shouldLongSnap() {
-        val testLayoutInfoProvider = TestLayoutInfoProvider()
-        rule.setContent {
-            val testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
-            VelocityEffect(testFlingBehavior, calculateVelocityThreshold() + 1)
-        }
-
-        rule.runOnIdle {
-            assertEquals(1, testLayoutInfoProvider.calculateApproachOffsetCount)
-        }
-    }
-
-    @Test
-    fun remainingScrollOffset_whenVelocityIsBelowThreshold_shouldRepresentShortSnapOffsets() {
-        val testLayoutInfoProvider = TestLayoutInfoProvider()
-        lateinit var testFlingBehavior: SnapFlingBehavior
-        val scrollOffset = mutableListOf<Float>()
-        rule.setContent {
-            testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
-            VelocityEffect(
-                testFlingBehavior,
-                calculateVelocityThreshold() - 1
-            ) { remainingScrollOffset ->
-                scrollOffset.add(remainingScrollOffset)
-            }
-        }
-
-        // Will Snap Back
-        rule.runOnIdle {
-            assertEquals(scrollOffset.first(), testLayoutInfoProvider.minOffset)
-            assertEquals(scrollOffset.last(), 0f)
-        }
-    }
-
-    @Test
-    fun remainingScrollOffset_whenVelocityIsAboveThreshold_shouldRepresentLongSnapOffsets() {
-        val testLayoutInfoProvider = TestLayoutInfoProvider()
-        lateinit var testFlingBehavior: SnapFlingBehavior
-        val scrollOffset = mutableListOf<Float>()
-        rule.setContent {
-            testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
-            VelocityEffect(
-                testFlingBehavior,
-                calculateVelocityThreshold() + 1
-            ) { remainingScrollOffset ->
-                scrollOffset.add(remainingScrollOffset)
-            }
-        }
-
-        rule.runOnIdle {
-            assertEquals(scrollOffset.first { it != 0f }, testLayoutInfoProvider.maxOffset)
-            assertEquals(scrollOffset.last(), 0f)
-        }
-    }
-
-    @Test
-    fun remainingScrollOffset_longSnap_targetShouldChangeInAccordanceWithAnimation() {
-        // Arrange
-        val initialOffset = 250f
-        val testLayoutInfoProvider = TestLayoutInfoProvider(approachOffset = initialOffset)
-        lateinit var testFlingBehavior: SnapFlingBehavior
-        val scrollOffset = mutableListOf<Float>()
-        rule.mainClock.autoAdvance = false
-        rule.setContent {
-            testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
-            VelocityEffect(
-                testFlingBehavior,
-                calculateVelocityThreshold() + 1
-            ) { remainingScrollOffset ->
-                scrollOffset.add(remainingScrollOffset)
-            }
-        }
-
-        // assert the initial value emitted by remainingScrollOffset was the one provider by the
-        // snap layout info provider
-        assertEquals(scrollOffset.first(), initialOffset)
-
-        // Act: Advance until remainingScrollOffset grows again
-        rule.mainClock.advanceTimeUntil {
-            scrollOffset.size > 2 &&
-                scrollOffset.last() > scrollOffset[scrollOffset.lastIndex - 1]
-        }
-
-        assertEquals(scrollOffset.last(), testLayoutInfoProvider.maxOffset)
-
-        rule.mainClock.autoAdvance = true
-        // Assert
-        rule.runOnIdle {
-            assertEquals(scrollOffset.last(), 0f)
-        }
-    }
-
-    @Test
-    fun performFling_afterSnappingVelocity_everythingWasConsumed_shouldReturnNoVelocity() {
-        val testLayoutInfoProvider = TestLayoutInfoProvider()
-        var afterFlingVelocity = 0f
-        rule.setContent {
-            val scrollableState = rememberScrollableState(consumeScrollDelta = { it })
-            val testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
-
-            LaunchedEffect(Unit) {
-                scrollableState.scroll {
-                    afterFlingVelocity = with(testFlingBehavior) {
-                        performFling(50000f)
-                    }
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertEquals(NoVelocity, afterFlingVelocity)
-        }
-    }
-
-    @Test
-    fun performFling_afterSnappingVelocity_didNotConsumeAllScroll_shouldReturnRemainingVelocity() {
-        val testLayoutInfoProvider = TestLayoutInfoProvider()
-        var afterFlingVelocity = 0f
-        rule.setContent {
-            // Consume only half
-            val scrollableState = rememberScrollableState(consumeScrollDelta = { it / 2f })
-            val testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
-
-            LaunchedEffect(Unit) {
-                scrollableState.scroll {
-                    afterFlingVelocity = with(testFlingBehavior) {
-                        performFling(50000f)
-                    }
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertNotEquals(NoVelocity, afterFlingVelocity)
-        }
-    }
-
-    @Test
-    fun findClosestOffset_noFlingDirection_shouldReturnAbsoluteDistance() {
-        val testLayoutInfoProvider = TestLayoutInfoProvider()
-        val offset = with(testLayoutInfoProvider) {
-            density.calculateSnappingOffset(0f)
-        }
-        assertEquals(offset, MinOffset)
-    }
-
-    @Test
-    fun findClosestOffset_flingDirection_shouldReturnCorrectBound() {
-        val testLayoutInfoProvider = TestLayoutInfoProvider()
-        val forwardOffset = with(testLayoutInfoProvider) {
-            density.calculateSnappingOffset(1f)
-        }
-        val backwardOffset = with(testLayoutInfoProvider) {
-            density.calculateSnappingOffset(-1f)
-        }
-        assertEquals(forwardOffset, MaxOffset)
-        assertEquals(backwardOffset, MinOffset)
-    }
-
-    @Test
-    fun approach_cannotDecay_useLowVelocityApproachAndSnap() {
-        val testLayoutInfoProvider = TestLayoutInfoProvider(approachOffset = SnapStep * 5)
-        var inspectSplineAnimationSpec: InspectSplineAnimationSpec? = null
-        rule.setContent {
-            val splineAnimationSpec = rememberInspectSplineAnimationSpec().also {
-                inspectSplineAnimationSpec = it
-            }
-            val testFlingBehavior = rememberSnapFlingBehavior(
-                snapLayoutInfoProvider = testLayoutInfoProvider,
-                highVelocityApproachSpec = splineAnimationSpec.generateDecayAnimationSpec(),
-                lowVelocityApproachSpec = inspectTweenAnimationSpec,
-                snapAnimationSpec = inspectSpringAnimationSpec
-            )
-            VelocityEffect(testFlingBehavior, calculateVelocityThreshold() * 2)
-        }
-
-        rule.runOnIdle {
-            assertEquals(0, inspectSplineAnimationSpec?.animationWasExecutions)
-            assertEquals(1, inspectTweenAnimationSpec.animationWasExecutions)
-            assertEquals(1, inspectSpringAnimationSpec.animationWasExecutions)
-        }
-    }
-
-    @Test
-    fun approach_canDecay_decayAndSnap() {
-        val testLayoutInfoProvider = TestLayoutInfoProvider(maxOffset = 100f)
-        var inspectSplineAnimationSpec: InspectSplineAnimationSpec? = null
-        rule.setContent {
-            val splineAnimationSpec = rememberInspectSplineAnimationSpec().also {
-                inspectSplineAnimationSpec = it
-            }
-            val testFlingBehavior = rememberSnapFlingBehavior(
-                snapLayoutInfoProvider = testLayoutInfoProvider,
-                highVelocityApproachSpec = splineAnimationSpec.generateDecayAnimationSpec(),
-                lowVelocityApproachSpec = inspectTweenAnimationSpec,
-                snapAnimationSpec = inspectSpringAnimationSpec
-            )
-            VelocityEffect(testFlingBehavior, calculateVelocityThreshold() * 5)
-        }
-
-        rule.runOnIdle {
-            assertEquals(1, inspectSplineAnimationSpec?.animationWasExecutions)
-            assertEquals(1, inspectSpringAnimationSpec.animationWasExecutions)
-            assertEquals(0, inspectTweenAnimationSpec.animationWasExecutions)
-        }
-    }
-
-    @Test
-    fun disableSystemAnimations_defaultFlingBehaviorShouldContinueToWork() {
-
-        lateinit var defaultFlingBehavior: SnapFlingBehavior
-        lateinit var scope: CoroutineScope
-        val state = LazyListState()
-        rule.setContent {
-            scope = rememberCoroutineScope()
-            defaultFlingBehavior = rememberSnapFlingBehavior(state) as SnapFlingBehavior
-
-            LazyRow(
-                modifier = Modifier.fillMaxWidth(),
-                state = state,
-                flingBehavior = defaultFlingBehavior as FlingBehavior
-            ) {
-                items(200) { Box(modifier = Modifier.size(20.dp)) }
-            }
-        }
-
-        // Act: Stop clock and fling, one frame should not settle immediately.
-        rule.mainClock.autoAdvance = false
-        scope.launch {
-            state.scroll {
-                with(defaultFlingBehavior) { performFling(10000f) }
-            }
-        }
-        rule.mainClock.advanceTimeByFrame()
-
-        // Assert
-        rule.runOnIdle {
-            Truth.assertThat(state.firstVisibleItemIndex).isEqualTo(0)
-        }
-
-        rule.mainClock.autoAdvance = true
-
-        val previousIndex = state.firstVisibleItemIndex
-
-        // Simulate turning off system wide animation
-        scope.launch {
-            state.scroll {
-                withContext(TestScrollMotionDurationScale(0f)) {
-                    with(defaultFlingBehavior) { performFling(10000f) }
-                }
-            }
-        }
-
-        // Act: Stop clock and fling, one frame should not settle immediately.
-        rule.mainClock.autoAdvance = false
-        scope.launch {
-            state.scroll {
-                with(defaultFlingBehavior) { performFling(10000f) }
-            }
-        }
-        rule.mainClock.advanceTimeByFrame()
-
-        // Assert
-        rule.runOnIdle {
-            Truth.assertThat(state.firstVisibleItemIndex).isEqualTo(previousIndex)
-        }
-
-        rule.mainClock.autoAdvance = true
-
-        // Assert: let it settle
-        rule.runOnIdle {
-            Truth.assertThat(state.firstVisibleItemIndex).isNotEqualTo(previousIndex)
-        }
-    }
-
-    @Test
-    fun defaultFlingBehavior_useScrollMotionDurationScale() {
-        // Arrange
-        var switchMotionDurationScale by mutableStateOf(false)
-        lateinit var defaultFlingBehavior: SnapFlingBehavior
-        lateinit var scope: CoroutineScope
-        val state = LazyListState()
-        rule.setContent {
-            scope = rememberCoroutineScope()
-            defaultFlingBehavior = rememberSnapFlingBehavior(state) as SnapFlingBehavior
-
-            LazyRow(
-                modifier = Modifier
-                    .testTag("snappingList")
-                    .fillMaxSize(),
-                state = state,
-                flingBehavior = defaultFlingBehavior as FlingBehavior
-            ) {
-                items(200) {
-                    Box(modifier = Modifier.size(150.dp)) {
-                        BasicText(text = it.toString())
-                    }
-                }
-            }
-
-            if (switchMotionDurationScale) {
-                defaultFlingBehavior.motionScaleDuration = TestScrollMotionDurationScale(1f)
-            } else {
-                defaultFlingBehavior.motionScaleDuration = TestScrollMotionDurationScale(0f)
-            }
-        }
-
-        // Act: Stop clock and fling, one frame should settle immediately.
-        rule.mainClock.autoAdvance = false
-        rule.onNodeWithTag("snappingList").performTouchInput {
-            swipeWithVelocity(centerRight, center, 10000f)
-        }
-        rule.mainClock.advanceTimeByFrame()
-
-        // Assert
-        rule.runOnIdle {
-            Truth.assertThat(state.firstVisibleItemIndex).isGreaterThan(0)
-        }
-
-        // Arrange
-        rule.mainClock.autoAdvance = true
-        switchMotionDurationScale = true // Let animations run normally
-        rule.waitForIdle()
-
-        val previousIndex = state.firstVisibleItemIndex
-        // Act: Stop clock and fling, one frame should not settle.
-        rule.mainClock.autoAdvance = false
-        scope.launch {
-            state.scroll {
-                with(defaultFlingBehavior) { performFling(10000f) }
-            }
-        }
-
-        // Assert: First index hasn't changed because animation hasn't started
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            Truth.assertThat(state.firstVisibleItemIndex).isEqualTo(previousIndex)
-        }
-        rule.mainClock.autoAdvance = true
-
-        // Wait for settling
-        rule.runOnIdle {
-            Truth.assertThat(state.firstVisibleItemIndex).isNotEqualTo(previousIndex)
-        }
-    }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-private fun VelocityEffect(
-    testFlingBehavior: FlingBehavior,
-    velocity: Float,
-    onSettlingDistanceUpdated: (Float) -> Unit = {}
-) {
-    val scrollableState = rememberScrollableState(consumeScrollDelta = { it })
-    LaunchedEffect(Unit) {
-        scrollableState.scroll {
-            with(testFlingBehavior as SnapFlingBehavior) {
-                performFling(velocity, onSettlingDistanceUpdated)
-            }
-        }
-    }
-}
-
-private class InspectSpringAnimationSpec(
-    private val animation: AnimationSpec<Float>
-) : AnimationSpec<Float> {
-
-    var animationWasExecutions = 0
-
-    override fun <V : AnimationVector> vectorize(
-        converter: TwoWayConverter<Float, V>
-    ): VectorizedAnimationSpec<V> {
-        animationWasExecutions++
-        return animation.vectorize(converter)
-    }
-}
-
-private class InspectSplineAnimationSpec(
-    private val splineBasedFloatDecayAnimationSpec: SplineBasedFloatDecayAnimationSpec
-) : FloatDecayAnimationSpec by splineBasedFloatDecayAnimationSpec {
-
-    private var valueFromNanosCalls = 0
-    val animationWasExecutions: Int
-        get() = valueFromNanosCalls / 2
-
-    override fun getValueFromNanos(
-        playTimeNanos: Long,
-        initialValue: Float,
-        initialVelocity: Float
-    ): Float {
-
-        if (playTimeNanos == 0L) {
-            valueFromNanosCalls++
-        }
-
-        return splineBasedFloatDecayAnimationSpec.getValueFromNanos(
-            playTimeNanos,
-            initialValue,
-            initialVelocity
-        )
-    }
-}
-
-@Composable
-private fun rememberInspectSplineAnimationSpec(): InspectSplineAnimationSpec {
-    val density = LocalDensity.current
-    return remember {
-        InspectSplineAnimationSpec(
-            SplineBasedFloatDecayAnimationSpec(density)
-        )
-    }
-}
-
-@Composable
-private fun calculateVelocityThreshold(): Float {
-    val density = LocalDensity.current
-    return with(density) { MinFlingVelocityDp.toPx() }
-}
-
-private const val SnapStep = 250f
-private const val MinOffset = -200f
-private const val MaxOffset = 300f
-
-@OptIn(ExperimentalFoundationApi::class)
-
-private class TestLayoutInfoProvider(
-    val minOffset: Float = MinOffset,
-    val maxOffset: Float = MaxOffset,
-    val snapStep: Float = SnapStep,
-    val approachOffset: Float = 0f
-) : SnapLayoutInfoProvider {
-    var calculateApproachOffsetCount = 0
-
-    override fun Density.calculateSnapStepSize(): Float {
-        return snapStep
-    }
-
-    override fun Density.calculateSnappingOffset(currentVelocity: Float): Float {
-        return calculateFinalOffset(currentVelocity, minOffset, maxOffset)
-    }
-
-    override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
-        calculateApproachOffsetCount++
-        return approachOffset
-    }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-private fun rememberSnapFlingBehavior(
-    snapLayoutInfoProvider: SnapLayoutInfoProvider,
-    highVelocityApproachSpec: DecayAnimationSpec<Float>,
-    lowVelocityApproachSpec: AnimationSpec<Float>,
-    snapAnimationSpec: AnimationSpec<Float>
-): FlingBehavior {
-    val density = LocalDensity.current
-    return remember(
-        snapLayoutInfoProvider,
-        highVelocityApproachSpec
-    ) {
-        SnapFlingBehavior(
-            snapLayoutInfoProvider = snapLayoutInfoProvider,
-            lowVelocityAnimationSpec = lowVelocityApproachSpec,
-            highVelocityAnimationSpec = highVelocityApproachSpec,
-            snapAnimationSpec = snapAnimationSpec,
-            density = density
-        )
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
deleted file mode 100644
index 107ee85..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
+++ /dev/null
@@ -1,622 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy.grid
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.list.TrackPlacedElement
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.BeyondBoundsLayout
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
-import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.modifier.modifierLocalConsumer
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.LayoutDirection.Ltr
-import androidx.compose.ui.unit.LayoutDirection.Rtl
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@OptIn(ExperimentalComposeUiApi::class)
-@MediumTest
-@RunWith(Parameterized::class)
-class LazyGridBeyondBoundsTest(param: Param) {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    // We need to wrap the inline class parameter in another class because Java can't instantiate
-    // the inline class.
-    class Param(
-        val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
-        val reverseLayout: Boolean,
-        val layoutDirection: LayoutDirection,
-    ) {
-        override fun toString() = "beyondBoundsLayoutDirection=$beyondBoundsLayoutDirection " +
-            "reverseLayout=$reverseLayout " +
-            "layoutDirection=$layoutDirection"
-    }
-
-    private val beyondBoundsLayoutDirection = param.beyondBoundsLayoutDirection
-    private val reverseLayout = param.reverseLayout
-    private val layoutDirection = param.layoutDirection
-    private val placedItems = mutableSetOf<Int>()
-    private var beyondBoundsLayout: BeyondBoundsLayout? = null
-    private lateinit var lazyGridState: LazyGridState
-
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "{0}")
-        fun initParameters() = buildList {
-            for (beyondBoundsLayoutDirection in listOf(Left, Right, Above, Below, Before, After)) {
-                for (reverseLayout in listOf(false, true)) {
-                    for (layoutDirection in listOf(Ltr, Rtl)) {
-                        add(Param(beyondBoundsLayoutDirection, reverseLayout, layoutDirection))
-                    }
-                }
-            }
-        }
-    }
-
-    @Test
-    fun onlyOneVisibleItemIsPlaced() {
-        // Arrange.
-        rule.setLazyContent(size = 10.toDp(), firstVisibleItem = 0) {
-            items(100) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-        }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(0)
-            assertThat(visibleItems).containsExactly(0)
-        }
-    }
-
-    @Test
-    fun onlyTwoVisibleItemsArePlaced() {
-        // Arrange.
-        rule.setLazyContent(size = 20.toDp(), firstVisibleItem = 0) {
-            items(100) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-        }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(0, 1)
-            assertThat(visibleItems).containsExactly(0, 1)
-        }
-    }
-
-    @Test
-    fun onlyThreeVisibleItemsArePlaced() {
-        // Arrange.
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
-            items(100) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-        }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(0, 1, 2)
-            assertThat(visibleItems).containsExactly(0, 1, 2)
-        }
-    }
-
-    @Test
-    fun emptyLazyList_doesNotCrash() {
-        // Arrange.
-        var addItems by mutableStateOf(true)
-        lateinit var beyondBoundsLayoutRef: BeyondBoundsLayout
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
-            if (addItems) {
-                item {
-                    Box(
-                        Modifier.modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                    )
-                }
-            }
-        }
-        rule.runOnIdle {
-            beyondBoundsLayoutRef = beyondBoundsLayout!!
-            addItems = false
-        }
-
-        // Act.
-        val hasMoreContent = rule.runOnIdle {
-            beyondBoundsLayoutRef.layout(beyondBoundsLayoutDirection) {
-                hasMoreContent
-            }
-        }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(hasMoreContent).isFalse()
-        }
-    }
-
-    @Test
-    fun oneExtraItemBeyondVisibleBounds() {
-        // Arrange.
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(Modifier
-                    .size(10.toDp())
-                    .trackPlaced(5)
-                    .modifierLocalConsumer {
-                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                    }
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                // Assert that the beyond bounds items are present.
-                if (expectedExtraItemsBeforeVisibleBounds()) {
-                    assertThat(placedItems).containsExactly(4, 5, 6, 7)
-                    assertThat(visibleItems).containsExactly(5, 6, 7)
-                } else {
-                    assertThat(placedItems).containsExactly(5, 6, 7, 8)
-                    assertThat(visibleItems).containsExactly(5, 6, 7)
-                }
-                // Just return true so that we stop as soon as we run this once.
-                // This should result in one extra item being added.
-                true
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-            assertThat(visibleItems).containsExactly(5, 6, 7)
-        }
-    }
-
-    @Test
-    fun oneExtraItemBeyondVisibleBounds_multipleCells() {
-        val itemSize = 50
-        val itemSizeDp = itemSize.toDp()
-        // Arrange.
-        rule.setLazyContent(cells = 2, size = itemSizeDp * 3, firstVisibleItem = 10) {
-            // item | item  | x5
-            // item | local | x1
-            // item | item  | x5
-            items(11) { index ->
-                Box(
-                    Modifier
-                        .size(itemSizeDp)
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(Modifier
-                    .size(itemSizeDp)
-                    .trackPlaced(11)
-                    .modifierLocalConsumer {
-                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                    }
-                )
-            }
-            items(10) { index ->
-                Box(
-                    Modifier
-                        .size(itemSizeDp)
-                        .trackPlaced(index + 12)
-                )
-            }
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                // Assert that the beyond bounds items are present.
-                if (expectedExtraItemsBeforeVisibleBounds()) {
-                    assertThat(placedItems).containsExactly(9, 10, 11, 12, 13, 14, 15)
-                    assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
-                } else {
-                    assertThat(placedItems).containsExactly(10, 11, 12, 13, 14, 15, 16)
-                    assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
-                }
-                // Just return true so that we stop as soon as we run this once.
-                // This should result in one extra item being added.
-                true
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(10, 11, 12, 13, 14, 15)
-            assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
-        }
-    }
-
-    @Test
-    fun twoExtraItemsBeyondVisibleBounds() {
-        // Arrange.
-        var extraItemCount = 2
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(5)
-                        .modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                if (--extraItemCount > 0) {
-                    // Return null to continue the search.
-                    null
-                } else {
-                    // Assert that the beyond bounds items are present.
-                    if (expectedExtraItemsBeforeVisibleBounds()) {
-                        assertThat(placedItems).containsExactly(3, 4, 5, 6, 7)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    } else {
-                        assertThat(placedItems).containsExactly(5, 6, 7, 8, 9)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    }
-                    // Return true to stop the search.
-                    true
-                }
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-            assertThat(visibleItems).containsExactly(5, 6, 7)
-        }
-    }
-
-    @Test
-    fun allBeyondBoundsItemsInSpecifiedDirection() {
-        // Arrange.
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                        .trackPlaced(5)
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                if (hasMoreContent) {
-                    // Just return null so that we keep adding more items till we reach the end.
-                    null
-                } else {
-                    // Assert that the beyond bounds items are present.
-                    if (expectedExtraItemsBeforeVisibleBounds()) {
-                        assertThat(placedItems).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    } else {
-                        assertThat(placedItems).containsExactly(5, 6, 7, 8, 9, 10)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    }
-                    // Return true to end the search.
-                    true
-                }
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-        }
-    }
-
-    @Test
-    fun beyondBoundsLayoutRequest_inDirectionPerpendicularToLazyListOrientation() {
-        // Arrange.
-        var beyondBoundsLayoutCount = 0
-        rule.setLazyContentInPerpendicularDirection(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(5)
-                        .modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-            assertThat(visibleItems).containsExactly(5, 6, 7)
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                beyondBoundsLayoutCount++
-                when (beyondBoundsLayoutDirection) {
-                    Left, Right, Above, Below -> {
-                        assertThat(placedItems).containsExactly(5, 6, 7)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    }
-                    Before, After -> {
-                        if (expectedExtraItemsBeforeVisibleBounds()) {
-                            assertThat(placedItems).containsExactly(4, 5, 6, 7)
-                            assertThat(visibleItems).containsExactly(5, 6, 7)
-                        } else {
-                            assertThat(placedItems).containsExactly(5, 6, 7, 8)
-                            assertThat(visibleItems).containsExactly(5, 6, 7)
-                        }
-                    }
-                }
-                // Just return true so that we stop as soon as we run this once.
-                // This should result in one extra item being added.
-                true
-            }
-        }
-
-        rule.runOnIdle {
-            when (beyondBoundsLayoutDirection) {
-                Left, Right, Above, Below -> {
-                    assertThat(beyondBoundsLayoutCount).isEqualTo(0)
-                }
-                Before, After -> {
-                    assertThat(beyondBoundsLayoutCount).isEqualTo(1)
-
-                    // Assert that the beyond bounds items are removed.
-                    assertThat(placedItems).containsExactly(5, 6, 7)
-                    assertThat(visibleItems).containsExactly(5, 6, 7)
-                }
-                else -> error("Unsupported BeyondBoundsLayoutDirection")
-            }
-        }
-    }
-
-    @Test
-    fun returningNullDoesNotCauseInfiniteLoop() {
-        // Arrange.
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                        .trackPlaced(5)
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-
-        // Act.
-        var count = 0
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                // Assert that we don't keep iterating when there is no ending condition.
-                assertThat(count++).isLessThan(lazyGridState.layoutInfo.totalItemsCount)
-                // Always return null to continue the search.
-                null
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-            assertThat(visibleItems).containsExactly(5, 6, 7)
-        }
-    }
-
-    private fun ComposeContentTestRule.setLazyContent(
-        size: Dp,
-        firstVisibleItem: Int,
-        cells: Int = 1,
-        content: LazyGridScope.() -> Unit
-    ) {
-        setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
-                lazyGridState = rememberLazyGridState(firstVisibleItem)
-                when (beyondBoundsLayoutDirection) {
-                    Left, Right, Before, After ->
-                        LazyHorizontalGrid(
-                            rows = GridCells.Fixed(cells),
-                            modifier = Modifier.size(size),
-                            state = lazyGridState,
-                            reverseLayout = reverseLayout,
-                            content = content
-                        )
-                    Above, Below ->
-                        LazyVerticalGrid(
-                            columns = GridCells.Fixed(cells),
-                            modifier = Modifier.size(size),
-                            state = lazyGridState,
-                            reverseLayout = reverseLayout,
-                            content = content
-                        )
-                    else -> unsupportedDirection()
-                }
-            }
-        }
-    }
-
-    private fun ComposeContentTestRule.setLazyContentInPerpendicularDirection(
-        size: Dp,
-        firstVisibleItem: Int,
-        content: LazyGridScope.() -> Unit
-    ) {
-        setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
-                lazyGridState = rememberLazyGridState(firstVisibleItem)
-                when (beyondBoundsLayoutDirection) {
-                    Left, Right, Before, After ->
-                        LazyVerticalGrid(
-                            columns = GridCells.Fixed(1),
-                            modifier = Modifier.size(size),
-                            state = lazyGridState,
-                            reverseLayout = reverseLayout,
-                            content = content
-                        )
-                    Above, Below ->
-                        LazyHorizontalGrid(
-                            rows = GridCells.Fixed(1),
-                            modifier = Modifier.size(size),
-                            state = lazyGridState,
-                            reverseLayout = reverseLayout,
-                            content = content
-                        )
-                    else -> unsupportedDirection()
-                }
-            }
-        }
-    }
-
-    private fun Int.toDp(): Dp = with(rule.density) { toDp() }
-
-    private val visibleItems: List<Int>
-        get() = lazyGridState.layoutInfo.visibleItemsInfo.map { it.index }
-
-    private fun expectedExtraItemsBeforeVisibleBounds() = when (beyondBoundsLayoutDirection) {
-        Right -> if (layoutDirection == Ltr) reverseLayout else !reverseLayout
-        Left -> if (layoutDirection == Ltr) !reverseLayout else reverseLayout
-        Above -> !reverseLayout
-        Below -> reverseLayout
-        After -> false
-        Before -> true
-        else -> error("Unsupported BeyondBoundsDirection")
-    }
-
-    private fun unsupportedDirection(): Nothing = error(
-        "Lazy list does not support beyond bounds layout for the specified direction"
-    )
-
-    private fun Modifier.trackPlaced(index: Int): Modifier =
-        this then TrackPlacedElement(placedItems, index)
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt
deleted file mode 100644
index 9c541a1..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy.list
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.BeyondBoundsLayout
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
-import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.modifier.modifierLocalConsumer
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.LargeTest
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-class LazyListBeyondBoundsAndExtraItemsTest(val config: Config) :
-    BaseLazyListTestWithOrientation(config.orientation) {
-
-    private val beyondBoundsLayoutDirection = config.beyondBoundsLayoutDirection
-    private val reverseLayout = config.reverseLayout
-    private val layoutDirection = config.layoutDirection
-    private val placedItems = mutableSetOf<Int>()
-
-    @OptIn(ExperimentalComposeUiApi::class)
-    @Test
-    fun verifyItemsArePlacedBeforeBeyondBoundsItems_oneBeyondBoundItem() {
-        // Arrange
-        var beyondBoundsLayout: BeyondBoundsLayout? = null
-        val lazyListState = LazyListState()
-        rule.setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
-                LazyColumnOrRow(
-                    modifier = Modifier.size(30.dp),
-                    state = lazyListState,
-                    beyondBoundsItemCount = 1,
-                    reverseLayout = reverseLayout
-                ) {
-                    items(5) { index ->
-                        Box(
-                            Modifier
-                                .size(10.dp)
-                                .trackPlaced(index)
-                        )
-                    }
-                    item {
-                        Box(
-                            Modifier
-                                .size(10.dp)
-                                .trackPlaced(5)
-                                .modifierLocalConsumer {
-                                    beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                                }
-                        )
-                    }
-                    items(5) { index ->
-                        Box(
-                            Modifier
-                                .size(10.dp)
-                                .trackPlaced(index + 6)
-                        )
-                    }
-                }
-            }
-        }
-        rule.runOnIdle { runBlocking { lazyListState.scrollToItem(5) } }
-
-        // Act
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                // Beyond bounds items are present.
-                if (expectedExtraItemsBeforeVisibleBounds()) {
-                    assertThat(placedItems).containsAtLeast(3, 4, 5, 6, 7, 8)
-                } else {
-                    assertThat(placedItems).containsAtLeast(4, 5, 6, 7, 8, 9)
-                }
-                assertThat(lazyListState.visibleItems).containsAtLeast(5, 6, 7)
-                true
-            }
-        }
-
-        // Beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsAtLeast(4, 5, 6, 7, 8)
-            assertThat(lazyListState.visibleItems).containsAtLeast(5, 6, 7)
-        }
-    }
-
-    @OptIn(ExperimentalComposeUiApi::class)
-    @Test
-    fun verifyItemsArePlacedBeforeBeyondBoundsItems_twoBeyondBoundItem() {
-        // Arrange
-        var beyondBoundsLayout: BeyondBoundsLayout? = null
-        val lazyListState = LazyListState()
-        var extraItemCount = 2
-        rule.setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
-                LazyColumnOrRow(
-                    modifier = Modifier.size(30.dp),
-                    state = lazyListState,
-                    beyondBoundsItemCount = 1,
-                    reverseLayout = reverseLayout
-                ) {
-                    items(5) { index ->
-                        Box(
-                            Modifier
-                                .size(10.dp)
-                                .trackPlaced(index)
-                        )
-                    }
-                    item {
-                        Box(
-                            Modifier
-                                .size(10.dp)
-                                .trackPlaced(5)
-                                .modifierLocalConsumer {
-                                    beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                                }
-                        )
-                    }
-                    items(5) { index ->
-                        Box(
-                            Modifier
-                                .size(10.dp)
-                                .trackPlaced(index + 6)
-                        )
-                    }
-                }
-            }
-        }
-        rule.runOnIdle { runBlocking { lazyListState.scrollToItem(5) } }
-
-        // Act
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                if (--extraItemCount > 0) {
-                    // Return null to continue the search.
-                    null
-                } else {
-                    // Beyond bounds items are present.
-                    if (expectedExtraItemsBeforeVisibleBounds()) {
-                        assertThat(placedItems).containsAtLeast(2, 3, 4, 5, 6, 7, 8)
-                    } else {
-                        assertThat(placedItems).containsAtLeast(4, 5, 6, 7, 8, 9, 10)
-                    }
-                    assertThat(lazyListState.visibleItems).containsAtLeast(5, 6, 7)
-                    true
-                }
-            }
-        }
-
-        // Beyond bounds items are removed
-        rule.runOnIdle {
-            assertThat(placedItems).containsAtLeast(4, 5, 6, 7, 8)
-            assertThat(lazyListState.visibleItems).containsAtLeast(5, 6, 7)
-        }
-    }
-
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "{0}")
-        fun params() = buildList {
-            for (orientation in listOf(Orientation.Horizontal, Orientation.Vertical)) {
-                for (beyondBoundsLayoutDirection in listOf(
-                    Left,
-                    Right,
-                    Above,
-                    Below,
-                    Before,
-                    After
-                )) {
-                    for (reverseLayout in listOf(false, true)) {
-                        for (layoutDirection in listOf(LayoutDirection.Ltr, LayoutDirection.Rtl)) {
-                            add(
-                                Config(
-                                    orientation,
-                                    beyondBoundsLayoutDirection,
-                                    reverseLayout,
-                                    layoutDirection
-                                )
-                            )
-                        }
-                    }
-                }
-            }
-        }
-
-        class Config(
-            val orientation: Orientation,
-            val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
-            val reverseLayout: Boolean,
-            val layoutDirection: LayoutDirection
-        ) {
-            override fun toString(): String {
-                return "orientation=$orientation " +
-                    "beyondBoundsLayoutDirection=$beyondBoundsLayoutDirection " +
-                    "reverseLayout=$reverseLayout " +
-                    "layoutDirection=$layoutDirection"
-            }
-        }
-    }
-
-    private val LazyListState.visibleItems: List<Int>
-        get() = layoutInfo.visibleItemsInfo.map { it.index }
-
-    private fun expectedExtraItemsBeforeVisibleBounds() = when (beyondBoundsLayoutDirection) {
-        Right -> if (layoutDirection == LayoutDirection.Ltr) reverseLayout else !reverseLayout
-        Left -> if (layoutDirection == LayoutDirection.Ltr) !reverseLayout else reverseLayout
-        Above -> !reverseLayout
-        Below -> reverseLayout
-        After -> false
-        Before -> true
-        else -> error("Unsupported BeyondBoundsDirection")
-    }
-
-    private fun Modifier.trackPlaced(index: Int): Modifier =
-        this then TrackPlacedElement(placedItems, index)
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
deleted file mode 100644
index 4e0a1ed..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
+++ /dev/null
@@ -1,597 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy.list
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListScope
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.BeyondBoundsLayout
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.modifier.modifierLocalConsumer
-import androidx.compose.ui.node.LayoutAwareModifierNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.LayoutDirection.Ltr
-import androidx.compose.ui.unit.LayoutDirection.Rtl
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@OptIn(ExperimentalComposeUiApi::class)
-@MediumTest
-@RunWith(Parameterized::class)
-class LazyListBeyondBoundsTest(param: Param) {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    // We need to wrap the inline class parameter in another class because Java can't instantiate
-    // the inline class.
-    class Param(
-        val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
-        val reverseLayout: Boolean,
-        val layoutDirection: LayoutDirection,
-    ) {
-        override fun toString() = "beyondBoundsLayoutDirection=$beyondBoundsLayoutDirection " +
-            "reverseLayout=$reverseLayout " +
-            "layoutDirection=$layoutDirection"
-    }
-
-    private val beyondBoundsLayoutDirection = param.beyondBoundsLayoutDirection
-    private val reverseLayout = param.reverseLayout
-    private val layoutDirection = param.layoutDirection
-    private val placedItems = mutableSetOf<Int>()
-    private var beyondBoundsLayout: BeyondBoundsLayout? = null
-    private lateinit var lazyListState: LazyListState
-
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "{0}")
-        fun initParameters() = buildList {
-            for (beyondBoundsLayoutDirection in listOf(Left, Right, Above, Below, Before, After)) {
-                for (reverseLayout in listOf(false, true)) {
-                    for (layoutDirection in listOf(Ltr, Rtl)) {
-                        add(Param(beyondBoundsLayoutDirection, reverseLayout, layoutDirection))
-                    }
-                }
-            }
-        }
-    }
-
-    @Test
-    fun onlyOneVisibleItemIsPlaced() {
-        // Arrange.
-        rule.setLazyContent(size = 10.toDp(), firstVisibleItem = 0) {
-            items(100) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-        }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(0)
-            assertThat(visibleItems).containsExactly(0)
-        }
-    }
-
-    @Test
-    fun onlyTwoVisibleItemsArePlaced() {
-        // Arrange.
-        rule.setLazyContent(size = 20.toDp(), firstVisibleItem = 0) {
-            items(100) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-        }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(0, 1)
-            assertThat(visibleItems).containsExactly(0, 1)
-        }
-    }
-
-    @Test
-    fun onlyThreeVisibleItemsArePlaced() {
-        // Arrange.
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
-            items(100) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-        }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(0, 1, 2)
-            assertThat(visibleItems).containsExactly(0, 1, 2)
-        }
-    }
-
-    @Test
-    fun emptyLazyList_doesNotCrash() {
-        // Arrange.
-        var addItems by mutableStateOf(true)
-        lateinit var beyondBoundsLayoutRef: BeyondBoundsLayout
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
-            if (addItems) {
-                item {
-                    Box(
-                        Modifier.modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                    )
-                }
-            }
-        }
-        rule.runOnIdle {
-            beyondBoundsLayoutRef = beyondBoundsLayout!!
-            addItems = false
-        }
-
-        // Act.
-        val hasMoreContent = rule.runOnIdle {
-            beyondBoundsLayoutRef.layout(beyondBoundsLayoutDirection) {
-                hasMoreContent
-            }
-        }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(hasMoreContent).isFalse()
-        }
-    }
-
-    @Test
-    fun oneExtraItemBeyondVisibleBounds() {
-        // Arrange.
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(Modifier
-                    .size(10.toDp())
-                    .trackPlaced(5)
-                    .modifierLocalConsumer {
-                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                    }
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                // Assert that the beyond bounds items are present.
-                if (expectedExtraItemsBeforeVisibleBounds()) {
-                    assertThat(placedItems).containsExactly(4, 5, 6, 7)
-                    assertThat(visibleItems).containsExactly(5, 6, 7)
-                } else {
-                    assertThat(placedItems).containsExactly(5, 6, 7, 8)
-                    assertThat(visibleItems).containsExactly(5, 6, 7)
-                }
-                // Just return true so that we stop as soon as we run this once.
-                // This should result in one extra item being added.
-                true
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-            assertThat(visibleItems).containsExactly(5, 6, 7)
-        }
-    }
-
-    @Test
-    fun twoExtraItemsBeyondVisibleBounds() {
-        // Arrange.
-        var extraItemCount = 2
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(5)
-                        .modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                if (--extraItemCount > 0) {
-                    // Return null to continue the search.
-                    null
-                } else {
-                    // Assert that the beyond bounds items are present.
-                    if (expectedExtraItemsBeforeVisibleBounds()) {
-                        assertThat(placedItems).containsExactly(3, 4, 5, 6, 7)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    } else {
-                        assertThat(placedItems).containsExactly(5, 6, 7, 8, 9)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    }
-                    // Return true to stop the search.
-                    true
-                }
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-            assertThat(visibleItems).containsExactly(5, 6, 7)
-        }
-    }
-
-    @Test
-    fun allBeyondBoundsItemsInSpecifiedDirection() {
-        // Arrange.
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                        .trackPlaced(5)
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                if (hasMoreContent) {
-                    // Just return null so that we keep adding more items till we reach the end.
-                    null
-                } else {
-                    // Assert that the beyond bounds items are present.
-                    if (expectedExtraItemsBeforeVisibleBounds()) {
-                        assertThat(placedItems).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    } else {
-                        assertThat(placedItems).containsExactly(5, 6, 7, 8, 9, 10)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    }
-                    // Return true to end the search.
-                    true
-                }
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-        }
-    }
-
-    @Test
-    fun beyondBoundsLayoutRequest_inDirectionPerpendicularToLazyListOrientation() {
-        // Arrange.
-        var beyondBoundsLayoutCount = 0
-        rule.setLazyContentInPerpendicularDirection(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(5)
-                        .modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-            assertThat(visibleItems).containsExactly(5, 6, 7)
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                beyondBoundsLayoutCount++
-                when (beyondBoundsLayoutDirection) {
-                    Left, Right, Above, Below -> {
-                        assertThat(placedItems).containsExactly(5, 6, 7)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    }
-                    Before, After -> {
-                        if (expectedExtraItemsBeforeVisibleBounds()) {
-                            assertThat(placedItems).containsExactly(4, 5, 6, 7)
-                            assertThat(visibleItems).containsExactly(5, 6, 7)
-                        } else {
-                            assertThat(placedItems).containsExactly(5, 6, 7, 8)
-                            assertThat(visibleItems).containsExactly(5, 6, 7)
-                        }
-                    }
-                }
-                // Just return true so that we stop as soon as we run this once.
-                // This should result in one extra item being added.
-                true
-            }
-        }
-
-        rule.runOnIdle {
-            when (beyondBoundsLayoutDirection) {
-                Left, Right, Above, Below -> {
-                    assertThat(beyondBoundsLayoutCount).isEqualTo(0)
-                }
-                Before, After -> {
-                    assertThat(beyondBoundsLayoutCount).isEqualTo(1)
-
-                    // Assert that the beyond bounds items are removed.
-                    assertThat(placedItems).containsExactly(5, 6, 7)
-                    assertThat(visibleItems).containsExactly(5, 6, 7)
-                }
-                else -> error("Unsupported BeyondBoundsLayoutDirection")
-            }
-        }
-    }
-
-    @Test
-    fun returningNullDoesNotCauseInfiniteLoop() {
-        // Arrange.
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                        .trackPlaced(5)
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-
-        // Act.
-        var count = 0
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                // Assert that we don't keep iterating when there is no ending condition.
-                assertThat(count++).isLessThan(lazyListState.layoutInfo.totalItemsCount)
-                // Always return null to continue the search.
-                null
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-            assertThat(visibleItems).containsExactly(5, 6, 7)
-        }
-    }
-
-    private fun ComposeContentTestRule.setLazyContent(
-        size: Dp,
-        firstVisibleItem: Int,
-        content: LazyListScope.() -> Unit
-    ) {
-        setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
-                lazyListState = rememberLazyListState(firstVisibleItem)
-                when (beyondBoundsLayoutDirection) {
-                    Left, Right, Before, After ->
-                        LazyRow(
-                            modifier = Modifier.size(size),
-                            state = lazyListState,
-                            reverseLayout = reverseLayout,
-                            content = content
-                        )
-                    Above, Below ->
-                        LazyColumn(
-                            modifier = Modifier.size(size),
-                            state = lazyListState,
-                            reverseLayout = reverseLayout,
-                            content = content
-                        )
-                    else -> unsupportedDirection()
-                }
-            }
-        }
-    }
-
-    private fun ComposeContentTestRule.setLazyContentInPerpendicularDirection(
-        size: Dp,
-        firstVisibleItem: Int,
-        content: LazyListScope.() -> Unit
-    ) {
-        setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
-                lazyListState = rememberLazyListState(firstVisibleItem)
-                when (beyondBoundsLayoutDirection) {
-                    Left, Right, Before, After ->
-                        LazyColumn(
-                            modifier = Modifier.size(size),
-                            state = lazyListState,
-                            reverseLayout = reverseLayout,
-                            content = content
-                        )
-                    Above, Below ->
-                        LazyRow(
-                            modifier = Modifier.size(size),
-                            state = lazyListState,
-                            reverseLayout = reverseLayout,
-                            content = content
-                        )
-                    else -> unsupportedDirection()
-                }
-            }
-        }
-    }
-
-    private fun Int.toDp(): Dp = with(rule.density) { toDp() }
-
-    private val visibleItems: List<Int>
-        get() = lazyListState.layoutInfo.visibleItemsInfo.map { it.index }
-
-    private fun expectedExtraItemsBeforeVisibleBounds() = when (beyondBoundsLayoutDirection) {
-        Right -> if (layoutDirection == Ltr) reverseLayout else !reverseLayout
-        Left -> if (layoutDirection == Ltr) !reverseLayout else reverseLayout
-        Above -> !reverseLayout
-        Below -> reverseLayout
-        After -> false
-        Before -> true
-        else -> error("Unsupported BeyondBoundsDirection")
-    }
-
-    private fun unsupportedDirection(): Nothing = error(
-        "Lazy list does not support beyond bounds layout for the specified direction"
-    )
-
-    private fun Modifier.trackPlaced(index: Int): Modifier =
-        this then TrackPlacedElement(placedItems, index)
-}
-
-internal data class TrackPlacedElement(
-    var placedItems: MutableSet<Int>,
-    var index: Int
-) : ModifierNodeElement<TrackPlacedNode>() {
-    override fun create() = TrackPlacedNode(placedItems, index)
-
-    override fun update(node: TrackPlacedNode) {
-        node.placedItems = placedItems
-        node.index = index
-    }
-
-    override fun InspectorInfo.inspectableProperties() {
-        name = "trackPlaced"
-        properties["index"] = index
-    }
-}
-
-internal class TrackPlacedNode(
-    var placedItems: MutableSet<Int>,
-    var index: Int
-) : LayoutAwareModifierNode, Modifier.Node() {
-    override fun onPlaced(coordinates: LayoutCoordinates) {
-        placedItems += index
-    }
-
-    override fun onDetach() {
-        placedItems -= index
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt
deleted file mode 100644
index 116e9ae..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt
+++ /dev/null
@@ -1,688 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy.staggeredgrid
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.list.TrackPlacedElement
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.BeyondBoundsLayout
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
-import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.modifier.modifierLocalConsumer
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.LayoutDirection.Ltr
-import androidx.compose.ui.unit.LayoutDirection.Rtl
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@OptIn(ExperimentalComposeUiApi::class)
-@MediumTest
-@RunWith(Parameterized::class)
-class LazyStaggeredGridBeyondBoundsTest(param: Param) {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    // We need to wrap the inline class parameter in another class because Java can't instantiate
-    // the inline class.
-    class Param(
-        val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
-        val reverseLayout: Boolean,
-        val layoutDirection: LayoutDirection,
-    ) {
-        override fun toString() = "beyondBoundsLayoutDirection=$beyondBoundsLayoutDirection " +
-            "reverseLayout=$reverseLayout " +
-            "layoutDirection=$layoutDirection"
-    }
-
-    private val beyondBoundsLayoutDirection = param.beyondBoundsLayoutDirection
-    private val reverseLayout = param.reverseLayout
-    private val layoutDirection = param.layoutDirection
-    private val placedItems = mutableSetOf<Int>()
-    private var beyondBoundsLayout: BeyondBoundsLayout? = null
-    private lateinit var lazyStaggeredGridState: LazyStaggeredGridState
-
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "{0}")
-        fun initParameters() = buildList {
-            for (beyondBoundsLayoutDirection in listOf(Left, Right, Above, Below, Before, After)) {
-                for (reverseLayout in listOf(false, true)) {
-                    for (layoutDirection in listOf(Ltr, Rtl)) {
-                        add(Param(beyondBoundsLayoutDirection, reverseLayout, layoutDirection))
-                    }
-                }
-            }
-        }
-    }
-
-    @Test
-    fun onlyOneVisibleItemIsPlaced() {
-        // Arrange.
-        rule.setLazyContent(size = 10.toDp(), firstVisibleItem = 0) {
-            items(100) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-        }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(0)
-            assertThat(visibleItems).containsExactly(0)
-        }
-    }
-
-    @Test
-    fun onlyTwoVisibleItemsArePlaced() {
-        // Arrange.
-        rule.setLazyContent(size = 20.toDp(), firstVisibleItem = 0) {
-            items(100) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-        }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(0, 1)
-            assertThat(visibleItems).containsExactly(0, 1)
-        }
-    }
-
-    @Test
-    fun onlyThreeVisibleItemsArePlaced() {
-        // Arrange.
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
-            items(100) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-        }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(0, 1, 2)
-            assertThat(visibleItems).containsExactly(0, 1, 2)
-        }
-    }
-
-    @Test
-    fun emptyLazyList_doesNotCrash() {
-        // Arrange.
-        var addItems by mutableStateOf(true)
-        lateinit var beyondBoundsLayoutRef: BeyondBoundsLayout
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
-            if (addItems) {
-                item {
-                    Box(
-                        Modifier.modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                    )
-                }
-            }
-        }
-        rule.runOnIdle {
-            beyondBoundsLayoutRef = beyondBoundsLayout!!
-            addItems = false
-        }
-
-        // Act.
-        val hasMoreContent = rule.runOnIdle {
-            beyondBoundsLayoutRef.layout(beyondBoundsLayoutDirection) {
-                hasMoreContent
-            }
-        }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(hasMoreContent).isFalse()
-        }
-    }
-
-    @Test
-    fun oneExtraItemBeyondVisibleBounds() {
-        // Arrange.
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(Modifier
-                    .size(10.toDp())
-                    .trackPlaced(5)
-                    .modifierLocalConsumer {
-                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                    }
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                // Assert that the beyond bounds items are present.
-                if (expectedExtraItemsBeforeVisibleBounds()) {
-                    assertThat(placedItems).containsExactly(4, 5, 6, 7)
-                    assertThat(visibleItems).containsExactly(5, 6, 7)
-                } else {
-                    assertThat(placedItems).containsExactly(5, 6, 7, 8)
-                    assertThat(visibleItems).containsExactly(5, 6, 7)
-                }
-                // Just return true so that we stop as soon as we run this once.
-                // This should result in one extra item being added.
-                true
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-            assertThat(visibleItems).containsExactly(5, 6, 7)
-        }
-    }
-
-    @Test
-    fun oneExtraItemBeyondVisibleBounds_multipleCells() {
-        val itemSize = 50
-        val itemSizeDp = itemSize.toDp()
-        // Arrange.
-        rule.setLazyContent(cells = 2, size = itemSizeDp * 3, firstVisibleItem = 10) {
-            // item | item  | x5
-            // item | local | x1
-            // item | item  | x5
-            items(11) { index ->
-                Box(
-                    Modifier
-                        .size(itemSizeDp)
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(Modifier
-                    .size(itemSizeDp)
-                    .trackPlaced(11)
-                    .modifierLocalConsumer {
-                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                    }
-                )
-            }
-            items(10) { index ->
-                Box(
-                    Modifier
-                        .size(itemSizeDp)
-                        .trackPlaced(index + 12)
-                )
-            }
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                // Assert that the beyond bounds items are present.
-                if (expectedExtraItemsBeforeVisibleBounds()) {
-                    assertThat(placedItems).containsExactly(9, 10, 11, 12, 13, 14, 15)
-                    assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
-                } else {
-                    assertThat(placedItems).containsExactly(10, 11, 12, 13, 14, 15, 16)
-                    assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
-                }
-                // Just return true so that we stop as soon as we run this once.
-                // This should result in one extra item being added.
-                true
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(10, 11, 12, 13, 14, 15)
-            assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
-        }
-    }
-
-    @Test
-    fun oneExtraItemBeyondVisibleBounds_multipleCells_staggered() {
-        val itemSize = 50
-        val itemSizeDp = itemSize.toDp()
-        // Arrange.
-        rule.setLazyContent(cells = 3, size = itemSizeDp * 2, firstVisibleItem = 4) {
-            // -------------
-            // |   | 1 |   |
-            // | 0 |---| 2 |
-            // |   | 3 |   |
-            // |-----------|
-            // |     4     |
-            // |-----------|
-            // |   | 6 |   |
-            // | 5 |---| 7 |
-            // |   | 8 |   |
-            // -------------
-            items(4) { index ->
-                Box(
-                    Modifier
-                        .size(itemSizeDp * if (index % 2 == 0) 2f else 1f)
-                        .trackPlaced(index)
-                )
-            }
-            item(span = StaggeredGridItemSpan.FullLine) {
-                Box(Modifier
-                    .size(itemSizeDp)
-                    .trackPlaced(4)
-                    .modifierLocalConsumer {
-                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                    }
-                )
-            }
-            items(4) { index ->
-                Box(
-                    Modifier
-                        .size(itemSizeDp * if (index % 2 == 0) 2f else 1f)
-                        .trackPlaced(index + 5)
-                )
-            }
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                // Assert that the beyond bounds items are present.
-                if (expectedExtraItemsBeforeVisibleBounds()) {
-                    assertThat(placedItems).containsExactly(3, 4, 5, 6, 7)
-                    assertThat(visibleItems).containsExactly(4, 5, 6, 7)
-                } else {
-                    assertThat(placedItems).containsExactly(4, 5, 6, 7, 8)
-                    assertThat(visibleItems).containsExactly(4, 5, 6, 7)
-                }
-                // Just return true so that we stop as soon as we run this once.
-                // This should result in one extra item being added.
-                true
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(4, 5, 6, 7)
-            assertThat(visibleItems).containsExactly(4, 5, 6, 7)
-        }
-    }
-
-    @Test
-    fun twoExtraItemsBeyondVisibleBounds() {
-        // Arrange.
-        var extraItemCount = 2
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(5)
-                        .modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                if (--extraItemCount > 0) {
-                    // Return null to continue the search.
-                    null
-                } else {
-                    // Assert that the beyond bounds items are present.
-                    if (expectedExtraItemsBeforeVisibleBounds()) {
-                        assertThat(placedItems).containsExactly(3, 4, 5, 6, 7)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    } else {
-                        assertThat(placedItems).containsExactly(5, 6, 7, 8, 9)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    }
-                    // Return true to stop the search.
-                    true
-                }
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-            assertThat(visibleItems).containsExactly(5, 6, 7)
-        }
-    }
-
-    @Test
-    fun allBeyondBoundsItemsInSpecifiedDirection() {
-        // Arrange.
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                        .trackPlaced(5)
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                if (hasMoreContent) {
-                    // Just return null so that we keep adding more items till we reach the end.
-                    null
-                } else {
-                    // Assert that the beyond bounds items are present.
-                    if (expectedExtraItemsBeforeVisibleBounds()) {
-                        assertThat(placedItems).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    } else {
-                        assertThat(placedItems).containsExactly(5, 6, 7, 8, 9, 10)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    }
-                    // Return true to end the search.
-                    true
-                }
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-        }
-    }
-
-    @Test
-    fun beyondBoundsLayoutRequest_inDirectionPerpendicularToLazyListOrientation() {
-        // Arrange.
-        var beyondBoundsLayoutCount = 0
-        rule.setLazyContentInPerpendicularDirection(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(5)
-                        .modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-            assertThat(visibleItems).containsExactly(5, 6, 7)
-        }
-
-        // Act.
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                beyondBoundsLayoutCount++
-                when (beyondBoundsLayoutDirection) {
-                    Left, Right, Above, Below -> {
-                        assertThat(placedItems).containsExactly(5, 6, 7)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
-                    }
-                    Before, After -> {
-                        if (expectedExtraItemsBeforeVisibleBounds()) {
-                            assertThat(placedItems).containsExactly(4, 5, 6, 7)
-                            assertThat(visibleItems).containsExactly(5, 6, 7)
-                        } else {
-                            assertThat(placedItems).containsExactly(5, 6, 7, 8)
-                            assertThat(visibleItems).containsExactly(5, 6, 7)
-                        }
-                    }
-                }
-                // Just return true so that we stop as soon as we run this once.
-                // This should result in one extra item being added.
-                true
-            }
-        }
-
-        rule.runOnIdle {
-            when (beyondBoundsLayoutDirection) {
-                Left, Right, Above, Below -> {
-                    assertThat(beyondBoundsLayoutCount).isEqualTo(0)
-                }
-                Before, After -> {
-                    assertThat(beyondBoundsLayoutCount).isEqualTo(1)
-
-                    // Assert that the beyond bounds items are removed.
-                    assertThat(placedItems).containsExactly(5, 6, 7)
-                    assertThat(visibleItems).containsExactly(5, 6, 7)
-                }
-                else -> error("Unsupported BeyondBoundsLayoutDirection")
-            }
-        }
-    }
-
-    @Test
-    fun returningNullDoesNotCauseInfiniteLoop() {
-        // Arrange.
-        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index)
-                )
-            }
-            item {
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .modifierLocalConsumer {
-                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                        }
-                        .trackPlaced(5)
-                )
-            }
-            items(5) { index ->
-                Box(
-                    Modifier
-                        .size(10.toDp())
-                        .trackPlaced(index + 6)
-                )
-            }
-        }
-
-        // Act.
-        var count = 0
-        rule.runOnUiThread {
-            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
-                // Assert that we don't keep iterating when there is no ending condition.
-                assertThat(count++).isLessThan(lazyStaggeredGridState.layoutInfo.totalItemsCount)
-                // Always return null to continue the search.
-                null
-            }
-        }
-
-        // Assert that the beyond bounds items are removed.
-        rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
-            assertThat(visibleItems).containsExactly(5, 6, 7)
-        }
-    }
-
-    private fun ComposeContentTestRule.setLazyContent(
-        size: Dp,
-        firstVisibleItem: Int,
-        cells: Int = 1,
-        content: LazyStaggeredGridScope.() -> Unit
-    ) {
-        setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
-                lazyStaggeredGridState = rememberLazyStaggeredGridState(firstVisibleItem)
-                when (beyondBoundsLayoutDirection) {
-                    Left, Right, Before, After ->
-                        LazyHorizontalStaggeredGrid(
-                            rows = StaggeredGridCells.Fixed(cells),
-                            modifier = Modifier.size(size),
-                            state = lazyStaggeredGridState,
-                            reverseLayout = reverseLayout,
-                            content = content
-                        )
-                    Above, Below ->
-                        LazyVerticalStaggeredGrid(
-                            columns = StaggeredGridCells.Fixed(cells),
-                            modifier = Modifier.size(size),
-                            state = lazyStaggeredGridState,
-                            reverseLayout = reverseLayout,
-                            content = content
-                        )
-                    else -> unsupportedDirection()
-                }
-            }
-        }
-    }
-
-    private fun ComposeContentTestRule.setLazyContentInPerpendicularDirection(
-        size: Dp,
-        firstVisibleItem: Int,
-        content: LazyStaggeredGridScope.() -> Unit
-    ) {
-        setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
-                lazyStaggeredGridState = rememberLazyStaggeredGridState(firstVisibleItem)
-                when (beyondBoundsLayoutDirection) {
-                    Left, Right, Before, After ->
-                        LazyVerticalStaggeredGrid(
-                            columns = StaggeredGridCells.Fixed(1),
-                            modifier = Modifier.size(size),
-                            state = lazyStaggeredGridState,
-                            reverseLayout = reverseLayout,
-                            content = content
-                        )
-                    Above, Below ->
-                        LazyHorizontalStaggeredGrid(
-                            rows = StaggeredGridCells.Fixed(1),
-                            modifier = Modifier.size(size),
-                            state = lazyStaggeredGridState,
-                            reverseLayout = reverseLayout,
-                            content = content
-                        )
-                    else -> unsupportedDirection()
-                }
-            }
-        }
-    }
-
-    private fun Int.toDp(): Dp = with(rule.density) { toDp() }
-
-    private val visibleItems: List<Int>
-        get() = lazyStaggeredGridState.layoutInfo.visibleItemsInfo.map { it.index }
-
-    private fun expectedExtraItemsBeforeVisibleBounds() = when (beyondBoundsLayoutDirection) {
-        Right -> if (layoutDirection == Ltr) reverseLayout else !reverseLayout
-        Left -> if (layoutDirection == Ltr) !reverseLayout else reverseLayout
-        Above -> !reverseLayout
-        Below -> reverseLayout
-        After -> false
-        Before -> true
-        else -> error("Unsupported BeyondBoundsDirection")
-    }
-
-    private fun unsupportedDirection(): Nothing = error(
-        "Lazy list does not support beyond bounds layout for the specified direction"
-    )
-
-    private fun Modifier.trackPlaced(index: Int): Modifier =
-        this then TrackPlacedElement(placedItems, index)
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
deleted file mode 100644
index d3dd669..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
+++ /dev/null
@@ -1,658 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.pager
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.LargeTest
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@OptIn(ExperimentalFoundationApi::class)
-@LargeTest
-@RunWith(Parameterized::class)
-class PagerScrollingTest(
-    val config: ParamConfig
-) : BasePagerTest(config) {
-
-    @Before
-    fun setUp() {
-        rule.mainClock.autoAdvance = false
-    }
-
-    @Test
-    fun swipeWithLowVelocity_positionalThresholdLessThanDefaultThreshold_shouldBounceBack() {
-        // Arrange
-        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
-        val swipeValue = 0.4f
-        val delta = pagerSize * swipeValue * scrollForwardSign
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta * -1
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-    }
-
-    @Test
-    fun swipeWithLowVelocity_positionalThresholdLessThanLowThreshold_shouldBounceBack() {
-        // Arrange
-        createPager(
-            initialPage = 5,
-            modifier = Modifier.fillMaxSize(),
-            snapPositionalThreshold = 0.2f
-        )
-        val swipeValue = 0.1f
-        val delta = pagerSize * swipeValue * scrollForwardSign
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta * -1
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-    }
-
-    @Test
-    fun swipeWithLowVelocity_positionalThresholdLessThanHighThreshold_shouldBounceBack() {
-        // Arrange
-        createPager(
-            initialPage = 5,
-            modifier = Modifier.fillMaxSize(),
-            snapPositionalThreshold = 0.8f
-        )
-        val swipeValue = 0.6f
-        val delta = pagerSize * swipeValue * scrollForwardSign
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta * -1
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-    }
-
-    @Test
-    fun swipeWithLowVelocity_positionalThresholdLessThanDefault_customPageSize_shouldBounceBack() {
-        // Arrange
-        createPager(initialPage = 2, modifier = Modifier.fillMaxSize(), pageSize = {
-            PageSize.Fixed(200.dp)
-        })
-
-        val delta = (2.4f * pageSize) * scrollForwardSign // 2.4 pages
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("4").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(4)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta * -1
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("2").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(2)
-    }
-
-    @Test
-    fun swipeWithLowVelocity_positionalThresholdOverDefaultThreshold_shouldGoToNextPage() {
-        // Arrange
-        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
-        val swipeValue = 0.51f
-        val delta = pagerSize * swipeValue * scrollForwardSign
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("6").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(6)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta * -1
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-    }
-
-    @Test
-    fun swipeWithLowVelocity_positionalThresholdOverLowThreshold_shouldGoToNextPage() {
-        // Arrange
-        createPager(
-            initialPage = 5,
-            modifier = Modifier.fillMaxSize(),
-            snapPositionalThreshold = 0.2f
-        )
-        val swipeValue = 0.21f
-        val delta = pagerSize * swipeValue * scrollForwardSign
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("6").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(6)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta * -1
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-    }
-
-    @Test
-    fun swipeWithLowVelocity_positionalThresholdOverThreshold_customPage_shouldGoToNextPage() {
-        // Arrange
-        createPager(
-            initialPage = 2,
-            modifier = Modifier.fillMaxSize(),
-            pageSize = {
-                PageSize.Fixed(200.dp)
-            }
-        )
-
-        val delta = 2.6f * pageSize * scrollForwardSign
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta * -1
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("2").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(2)
-    }
-
-    @Test
-    fun swipeWithLowVelocity_positionalThresholdOverHighThreshold_shouldGoToNextPage() {
-        // Arrange
-        createPager(
-            initialPage = 5,
-            modifier = Modifier.fillMaxSize(),
-            snapPositionalThreshold = 0.8f
-        )
-        val swipeValue = 0.81f
-        val delta = pagerSize * swipeValue * scrollForwardSign
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("6").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(6)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
-                    delta * -1
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-    }
-
-    @Test
-    fun swipeWithLowVelocity_customVelocityThreshold_shouldBounceBack() {
-        // Arrange
-        val snapVelocityThreshold = 200.dp
-        createPager(
-            initialPage = 5,
-            modifier = Modifier.fillMaxSize(),
-            snapVelocityThreshold = snapVelocityThreshold
-        )
-        val delta = pagerSize * 0.4f * scrollForwardSign
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * snapVelocityThreshold.toPx() },
-                    delta
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 0.5f * snapVelocityThreshold.toPx() },
-                    delta * -1
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-    }
-
-    @Test
-    fun swipeWithHighVelocity_defaultVelocityThreshold_shouldGoToNextPage() {
-        // Arrange
-        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
-        // make sure the scroll distance is not enough to go to next page
-        val delta = pagerSize * 0.4f * scrollForwardSign
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
-                    delta
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("6").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(6)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
-                    delta * -1
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-    }
-
-    @Test
-    fun swipeWithHighVelocity_customVelocityThreshold_shouldGoToNextPage() {
-        // Arrange
-        val snapVelocityThreshold = 200.dp
-        createPager(
-            initialPage = 5,
-            modifier = Modifier.fillMaxSize(),
-            snapVelocityThreshold = snapVelocityThreshold
-        )
-        // make sure the scroll distance is not enough to go to next page
-        val delta = pagerSize * 0.4f * scrollForwardSign
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 1.1f * snapVelocityThreshold.toPx() },
-                    delta
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("6").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(6)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 1.1f * snapVelocityThreshold.toPx() },
-                    delta * -1
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-    }
-
-    @Test
-    fun swipeWithHighVelocity_overHalfPage_shouldGoToNextPage() {
-        // Arrange
-        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
-        // make sure the scroll distance is not enough to go to next page
-        val delta = pagerSize * 0.8f * scrollForwardSign
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
-                    delta
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("6").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(6)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
-                    delta * -1
-                )
-            }
-        }
-
-        // Assert
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(5)
-    }
-
-    @Test
-    fun scrollWithoutVelocity_shouldSettlingInClosestPage() {
-        // Arrange
-        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
-        // This will scroll 1 whole page before flinging
-        val delta = pagerSize * 1.4f * scrollForwardSign
-
-        // Act - forward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(0f, delta)
-            }
-        }
-
-        // Assert
-        assertThat(pagerState.currentPage).isAtMost(7)
-        rule.onNodeWithTag("${pagerState.currentPage}").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(pagerState.currentPage)
-
-        // Act - backward
-        runAndWaitForPageSettling {
-            onPager().performTouchInput {
-                swipeWithVelocityAcrossMainAxis(0f, delta * -1)
-            }
-        }
-
-        // Assert
-        assertThat(pagerState.currentPage).isAtLeast(5)
-        rule.onNodeWithTag("${pagerState.currentPage}").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(pagerState.currentPage)
-    }
-
-    @Test
-    fun scrollWithSameVelocity_shouldYieldSameResult_forward() {
-        // Arrange
-        var initialPage = 1
-        createPager(
-            pageSize = { PageSize.Fixed(200.dp) },
-            initialPage = initialPage,
-            modifier = Modifier.fillMaxSize(),
-            pageCount = { 100 },
-            snappingPage = PagerSnapDistance.atMost(3)
-        )
-        // This will scroll 0.5 page before flinging
-        val delta = pagerSize * 0.5f * scrollForwardSign
-
-        // Act - forward
-        onPager().performTouchInput {
-            swipeWithVelocityAcrossMainAxis(2000f, delta)
-        }
-        rule.waitForIdle()
-
-        val pageDisplacement = pagerState.currentPage - initialPage
-
-        // Repeat starting from different places
-        // reset
-        initialPage = 10
-        rule.runOnIdle {
-            runBlocking { pagerState.scrollToPage(initialPage) }
-        }
-
-        onPager().performTouchInput {
-            swipeWithVelocityAcrossMainAxis(2000f, delta)
-        }
-        rule.waitForIdle()
-
-        assertThat(pagerState.currentPage - initialPage).isEqualTo(pageDisplacement)
-
-        initialPage = 50
-        rule.runOnIdle {
-            runBlocking { pagerState.scrollToPage(initialPage) }
-        }
-
-        onPager().performTouchInput {
-            swipeWithVelocityAcrossMainAxis(2000f, delta)
-        }
-        rule.waitForIdle()
-
-        assertThat(pagerState.currentPage - initialPage).isEqualTo(pageDisplacement)
-    }
-
-    @Test
-    fun scrollWithSameVelocity_shouldYieldSameResult_backward() {
-        // Arrange
-        var initialPage = 90
-        createPager(
-            pageSize = { PageSize.Fixed(200.dp) },
-            initialPage = initialPage,
-            modifier = Modifier.fillMaxSize(),
-            pageCount = { 100 },
-            snappingPage = PagerSnapDistance.atMost(3)
-        )
-        // This will scroll 0.5 page before flinging
-        val delta = pagerSize * -0.5f * scrollForwardSign
-
-        // Act - forward
-        onPager().performTouchInput {
-            swipeWithVelocityAcrossMainAxis(2000f, delta)
-        }
-        rule.waitForIdle()
-
-        val pageDisplacement = pagerState.currentPage - initialPage
-
-        // Repeat starting from different places
-        // reset
-        initialPage = 70
-        rule.runOnIdle {
-            runBlocking { pagerState.scrollToPage(initialPage) }
-        }
-
-        onPager().performTouchInput {
-            swipeWithVelocityAcrossMainAxis(2000f, delta)
-        }
-        rule.waitForIdle()
-
-        assertThat(pagerState.currentPage - initialPage).isEqualTo(pageDisplacement)
-
-        initialPage = 30
-        rule.runOnIdle {
-            runBlocking { pagerState.scrollToPage(initialPage) }
-        }
-
-        onPager().performTouchInput {
-            swipeWithVelocityAcrossMainAxis(2000f, delta)
-        }
-        rule.waitForIdle()
-
-        assertThat(pagerState.currentPage - initialPage).isEqualTo(pageDisplacement)
-    }
-
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "{0}")
-        fun params() = mutableListOf<ParamConfig>().apply {
-            for (orientation in TestOrientation) {
-                for (pageSpacing in TestPageSpacing) {
-                    add(
-                        ParamConfig(
-                            orientation = orientation,
-                            pageSpacing = pageSpacing
-                        )
-                    )
-                }
-            }
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt
deleted file mode 100644
index 722fb27..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt
+++ /dev/null
@@ -1,315 +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.compose.foundation.pager
-
-import androidx.compose.foundation.AutoTestFrameClock
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.junit4.StateRestorationTester
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.LargeTest
-import com.google.common.truth.Truth
-import kotlin.test.assertFalse
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@OptIn(ExperimentalFoundationApi::class)
-@LargeTest
-@RunWith(Parameterized::class)
-class PagerStateNonGestureScrollingTest(val config: ParamConfig) : BasePagerTest(config) {
-    @Test
-    fun pagerStateNotAttached_shouldReturnDefaultValues_andChangeAfterAttached() = runBlocking {
-        // Arrange
-        val state = PagerStateImpl(5, 0.2f) { DefaultPageCount }
-
-        Truth.assertThat(state.currentPage).isEqualTo(5)
-        Truth.assertThat(state.currentPageOffsetFraction).isEqualTo(0.2f)
-
-        val currentPage = derivedStateOf { state.currentPage }
-        val currentPageOffsetFraction = derivedStateOf { state.currentPageOffsetFraction }
-
-        rule.setContent {
-            HorizontalOrVerticalPager(
-                state = state,
-                modifier = Modifier
-                    .fillMaxSize()
-                    .testTag(PagerTestTag)
-                    .onSizeChanged { pagerSize = if (vertical) it.height else it.width },
-                pageSize = PageSize.Fill,
-                reverseLayout = config.reverseLayout,
-                pageSpacing = config.pageSpacing,
-                contentPadding = config.mainAxisContentPadding,
-            ) {
-                Page(index = it)
-            }
-        }
-
-        withContext(Dispatchers.Main + AutoTestFrameClock()) {
-            state.scrollToPage(state.currentPage + 1)
-        }
-
-        rule.runOnIdle {
-            Truth.assertThat(currentPage.value).isEqualTo(6)
-            Truth.assertThat(currentPageOffsetFraction.value).isEqualTo(0.0f)
-        }
-    }
-
-    @Test
-    fun initialPageOnPagerState_shouldDisplayThatPageFirst() {
-        // Arrange
-
-        // Act
-        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
-
-        // Assert
-        rule.onNodeWithTag("4").assertDoesNotExist()
-        rule.onNodeWithTag("5").assertIsDisplayed()
-        rule.onNodeWithTag("6").assertDoesNotExist()
-        confirmPageIsInCorrectPosition(pagerState.currentPage)
-    }
-
-    @Test
-    fun testStateRestoration() {
-        // Arrange
-        val tester = StateRestorationTester(rule)
-        lateinit var state: PagerState
-        tester.setContent {
-            state = rememberPagerState(pageCount = { DefaultPageCount })
-            scope = rememberCoroutineScope()
-            HorizontalOrVerticalPager(
-                state = state,
-                modifier = Modifier.fillMaxSize()
-            ) {
-                Page(it)
-            }
-        }
-
-        // Act
-        rule.runOnIdle {
-            scope.launch {
-                state.scrollToPage(5)
-            }
-            runBlocking {
-                state.scroll {
-                    scrollBy(50f)
-                }
-            }
-        }
-
-        val previousPage = state.currentPage
-        val previousOffset = state.currentPageOffsetFraction
-        tester.emulateSavedInstanceStateRestore()
-
-        // Assert
-        rule.runOnIdle {
-            Truth.assertThat(state.currentPage).isEqualTo(previousPage)
-            Truth.assertThat(state.currentPageOffsetFraction).isEqualTo(previousOffset)
-        }
-    }
-
-    @Test
-    fun currentPageOffsetFraction_shouldNeverBeNan() {
-        rule.setContent {
-            val state = rememberPagerState(pageCount = { 10 })
-            // Read state in composition, should never be Nan
-            assertFalse { state.currentPageOffsetFraction.isNaN() }
-            HorizontalOrVerticalPager(state = state) {
-                Page(index = it)
-            }
-        }
-    }
-
-    @Test
-    fun currentPage_pagerWithKeys_shouldBeTheSameAfterDatasetUpdate() {
-        // Arrange
-        class Data(val id: Int, val item: String)
-
-        val data = mutableListOf(
-            Data(3, "A"),
-            Data(4, "B"),
-            Data(5, "C")
-        )
-
-        val extraData = mutableListOf(
-            Data(0, "D"),
-            Data(1, "E"),
-            Data(2, "F")
-        )
-
-        val dataset = mutableStateOf<List<Data>>(data)
-
-        createPager(
-            modifier = Modifier.fillMaxSize(),
-            initialPage = 1,
-            key = { dataset.value[it].id },
-            pageCount = {
-                dataset.value.size
-            }, pageContent = {
-                val item = dataset.value[it]
-                Box(modifier = Modifier.fillMaxSize().testTag(item.item))
-            })
-
-        Truth.assertThat(dataset.value[pagerState.currentPage].item).isEqualTo("B")
-
-        rule.runOnIdle {
-            dataset.value = extraData + data // add new data
-        }
-
-        rule.waitForIdle()
-        Truth.assertThat(pagerState.pageCount).isEqualTo(6) // all data is present
-        rule.onNodeWithTag("B").assertIsDisplayed() // scroll kept
-        Truth.assertThat(pagerState.currentPage).isEqualTo(4)
-        Truth.assertThat(pagerState.currentPageOffsetFraction).isEqualTo(0.0f)
-    }
-
-    @Test
-    fun calculatePageCountOffset_shouldBeBasedOnCurrentPage() {
-        val pageToOffsetCalculations = mutableMapOf<Int, Float>()
-        createPager(modifier = Modifier.fillMaxSize(), pageSize = { PageSize.Fixed(20.dp) }) {
-            pageToOffsetCalculations[it] = pagerState.getOffsetFractionForPage(it)
-            Page(index = it)
-        }
-
-        for ((page, offset) in pageToOffsetCalculations) {
-            val currentPage = pagerState.currentPage
-            val currentPageOffset = pagerState.currentPageOffsetFraction
-            Truth.assertThat(offset).isEqualTo((currentPage - page) + currentPageOffset)
-        }
-    }
-
-    @Test
-    fun scrollToPage_usingLaunchedEffect() {
-
-        createPager(additionalContent = {
-            LaunchedEffect(pagerState) {
-                pagerState.scrollToPage(10)
-            }
-        })
-
-        Truth.assertThat(pagerState.currentPage).isEqualTo(10)
-        confirmPageIsInCorrectPosition(10)
-    }
-
-    @Test
-    fun scrollToPageWithOffset_usingLaunchedEffect() {
-        createPager(additionalContent = {
-            LaunchedEffect(pagerState) {
-                pagerState.scrollToPage(10, 0.4f)
-            }
-        })
-
-        Truth.assertThat(pagerState.currentPage).isEqualTo(10)
-        confirmPageIsInCorrectPosition(10, pageOffset = 0.4f)
-    }
-
-    @Test
-    fun animatedScrollToPage_usingLaunchedEffect() {
-
-        createPager(additionalContent = {
-            LaunchedEffect(pagerState) {
-                pagerState.animateScrollToPage(10)
-            }
-        })
-
-        Truth.assertThat(pagerState.currentPage).isEqualTo(10)
-        confirmPageIsInCorrectPosition(10)
-    }
-
-    @Test
-    fun animatedScrollToPage_emptyPager_shouldNotReact() {
-        createPager(pageCount = { 0 }, additionalContent = {
-            LaunchedEffect(pagerState) {
-                pagerState.animateScrollToPage(10)
-            }
-        })
-        Truth.assertThat(pagerState.currentPage).isEqualTo(0)
-    }
-
-    @Test
-    fun animatedScrollToPageWithOffset_usingLaunchedEffect() {
-
-        createPager(additionalContent = {
-            LaunchedEffect(pagerState) {
-                pagerState.animateScrollToPage(10, 0.4f)
-            }
-        })
-
-        Truth.assertThat(pagerState.currentPage).isEqualTo(10)
-        confirmPageIsInCorrectPosition(10, pageOffset = 0.4f)
-    }
-
-    @Test
-    fun animatedScrollToPage_viewPortNumberOfPages_usingLaunchedEffect_shouldNotPlaceALlPages() {
-
-        createPager(additionalContent = {
-            LaunchedEffect(pagerState) {
-                pagerState.animateScrollToPage(DefaultPageCount - 1)
-            }
-        })
-
-        // Assert
-        rule.runOnIdle {
-            Truth.assertThat(pagerState.currentPage).isEqualTo(DefaultPageCount - 1)
-            Truth.assertThat(placed).doesNotContain(DefaultPageCount / 2 - 1)
-            Truth.assertThat(placed).doesNotContain(DefaultPageCount / 2)
-            Truth.assertThat(placed).doesNotContain(DefaultPageCount / 2 + 1)
-        }
-        confirmPageIsInCorrectPosition(pagerState.currentPage)
-    }
-
-    @Test
-    fun scrollTo_beforeFirstLayout_shouldWaitForStateAndLayoutSetting() {
-        // Arrange
-
-        rule.mainClock.autoAdvance = false
-
-        // Act
-        createPager(modifier = Modifier.fillMaxSize(), additionalContent = {
-            LaunchedEffect(pagerState) {
-                pagerState.scrollToPage(5)
-            }
-        })
-
-        // Assert
-        Truth.assertThat(pagerState.currentPage).isEqualTo(5)
-    }
-
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "{0}")
-        fun params() = mutableListOf<ParamConfig>().apply {
-            for (orientation in TestOrientation) {
-                add(ParamConfig(orientation = orientation))
-            }
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
deleted file mode 100644
index 816340a..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
+++ /dev/null
@@ -1,779 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.pager
-
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.AutoTestFrameClock
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performTouchInput
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.assertTrue
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@OptIn(ExperimentalFoundationApi::class)
-@LargeTest
-@RunWith(Parameterized::class)
-class PagerStateTest(val config: ParamConfig) : BasePagerTest(config) {
-
-    @Test
-    fun scrollToPage_shouldPlacePagesCorrectly() = runBlocking {
-        // Arrange
-        createPager(modifier = Modifier.fillMaxSize())
-
-        // Act and Assert
-        repeat(DefaultAnimationRepetition) {
-            assertThat(pagerState.currentPage).isEqualTo(it)
-            val nextPage = pagerState.currentPage + 1
-            withContext(Dispatchers.Main + AutoTestFrameClock()) {
-                pagerState.scrollToPage(nextPage)
-            }
-            rule.mainClock.advanceTimeUntil { pagerState.currentPage == nextPage }
-            confirmPageIsInCorrectPosition(pagerState.currentPage)
-        }
-    }
-
-    @SdkSuppress(maxSdkVersion = 32) // b/269176638
-    @Test
-    fun scrollToPage_usedOffset_shouldPlacePagesCorrectly() = runBlocking {
-        // Arrange
-
-        suspend fun scrollToPageWithOffset(page: Int, offset: Float) {
-            withContext(Dispatchers.Main + AutoTestFrameClock()) {
-                pagerState.scrollToPage(page, offset)
-            }
-        }
-
-        // Arrange
-        createPager(modifier = Modifier.fillMaxSize())
-
-        // Act
-        scrollToPageWithOffset(10, 0.5f)
-
-        // Assert
-        confirmPageIsInCorrectPosition(pagerState.currentPage, 10, pageOffset = 0.5f)
-
-        // Act
-        scrollToPageWithOffset(4, 0.2f)
-
-        // Assert
-        confirmPageIsInCorrectPosition(pagerState.currentPage, 4, pageOffset = 0.2f)
-
-        // Act
-        scrollToPageWithOffset(12, -0.4f)
-
-        // Assert
-        confirmPageIsInCorrectPosition(pagerState.currentPage, 12, pageOffset = -0.4f)
-
-        // Act
-        scrollToPageWithOffset(DefaultPageCount - 1, 0.5f)
-
-        // Assert
-        confirmPageIsInCorrectPosition(pagerState.currentPage, DefaultPageCount - 1)
-
-        // Act
-        scrollToPageWithOffset(0, -0.5f)
-
-        // Assert
-        confirmPageIsInCorrectPosition(pagerState.currentPage, 0)
-    }
-
-    @Test
-    fun scrollToPage_longSkipShouldNotPlaceIntermediatePages() = runBlocking {
-        // Arrange
-
-        createPager(modifier = Modifier.fillMaxSize())
-
-        // Act
-        assertThat(pagerState.currentPage).isEqualTo(0)
-        withContext(Dispatchers.Main + AutoTestFrameClock()) {
-            pagerState.scrollToPage(DefaultPageCount - 1)
-        }
-
-        // Assert
-        rule.runOnIdle {
-            assertThat(pagerState.currentPage).isEqualTo(DefaultPageCount - 1)
-            assertThat(placed).doesNotContain(DefaultPageCount / 2 - 1)
-            assertThat(placed).doesNotContain(DefaultPageCount / 2)
-            assertThat(placed).doesNotContain(DefaultPageCount / 2 + 1)
-        }
-        confirmPageIsInCorrectPosition(pagerState.currentPage)
-    }
-
-    @Test
-    fun animateScrollToPage_shouldPlacePagesCorrectly() = runBlocking {
-        // Arrange
-
-        createPager(modifier = Modifier.fillMaxSize())
-
-        // Act and Assert
-        repeat(DefaultAnimationRepetition) {
-            assertThat(pagerState.currentPage).isEqualTo(it)
-            val nextPage = pagerState.currentPage + 1
-            withContext(Dispatchers.Main + AutoTestFrameClock()) {
-                pagerState.animateScrollToPage(nextPage)
-            }
-            rule.mainClock.advanceTimeUntil { pagerState.currentPage == nextPage }
-            confirmPageIsInCorrectPosition(pagerState.currentPage)
-        }
-    }
-
-    @Test
-    fun animateScrollToPage_usedOffset_shouldPlacePagesCorrectly() = runBlocking {
-        // Arrange
-
-        suspend fun animateScrollToPageWithOffset(page: Int, offset: Float) {
-            withContext(Dispatchers.Main + AutoTestFrameClock()) {
-                pagerState.animateScrollToPage(page, offset)
-            }
-        }
-
-        // Arrange
-        createPager(modifier = Modifier.fillMaxSize())
-
-        // Act
-        animateScrollToPageWithOffset(10, 0.5f)
-
-        // Assert
-        confirmPageIsInCorrectPosition(pagerState.currentPage, 10, pageOffset = 0.5f)
-
-        // Act
-        animateScrollToPageWithOffset(4, 0.2f)
-
-        // Assert
-        confirmPageIsInCorrectPosition(pagerState.currentPage, 4, pageOffset = 0.2f)
-
-        // Act
-        animateScrollToPageWithOffset(12, -0.4f)
-
-        // Assert
-        confirmPageIsInCorrectPosition(pagerState.currentPage, 12, pageOffset = -0.4f)
-
-        // Act
-        animateScrollToPageWithOffset(DefaultPageCount - 1, 0.5f)
-
-        // Assert
-        confirmPageIsInCorrectPosition(pagerState.currentPage, DefaultPageCount - 1)
-
-        // Act
-        animateScrollToPageWithOffset(0, -0.5f)
-
-        // Assert
-        confirmPageIsInCorrectPosition(pagerState.currentPage, 0)
-    }
-
-    @Test
-    fun animateScrollToPage_longSkipShouldNotPlaceIntermediatePages() = runBlocking {
-        // Arrange
-
-        createPager(modifier = Modifier.fillMaxSize())
-
-        // Act
-        assertThat(pagerState.currentPage).isEqualTo(0)
-        withContext(Dispatchers.Main + AutoTestFrameClock()) {
-            pagerState.animateScrollToPage(DefaultPageCount - 1)
-        }
-
-        // Assert
-        rule.runOnIdle {
-            assertThat(pagerState.currentPage).isEqualTo(DefaultPageCount - 1)
-            assertThat(placed).doesNotContain(DefaultPageCount / 2 - 1)
-            assertThat(placed).doesNotContain(DefaultPageCount / 2)
-            assertThat(placed).doesNotContain(DefaultPageCount / 2 + 1)
-        }
-        confirmPageIsInCorrectPosition(pagerState.currentPage)
-    }
-
-    @Test
-    fun scrollToPage_shouldCoerceWithinRange() = runBlocking {
-        // Arrange
-
-        createPager(modifier = Modifier.fillMaxSize())
-
-        // Act
-        assertThat(pagerState.currentPage).isEqualTo(0)
-        withContext(Dispatchers.Main + AutoTestFrameClock()) {
-            pagerState.scrollToPage(DefaultPageCount)
-        }
-
-        // Assert
-        rule.runOnIdle { assertThat(pagerState.currentPage).isEqualTo(DefaultPageCount - 1) }
-
-        // Act
-        withContext(Dispatchers.Main + AutoTestFrameClock()) {
-            pagerState.scrollToPage(-1)
-        }
-
-        // Assert
-        rule.runOnIdle { assertThat(pagerState.currentPage).isEqualTo(0) }
-    }
-
-    @Test
-    fun animateScrollToPage_shouldCoerceWithinRange() = runBlocking {
-        // Arrange
-
-        createPager(modifier = Modifier.fillMaxSize())
-
-        // Act
-        assertThat(pagerState.currentPage).isEqualTo(0)
-        withContext(Dispatchers.Main + AutoTestFrameClock()) {
-            pagerState.animateScrollToPage(DefaultPageCount)
-        }
-
-        // Assert
-        rule.runOnIdle { assertThat(pagerState.currentPage).isEqualTo(DefaultPageCount - 1) }
-
-        // Act
-        withContext(Dispatchers.Main + AutoTestFrameClock()) {
-            pagerState.animateScrollToPage(-1)
-        }
-
-        // Assert
-        rule.runOnIdle { assertThat(pagerState.currentPage).isEqualTo(0) }
-    }
-
-    @Test
-    fun animateScrollToPage_moveToSamePageWithOffset_shouldScroll() = runBlocking {
-        // Arrange
-        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
-
-        // Act
-        assertThat(pagerState.currentPage).isEqualTo(5)
-
-        withContext(Dispatchers.Main + AutoTestFrameClock()) {
-            pagerState.animateScrollToPage(5, 0.4f)
-        }
-
-        // Assert
-        rule.runOnIdle { assertThat(pagerState.currentPage).isEqualTo(5) }
-        rule.runOnIdle { assertThat(pagerState.currentPageOffsetFraction).isWithin(0.01f).of(0.4f) }
-    }
-
-    @Test
-    fun animateScrollToPage_withPassedAnimation() = runBlocking {
-        // Arrange
-        rule.mainClock.autoAdvance = false
-        createPager(modifier = Modifier.fillMaxSize())
-        val differentAnimation: AnimationSpec<Float> = tween()
-
-        // Act and Assert
-        repeat(DefaultAnimationRepetition) {
-            assertThat(pagerState.currentPage).isEqualTo(it)
-            val nextPage = pagerState.currentPage + 1
-            withContext(Dispatchers.Main + AutoTestFrameClock()) {
-                pagerState.animateScrollToPage(
-                    nextPage,
-                    animationSpec = differentAnimation
-                )
-            }
-            rule.mainClock.advanceTimeUntil { pagerState.currentPage == nextPage }
-            confirmPageIsInCorrectPosition(pagerState.currentPage)
-        }
-    }
-
-    @Test
-    fun currentPage_shouldChangeWhenClosestPageToSnappedPositionChanges() {
-        // Arrange
-
-        createPager(modifier = Modifier.fillMaxSize())
-        var previousCurrentPage = pagerState.currentPage
-
-        // Act
-        // Move less than half an item
-        val firstDelta = (pagerSize * 0.4f) * scrollForwardSign
-        onPager().performTouchInput {
-            down(layoutStart)
-            if (vertical) {
-                moveBy(Offset(0f, firstDelta))
-            } else {
-                moveBy(Offset(firstDelta, 0f))
-            }
-        }
-
-        // Assert
-        rule.runOnIdle {
-            assertThat(pagerState.currentPage).isEqualTo(previousCurrentPage)
-        }
-        // Release pointer
-        onPager().performTouchInput { up() }
-
-        rule.runOnIdle {
-            previousCurrentPage = pagerState.currentPage
-        }
-        confirmPageIsInCorrectPosition(pagerState.currentPage)
-
-        // Arrange
-        // Pass closest to snap position threshold (over half an item)
-        val secondDelta = (pagerSize * 0.6f) * scrollForwardSign
-
-        // Act
-        onPager().performTouchInput {
-            down(layoutStart)
-            if (vertical) {
-                moveBy(Offset(0f, secondDelta))
-            } else {
-                moveBy(Offset(secondDelta, 0f))
-            }
-        }
-
-        // Assert
-        rule.runOnIdle {
-            assertThat(pagerState.currentPage).isEqualTo(previousCurrentPage + 1)
-        }
-
-        onPager().performTouchInput { up() }
-        rule.waitForIdle()
-        confirmPageIsInCorrectPosition(pagerState.currentPage)
-    }
-
-    @Test
-    fun targetPage_performScrollBelowMinThreshold_shouldNotShowNextPage() {
-        // Arrange
-        createPager(
-            modifier = Modifier.fillMaxSize(),
-            snappingPage = PagerSnapDistance.atMost(3)
-        )
-        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
-
-        rule.mainClock.autoAdvance = false
-        // Act
-        // Moving less than threshold
-        val forwardDelta =
-            scrollForwardSign.toFloat() * with(rule.density) { DefaultPositionThreshold.toPx() / 2 }
-
-        var previousTargetPage = pagerState.targetPage
-
-        onPager().performTouchInput {
-            down(layoutStart)
-            moveBy(Offset(forwardDelta, forwardDelta))
-        }
-
-        // Assert
-        assertThat(pagerState.targetPage).isEqualTo(previousTargetPage)
-
-        // Reset
-        rule.mainClock.autoAdvance = true
-        onPager().performTouchInput { up() }
-        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
-
-        // Act
-        // Moving more than threshold
-        val backwardDelta = scrollForwardSign.toFloat() * with(rule.density) {
-            -DefaultPositionThreshold.toPx() / 2
-        }
-
-        previousTargetPage = pagerState.targetPage
-
-        onPager().performTouchInput {
-            down(layoutStart)
-            moveBy(Offset(backwardDelta, backwardDelta))
-        }
-
-        // Assert
-        assertThat(pagerState.targetPage).isEqualTo(previousTargetPage)
-    }
-
-    @Test
-    fun targetPage_performScroll_shouldShowNextPage() {
-        // Arrange
-        createPager(
-            modifier = Modifier.fillMaxSize(),
-            snappingPage = PagerSnapDistance.atMost(3)
-        )
-        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
-
-        rule.mainClock.autoAdvance = false
-        // Act
-        // Moving forward
-        val forwardDelta = pagerSize * 0.4f * scrollForwardSign.toFloat()
-        onPager().performTouchInput {
-            down(layoutStart)
-            moveBy(Offset(forwardDelta, forwardDelta))
-        }
-
-        // Assert
-        assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage + 1)
-        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
-
-        // Reset
-        rule.mainClock.autoAdvance = true
-        onPager().performTouchInput { up() }
-        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
-        rule.runOnIdle {
-            runBlocking { pagerState.scrollToPage(5) }
-        }
-
-        rule.mainClock.autoAdvance = false
-        // Act
-        // Moving backward
-        val backwardDelta = -pagerSize * 0.4f * scrollForwardSign.toFloat()
-        onPager().performTouchInput {
-            down(layoutEnd)
-            moveBy(Offset(backwardDelta, backwardDelta))
-        }
-
-        // Assert
-        assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage - 1)
-        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
-
-        rule.mainClock.autoAdvance = true
-        onPager().performTouchInput { up() }
-        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
-    }
-
-    @Test
-    fun targetPage_performingFling_shouldGoToPredictedPage() {
-        // Arrange
-
-        createPager(
-            modifier = Modifier.fillMaxSize(),
-            snappingPage = PagerSnapDistance.atMost(3)
-        )
-        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
-
-        rule.mainClock.autoAdvance = false
-        // Act
-        // Moving forward
-        var previousTarget = pagerState.targetPage
-        val forwardDelta = pagerSize * scrollForwardSign.toFloat()
-        onPager().performTouchInput {
-            swipeWithVelocityAcrossMainAxis(20000f, forwardDelta)
-        }
-        rule.mainClock.advanceTimeUntil { pagerState.targetPage != previousTarget }
-        var flingOriginIndex = pagerState.firstVisiblePage
-        // Assert
-        assertThat(pagerState.targetPage).isEqualTo(flingOriginIndex + 3)
-        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
-
-        rule.mainClock.autoAdvance = true
-        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
-        rule.mainClock.autoAdvance = false
-        // Act
-        // Moving backward
-        previousTarget = pagerState.targetPage
-        val backwardDelta = -pagerSize * scrollForwardSign.toFloat()
-        onPager().performTouchInput {
-            swipeWithVelocityAcrossMainAxis(20000f, backwardDelta)
-        }
-        rule.mainClock.advanceTimeUntil { pagerState.targetPage != previousTarget }
-
-        // Assert
-        flingOriginIndex = pagerState.firstVisiblePage + 1
-        assertThat(pagerState.targetPage).isEqualTo(flingOriginIndex - 3)
-        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
-
-        rule.mainClock.autoAdvance = true
-        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
-    }
-
-    @Test
-    fun targetPage_shouldReflectTargetWithAnimation() {
-        // Arrange
-
-        createPager(
-            modifier = Modifier.fillMaxSize()
-        )
-        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
-
-        rule.mainClock.autoAdvance = false
-        // Act
-        // Moving forward
-        var previousTarget = pagerState.targetPage
-        rule.runOnIdle {
-            scope.launch {
-                pagerState.animateScrollToPage(DefaultPageCount - 1)
-            }
-        }
-        rule.mainClock.advanceTimeUntil { pagerState.targetPage != previousTarget }
-
-        // Assert
-        assertThat(pagerState.targetPage).isEqualTo(DefaultPageCount - 1)
-        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
-
-        rule.mainClock.autoAdvance = true
-        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
-        rule.mainClock.autoAdvance = false
-
-        // Act
-        // Moving backward
-        previousTarget = pagerState.targetPage
-        rule.runOnIdle {
-            scope.launch {
-                pagerState.animateScrollToPage(0)
-            }
-        }
-        rule.mainClock.advanceTimeUntil { pagerState.targetPage != previousTarget }
-
-        // Assert
-        assertThat(pagerState.targetPage).isEqualTo(0)
-        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
-
-        rule.mainClock.autoAdvance = true
-        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
-    }
-
-    @Test
-    fun targetPage_valueAfterScrollingAfterMidpoint() {
-        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
-
-        var previousCurrentPage = pagerState.currentPage
-
-        val forwardDelta = (pagerSize * 0.7f) * scrollForwardSign
-        onPager().performTouchInput {
-            down(layoutStart)
-            if (vertical) {
-                moveBy(Offset(0f, forwardDelta))
-            } else {
-                moveBy(Offset(forwardDelta, 0f))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(pagerState.currentPage).isNotEqualTo(previousCurrentPage)
-            assertThat(pagerState.targetPage).isEqualTo(previousCurrentPage + 1)
-        }
-
-        onPager().performTouchInput { up() }
-
-        rule.runOnIdle {
-            previousCurrentPage = pagerState.currentPage
-        }
-
-        val backwardDelta = (pagerSize * 0.7f) * scrollForwardSign * -1
-        onPager().performTouchInput {
-            down(layoutEnd)
-            if (vertical) {
-                moveBy(Offset(0f, backwardDelta))
-            } else {
-                moveBy(Offset(backwardDelta, 0f))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(pagerState.currentPage).isNotEqualTo(previousCurrentPage)
-            assertThat(pagerState.targetPage).isEqualTo(previousCurrentPage - 1)
-        }
-
-        onPager().performTouchInput { up() }
-    }
-
-    @Test
-    fun targetPage_valueAfterScrollingForwardAndBackward() {
-        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
-
-        val startCurrentPage = pagerState.currentPage
-
-        val forwardDelta = (pagerSize * 0.8f) * scrollForwardSign
-        onPager().performTouchInput {
-            down(layoutStart)
-            if (vertical) {
-                moveBy(Offset(0f, forwardDelta))
-            } else {
-                moveBy(Offset(forwardDelta, 0f))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(pagerState.currentPage).isNotEqualTo(startCurrentPage)
-            assertThat(pagerState.targetPage).isEqualTo(startCurrentPage + 1)
-        }
-
-        val backwardDelta = (pagerSize * 0.2f) * scrollForwardSign * -1
-        onPager().performTouchInput {
-            if (vertical) {
-                moveBy(Offset(0f, backwardDelta))
-            } else {
-                moveBy(Offset(backwardDelta, 0f))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(pagerState.currentPage).isNotEqualTo(startCurrentPage)
-            assertThat(pagerState.targetPage).isEqualTo(startCurrentPage)
-        }
-
-        onPager().performTouchInput { up() }
-    }
-
-    @Test
-    fun settledPage_onAnimationScroll_shouldChangeOnScrollFinishedOnly() {
-        // Arrange
-        var settledPageChanges = 0
-        createPager(
-            modifier = Modifier.fillMaxSize(),
-            additionalContent = {
-                LaunchedEffect(key1 = pagerState.settledPage) {
-                    settledPageChanges++
-                }
-            }
-        )
-
-        // Settle page changed once for first composition
-        rule.runOnIdle {
-            assertThat(pagerState.settledPage).isEqualTo(pagerState.currentPage)
-            assertTrue { settledPageChanges == 1 }
-        }
-
-        settledPageChanges = 0
-        val previousSettled = pagerState.settledPage
-        rule.mainClock.autoAdvance = false
-        // Act
-        // Moving forward
-        rule.runOnIdle {
-            scope.launch {
-                pagerState.animateScrollToPage(DefaultPageCount - 1)
-            }
-        }
-
-        // Settled page shouldn't change whilst scroll is in progress.
-        assertTrue { pagerState.isScrollInProgress }
-        assertTrue { settledPageChanges == 0 }
-        assertThat(pagerState.settledPage).isEqualTo(previousSettled)
-
-        rule.mainClock.advanceTimeUntil { settledPageChanges != 0 }
-
-        rule.runOnIdle {
-            assertTrue { !pagerState.isScrollInProgress }
-            assertThat(pagerState.settledPage).isEqualTo(pagerState.currentPage)
-        }
-    }
-
-    @Test
-    fun settledPage_onGestureScroll_shouldChangeOnScrollFinishedOnly() {
-        // Arrange
-        var settledPageChanges = 0
-        createPager(
-            modifier = Modifier.fillMaxSize(),
-            additionalContent = {
-                LaunchedEffect(key1 = pagerState.settledPage) {
-                    settledPageChanges++
-                }
-            }
-        )
-
-        settledPageChanges = 0
-        val previousSettled = pagerState.settledPage
-        rule.mainClock.autoAdvance = false
-        // Act
-        // Moving forward
-        val forwardDelta = pagerSize / 2f * scrollForwardSign.toFloat()
-        rule.onNodeWithTag(PagerTestTag).performTouchInput {
-            swipeWithVelocityAcrossMainAxis(10000f, forwardDelta)
-        }
-
-        // Settled page shouldn't change whilst scroll is in progress.
-        assertTrue { pagerState.isScrollInProgress }
-        assertTrue { settledPageChanges == 0 }
-        assertThat(pagerState.settledPage).isEqualTo(previousSettled)
-
-        rule.mainClock.advanceTimeUntil { settledPageChanges != 0 }
-
-        rule.runOnIdle {
-            assertTrue { !pagerState.isScrollInProgress }
-            assertThat(pagerState.settledPage).isEqualTo(pagerState.currentPage)
-        }
-    }
-
-    @Test
-    fun currentPageOffset_shouldReflectScrollingOfCurrentPage() {
-        // Arrange
-        createPager(initialPage = DefaultPageCount / 2, modifier = Modifier.fillMaxSize())
-
-        // No offset initially
-        rule.runOnIdle {
-            assertThat(pagerState.currentPageOffsetFraction).isWithin(0.01f).of(0f)
-        }
-
-        // Act
-        // Moving forward
-        onPager().performTouchInput {
-            down(layoutStart)
-            if (vertical) {
-                moveBy(Offset(0f, scrollForwardSign * pagerSize / 4f))
-            } else {
-                moveBy(Offset(scrollForwardSign * pagerSize / 4f, 0f))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(pagerState.currentPageOffsetFraction).isWithin(0.1f).of(0.25f)
-        }
-
-        onPager().performTouchInput { up() }
-        rule.waitForIdle()
-
-        // Reset
-        rule.runOnIdle {
-            scope.launch {
-                pagerState.scrollToPage(DefaultPageCount / 2)
-            }
-        }
-
-        // No offset initially (again)
-        rule.runOnIdle {
-            assertThat(pagerState.currentPageOffsetFraction).isWithin(0.01f).of(0f)
-        }
-
-        // Act
-        // Moving backward
-        onPager().performTouchInput {
-            down(layoutStart)
-            if (vertical) {
-                moveBy(Offset(0f, -scrollForwardSign * pagerSize / 4f))
-            } else {
-                moveBy(Offset(-scrollForwardSign * pagerSize / 4f, 0f))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(pagerState.currentPageOffsetFraction).isWithin(0.1f).of(-0.25f)
-        }
-    }
-
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "{0}")
-        fun params() = mutableListOf<ParamConfig>().apply {
-            for (orientation in TestOrientation) {
-                for (reverseLayout in TestReverseLayout) {
-                    for (layoutDirection in TestLayoutDirection) {
-                        add(
-                            ParamConfig(
-                                orientation = orientation,
-                                reverseLayout = reverseLayout,
-                                layoutDirection = layoutDirection
-                            )
-                        )
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/FontScalingScreenshotTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/FontScalingScreenshotTest.kt
deleted file mode 100644
index 4d27892..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/FontScalingScreenshotTest.kt
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright 2021 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.text
-
-import android.os.Build
-import androidx.activity.ComponentActivity
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.GOLDEN_UI
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.testutils.assertAgainstGolden
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.drawText
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.rememberTextMeasurer
-import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.em
-import androidx.compose.ui.unit.sp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.screenshot.AndroidXScreenshotTestRule
-import androidx.testutils.AndroidFontScaleHelper
-import org.junit.After
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
-class FontScalingScreenshotTest {
-    @get:Rule
-    val rule = createAndroidComposeRule<ComponentActivity>()
-
-    @get:Rule
-    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_UI)
-
-    private val containerTag = "container"
-
-    @After
-    fun teardown() {
-        AndroidFontScaleHelper.resetSystemFontScale(rule.activityRule.scenario)
-    }
-
-    @Test
-    fun fontScaling1x_lineHeightDoubleSp() {
-        AndroidFontScaleHelper.setSystemFontScale(1f, rule.activityRule.scenario)
-        rule.waitForIdle()
-
-        rule.setContent {
-            TestLayout(lineHeight = 28.sp)
-        }
-        rule.onNodeWithTag(containerTag)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, "fontScaling1x_lineHeightDoubleSp")
-    }
-
-    @Test
-    fun fontScaling2x_lineHeightDoubleSp() {
-        AndroidFontScaleHelper.setSystemFontScale(2f, rule.activityRule.scenario)
-        rule.waitForIdle()
-
-        rule.setContent {
-            TestLayout(lineHeight = 28.sp)
-        }
-        rule.onNodeWithTag(containerTag)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, "fontScaling2x_lineHeightDoubleSp")
-    }
-
-    @Test
-    fun fontScaling1x_lineHeightDoubleEm() {
-        AndroidFontScaleHelper.setSystemFontScale(1f, rule.activityRule.scenario)
-        rule.waitForIdle()
-
-        rule.setContent {
-            TestLayout(lineHeight = 2.em)
-        }
-        rule.onNodeWithTag(containerTag)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, "fontScaling1x_lineHeightDoubleEm")
-    }
-
-    @Test
-    fun fontScaling2x_lineHeightDoubleEm() {
-        AndroidFontScaleHelper.setSystemFontScale(2f, rule.activityRule.scenario)
-        rule.waitForIdle()
-
-        rule.setContent {
-            TestLayout(lineHeight = 2.em)
-        }
-        rule.onNodeWithTag(containerTag)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, "fontScaling2x_lineHeightDoubleEm")
-    }
-
-    @Test
-    fun fontScaling1x_drawText() {
-        AndroidFontScaleHelper.setSystemFontScale(1f, rule.activityRule.scenario)
-        rule.waitForIdle()
-
-        rule.setContent {
-            TestDrawTextLayout()
-        }
-        rule.onNodeWithTag(containerTag)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, "fontScaling1x_drawText")
-    }
-
-    @Test
-    fun fontScaling2x_drawText() {
-        AndroidFontScaleHelper.setSystemFontScale(2f, rule.activityRule.scenario)
-        rule.waitForIdle()
-
-        rule.setContent {
-            TestDrawTextLayout()
-        }
-        rule.onNodeWithTag(containerTag)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, "fontScaling2x_drawText")
-    }
-
-    @Composable
-    private fun TestLayout(lineHeight: TextUnit) {
-        Column(
-            modifier = Modifier.testTag(containerTag),
-        ) {
-            BasicText(
-                text = buildAnnotatedString {
-                    append("Hello ")
-                    pushStyle(SpanStyle(
-                        fontSize = 28.sp,
-                        fontWeight = FontWeight.Bold
-                    ))
-                    append("Accessibility")
-                    pop()
-                },
-                style = TextStyle(
-                    fontSize = 36.sp,
-                    fontStyle = FontStyle.Italic,
-                    fontFamily = FontFamily.Monospace
-                )
-            )
-            BasicText(
-                text = "Here's a subtitle",
-                style = TextStyle(
-                    fontSize = 20.sp
-                )
-            )
-            BasicText(
-                text = sampleText,
-                style = TextStyle(
-                    fontSize = 14.sp,
-                    fontStyle = FontStyle.Italic,
-                    lineHeight = lineHeight
-                )
-            )
-        }
-    }
-
-    @Composable
-    private fun TestDrawTextLayout() {
-        val textMeasurer = rememberTextMeasurer()
-
-        Column(
-            modifier = Modifier.testTag(containerTag),
-        ) {
-            Canvas(Modifier.fillMaxSize()) {
-                 drawText(
-                     textMeasurer = textMeasurer,
-                     style = TextStyle(
-                        fontSize = 14.sp,
-                        lineHeight = 28.sp
-                     ),
-                     text = sampleText
-                )
-            }
-        }
-    }
-
-    companion object {
-        private val sampleText = """
-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
-et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
-aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
-cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
-culpa qui officia deserunt mollit anim id est laborum.
-
-Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
-totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
-dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
-sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro
-quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non
-numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim
-ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid
-ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse
-quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
-    """.trimIndent()
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/SelectionControllerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/SelectionControllerTest.kt
deleted file mode 100644
index 225ffb2..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/SelectionControllerTest.kt
+++ /dev/null
@@ -1,279 +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.compose.foundation.text.modifiers
-
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.text.ceilToIntPx
-import androidx.compose.foundation.text.selection.Selectable
-import androidx.compose.foundation.text.selection.Selection
-import androidx.compose.foundation.text.selection.Selection.AnchorInfo
-import androidx.compose.foundation.text.selection.SelectionAdjustment
-import androidx.compose.foundation.text.selection.SelectionLayoutBuilder
-import androidx.compose.foundation.text.selection.SelectionRegistrar
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Path
-import androidx.compose.ui.graphics.asAndroidBitmap
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.style.ResolvedTextDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import org.junit.Assert
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-class SelectionControllerTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Test
-    @SdkSuppress(minSdkVersion = 26)
-    fun drawWithClip_doesClip() {
-        val canvasSize = 10.dp
-        val pathSize = 10_000f
-        val path = Path().also {
-            it.addRect(Rect(0f, 0f, pathSize, pathSize))
-        }
-
-        val subject = SelectionController(
-            FixedSelectionFake(0, 1000, 200),
-            Color.White,
-            params = FakeParams(
-                path, true
-            )
-        )
-        var size: Size? = null
-
-        rule.setContent {
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind { drawRect(Color.Black) }) {
-                Canvas(Modifier.size(canvasSize)) {
-                    size = this.size
-                    subject.draw(this)
-                }
-            }
-        }
-
-        rule.waitForIdle()
-        assertClipped(size!!, true)
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 26)
-    fun drawWithOut_doesNotClip() {
-        val canvasSize = 10.dp
-        val pathSize = 10_000f
-        val path = Path().also {
-            it.addRect(Rect(0f, 0f, pathSize, pathSize))
-        }
-
-        val subject = SelectionController(
-            FixedSelectionFake(0, 1000, 200),
-            Color.White,
-            params = FakeParams(
-                path, false
-            )
-        )
-        var size: Size? = null
-
-        rule.setContent {
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind { drawRect(Color.Black) }) {
-                Canvas(Modifier.size(canvasSize)) {
-                    size = this.size
-                    drawRect(Color.Black)
-                    subject.draw(this)
-                }
-            }
-        }
-
-        rule.waitForIdle()
-        assertClipped(size!!, false)
-    }
-
-    @RequiresApi(Build.VERSION_CODES.O)
-    private fun assertClipped(size: Size, isClipped: Boolean) {
-        val expectedColor = if (isClipped) { Color.Black } else { Color.White }
-        rule.onRoot().captureToImage().asAndroidBitmap().apply {
-            Assert.assertEquals(
-                expectedColor.toArgb(),
-                getPixel(
-                    size.width.ceilToIntPx() + 5,
-                    size.height.ceilToIntPx() + 5
-                )
-            )
-        }
-    }
-}
-
-/**
- * Fake that always has selection
- */
-private class FixedSelectionFake(
-    val start: Int,
-    val end: Int,
-    val lastVisible: Int
-) : SelectionRegistrar {
-
-    var selectableId = 0L
-    var allSelectables = mutableListOf<Long>()
-
-    override val subselections: Map<Long, Selection>
-        get() = allSelectables.associateWith { selectionId ->
-            Selection(
-                AnchorInfo(ResolvedTextDirection.Ltr, start, selectionId),
-                AnchorInfo(ResolvedTextDirection.Ltr, end, selectionId)
-            )
-        }
-
-    override fun subscribe(selectable: Selectable): Selectable {
-        return FakeSelectableWithLastVisibleOffset(selectable.selectableId, lastVisible)
-    }
-
-    override fun unsubscribe(selectable: Selectable) {
-        // nothing
-    }
-
-    override fun nextSelectableId(): Long {
-        return selectableId++.also {
-            allSelectables.add(it)
-        }
-    }
-
-    override fun notifyPositionChange(selectableId: Long) {
-        FAKE("Not yet implemented")
-    }
-
-    override fun notifySelectionUpdateStart(
-        layoutCoordinates: LayoutCoordinates,
-        startPosition: Offset,
-        adjustment: SelectionAdjustment,
-        isInTouchMode: Boolean
-    ) {
-        FAKE("Selection not editable")
-    }
-
-    override fun notifySelectionUpdateSelectAll(selectableId: Long, isInTouchMode: Boolean) {
-        FAKE()
-    }
-
-    override fun notifySelectionUpdate(
-        layoutCoordinates: LayoutCoordinates,
-        newPosition: Offset,
-        previousPosition: Offset,
-        isStartHandle: Boolean,
-        adjustment: SelectionAdjustment,
-        isInTouchMode: Boolean
-    ): Boolean {
-        FAKE("Selection not editable")
-    }
-
-    override fun notifySelectionUpdateEnd() {
-        FAKE("Selection not editable")
-    }
-
-    override fun notifySelectableChange(selectableId: Long) {
-        FAKE("Selection not editable")
-    }
-}
-
-private class FakeSelectableWithLastVisibleOffset(
-    override val selectableId: Long,
-    private val lastVisible: Int
-) : Selectable {
-    override fun appendSelectableInfoToBuilder(builder: SelectionLayoutBuilder) {
-        FAKE()
-    }
-
-    override fun getSelectAllSelection(): Selection? {
-        FAKE()
-    }
-
-    override fun getHandlePosition(selection: Selection, isStartHandle: Boolean): Offset {
-        FAKE()
-    }
-
-    override fun getLayoutCoordinates(): LayoutCoordinates? {
-        FAKE()
-    }
-
-    override fun getText(): AnnotatedString {
-        FAKE()
-    }
-
-    override fun getBoundingBox(offset: Int): Rect {
-        FAKE()
-    }
-
-    override fun getLineLeft(offset: Int): Float {
-        FAKE()
-    }
-
-    override fun getLineRight(offset: Int): Float {
-        FAKE()
-    }
-
-    override fun getCenterYForOffset(offset: Int): Float {
-        FAKE()
-    }
-
-    override fun getRangeOfLineContaining(offset: Int): TextRange {
-        FAKE()
-    }
-
-    override fun getLastVisibleOffset(): Int {
-        return lastVisible
-    }
-}
-
-private class FakeParams(
-    val path: Path,
-    override val shouldClip: Boolean
-) : StaticTextSelectionParams(null, null) {
-
-    override fun getPathForRange(start: Int, end: Int): Path? {
-        return path
-    }
-}
-
-private fun FAKE(reason: String = "Unsupported fake method on fake"): Nothing =
-    throw NotImplementedError("No support in fake: $reason")
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicSecureTextFieldTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicSecureTextFieldTest.kt
deleted file mode 100644
index 0def91c..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicSecureTextFieldTest.kt
+++ /dev/null
@@ -1,677 +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.compose.foundation.text2
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.text.selection.FakeTextToolbar
-import androidx.compose.foundation.text.selection.fetchTextLayoutResult
-import androidx.compose.foundation.text2.input.TextFieldState
-import androidx.compose.foundation.text2.input.TextObfuscationMode
-import androidx.compose.foundation.text2.input.internal.selection.FakeClipboardManager
-import androidx.compose.foundation.text2.input.rememberTextFieldState
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalClipboardManager
-import androidx.compose.ui.platform.LocalTextToolbar
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsActions
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.semantics.getOrNull
-import androidx.compose.ui.test.ExperimentalTestApi
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.assert
-import androidx.compose.ui.test.assertTextEquals
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performSemanticsAction
-import androidx.compose.ui.test.performTextInput
-import androidx.compose.ui.test.performTextInputSelection
-import androidx.compose.ui.test.performTextReplacement
-import androidx.compose.ui.test.requestFocus
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class BasicSecureTextFieldTest {
-
-    // Keyboard shortcut tests for BasicSecureTextField are in TextFieldKeyEventTest
-
-    @get:Rule
-    val rule = createComposeRule().apply {
-        mainClock.autoAdvance = false
-    }
-
-    @get:Rule
-    val inputMethodInterceptor = InputMethodInterceptorRule(rule)
-
-    private val Tag = "BasicSecureTextField"
-
-    @Test
-    fun passwordSemanticsAreSet() {
-        rule.setContent {
-            BasicSecureTextField(
-                state = remember {
-                    TextFieldState("Hello", initialSelectionInChars = TextRange(0, 1))
-                },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        rule.onNodeWithTag(Tag).requestFocus()
-        rule.waitForIdle()
-        rule.onNodeWithTag(Tag).assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Password))
-        rule.onNodeWithTag(Tag).assert(SemanticsMatcher.keyIsDefined(SemanticsActions.PasteText))
-        // temporarily define copy and cut actions on BasicSecureTextField but make them no-op
-        rule.onNodeWithTag(Tag).assert(SemanticsMatcher.keyIsDefined(SemanticsActions.CopyText))
-        rule.onNodeWithTag(Tag).assert(SemanticsMatcher.keyIsDefined(SemanticsActions.CutText))
-    }
-
-    @Test
-    fun lastTypedCharacterIsRevealedTemporarily() {
-        rule.setContent {
-            BasicSecureTextField(
-                state = rememberTextFieldState(),
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        with(rule.onNodeWithTag(Tag)) {
-            performTextInput("a")
-            rule.mainClock.advanceTimeBy(200)
-            assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("a")
-            rule.mainClock.advanceTimeBy(1500)
-            assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("\u2022")
-        }
-    }
-
-    @Test
-    fun lastTypedCharacterIsRevealed_hidesAfterAnotherCharacterIsTyped() {
-        rule.setContent {
-            BasicSecureTextField(
-                state = rememberTextFieldState(),
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        with(rule.onNodeWithTag(Tag)) {
-            performTextInput("a")
-            rule.mainClock.advanceTimeBy(200)
-            assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("a")
-            performTextInput("b")
-            rule.mainClock.advanceTimeBy(50)
-            assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("\u2022b")
-        }
-    }
-
-    @OptIn(ExperimentalTestApi::class)
-    @Test
-    fun lastTypedCharacterIsRevealed_whenInsertedInMiddle() {
-        rule.setContent {
-            BasicSecureTextField(
-                state = rememberTextFieldState(),
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        with(rule.onNodeWithTag(Tag)) {
-            performTextInput("abc")
-            rule.mainClock.advanceTimeBy(200)
-            assertThat(fetchTextLayoutResult().layoutInput.text.text)
-                .isEqualTo("\u2022\u2022\u2022")
-            performTextInputSelection(TextRange(1))
-            performTextInput("d")
-            rule.mainClock.advanceTimeBy(50)
-            assertThat(fetchTextLayoutResult().layoutInput.text.text)
-                .isEqualTo("\u2022d\u2022\u2022")
-        }
-    }
-
-    @Test
-    fun lastTypedCharacterIsRevealed_hidesAfterFocusIsLost() {
-        rule.setContent {
-            Column {
-                BasicSecureTextField(
-                    state = rememberTextFieldState(),
-                    modifier = Modifier.testTag(Tag)
-                )
-                Box(
-                    modifier = Modifier
-                        .size(1.dp)
-                        .testTag("otherFocusable")
-                        .focusable()
-                )
-            }
-        }
-
-        with(rule.onNodeWithTag(Tag)) {
-            performTextInput("a")
-            rule.mainClock.advanceTimeBy(200)
-            assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("a")
-            rule.onNodeWithTag("otherFocusable")
-                .requestFocus()
-            rule.mainClock.advanceTimeBy(50)
-            assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("\u2022")
-        }
-    }
-
-    @Test
-    fun lastTypedCharacterIsRevealed_hidesAfterAnotherCharacterRemoved() {
-        rule.setContent {
-            BasicSecureTextField(
-                state = rememberTextFieldState(),
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        with(rule.onNodeWithTag(Tag)) {
-            performTextInput("abc")
-            rule.mainClock.advanceTimeBy(200)
-            performTextInput("d")
-            rule.mainClock.advanceTimeBy(50)
-            assertThat(fetchTextLayoutResult().layoutInput.text.text)
-                .isEqualTo("\u2022\u2022\u2022d")
-            performTextReplacement("bcd")
-            assertThat(fetchTextLayoutResult().layoutInput.text.text)
-                .isEqualTo("\u2022\u2022\u2022")
-        }
-    }
-
-    @Test
-    fun obfuscationMethodVisible_doesNotHideAnything() {
-        rule.setContent {
-            BasicSecureTextField(
-                state = rememberTextFieldState(),
-                textObfuscationMode = TextObfuscationMode.Visible,
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        with(rule.onNodeWithTag(Tag)) {
-            performTextInput("abc")
-            rule.mainClock.advanceTimeBy(200)
-            assertThat(fetchTextLayoutResult().layoutInput.text.text)
-                .isEqualTo("abc")
-            rule.mainClock.advanceTimeBy(1500)
-            assertThat(fetchTextLayoutResult().layoutInput.text.text)
-                .isEqualTo("abc")
-        }
-    }
-
-    @Test
-    fun obfuscationMethodVisible_revealsEverythingWhenSwitchedTo() {
-        var obfuscationMode by mutableStateOf(TextObfuscationMode.Hidden)
-        rule.setContent {
-            BasicSecureTextField(
-                state = rememberTextFieldState(),
-                textObfuscationMode = obfuscationMode,
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        with(rule.onNodeWithTag(Tag)) {
-            performTextInput("abc")
-            rule.mainClock.advanceTimeBy(200)
-            assertThat(fetchTextLayoutResult().layoutInput.text.text)
-                .isEqualTo("\u2022\u2022\u2022")
-            obfuscationMode = TextObfuscationMode.Visible
-            rule.mainClock.advanceTimeByFrame()
-            assertThat(fetchTextLayoutResult().layoutInput.text.text)
-                .isEqualTo("abc")
-        }
-    }
-
-    @Test
-    fun obfuscationMethodHidden_hidesEverything() {
-        rule.setContent {
-            BasicSecureTextField(
-                state = rememberTextFieldState(),
-                textObfuscationMode = TextObfuscationMode.Hidden,
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        with(rule.onNodeWithTag(Tag)) {
-            performTextInput("abc")
-            rule.mainClock.advanceTimeByFrame()
-            assertThat(fetchTextLayoutResult().layoutInput.text.text)
-                .isEqualTo("\u2022\u2022\u2022")
-            performTextInput("d")
-            rule.mainClock.advanceTimeByFrame()
-            assertThat(fetchTextLayoutResult().layoutInput.text.text)
-                .isEqualTo("\u2022\u2022\u2022\u2022")
-        }
-    }
-
-    @Test
-    fun obfuscationMethodHidden_hidesEverythingWhenSwitchedTo() {
-        var obfuscationMode by mutableStateOf(TextObfuscationMode.Visible)
-        rule.setContent {
-            BasicSecureTextField(
-                state = rememberTextFieldState(),
-                textObfuscationMode = obfuscationMode,
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        with(rule.onNodeWithTag(Tag)) {
-            performTextInput("abc")
-            rule.mainClock.advanceTimeByFrame()
-            assertThat(fetchTextLayoutResult().layoutInput.text.text)
-                .isEqualTo("abc")
-            obfuscationMode = TextObfuscationMode.Hidden
-            rule.mainClock.advanceTimeByFrame()
-            assertThat(fetchTextLayoutResult().layoutInput.text.text)
-                .isEqualTo("\u2022\u2022\u2022")
-        }
-    }
-
-    @OptIn(ExperimentalTestApi::class)
-    @Test
-    fun semantics_copy() {
-        val state = TextFieldState("Hello World!")
-        val clipboardManager = FakeClipboardManager("initial")
-        rule.setContent {
-            CompositionLocalProvider(LocalClipboardManager provides clipboardManager) {
-                BasicSecureTextField(
-                    state = state,
-                    modifier = Modifier.testTag(Tag)
-                )
-            }
-        }
-
-        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0, 5))
-        rule.onNodeWithTag(Tag).performSemanticsAction(SemanticsActions.CopyText)
-
-        rule.runOnIdle {
-            assertThat(clipboardManager.getText()?.toString()).isEqualTo("initial")
-        }
-    }
-
-    @OptIn(ExperimentalTestApi::class)
-    @Test
-    fun semantics_cut() {
-        val state = TextFieldState("Hello World!")
-        val clipboardManager = FakeClipboardManager("initial")
-        rule.setContent {
-            CompositionLocalProvider(LocalClipboardManager provides clipboardManager) {
-                BasicSecureTextField(
-                    state = state,
-                    modifier = Modifier.testTag(Tag)
-                )
-            }
-        }
-
-        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0, 5))
-        rule.onNodeWithTag(Tag).performSemanticsAction(SemanticsActions.CutText)
-
-        rule.runOnIdle {
-            assertThat(clipboardManager.getText()?.toString()).isEqualTo("initial")
-            assertThat(state.text.toString()).isEqualTo("Hello World!")
-        }
-    }
-
-    @OptIn(ExperimentalTestApi::class)
-    @Test
-    fun toolbarDoesNotShowCopyOrCut() {
-        var copyOptionAvailable = false
-        var cutOptionAvailable = false
-        var showMenuRequested = false
-        val textToolbar = FakeTextToolbar(
-            onShowMenu = { _, onCopyRequested, _, onCutRequested, _ ->
-                showMenuRequested = true
-                copyOptionAvailable = onCopyRequested != null
-                cutOptionAvailable = onCutRequested != null
-            },
-            onHideMenu = {}
-        )
-        val state = TextFieldState("Hello")
-        rule.setContent {
-            CompositionLocalProvider(LocalTextToolbar provides textToolbar) {
-                BasicSecureTextField(
-                    state = state,
-                    modifier = Modifier.testTag(Tag)
-                )
-            }
-        }
-
-        rule.onNodeWithTag(Tag).requestFocus()
-        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0, 5))
-
-        rule.runOnIdle {
-            assertThat(showMenuRequested).isTrue()
-            assertThat(copyOptionAvailable).isFalse()
-            assertThat(cutOptionAvailable).isFalse()
-        }
-    }
-
-    @Test
-    fun stringValue_updatesFieldText_whenTextChangedFromCode_whileUnfocused() {
-        var text by mutableStateOf("hello")
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = { text = it },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        rule.runOnIdle {
-            text = "world"
-        }
-        // Auto-advance is disabled.
-        rule.mainClock.advanceTimeByFrame()
-
-        assertThat(
-            rule.onNodeWithTag(Tag).fetchSemanticsNode().config[SemanticsProperties.EditableText]
-                .text
-        ).isEqualTo("world")
-    }
-
-    @Test
-    fun textFieldValue_updatesFieldText_whenTextChangedFromCode_whileUnfocused() {
-        var text by mutableStateOf(TextFieldValue("hello"))
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = { text = it },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        rule.runOnIdle {
-            text = text.copy(text = "world")
-        }
-        // Auto-advance is disabled.
-        rule.mainClock.advanceTimeByFrame()
-
-        assertThat(
-            rule.onNodeWithTag(Tag).fetchSemanticsNode().config[SemanticsProperties.EditableText]
-                .text
-        ).isEqualTo("world")
-    }
-
-    @Test
-    fun textFieldValue_updatesFieldSelection_whenSelectionChangedFromCode_whileUnfocused() {
-        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(1)))
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = { text = it },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        rule.runOnIdle {
-            text = text.copy(selection = TextRange(2))
-        }
-        // Auto-advance is disabled.
-        rule.mainClock.advanceTimeByFrame()
-
-        assertTextSelection(TextRange(2))
-    }
-
-    @Test
-    fun stringValue_doesNotUpdateField_whenTextChangedFromCode_whileFocused() {
-        var text by mutableStateOf("hello")
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = { text = it },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-        requestFocus(Tag)
-
-        rule.runOnIdle {
-            text = "world"
-        }
-
-        rule.onNodeWithTag(Tag).assertTextEquals("hello")
-    }
-
-    @Test
-    fun textFieldValue_doesNotUpdateField_whenTextChangedFromCode_whileFocused() {
-        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(1)))
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = { text = it },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-        requestFocus(Tag)
-
-        rule.runOnIdle {
-            text = TextFieldValue(text = "world", selection = TextRange(2))
-        }
-
-        rule.onNodeWithTag(Tag).assertTextEquals("hello")
-    }
-
-    @Test
-    fun stringValue_doesNotInvokeCallback_onFocus() {
-        var text by mutableStateOf("")
-        var onValueChangedCount = 0
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = {
-                    text = it
-                    onValueChangedCount++
-                },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-        assertThat(onValueChangedCount).isEqualTo(0)
-
-        requestFocus(Tag)
-
-        rule.runOnIdle {
-            assertThat(onValueChangedCount).isEqualTo(0)
-        }
-    }
-
-    @Test
-    fun stringValue_doesNotInvokeCallback_whenOnlySelectionChanged() {
-        var text by mutableStateOf("")
-        var onValueChangedCount = 0
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = {
-                    text = it
-                    onValueChangedCount++
-                },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-        requestFocus(Tag)
-        assertThat(onValueChangedCount).isEqualTo(0)
-
-        // Act: wiggle the cursor around a bit.
-        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0))
-        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(5))
-
-        rule.runOnIdle {
-            assertThat(onValueChangedCount).isEqualTo(0)
-        }
-    }
-
-    @Test
-    fun stringValue_doesNotInvokeCallback_whenOnlyCompositionChanged() {
-        var text by mutableStateOf("")
-        var onValueChangedCount = 0
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = {
-                    text = it
-                    onValueChangedCount++
-                },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-        requestFocus(Tag)
-        assertThat(onValueChangedCount).isEqualTo(0)
-
-        // Act: wiggle the composition around a bit
-        inputMethodInterceptor.withInputConnection { setComposingRegion(0, 0) }
-        inputMethodInterceptor.withInputConnection { setComposingRegion(3, 5) }
-
-        rule.runOnIdle {
-            assertThat(onValueChangedCount).isEqualTo(0)
-        }
-    }
-
-    @Test
-    fun stringValue_doesNotInvokeCallback_whenTextChangedFromCode_whileUnfocused() {
-        var text by mutableStateOf("")
-        var onValueChangedCount = 0
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = {
-                    text = it
-                    onValueChangedCount++
-                },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-        assertThat(onValueChangedCount).isEqualTo(0)
-
-        rule.runOnIdle {
-            text = "hello"
-        }
-
-        rule.runOnIdle {
-            assertThat(onValueChangedCount).isEqualTo(0)
-        }
-    }
-
-    @Test
-    fun stringValue_doesNotInvokeCallback_whenTextChangedFromCode_whileFocused() {
-        var text by mutableStateOf("")
-        var onValueChangedCount = 0
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = {
-                    text = it
-                    onValueChangedCount++
-                },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-        assertThat(onValueChangedCount).isEqualTo(0)
-        requestFocus(Tag)
-
-        rule.runOnIdle {
-            text = "hello"
-        }
-
-        rule.runOnIdle {
-            assertThat(onValueChangedCount).isEqualTo(0)
-        }
-    }
-
-    @Test
-    fun textFieldValue_usesInitialSelectionFromValue() {
-        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(2)))
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = { text = it },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        assertTextSelection(TextRange(2))
-    }
-
-    @Test
-    fun textFieldValue_reportsSelectionChangesInCallback() {
-        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(1)))
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = { text = it },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(2))
-
-        rule.runOnIdle {
-            assertThat(text.selection).isEqualTo(TextRange(2))
-        }
-    }
-
-    @Test
-    fun textFieldValue_reportsCompositionChangesInCallback() {
-        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(1)))
-        rule.setContent {
-            BasicSecureTextField(
-                value = text,
-                onValueChange = { text = it },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-        requestFocus(Tag)
-
-        inputMethodInterceptor.withInputConnection { setComposingRegion(0, 0) }
-        rule.runOnIdle {
-            assertWithMessage(
-                "After setting composing region to 0, 0, TextFieldState's composition is:"
-            ).that(text.composition).isNull()
-        }
-
-        inputMethodInterceptor.withInputConnection { setComposingRegion(1, 4) }
-        rule.runOnIdle {
-            assertWithMessage(
-                "After setting composing region to 1, 4, TextFieldState's composition is:"
-            ).that(text.composition).isEqualTo(TextRange(1, 4))
-        }
-    }
-
-    private fun requestFocus(tag: String) =
-        rule.onNodeWithTag(tag).requestFocus()
-
-    private fun assertTextSelection(expected: TextRange) {
-        val selection = rule.onNodeWithTag(Tag).fetchSemanticsNode()
-            .config.getOrNull(SemanticsProperties.TextSelectionRange)
-        assertWithMessage("Expected selection to be $expected")
-            .that(selection).isEqualTo(expected)
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2ImmIntegrationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2ImmIntegrationTest.kt
deleted file mode 100644
index 59017e0..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2ImmIntegrationTest.kt
+++ /dev/null
@@ -1,382 +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.compose.foundation.text2
-
-import android.view.KeyEvent
-import android.view.inputmethod.ExtractedText
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text2.input.TextFieldState
-import androidx.compose.foundation.text2.input.internal.ComposeInputMethodManager
-import androidx.compose.foundation.text2.input.placeCursorAtEnd
-import androidx.compose.foundation.text2.input.selectAll
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performKeyInput
-import androidx.compose.ui.test.pressKey
-import androidx.compose.ui.test.requestFocus
-import androidx.compose.ui.text.TextRange
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(
-    ExperimentalFoundationApi::class,
-    ExperimentalTestApi::class,
-)
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-internal class BasicTextField2ImmIntegrationTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @get:Rule
-    val immRule = ComposeInputMethodManagerTestRule()
-
-    @get:Rule
-    val inputMethodInterceptor = InputMethodInterceptorRule(rule)
-
-    private val Tag = "BasicTextField2"
-    private val imm = FakeInputMethodManager()
-
-    @Before
-    fun setUp() {
-        immRule.setFactory { imm }
-    }
-
-    @Test
-    fun becomesTextEditor_whenFocusGained() {
-        val state = TextFieldState()
-        rule.setContent {
-            BasicTextField2(state, Modifier.testTag(Tag))
-        }
-
-        requestFocus(Tag)
-
-        inputMethodInterceptor.withInputConnection {
-            commitText("hello", 0)
-            assertThat(state.text.toString()).isEqualTo("hello")
-        }
-    }
-
-    @Test
-    fun stopsBeingTextEditor_whenFocusLost() {
-        val state = TextFieldState()
-        var focusManager: FocusManager? = null
-        rule.setContent {
-            focusManager = LocalFocusManager.current
-            BasicTextField2(state, Modifier.testTag(Tag))
-        }
-        requestFocus(Tag)
-        rule.runOnIdle {
-            focusManager!!.clearFocus()
-        }
-        inputMethodInterceptor.assertNoSessionActive()
-    }
-
-    @Test
-    fun stopsBeingTextEditor_whenChangedToReadOnly() {
-        val state = TextFieldState()
-        var readOnly by mutableStateOf(false)
-        rule.setContent {
-            BasicTextField2(state, Modifier.testTag(Tag), readOnly = readOnly)
-        }
-        requestFocus(Tag)
-        inputMethodInterceptor.assertSessionActive()
-
-        readOnly = true
-
-        inputMethodInterceptor.assertNoSessionActive()
-    }
-
-    @Test
-    fun stopsBeingTextEditor_whenChangedToDisabled() {
-        val state = TextFieldState()
-        var enabled by mutableStateOf(true)
-        rule.setContent {
-            BasicTextField2(state, Modifier.testTag(Tag), enabled = enabled)
-        }
-        requestFocus(Tag)
-        inputMethodInterceptor.assertSessionActive()
-
-        enabled = false
-
-        inputMethodInterceptor.assertNoSessionActive()
-    }
-
-    @Test
-    fun staysTextEditor_whenFocusTransferred() {
-        val state1 = TextFieldState()
-        val state2 = TextFieldState()
-        rule.setContent {
-            BasicTextField2(state1, Modifier.testTag(Tag + 1))
-            BasicTextField2(state2, Modifier.testTag(Tag + 2))
-        }
-
-        requestFocus(Tag + 1)
-        requestFocus(Tag + 2)
-
-        inputMethodInterceptor.withInputConnection {
-            commitText("hello", 0)
-            endBatchEdit()
-            assertThat(state2.text.toString()).isEqualTo("hello")
-            assertThat(state1.text.toString()).isEmpty()
-        }
-    }
-
-    @Test
-    fun stopsBeingTextEditor_whenRemovedFromCompositionWhileFocused() {
-        val state = TextFieldState()
-        var compose by mutableStateOf(true)
-        rule.setContent {
-            if (compose) {
-                BasicTextField2(state, Modifier.testTag(Tag))
-            }
-        }
-        requestFocus(Tag)
-        rule.runOnIdle {
-            compose = false
-        }
-
-        inputMethodInterceptor.assertNoSessionActive()
-    }
-
-    @Test
-    fun inputRestarted_whenStateInstanceChanged() {
-        val state1 = TextFieldState()
-        val state2 = TextFieldState()
-        var state by mutableStateOf(state1)
-        rule.setContent {
-            BasicTextField2(state, Modifier.testTag(Tag))
-        }
-        requestFocus(Tag)
-
-        state = state2
-
-        inputMethodInterceptor.withInputConnection {
-            commitText("hello", 0)
-            assertThat(state2.text.toString()).isEqualTo("hello")
-            assertThat(state1.text.toString()).isEmpty()
-        }
-    }
-
-    @Test
-    fun immUpdated_whenFilterChangesText_fromInputConnection() {
-        val state = TextFieldState()
-        rule.setContent {
-            BasicTextField2(
-                state = state,
-                modifier = Modifier.testTag(Tag),
-                inputTransformation = { _, new ->
-                    // Force the selection not to change.
-                    val initialSelection = new.selectionInChars
-                    new.append("world")
-                    new.selectCharsIn(initialSelection)
-                }
-            )
-        }
-        requestFocus(Tag)
-        inputMethodInterceptor.withInputConnection {
-            // TODO move this before withInputConnection?
-            imm.resetCalls()
-
-            commitText("hello", 1)
-
-            assertThat(state.text.toString()).isEqualTo("helloworld")
-        }
-
-        rule.runOnIdle {
-            imm.expectCall("restartInput")
-            imm.expectNoMoreCalls()
-        }
-    }
-
-    @Test
-    fun immUpdated_whenFilterChangesText_fromKeyEvent() {
-        val state = TextFieldState()
-        rule.setContent {
-            BasicTextField2(
-                state = state,
-                modifier = Modifier.testTag(Tag),
-                inputTransformation = { _, new ->
-                    val initialSelection = new.selectionInChars
-                    new.append("world")
-                    new.selectCharsIn(initialSelection)
-                }
-            )
-        }
-        requestFocus(Tag)
-        rule.runOnIdle { imm.resetCalls() }
-
-        rule.onNodeWithTag(Tag).performKeyInput { pressKey(Key.A) }
-
-        rule.runOnIdle {
-            imm.expectCall("restartInput")
-            imm.expectNoMoreCalls()
-        }
-    }
-
-    @Test
-    fun immUpdated_whenFilterChangesSelection_fromInputConnection() {
-        val state = TextFieldState()
-        rule.setContent {
-            BasicTextField2(
-                state = state,
-                modifier = Modifier.testTag(Tag),
-                inputTransformation = { _, new -> new.selectAll() }
-            )
-        }
-        requestFocus(Tag)
-        inputMethodInterceptor.withInputConnection {
-            imm.resetCalls()
-            setComposingText("hello", 1)
-        }
-
-        rule.runOnIdle {
-            imm.expectCall("updateSelection(0, 5, 0, 5)")
-            imm.expectNoMoreCalls()
-        }
-    }
-
-    @Test
-    fun immUpdated_whenEditChangesText() {
-        val state = TextFieldState()
-        rule.setContent {
-            BasicTextField2(state, Modifier.testTag(Tag))
-        }
-        requestFocus(Tag)
-        rule.runOnIdle {
-            imm.resetCalls()
-
-            state.edit {
-                append("hello")
-                placeCursorBeforeCharAt(0)
-            }
-        }
-
-        rule.runOnIdle {
-            imm.expectCall("restartInput")
-            imm.expectNoMoreCalls()
-        }
-    }
-
-    @Test
-    fun immUpdated_whenEditChangesSelection() {
-        val state = TextFieldState("hello", initialSelectionInChars = TextRange(0))
-        rule.setContent {
-            BasicTextField2(state, Modifier.testTag(Tag))
-        }
-        requestFocus(Tag)
-        rule.runOnIdle {
-            imm.resetCalls()
-
-            state.edit {
-                placeCursorAtEnd()
-            }
-        }
-
-        rule.runOnIdle {
-            imm.expectCall("updateSelection(5, 5, -1, -1)")
-            imm.expectNoMoreCalls()
-        }
-    }
-
-    @Test
-    fun immUpdated_whenEditChangesTextAndSelection() {
-        val state = TextFieldState()
-        rule.setContent {
-            BasicTextField2(state, Modifier.testTag(Tag))
-        }
-        requestFocus(Tag)
-        rule.runOnIdle {
-            imm.resetCalls()
-
-            state.edit {
-                append("hello")
-                placeCursorAtEnd()
-            }
-        }
-
-        rule.runOnIdle {
-            imm.expectCall("updateSelection(5, 5, -1, -1)")
-            imm.expectCall("restartInput")
-            imm.expectNoMoreCalls()
-        }
-    }
-
-    private fun requestFocus(tag: String) =
-        rule.onNodeWithTag(tag).requestFocus()
-
-    private class FakeInputMethodManager : ComposeInputMethodManager {
-        private val calls = mutableListOf<String>()
-
-        fun expectCall(description: String) {
-            assertThat(calls.removeFirst()).isEqualTo(description)
-        }
-
-        fun expectNoMoreCalls() {
-            assertThat(calls).isEmpty()
-        }
-
-        fun resetCalls() {
-            calls.clear()
-        }
-
-        override fun restartInput() {
-            calls += "restartInput"
-        }
-
-        override fun showSoftInput() {
-            calls += "showSoftInput"
-        }
-
-        override fun hideSoftInput() {
-            calls += "hideSoftInput"
-        }
-
-        override fun updateExtractedText(token: Int, extractedText: ExtractedText) {
-            calls += "updateExtractedText"
-        }
-
-        override fun updateSelection(
-            selectionStart: Int,
-            selectionEnd: Int,
-            compositionStart: Int,
-            compositionEnd: Int
-        ) {
-            calls += "updateSelection($selectionStart, $selectionEnd, " +
-                "$compositionStart, $compositionEnd)"
-        }
-
-        override fun sendKeyEvent(event: KeyEvent) {
-            calls += "sendKeyEvent"
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldCodepointTransformationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldCodepointTransformationTest.kt
deleted file mode 100644
index 5988872..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldCodepointTransformationTest.kt
+++ /dev/null
@@ -1,218 +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.compose.foundation.text2
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text.selection.fetchTextLayoutResult
-import androidx.compose.foundation.text2.input.CodepointTransformation
-import androidx.compose.foundation.text2.input.TextFieldLineLimits
-import androidx.compose.foundation.text2.input.TextFieldState
-import androidx.compose.foundation.text2.input.mask
-import androidx.compose.foundation.text2.input.setTextAndPlaceCursorAtEnd
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performTextInput
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalFoundationApi::class)
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class TextFieldCodepointTransformationTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    private val Tag = "BasicTextField2"
-
-    @Test
-    fun textField_rendersTheResultOf_codepointTransformation() {
-        val state = TextFieldState()
-        state.setTextAndPlaceCursorAtEnd("Hello")
-        rule.setContent {
-            BasicTextField2(
-                state = state,
-                codepointTransformation = { _, codepoint -> codepoint + 1 },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        assertLayoutText("Ifmmp") // one character after in lexical order
-    }
-
-    @Test
-    fun textField_rendersTheResultOf_codepointTransformation_codepointIndex() {
-        val state = TextFieldState()
-        state.setTextAndPlaceCursorAtEnd("Hello")
-        rule.setContent {
-            BasicTextField2(
-                state = state,
-                codepointTransformation = { index, codepoint ->
-                    if (index % 2 == 0) codepoint + 1 else codepoint - 1
-                },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        assertLayoutText("Idmkp") // one character after and before in lexical order
-    }
-
-    @Test
-    fun textField_toggleCodepointTransformation_affectsNextFrame() {
-        rule.mainClock.autoAdvance = false
-        val state = TextFieldState()
-        state.setTextAndPlaceCursorAtEnd("Hello")
-        var codepointTransformation: CodepointTransformation? by mutableStateOf(null)
-        rule.setContent {
-            BasicTextField2(
-                state = state,
-                codepointTransformation = codepointTransformation,
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        assertLayoutText("Hello") // no change
-        codepointTransformation = CodepointTransformation.mask('c')
-
-        rule.mainClock.advanceTimeByFrame()
-        assertLayoutText("ccccc") // all characters turn to c
-    }
-
-    @Test
-    fun textField_statefulCodepointTransformation_reactsToStateChange() {
-        val state = TextFieldState()
-        state.setTextAndPlaceCursorAtEnd("Hello")
-        var mask by mutableStateOf('-')
-        rule.setContent {
-            BasicTextField2(
-                state = state,
-                codepointTransformation = CodepointTransformation.mask(mask),
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        assertLayoutText("-----")
-        mask = '@'
-
-        rule.waitForIdle()
-        assertLayoutText("@@@@@")
-    }
-
-    @Test
-    fun textField_removingCodepointTransformation_rendersTextNormally() {
-        val state = TextFieldState()
-        state.setTextAndPlaceCursorAtEnd("Hello")
-        var codepointTransformation by mutableStateOf<CodepointTransformation?>(
-            CodepointTransformation.mask('*')
-        )
-        rule.setContent {
-            BasicTextField2(
-                state = state,
-                codepointTransformation = codepointTransformation,
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        assertLayoutText("*****")
-        codepointTransformation = null
-
-        rule.waitForIdle()
-        assertLayoutText("Hello")
-    }
-
-    @Test
-    fun textField_codepointTransformation_continuesToRenderUpdatedText() {
-        val state = TextFieldState()
-        state.setTextAndPlaceCursorAtEnd("Hello")
-        rule.setContent {
-            BasicTextField2(
-                state = state,
-                codepointTransformation = CodepointTransformation.mask('*'),
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        assertLayoutText("*****")
-        rule.waitForIdle()
-        rule.onNodeWithTag(Tag).performTextInput(", World!")
-        assertLayoutText("*".repeat("Hello, World!".length))
-    }
-
-    @Test
-    fun textField_singleLine_removesLineFeedViaCodepointTransformation() {
-        val state = TextFieldState()
-        state.setTextAndPlaceCursorAtEnd("Hello\nWorld")
-        rule.setContent {
-            BasicTextField2(
-                state = state,
-                lineLimits = TextFieldLineLimits.SingleLine,
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        assertLayoutText("Hello World")
-        rule.onNodeWithTag(Tag).performTextInput("\n")
-        assertLayoutText("Hello World ")
-    }
-
-    @Test
-    fun textField_singleLine_removesCarriageReturnViaCodepointTransformation() {
-        val state = TextFieldState()
-        state.setTextAndPlaceCursorAtEnd("Hello\rWorld")
-        rule.setContent {
-            BasicTextField2(
-                state = state,
-                lineLimits = TextFieldLineLimits.SingleLine,
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        assertLayoutText("Hello\uFEFFWorld")
-    }
-
-    @Test
-    fun textField_singleLine_doesNotOverrideGivenCodepointTransformation() {
-        val state = TextFieldState()
-        state.setTextAndPlaceCursorAtEnd("Hello\nWorld")
-        rule.setContent {
-            BasicTextField2(
-                state = state,
-                lineLimits = TextFieldLineLimits.SingleLine,
-                codepointTransformation = { _, codepoint -> codepoint },
-                modifier = Modifier.testTag(Tag)
-            )
-        }
-
-        assertLayoutText("Hello\nWorld")
-    }
-
-    // TODO: add more tests when selection is added
-
-    private fun assertLayoutText(text: String) {
-        assertThat(rule.onNodeWithTag(Tag).fetchTextLayoutResult().layoutInput.text.text)
-            .isEqualTo(text)
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldKeyEventTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldKeyEventTest.kt
deleted file mode 100644
index 9af189a..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldKeyEventTest.kt
+++ /dev/null
@@ -1,722 +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.compose.foundation.text2
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.text.TEST_FONT_FAMILY
-import androidx.compose.foundation.text2.input.TextFieldLineLimits.MultiLine
-import androidx.compose.foundation.text2.input.TextFieldLineLimits.SingleLine
-import androidx.compose.foundation.text2.input.TextFieldState
-import androidx.compose.foundation.text2.input.internal.selection.FakeClipboardManager
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.platform.ClipboardManager
-import androidx.compose.ui.platform.LocalClipboardManager
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
-import androidx.compose.ui.test.KeyInjectionScope
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performKeyInput
-import androidx.compose.ui.test.pressKey
-import androidx.compose.ui.test.withKeyDown
-import androidx.compose.ui.test.withKeysDown
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-@OptIn(
-    ExperimentalFoundationApi::class,
-    ExperimentalTestApi::class
-)
-class TextFieldKeyEventTest {
-    @get:Rule
-    val rule = createComposeRule()
-
-    private val tag = "TextFieldTestTag"
-
-    private var defaultDensity = Density(1f)
-
-    @Test
-    fun textField_typedEvents() {
-        keysSequenceTest {
-            pressKey(Key.H)
-            press(Key.ShiftLeft + Key.I)
-            expectedText("hI")
-        }
-    }
-
-    @Test
-    fun textField_copyPaste() {
-        keysSequenceTest("hello") {
-            withKeyDown(Key.CtrlLeft) {
-                pressKey(Key.A)
-                pressKey(Key.C)
-            }
-            pressKey(Key.DirectionRight)
-            pressKey(Key.Spacebar)
-            press(Key.CtrlLeft + Key.V)
-            expectedText("hello hello")
-        }
-    }
-
-    @Test
-    fun secureTextField_doesNotAllowCopy() {
-        keysSequenceTest("hello", secure = true) {
-            clipboardManager.setText(AnnotatedString("world"))
-            withKeyDown(Key.CtrlLeft) {
-                pressKey(Key.A)
-                pressKey(Key.C)
-            }
-            pressKey(Key.Copy) // also attempt direct copy
-            expectedClipboardText("world")
-        }
-    }
-
-    @Test
-    fun textField_directCopyPaste() {
-        keysSequenceTest("hello") {
-            press(Key.CtrlLeft + Key.A)
-            pressKey(Key.Copy)
-            expectedText("hello")
-            pressKey(Key.DirectionRight)
-            pressKey(Key.Spacebar)
-            pressKey(Key.Paste)
-            expectedText("hello hello")
-        }
-    }
-
-    @Test
-    fun textField_directCutPaste() {
-        keysSequenceTest("hello") {
-            press(Key.CtrlLeft + Key.A)
-            pressKey(Key.Cut)
-            expectedText("")
-            pressKey(Key.Paste)
-            expectedText("hello")
-        }
-    }
-
-    @Test
-    fun secureTextField_doesNotAllowCut() {
-        keysSequenceTest("hello", secure = true) {
-            clipboardManager.setText(AnnotatedString("world"))
-            withKeyDown(Key.CtrlLeft) {
-                pressKey(Key.A)
-                pressKey(Key.X)
-            }
-            pressKey(Key.Cut) // Also attempts direct cut
-            expectedText("hello")
-            expectedClipboardText("world")
-        }
-    }
-
-    @Test
-    fun textField_linesNavigation() {
-        keysSequenceTest("hello\nworld") {
-            pressKey(Key.DirectionDown)
-            pressKey(Key.A)
-            pressKey(Key.DirectionUp)
-            pressKey(Key.A)
-            expectedText("haello\naworld")
-            pressKey(Key.DirectionUp)
-            pressKey(Key.A)
-            expectedText("ahaello\naworld")
-        }
-    }
-
-    @Test
-    fun textField_linesNavigation_cache() {
-        keysSequenceTest("hello\n\nworld") {
-            pressKey(Key.DirectionRight)
-            pressKey(Key.DirectionDown)
-            pressKey(Key.DirectionDown)
-            pressKey(Key.Zero)
-            expectedText("hello\n\nw0orld")
-        }
-    }
-
-    @Test
-    fun textField_newLine() {
-        keysSequenceTest("hello") {
-            pressKey(Key.Enter)
-            expectedText("\nhello")
-        }
-    }
-
-    @Test
-    fun textField_backspace() {
-        keysSequenceTest("hello") {
-            pressKey(Key.DirectionRight)
-            pressKey(Key.DirectionRight)
-            pressKey(Key.Backspace)
-            expectedText("hllo")
-        }
-    }
-
-    @Test
-    fun textField_delete() {
-        keysSequenceTest("hello") {
-            pressKey(Key.Delete)
-            expectedText("ello")
-        }
-    }
-
-    @Test
-    fun textField_delete_atEnd() {
-        keysSequenceTest("hello", TextRange(5)) {
-            pressKey(Key.Delete)
-            expectedText("hello")
-        }
-    }
-
-    @Test
-    fun textField_delete_whenEmpty() {
-        keysSequenceTest {
-            pressKey(Key.Delete)
-            expectedText("")
-        }
-    }
-
-    @Test
-    fun textField_nextWord() {
-        keysSequenceTest("hello world") {
-            press(Key.CtrlLeft + Key.DirectionRight)
-            pressKey(Key.Zero)
-            expectedText("hello0 world")
-            press(Key.CtrlLeft + Key.DirectionRight)
-            pressKey(Key.Zero)
-            expectedText("hello0 world0")
-        }
-    }
-
-    @Test
-    fun textField_nextWord_doubleSpace() {
-        keysSequenceTest("hello  world") {
-            press(Key.CtrlLeft + Key.DirectionRight)
-            pressKey(Key.DirectionRight)
-            press(Key.CtrlLeft + Key.DirectionRight)
-            pressKey(Key.Zero)
-            expectedText("hello  world0")
-        }
-    }
-
-    @Test
-    fun textField_prevWord() {
-        keysSequenceTest("hello world") {
-            withKeyDown(Key.CtrlLeft) {
-                pressKey(Key.DirectionRight)
-                pressKey(Key.DirectionRight)
-                pressKey(Key.DirectionLeft)
-            }
-            pressKey(Key.Zero)
-            expectedText("hello 0world")
-        }
-    }
-
-    @Test
-    fun textField_HomeAndEnd() {
-        keysSequenceTest("hello world") {
-            pressKey(Key.MoveEnd)
-            pressKey(Key.Zero)
-            pressKey(Key.MoveHome)
-            pressKey(Key.Zero)
-            expectedText("0hello world0")
-        }
-    }
-
-    @Test
-    fun textField_byWordSelection() {
-        keysSequenceTest("hello  world\nhi") {
-            withKeysDown(listOf(Key.ShiftLeft, Key.CtrlLeft)) {
-                pressKey(Key.DirectionRight)
-                expectedSelection(TextRange(0, 5))
-                pressKey(Key.DirectionRight)
-                expectedSelection(TextRange(0, 12))
-                pressKey(Key.DirectionRight)
-                expectedSelection(TextRange(0, 15))
-                pressKey(Key.DirectionLeft)
-                expectedSelection(TextRange(0, 13))
-            }
-        }
-    }
-
-    @Test
-    fun textField_lineEndStart() {
-        keysSequenceTest(initText = "hi\nhello world\nhi") {
-            pressKey(Key.MoveEnd)
-            pressKey(Key.DirectionRight)
-            pressKey(Key.Zero)
-            expectedText("hi\n0hello world\nhi")
-            pressKey(Key.MoveEnd)
-            pressKey(Key.Zero)
-            expectedText("hi\n0hello world0\nhi")
-            withKeyDown(Key.ShiftLeft) { pressKey(Key.MoveHome) }
-            expectedSelection(TextRange(16, 3))
-            pressKey(Key.MoveHome)
-            pressKey(Key.DirectionRight)
-            withKeyDown(Key.ShiftLeft) { pressKey(Key.MoveEnd) }
-            expectedSelection(TextRange(4, 16))
-            expectedText("hi\n0hello world0\nhi")
-        }
-    }
-
-    @Test
-    fun textField_altLineLeftRight() {
-        keysSequenceTest(initText = "hi\nhello world\nhi") {
-            withKeyDown(Key.AltLeft) { pressKey(Key.DirectionRight) }
-            pressKey(Key.DirectionRight)
-            pressKey(Key.Zero)
-            expectedText("hi\n0hello world\nhi")
-            withKeyDown(Key.AltLeft) { pressKey(Key.DirectionRight) }
-            pressKey(Key.Zero)
-            expectedText("hi\n0hello world0\nhi")
-            withKeysDown(listOf(Key.ShiftLeft, Key.AltLeft)) { pressKey(Key.DirectionLeft) }
-            expectedSelection(TextRange(16, 3))
-            withKeyDown(Key.AltLeft) { pressKey(Key.DirectionLeft) }
-            pressKey(Key.DirectionRight)
-            withKeysDown(listOf(Key.ShiftLeft, Key.AltLeft)) { pressKey(Key.DirectionRight) }
-            expectedSelection(TextRange(4, 16))
-            expectedText("hi\n0hello world0\nhi")
-        }
-    }
-
-    @Test
-    fun textField_altTop() {
-        keysSequenceTest(initText = "hi\nhello world\nhi") {
-            pressKey(Key.MoveEnd)
-            repeat(3) { pressKey(Key.DirectionRight) }
-            pressKey(Key.Zero)
-            expectedText("hi\nhe0llo world\nhi")
-            withKeyDown(Key.AltLeft) { pressKey(Key.DirectionUp) }
-            pressKey(Key.Zero)
-            expectedText("0hi\nhe0llo world\nhi")
-            pressKey(Key.MoveEnd)
-            repeat(3) { pressKey(Key.DirectionRight) }
-            withKeysDown(listOf(Key.ShiftLeft, Key.AltLeft)) { pressKey(Key.DirectionUp) }
-            expectedSelection(TextRange(6, 0))
-            expectedText("0hi\nhe0llo world\nhi")
-        }
-    }
-
-    @Test
-    fun textField_altBottom() {
-        keysSequenceTest(initText = "hi\nhello world\nhi") {
-            pressKey(Key.MoveEnd)
-            repeat(3) { pressKey(Key.DirectionRight) }
-            pressKey(Key.Zero)
-            expectedText("hi\nhe0llo world\nhi")
-            withKeysDown(listOf(Key.ShiftLeft, Key.AltLeft)) { pressKey(Key.DirectionDown) }
-            expectedSelection(TextRange(6, 18))
-            pressKey(Key.DirectionLeft)
-            pressKey(Key.Zero)
-            expectedText("hi\nhe00llo world\nhi")
-            withKeyDown(Key.AltLeft) { pressKey(Key.DirectionDown) }
-            pressKey(Key.Zero)
-            expectedText("hi\nhe00llo world\nhi0")
-        }
-    }
-
-    @Test
-    fun textField_deleteWords() {
-        keysSequenceTest("hello world\nhi world") {
-            pressKey(Key.MoveEnd)
-            withKeyDown(Key.CtrlLeft) {
-                pressKey(Key.Backspace)
-                expectedText("hello \nhi world")
-                pressKey(Key.Delete)
-            }
-            expectedText("hello  world")
-        }
-    }
-
-    @Test
-    fun textField_deleteToBeginningOfLine() {
-        keysSequenceTest("hello world\nhi world") {
-            press(Key.CtrlLeft + Key.DirectionRight)
-
-            withKeyDown(Key.AltLeft) {
-                pressKey(Key.Backspace)
-                expectedText(" world\nhi world")
-                pressKey(Key.Backspace)
-                expectedText(" world\nhi world")
-            }
-
-            repeat(3) { pressKey(Key.DirectionRight) }
-
-            press(Key.AltLeft + Key.Backspace)
-            expectedText("rld\nhi world")
-            pressKey(Key.DirectionDown)
-            pressKey(Key.MoveEnd)
-
-            withKeyDown(Key.AltLeft) {
-                pressKey(Key.Backspace)
-                expectedText("rld\n")
-                pressKey(Key.Backspace)
-                expectedText("rld\n")
-            }
-        }
-    }
-
-    @Test
-    fun textField_deleteToEndOfLine() {
-        keysSequenceTest("hello world\nhi world") {
-            press(Key.CtrlLeft + Key.DirectionRight)
-            withKeyDown(Key.AltLeft) {
-                pressKey(Key.Delete)
-                expectedText("hello\nhi world")
-                pressKey(Key.Delete)
-                expectedText("hello\nhi world")
-            }
-
-            repeat(3) { pressKey(Key.DirectionRight) }
-
-            press(Key.AltLeft + Key.Delete)
-            expectedText("hello\nhi")
-
-            pressKey(Key.MoveHome)
-            withKeyDown(Key.AltLeft) {
-                pressKey(Key.Delete)
-                expectedText("hello\n")
-                pressKey(Key.Delete)
-                expectedText("hello\n")
-            }
-        }
-    }
-
-    @Test
-    fun textField_paragraphNavigation() {
-        keysSequenceTest("hello world\nhi") {
-            press(Key.CtrlLeft + Key.DirectionDown)
-            pressKey(Key.Zero)
-            expectedText("hello world0\nhi")
-            withKeyDown(Key.CtrlLeft) {
-                pressKey(Key.DirectionDown)
-                pressKey(Key.DirectionUp)
-            }
-            pressKey(Key.Zero)
-            expectedText("hello world0\n0hi")
-            withKeyDown(Key.CtrlLeft) {
-                pressKey(Key.DirectionUp)
-                pressKey(Key.DirectionUp)
-            }
-            pressKey(Key.Zero)
-            expectedText("0hello world0\n0hi")
-        }
-    }
-
-    @Test
-    fun textField_selectionCaret() {
-        keysSequenceTest("hello world") {
-            press(Key.CtrlLeft + Key.ShiftLeft + Key.DirectionRight)
-            expectedSelection(TextRange(0, 5))
-            press(Key.ShiftLeft + Key.DirectionRight)
-            expectedSelection(TextRange(0, 6))
-            press(Key.CtrlLeft + Key.Backslash)
-            expectedSelection(TextRange(6, 6))
-            press(Key.CtrlLeft + Key.ShiftLeft + Key.DirectionLeft)
-            expectedSelection(TextRange(6, 0))
-            press(Key.ShiftLeft + Key.DirectionRight)
-            expectedSelection(TextRange(6, 1))
-        }
-    }
-
-    @Test
-    fun textField_pageNavigationDown() {
-        keysSequenceTest(
-            initText = "A\nB\nC\nD\nE",
-            modifier = Modifier.requiredSize(73.dp)
-        ) {
-            pressKey(Key.PageDown)
-            expectedSelection(TextRange(4))
-        }
-    }
-
-    @Test
-    fun textField_pageNavigationDown_exactFit() {
-        keysSequenceTest(
-            initText = "A\nB\nC\nD\nE",
-            modifier = Modifier.requiredSize(90.dp) // exactly 3 lines fit
-        ) {
-            pressKey(Key.PageDown)
-            expectedSelection(TextRange(6))
-        }
-    }
-
-    @Test
-    fun textField_pageNavigationUp() {
-        keysSequenceTest(
-            initText = "A\nB\nC\nD\nE",
-            initSelection = TextRange(8), // just before 5
-            modifier = Modifier.requiredSize(73.dp)
-        ) {
-            pressKey(Key.PageUp)
-            expectedSelection(TextRange(4))
-        }
-    }
-
-    @Test
-    fun textField_pageNavigationUp_exactFit() {
-        keysSequenceTest(
-            initText = "A\nB\nC\nD\nE",
-            initSelection = TextRange(8), // just before 5
-            modifier = Modifier.requiredSize(90.dp) // exactly 3 lines fit
-        ) {
-            pressKey(Key.PageUp)
-            expectedSelection(TextRange(2))
-        }
-    }
-
-    @Test
-    fun textField_pageNavigationUp_cantGoUp() {
-        keysSequenceTest(
-            initText = "1\n2\n3\n4\n5",
-            initSelection = TextRange(0),
-            modifier = Modifier.requiredSize(90.dp)
-        ) {
-            pressKey(Key.PageUp)
-            expectedSelection(TextRange(0))
-        }
-    }
-
-    @Test
-    fun textField_tabSingleLine() {
-        keysSequenceTest("text", singleLine = true) {
-            pressKey(Key.Tab)
-            expectedText("text") // no change, should try focus change instead
-        }
-    }
-
-    @Test
-    fun textField_tabMultiLine() {
-        keysSequenceTest("text") {
-            pressKey(Key.Tab)
-            expectedText("\ttext")
-        }
-    }
-
-    @Test
-    fun textField_shiftTabSingleLine() {
-        keysSequenceTest("text", singleLine = true) {
-            press(Key.ShiftLeft + Key.Tab)
-            expectedText("text") // no change, should try focus change instead
-        }
-    }
-
-    @Test
-    fun textField_enterSingleLine() {
-        keysSequenceTest("text", singleLine = true) {
-            pressKey(Key.Enter)
-            expectedText("text") // no change, should do ime action instead
-        }
-    }
-
-    @Test
-    fun textField_enterMultiLine() {
-        keysSequenceTest("text") {
-            pressKey(Key.Enter)
-            expectedText("\ntext")
-        }
-    }
-
-    @Test
-    fun textField_withActiveSelection_tabSingleLine() {
-        keysSequenceTest("text", singleLine = true) {
-            pressKey(Key.DirectionRight)
-            withKeyDown(Key.ShiftLeft) {
-                pressKey(Key.DirectionRight)
-                pressKey(Key.DirectionRight)
-            }
-            pressKey(Key.Tab)
-            expectedText("text") // no change, should try focus change instead
-        }
-    }
-
-    @Test
-    fun textField_withActiveSelection_tabMultiLine() {
-        keysSequenceTest("text") {
-            pressKey(Key.DirectionRight)
-            withKeyDown(Key.ShiftLeft) {
-                pressKey(Key.DirectionRight)
-                pressKey(Key.DirectionRight)
-            }
-            pressKey(Key.Tab)
-            expectedText("t\tt")
-        }
-    }
-
-    @Test
-    fun textField_selectToLeft() {
-        keysSequenceTest("hello world hello") {
-            pressKey(Key.MoveEnd)
-            expectedSelection(TextRange(17))
-            withKeyDown(Key.ShiftLeft) {
-                pressKey(Key.DirectionLeft)
-                pressKey(Key.DirectionLeft)
-                pressKey(Key.DirectionLeft)
-            }
-            expectedSelection(TextRange(17, 14))
-        }
-    }
-
-    @Test
-    fun textField_withActiveSelection_shiftTabSingleLine() {
-        keysSequenceTest("text", singleLine = true) {
-            pressKey(Key.DirectionRight)
-            withKeyDown(Key.ShiftLeft) {
-                pressKey(Key.DirectionRight)
-                pressKey(Key.DirectionRight)
-                pressKey(Key.Tab)
-            }
-            expectedText("text") // no change, should try focus change instead
-        }
-    }
-
-    @Test
-    fun textField_withActiveSelection_enterSingleLine() {
-        keysSequenceTest("text", singleLine = true) {
-            pressKey(Key.DirectionRight)
-            withKeyDown(Key.ShiftLeft) {
-                pressKey(Key.DirectionRight)
-                pressKey(Key.DirectionRight)
-            }
-            pressKey(Key.Enter)
-            expectedText("text") // no change, should do ime action instead
-        }
-    }
-
-    @Test
-    fun textField_withActiveSelection_enterMultiLine() {
-        keysSequenceTest("text") {
-            pressKey(Key.DirectionRight)
-            withKeyDown(Key.ShiftLeft) {
-                pressKey(Key.DirectionRight)
-                pressKey(Key.DirectionRight)
-            }
-            pressKey(Key.Enter)
-            expectedText("t\nt")
-        }
-    }
-
-    private inner class SequenceScope(
-        val state: TextFieldState,
-        val clipboardManager: ClipboardManager,
-        private val keyInjectionScope: KeyInjectionScope
-    ) : KeyInjectionScope by keyInjectionScope {
-
-        fun press(keys: List<Key>) {
-            require(keys.isNotEmpty()) { "At least one key must be specified for press action" }
-            if (keys.size == 1) {
-                pressKey(keys.first())
-            } else {
-                withKeysDown(keys.dropLast(1)) { pressKey(keys.last()) }
-            }
-        }
-
-        infix operator fun Key.plus(other: Key): MutableList<Key> {
-            return mutableListOf(this, other)
-        }
-
-        fun expectedText(text: String) {
-            rule.runOnIdle {
-                assertThat(state.text.toString()).isEqualTo(text)
-            }
-        }
-
-        fun expectedSelection(selection: TextRange) {
-            rule.runOnIdle {
-                assertThat(state.text.selectionInChars).isEqualTo(selection)
-            }
-        }
-
-        fun expectedClipboardText(text: String) {
-            rule.runOnIdle {
-                assertThat(clipboardManager.getText()?.text).isEqualTo(text)
-            }
-        }
-    }
-
-    private fun keysSequenceTest(
-        initText: String = "",
-        initSelection: TextRange = TextRange.Zero,
-        modifier: Modifier = Modifier.fillMaxSize(),
-        singleLine: Boolean = false,
-        secure: Boolean = false,
-        sequence: SequenceScope.() -> Unit,
-    ) {
-        val state = TextFieldState(initText, initSelection)
-        val focusRequester = FocusRequester()
-        val clipboardManager = FakeClipboardManager("InitialTestText")
-        rule.setContent {
-            CompositionLocalProvider(
-                LocalDensity provides defaultDensity,
-                LocalClipboardManager provides clipboardManager,
-            ) {
-                if (!secure) {
-                    BasicTextField2(
-                        state = state,
-                        textStyle = TextStyle(
-                            fontFamily = TEST_FONT_FAMILY,
-                            fontSize = 30.sp
-                        ),
-                        modifier = modifier
-                            .focusRequester(focusRequester)
-                            .testTag(tag),
-                        lineLimits = if (singleLine) SingleLine else MultiLine(),
-                    )
-                } else {
-                    BasicSecureTextField(
-                        state = state,
-                        textStyle = TextStyle(
-                            fontFamily = TEST_FONT_FAMILY,
-                            fontSize = 30.sp
-                        ),
-                        modifier = modifier
-                            .focusRequester(focusRequester)
-                            .testTag(tag)
-                    )
-                }
-            }
-        }
-
-        rule.runOnIdle { focusRequester.requestFocus() }
-
-        rule.waitForIdle()
-        rule.mainClock.advanceTimeBy(1000)
-
-        rule.onNodeWithTag(tag).performKeyInput {
-            sequence(SequenceScope(state, clipboardManager, this@performKeyInput))
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSessionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSessionTest.kt
deleted file mode 100644
index fc82f19..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSessionTest.kt
+++ /dev/null
@@ -1,240 +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.compose.foundation.text2.input.internal
-
-import android.text.InputType
-import android.view.View
-import android.view.inputmethod.EditorInfo
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.text2.input.TextFieldState
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.platform.PlatformTextInputModifierNode
-import androidx.compose.ui.platform.PlatformTextInputSession
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.platform.textInputSession
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.requestFocus
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.text.input.ImeOptions
-import androidx.compose.ui.text.input.KeyboardCapitalization
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.assertFalse
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalFoundationApi::class)
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class AndroidTextInputSessionTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    private lateinit var coroutineScope: CoroutineScope
-    private lateinit var hostView: View
-    private lateinit var textInputNode: PlatformTextInputModifierNode
-
-    @Before
-    fun setup() {
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            hostView = LocalView.current
-            Box(
-                modifier = Modifier
-                    .size(1.dp)
-                    .testTag("tag")
-                    .then(TestTextElement())
-                    .focusable()
-            )
-        }
-        rule.onNodeWithTag("tag").requestFocus()
-        rule.waitForIdle()
-    }
-
-    @Test
-    fun createInputConnection_modifiesEditorInfo() {
-        val state = TextFieldState("hello", initialSelectionInChars = TextRange(0, 5))
-        launchInputSessionWithDefaultsForTest(state)
-        val editorInfo = EditorInfo()
-
-        rule.runOnUiThread {
-            hostView.onCreateInputConnection(editorInfo)
-        }
-
-        assertThat(editorInfo.initialSelStart).isEqualTo(0)
-        assertThat(editorInfo.initialSelEnd).isEqualTo(5)
-        assertThat(editorInfo.inputType).isEqualTo(
-            InputType.TYPE_CLASS_TEXT or
-                InputType.TYPE_TEXT_FLAG_MULTI_LINE or
-                InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
-        )
-        assertThat(editorInfo.imeOptions).isEqualTo(
-            EditorInfo.IME_FLAG_NO_FULLSCREEN or
-                EditorInfo.IME_FLAG_NO_ENTER_ACTION
-        )
-    }
-
-    @Test
-    fun inputConnection_sendsUpdates_toActiveSession() {
-        val state1 = TextFieldState()
-        val state2 = TextFieldState()
-        launchInputSessionWithDefaultsForTest(state1)
-
-        rule.runOnIdle {
-            hostView.onCreateInputConnection(EditorInfo())
-                .commitText("hello", 1)
-
-            assertThat(state1.text.toString()).isEqualTo("hello")
-            assertThat(state2.text.toString()).isEqualTo("")
-        }
-
-        launchInputSessionWithDefaultsForTest(state2)
-
-        rule.runOnIdle {
-            hostView.onCreateInputConnection(EditorInfo())
-                .commitText("world", 1)
-
-            assertThat(state1.text.toString()).isEqualTo("hello")
-            assertThat(state2.text.toString()).isEqualTo("world")
-        }
-    }
-
-    @Test
-    fun inputConnection_sendsEditorAction_toActiveSession() {
-        var imeActionFromOne: ImeAction? = null
-        var imeActionFromTwo: ImeAction? = null
-
-        launchInputSessionWithDefaultsForTest(
-            imeOptions = ImeOptions(imeAction = ImeAction.Done),
-            onImeAction = { imeActionFromOne = it }
-        )
-
-        rule.runOnIdle {
-            hostView.onCreateInputConnection(EditorInfo())
-                .performEditorAction(EditorInfo.IME_ACTION_DONE)
-
-            assertThat(imeActionFromOne).isEqualTo(ImeAction.Done)
-            assertThat(imeActionFromTwo).isNull()
-        }
-
-        launchInputSessionWithDefaultsForTest(
-            imeOptions = ImeOptions(imeAction = ImeAction.Go),
-            onImeAction = { imeActionFromTwo = it }
-        )
-
-        rule.runOnIdle {
-            hostView.onCreateInputConnection(EditorInfo())
-                .performEditorAction(EditorInfo.IME_ACTION_GO)
-
-            assertThat(imeActionFromOne).isEqualTo(ImeAction.Done)
-            assertThat(imeActionFromTwo).isEqualTo(ImeAction.Go)
-        }
-    }
-
-    @Test
-    fun createInputConnection_updatesEditorInfo() {
-        launchInputSessionWithDefaultsForTest(
-            imeOptions = ImeOptions(
-                singleLine = true,
-                keyboardType = KeyboardType.Email,
-                autoCorrect = false,
-                imeAction = ImeAction.Search,
-                capitalization = KeyboardCapitalization.Words
-            )
-        )
-        val editorInfo = EditorInfo()
-
-        rule.runOnIdle {
-            hostView.onCreateInputConnection(editorInfo)
-        }
-
-        assertThat(editorInfo.inputType).isEqualTo(
-            InputType.TYPE_CLASS_TEXT or
-                InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS or
-                InputType.TYPE_TEXT_FLAG_CAP_WORDS
-        )
-        assertThat(editorInfo.imeOptions).isEqualTo(
-            EditorInfo.IME_ACTION_SEARCH or EditorInfo.IME_FLAG_NO_FULLSCREEN
-        )
-    }
-
-    @Test
-    fun debugMode_isDisabled() {
-        // run this in presubmit to check that we are not accidentally enabling logs on prod
-        assertFalse(
-            TIA_DEBUG,
-            "Oops, looks like you accidentally enabled logging. Don't worry, we've all " +
-                "been there. Just remember to turn it off before you deploy your code."
-        )
-    }
-
-    private fun launchInputSessionWithDefaultsForTest(
-        state: TextFieldState = TextFieldState(),
-        imeOptions: ImeOptions = ImeOptions.Default,
-        onImeAction: (ImeAction) -> Unit = {}
-    ) {
-        coroutineScope.launch {
-            textInputNode.textInputSession {
-                inputSessionWithDefaultsForTest(
-                    state,
-                    imeOptions,
-                    onImeAction
-                )
-            }
-        }
-    }
-
-    private suspend fun PlatformTextInputSession.inputSessionWithDefaultsForTest(
-        state: TextFieldState = TextFieldState(),
-        imeOptions: ImeOptions = ImeOptions.Default,
-        onImeAction: (ImeAction) -> Unit = {}
-    ): Nothing = platformSpecificTextInputSession(
-        state = state,
-        imeOptions = imeOptions,
-        filter = null,
-        onImeAction = onImeAction
-    )
-
-    private inner class TestTextElement : ModifierNodeElement<TestTextNode>() {
-        override fun create(): TestTextNode = TestTextNode()
-        override fun update(node: TestTextNode) {}
-        override fun hashCode(): Int = 0
-        override fun equals(other: Any?): Boolean = other is TestTextElement
-    }
-
-    private inner class TestTextNode : Modifier.Node(), PlatformTextInputModifierNode {
-        override fun onAttach() {
-            textInputNode = this
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldLayoutStateCacheTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldLayoutStateCacheTest.kt
deleted file mode 100644
index 68e4000..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldLayoutStateCacheTest.kt
+++ /dev/null
@@ -1,741 +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.compose.foundation.text2.input.internal
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text2.input.CodepointTransformation
-import androidx.compose.foundation.text2.input.TextFieldState
-import androidx.compose.foundation.text2.input.setTextAndPlaceCursorAtEnd
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.ObserverHandle
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.runtime.snapshots.SnapshotStateObserver
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.createFontFamilyResolver
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.sp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.test.platform.app.InstrumentationRegistry
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlin.test.assertNotNull
-import org.junit.After
-import org.junit.Before
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalFoundationApi::class)
-@RunWith(AndroidJUnit4::class)
-@MediumTest
-class TextFieldLayoutStateCacheTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    private var textFieldState = TextFieldState()
-    private var codepointTransformation: CodepointTransformation? = null
-    private var textStyle = TextStyle()
-    private var singleLine = false
-    private var softWrap = false
-    private var cache = TextFieldLayoutStateCache()
-    private var density = Density(1f, 1f)
-    private var layoutDirection = LayoutDirection.Ltr
-    private var fontFamilyResolver =
-        createFontFamilyResolver(InstrumentationRegistry.getInstrumentation().context)
-    private var constraints = Constraints()
-
-    private lateinit var globalWriteObserverHandle: ObserverHandle
-
-    @Before
-    fun setUp() {
-        globalWriteObserverHandle = Snapshot.registerGlobalWriteObserver {
-            Snapshot.sendApplyNotifications()
-        }
-    }
-
-    @After
-    fun tearDown() {
-        globalWriteObserverHandle.dispose()
-    }
-
-    @Test
-    fun updateAllInputs_doesntInvalidateSnapshot_whenNothingChanged() {
-        assertInvalidationsOnChange(0) {
-            updateNonMeasureInputs()
-            updateMeasureInputs()
-        }
-    }
-
-    @Test
-    fun updateNonMeasureInputs_invalidatesSnapshot_whenTextContentChanged() {
-        textFieldState.edit {
-            replace(0, length, "")
-            placeCursorBeforeCharAt(0)
-        }
-        assertInvalidationsOnChange(1) {
-            textFieldState.edit {
-                append("hello")
-                placeCursorBeforeCharAt(0)
-            }
-        }
-    }
-
-    @Test
-    fun updateNonMeasureInputs_invalidatesSnapshot_whenTextSelectionChanged() {
-        textFieldState.edit {
-            append("hello")
-            placeCursorBeforeCharAt(0)
-        }
-        assertInvalidationsOnChange(1) {
-            textFieldState.edit {
-                placeCursorBeforeCharAt(1)
-            }
-        }
-    }
-
-    @Test
-    fun updateNonMeasureInputs_invalidatesSnapshot_whenCodepointTransformationChanged() {
-        assertInvalidationsOnChange(1) {
-            codepointTransformation = CodepointTransformation { _, codepoint -> codepoint }
-            updateNonMeasureInputs()
-        }
-    }
-
-    @Test
-    fun updateNonMeasureInputs_invalidatesSnapshot_whenStyleLayoutAffectingAttrsChanged() {
-        textStyle = TextStyle(fontSize = 12.sp)
-        assertInvalidationsOnChange(1) {
-            textStyle = TextStyle(fontSize = 23.sp)
-            updateNonMeasureInputs()
-        }
-    }
-
-    @Test
-    fun updateNonMeasureInputs_doesntInvalidateSnapshot_whenStyleDrawAffectingAttrsChanged() {
-        textStyle = TextStyle(color = Color.Black)
-        assertInvalidationsOnChange(0) {
-            textStyle = TextStyle(color = Color.Blue)
-            updateNonMeasureInputs()
-        }
-    }
-
-    @Test
-    fun updateNonMeasureInputs_invalidatesSnapshot_whenSingleLineChanged() {
-        assertInvalidationsOnChange(1) {
-            singleLine = !singleLine
-            updateNonMeasureInputs()
-        }
-    }
-
-    @Test
-    fun updateNonMeasureInputs_invalidatesSnapshot_whenSoftWrapChanged() {
-        assertInvalidationsOnChange(1) {
-            softWrap = !softWrap
-            updateNonMeasureInputs()
-        }
-    }
-
-    @Test
-    fun updateMeasureInputs_invalidatesSnapshot_whenDensityInstanceChangedWithDifferentValues() {
-        density = Density(1f, 1f)
-        assertInvalidationsOnChange(1) {
-            density = Density(1f, 2f)
-            updateMeasureInputs()
-        }
-    }
-
-    @Test
-    fun updateMeasureInputs_doesntInvalidateSnapshot_whenDensityInstanceChangedWithSameValues() {
-        density = Density(1f, 1f)
-        assertInvalidationsOnChange(0) {
-            density = Density(1f, 1f)
-            updateMeasureInputs()
-        }
-    }
-
-    @Test
-    fun updateMeasureInputs_invalidatesSnapshot_whenDensityValueChangedWithSameInstance() {
-        var densityValue = 1f
-        density = object : Density {
-            override val density: Float
-                get() = densityValue
-            override val fontScale: Float = 1f
-        }
-        assertInvalidationsOnChange(1) {
-            densityValue = 2f
-            updateMeasureInputs()
-        }
-    }
-
-    @Test
-    fun updateMeasureInputs_invalidatesSnapshot_whenFontScaleChangedWithSameInstance() {
-        var fontScale = 1f
-        density = object : Density {
-            override val density: Float = 1f
-            override val fontScale: Float
-                get() = fontScale
-        }
-        assertInvalidationsOnChange(1) {
-            fontScale = 2f
-            updateMeasureInputs()
-        }
-    }
-
-    @Test
-    fun updateMeasureInputs_invalidatesSnapshot_whenLayoutDirectionChanged() {
-        layoutDirection = LayoutDirection.Ltr
-        assertInvalidationsOnChange(1) {
-            layoutDirection = LayoutDirection.Rtl
-            updateMeasureInputs()
-        }
-    }
-
-    @Test
-    fun updateMeasureInputs_invalidatesSnapshot_whenFontFamilyResolverChanged() {
-        assertInvalidationsOnChange(1) {
-            fontFamilyResolver =
-                createFontFamilyResolver(InstrumentationRegistry.getInstrumentation().context)
-            updateMeasureInputs()
-        }
-    }
-
-    @Ignore("b/294443266: figure out how to make fonts stale for test")
-    @Test
-    fun updateMeasureInputs_invalidatesSnapshot_whenFontFamilyResolverFontChanged() {
-        fontFamilyResolver =
-            createFontFamilyResolver(InstrumentationRegistry.getInstrumentation().context)
-        assertInvalidationsOnChange(1) {
-            TODO("b/294443266: make fonts stale")
-        }
-    }
-
-    @Test
-    fun updateMeasureInputs_invalidatesSnapshot_whenConstraintsChanged() {
-        constraints = Constraints.fixed(5, 5)
-        assertInvalidationsOnChange(1) {
-            constraints = Constraints.fixed(6, 5)
-            updateMeasureInputs()
-        }
-    }
-
-    /**
-     * A scope that reads the layout cache and has a full cache hit – that is, all the inputs match
-     * – will never run the CodepointTransformation, and thus never register a "read" of any of the
-     * state objects the transformation function happens to read. If those inputs change, all scopes
-     * should be invalidated, even the ones that never actually ran the transformation function.
-     *
-     * The first time we compute layout with a transformation function, we invoke the transformation
-     * function. This function is provided externally and may perform zero or more state reads. The
-     * first time the layout is computed, those state reads will be seen by whatever snapshot
-     * observer is observing the layout call, and when they change, that reader will be invalidated.
-     * However, if somewhere else some different code asks for the layout, and none of the inputs
-     * have changed, it will return the cached value without ever running the transformation
-     * function. This means that when states read by the transformation change, that second reader
-     * won't be invalidated since it never observed those reads.
-     *
-     * To fix this, we manually record reads done by the transformation function and re-read them
-     * explicitly when checking for a full cache hit.
-     */
-    @Test
-    fun invalidatesAllReaders_whenTransformationDependenciesChanged_producingSameVisualText() {
-        var transformationState by mutableStateOf(1)
-        var transformationInvocations = 0
-        codepointTransformation = CodepointTransformation { _, codepoint ->
-            transformationInvocations++
-            @Suppress("UNUSED_EXPRESSION")
-            transformationState
-            codepoint + 1
-        }
-        // Transformation isn't applied if there's no text. Keep this at 1 char to make the math
-        // simpler.
-        textFieldState.setTextAndPlaceCursorAtEnd("h")
-        val expectedVisualText = "i"
-
-        fun assertVisualText() {
-            assertThat(cache.value?.layoutInput?.text?.text).isEqualTo(expectedVisualText)
-        }
-
-        updateNonMeasureInputs()
-        updateMeasureInputs()
-        var primaryInvalidations = 0
-        var secondaryInvalidations = 0
-
-        val primaryObserver = SnapshotStateObserver(onChangedExecutor = { it() })
-        val secondaryObserver = SnapshotStateObserver(onChangedExecutor = { it() })
-        try {
-            primaryObserver.start()
-            secondaryObserver.start()
-
-            // This will compute the initial layout.
-            primaryObserver.observeReads(Unit, onValueChangedForScope = {
-                primaryInvalidations++
-                assertVisualText()
-            }) { assertVisualText() }
-            assertThat(transformationInvocations).isEqualTo(2)
-
-            // This should be a full cache hit.
-            secondaryObserver.observeReads(Unit, onValueChangedForScope = {
-                secondaryInvalidations++
-                assertVisualText()
-            }) { assertVisualText() }
-            assertThat(transformationInvocations).isEqualTo(3)
-
-            // Invalidate the transformation.
-            transformationState++
-        } finally {
-            primaryObserver.stop()
-            secondaryObserver.stop()
-        }
-
-        assertVisualText()
-        assertThat(transformationInvocations).isEqualTo(6)
-        assertThat(primaryInvalidations).isEqualTo(1)
-        assertThat(secondaryInvalidations).isEqualTo(1)
-    }
-
-    @Test
-    fun invalidatesAllReaders_whenTransformationDependenciesChanged_producingNewVisualText() {
-        var transformationState by mutableStateOf(1)
-        var transformationInvocations = 0
-        codepointTransformation = CodepointTransformation { _, codepoint ->
-            transformationInvocations++
-            codepoint + transformationState
-        }
-        // Transformation isn't applied if there's no text. Keep this at 1 char to make the math
-        // simpler.
-        textFieldState.setTextAndPlaceCursorAtEnd("h")
-        var expectedVisualText = "i"
-
-        fun assertVisualText() {
-            assertThat(cache.value?.layoutInput?.text?.text).isEqualTo(expectedVisualText)
-        }
-
-        updateNonMeasureInputs()
-        updateMeasureInputs()
-        var primaryInvalidations = 0
-        var secondaryInvalidations = 0
-
-        val primaryObserver = SnapshotStateObserver(onChangedExecutor = { it() })
-        val secondaryObserver = SnapshotStateObserver(onChangedExecutor = { it() })
-        try {
-            primaryObserver.start()
-            secondaryObserver.start()
-
-            // This will compute the initial layout.
-            primaryObserver.observeReads(Unit, onValueChangedForScope = {
-                primaryInvalidations++
-                assertVisualText()
-            }) { assertVisualText() }
-            assertThat(transformationInvocations).isEqualTo(2)
-
-            // This should be a full cache hit.
-            secondaryObserver.observeReads(Unit, onValueChangedForScope = {
-                secondaryInvalidations++
-                assertVisualText()
-            }) { assertVisualText() }
-            assertThat(transformationInvocations).isEqualTo(3)
-
-            // Invalidate the transformation.
-            expectedVisualText = "j"
-            transformationState++
-        } finally {
-            primaryObserver.stop()
-            secondaryObserver.stop()
-        }
-
-        assertVisualText()
-        // Two more reads means two more applications of the transformation.
-        assertThat(transformationInvocations).isEqualTo(6)
-        assertThat(primaryInvalidations).isEqualTo(1)
-        assertThat(secondaryInvalidations).isEqualTo(1)
-    }
-
-    @Test
-    fun value_returnsNewLayout_whenTextContentsChanged() {
-        textFieldState.edit {
-            replace(0, length, "h")
-            placeCursorBeforeCharAt(0)
-        }
-        assertLayoutChange(
-            change = {
-                textFieldState.edit {
-                    replace(0, length, "hello")
-                    placeCursorBeforeCharAt(0)
-                }
-            },
-        ) { old, new ->
-            assertThat(old.layoutInput.text.text).isEqualTo("h")
-            assertThat(new.layoutInput.text.text).isEqualTo("hello")
-        }
-    }
-
-    @Test
-    fun value_returnsCachedLayout_whenTextSelectionChanged() {
-        textFieldState.edit {
-            replace(0, length, "hello")
-            placeCursorBeforeCharAt(0)
-        }
-        assertLayoutChange(
-            change = {
-                textFieldState.edit {
-                    placeCursorBeforeCharAt(1)
-                }
-            }
-        ) { old, new ->
-            assertThat(new).isSameInstanceAs(old)
-        }
-    }
-
-    @Test
-    fun value_returnsNewLayout_whenCodepointTransformationInstanceChangedWithDifferentOutput() {
-        textFieldState.setTextAndPlaceCursorAtEnd("h")
-        codepointTransformation = CodepointTransformation { _, codepoint -> codepoint }
-        assertLayoutChange(
-            change = {
-                codepointTransformation = CodepointTransformation { _, codepoint -> codepoint + 1 }
-                updateNonMeasureInputs()
-            }
-        ) { old, new ->
-            assertThat(old.layoutInput.text.text).isEqualTo("h")
-            assertThat(new.layoutInput.text.text).isEqualTo("i")
-        }
-    }
-
-    @Test
-    fun value_returnsCachedLayout_whenCodepointTransformationInstanceChangedWithSameOutput() {
-        textFieldState.setTextAndPlaceCursorAtEnd("h")
-        codepointTransformation = CodepointTransformation { _, codepoint -> codepoint }
-        assertLayoutChange(
-            change = {
-                codepointTransformation = CodepointTransformation { _, codepoint -> codepoint }
-                updateNonMeasureInputs()
-            }
-        ) { old, new ->
-            assertThat(new).isSameInstanceAs(old)
-        }
-    }
-
-    @Test
-    fun value_returnsNewLayout_whenStyleLayoutAffectingAttributesChanged() {
-        textStyle = TextStyle(fontSize = 12.sp)
-        assertLayoutChange(
-            change = {
-                textStyle = TextStyle(fontSize = 23.sp)
-                updateNonMeasureInputs()
-            }
-        ) { old, new ->
-            assertThat(old.layoutInput.style.fontSize).isEqualTo(12.sp)
-            assertThat(new.layoutInput.style.fontSize).isEqualTo(23.sp)
-        }
-    }
-
-    @Test
-    fun value_returnsCachedLayout_whenStyleDrawAffectingAttributesChanged() {
-        textStyle = TextStyle(color = Color.Black)
-        assertLayoutChange(
-            change = {
-                textStyle = TextStyle(color = Color.Blue)
-                updateNonMeasureInputs()
-            }
-        ) { old, new ->
-            assertThat(new).isSameInstanceAs(old)
-        }
-    }
-
-    @Test
-    fun value_returnsNewLayout_whenSingleLineChanged() {
-        assertLayoutChange(
-            change = {
-                singleLine = !singleLine
-                updateNonMeasureInputs()
-            }
-        ) { old, new ->
-            assertThat(new).isNotSameInstanceAs(old)
-        }
-    }
-
-    @Test
-    fun value_returnsNewLayout_whenSoftWrapChanged() {
-        assertLayoutChange(
-            change = {
-                softWrap = !softWrap
-                updateNonMeasureInputs()
-            }
-        ) { old, new ->
-            assertThat(old.layoutInput.softWrap).isEqualTo(!softWrap)
-            assertThat(new.layoutInput.softWrap).isEqualTo(softWrap)
-        }
-    }
-
-    @Test
-    fun value_returnsNewLayout_whenDensityValueChangedWithSameInstance() {
-        var densityValue = 1f
-        density = object : Density {
-            override val density: Float
-                get() = densityValue
-            override val fontScale: Float = 1f
-        }
-        assertLayoutChange(
-            change = {
-                densityValue = 2f
-                updateMeasureInputs()
-            }
-        ) { old, new ->
-            assertThat(new).isNotSameInstanceAs(old)
-        }
-    }
-
-    @Test
-    fun value_returnsNewLayout_whenFontScaleChangedWithSameInstance() {
-        var fontScale = 1f
-        density = object : Density {
-            override val density: Float = 1f
-            override val fontScale: Float
-                get() = fontScale
-        }
-        assertLayoutChange(
-            change = {
-                fontScale = 2f
-                updateMeasureInputs()
-            }
-        ) { old, new ->
-            assertThat(new).isNotSameInstanceAs(old)
-        }
-    }
-
-    @Test
-    fun value_returnsCachedLayout_whenDensityInstanceChangedWithSameValues() {
-        density = Density(1f, 1f)
-        assertLayoutChange(
-            change = {
-                density = Density(1f, 1f)
-                updateMeasureInputs()
-            }
-        ) { old, new ->
-            assertThat(new).isSameInstanceAs(old)
-        }
-    }
-
-    @Test
-    fun value_returnsNewLayout_whenLayoutDirectionChanged() {
-        layoutDirection = LayoutDirection.Ltr
-        assertLayoutChange(
-            change = {
-                layoutDirection = LayoutDirection.Rtl
-                updateMeasureInputs()
-            }
-        ) { old, new ->
-            assertThat(old.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Ltr)
-            assertThat(new.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Rtl)
-        }
-    }
-
-    @Test
-    fun value_returnsNewLayout_whenFontFamilyResolverChanged() {
-        assertLayoutChange(
-            change = {
-                fontFamilyResolver =
-                    createFontFamilyResolver(InstrumentationRegistry.getInstrumentation().context)
-                updateMeasureInputs()
-            }
-        ) { old, new ->
-            assertThat(new).isNotSameInstanceAs(old)
-        }
-    }
-
-    @Ignore("b/294443266: figure out how to make fonts stale for test")
-    @Test
-    fun value_returnsNewLayout_whenFontFamilyResolverFontChanged() {
-        fontFamilyResolver =
-            createFontFamilyResolver(InstrumentationRegistry.getInstrumentation().context)
-        assertLayoutChange(
-            change = {
-                TODO("b/294443266: make fonts stale")
-            }
-        ) { old, new ->
-            assertThat(new).isNotSameInstanceAs(old)
-        }
-    }
-
-    @Test
-    fun value_returnsNewLayout_whenConstraintsChanged() {
-        constraints = Constraints.fixed(5, 5)
-        assertLayoutChange(
-            change = {
-                constraints = Constraints.fixed(6, 5)
-                updateMeasureInputs()
-            }
-        ) { old, new ->
-            assertThat(old.layoutInput.constraints).isEqualTo(Constraints.fixed(5, 5))
-            assertThat(new.layoutInput.constraints).isEqualTo(Constraints.fixed(6, 5))
-        }
-    }
-
-    @Test
-    fun cacheUpdateInSnapshot_onlyVisibleToParentSnapshotAfterApply() {
-        layoutDirection = LayoutDirection.Ltr
-        updateNonMeasureInputs()
-        updateMeasureInputs()
-        val initialLayout = cache.value!!
-        val snapshot = Snapshot.takeMutableSnapshot()
-
-        try {
-            snapshot.enter {
-                layoutDirection = LayoutDirection.Rtl
-                updateMeasureInputs()
-
-                val newLayout = cache.value!!
-                assertThat(initialLayout.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Ltr)
-                assertThat(newLayout.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Rtl)
-                assertThat(cache.value!!).isSameInstanceAs(newLayout)
-            }
-
-            // Not visible in parent yet.
-            assertThat(initialLayout.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Ltr)
-            assertThat(cache.value!!).isSameInstanceAs(initialLayout)
-            snapshot.apply().check()
-
-            // Now visible in parent.
-            val newLayout = cache.value!!
-            assertThat(initialLayout.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Ltr)
-            assertThat(newLayout.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Rtl)
-            assertThat(cache.value!!).isSameInstanceAs(newLayout)
-        } finally {
-            snapshot.dispose()
-        }
-    }
-
-    @Test
-    fun cachedValue_recomputed_afterSnapshotWithConflictingInputsApplied() {
-        softWrap = false
-        layoutDirection = LayoutDirection.Ltr
-        updateNonMeasureInputs()
-        updateMeasureInputs()
-        val snapshot = Snapshot.takeMutableSnapshot()
-
-        try {
-            softWrap = true
-            updateNonMeasureInputs()
-            val initialLayout = cache.value!!
-
-            snapshot.enter {
-                layoutDirection = LayoutDirection.Rtl
-                updateMeasureInputs()
-                with(cache.value!!) {
-                    assertThat(layoutInput.softWrap).isEqualTo(false)
-                    assertThat(layoutInput.layoutDirection).isEqualTo(LayoutDirection.Rtl)
-                    assertThat(cache.value!!).isSameInstanceAs(this)
-                }
-            }
-
-            // Parent only sees its update.
-            with(cache.value!!) {
-                assertThat(layoutInput.softWrap).isEqualTo(true)
-                assertThat(layoutInput.layoutDirection).isEqualTo(LayoutDirection.Ltr)
-                assertThat(this).isSameInstanceAs(initialLayout)
-                assertThat(cache.value!!).isSameInstanceAs(this)
-            }
-            snapshot.apply().check()
-
-            // Cache should now reflect merged inputs.
-            with(cache.value!!) {
-                assertThat(layoutInput.softWrap).isEqualTo(true)
-                assertThat(layoutInput.layoutDirection).isEqualTo(LayoutDirection.Rtl)
-                assertThat(cache.value!!).isSameInstanceAs(this)
-            }
-        } finally {
-            snapshot.dispose()
-        }
-    }
-
-    private fun assertLayoutChange(
-        change: () -> Unit,
-        compare: (old: TextLayoutResult, new: TextLayoutResult) -> Unit
-    ) {
-        updateNonMeasureInputs()
-        updateMeasureInputs()
-        val initialLayout = cache.value
-
-        change()
-        val newLayout = cache.value
-
-        assertNotNull(newLayout)
-        assertNotNull(initialLayout)
-        compare(initialLayout, newLayout)
-    }
-
-    private fun assertInvalidationsOnChange(
-        expectedInvalidations: Int,
-        update: () -> Unit,
-    ) {
-        updateNonMeasureInputs()
-        updateMeasureInputs()
-        var invalidations = 0
-
-        observingLayoutCache({ invalidations++ }) {
-            update()
-        }
-
-        assertWithMessage("Expected $expectedInvalidations invalidations")
-            .that(invalidations).isEqualTo(expectedInvalidations)
-    }
-
-    private fun updateNonMeasureInputs() {
-        cache.updateNonMeasureInputs(
-            textFieldState = textFieldState,
-            codepointTransformation = codepointTransformation,
-            textStyle = textStyle,
-            singleLine = singleLine,
-            softWrap = softWrap
-        )
-    }
-
-    private fun updateMeasureInputs() {
-        cache.layoutWithNewMeasureInputs(
-            density = density,
-            layoutDirection = layoutDirection,
-            fontFamilyResolver = fontFamilyResolver,
-            constraints = constraints
-        )
-    }
-
-    private fun observingLayoutCache(
-        onLayoutStateInvalidated: (TextLayoutResult?) -> Unit,
-        block: () -> Unit
-    ) {
-        val observer = SnapshotStateObserver(onChangedExecutor = { it() })
-        observer.start()
-        try {
-            observer.observeReads(Unit, onValueChangedForScope = {
-                onLayoutStateInvalidated(cache.value)
-            }) { cache.value }
-            block()
-        } finally {
-            observer.stop()
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/AndroidManifest.xml b/compose/foundation/foundation/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/AndroidManifest.xml
rename to compose/foundation/foundation/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/AutoTestFrameClock.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/AutoTestFrameClock.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/AutoTestFrameClock.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/AutoTestFrameClock.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BackgroundTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BackgroundTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BackgroundTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BackgroundTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicTooltipTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BasicTooltipTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicTooltipTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BasicTooltipTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BorderTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BorderTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BorderTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BorderTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/CanvasTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CanvasTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/CanvasTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CanvasTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/DraggableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/DraggableTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/DraggableTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/DraggableTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/EmbeddedGraphicsSurfaceTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/EmbeddedGraphicsSurfaceTest.kt
new file mode 100644
index 0000000..6a10cd1
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/EmbeddedGraphicsSurfaceTest.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2020 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
+
+import android.graphics.PorterDuff
+import android.os.Build
+import android.view.Surface
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.unit.dp
+import androidx.core.graphics.ColorUtils
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import kotlin.math.roundToInt
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@RunWith(AndroidJUnit4::class)
+class EmbeddedGraphicsSurfaceTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    val size = 48.dp
+
+    @Test
+    fun testOnSurface() {
+        var surfaceRef: Surface? = null
+        var surfaceWidth = 0
+        var surfaceHeight = 0
+        var expectedSize = 0
+
+        rule.setContent {
+            expectedSize = with(LocalDensity.current) {
+                size.toPx().roundToInt()
+            }
+
+            EmbeddedGraphicsSurface(modifier = Modifier.size(size)) {
+                onSurface { surface, width, height ->
+                    surfaceRef = surface
+                    surfaceWidth = width
+                    surfaceHeight = height
+                }
+            }
+        }
+
+        rule.onRoot()
+            .assertWidthIsEqualTo(size)
+            .assertHeightIsEqualTo(size)
+            .assertIsDisplayed()
+
+        rule.runOnIdle {
+            assertNotNull(surfaceRef)
+            assertEquals(expectedSize, surfaceWidth)
+            assertEquals(expectedSize, surfaceHeight)
+        }
+    }
+
+    @Test
+    fun testOnSurfaceChanged() {
+        var surfaceWidth = 0
+        var surfaceHeight = 0
+        var expectedSize = 0
+
+        var desiredSize by mutableStateOf(size)
+
+        rule.setContent {
+            expectedSize = with(LocalDensity.current) {
+                desiredSize.toPx().roundToInt()
+            }
+
+            EmbeddedGraphicsSurface(modifier = Modifier.size(desiredSize)) {
+                onSurface { surface, initWidth, initHeight ->
+                    surfaceWidth = initWidth
+                    surfaceHeight = initHeight
+
+                    surface.onChanged { newWidth, newHeight ->
+                        surfaceWidth = newWidth
+                        surfaceHeight = newHeight
+                    }
+                }
+            }
+        }
+
+        rule.onRoot()
+            .assertWidthIsEqualTo(desiredSize)
+            .assertHeightIsEqualTo(desiredSize)
+
+        rule.runOnIdle {
+            assertEquals(expectedSize, surfaceWidth)
+            assertEquals(expectedSize, surfaceHeight)
+        }
+
+        desiredSize = size * 2
+        val prevSurfaceWidth = surfaceWidth
+        val prevSurfaceHeight = surfaceHeight
+
+        rule.onRoot()
+            .assertWidthIsEqualTo(desiredSize)
+            .assertHeightIsEqualTo(desiredSize)
+
+        rule.runOnIdle {
+            assertNotEquals(prevSurfaceWidth, surfaceWidth)
+            assertNotEquals(prevSurfaceHeight, surfaceHeight)
+            assertEquals(expectedSize, surfaceWidth)
+            assertEquals(expectedSize, surfaceHeight)
+        }
+    }
+
+    @Test
+    fun testOnSurfaceDestroyed() {
+        var surfaceRef: Surface? = null
+        var visible by mutableStateOf(true)
+
+        rule.setContent {
+            if (visible) {
+                EmbeddedGraphicsSurface(modifier = Modifier.size(size)) {
+                    onSurface { surface, _, _ ->
+                        surfaceRef = surface
+
+                        surface.onDestroyed {
+                            surfaceRef = null
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertNotNull(surfaceRef)
+        }
+
+        visible = false
+
+        rule.runOnIdle {
+            assertNull(surfaceRef)
+        }
+    }
+
+    @Test
+    fun testOnSurfaceRecreated() {
+        var surfaceCreatedCount = 0
+        var surfaceDestroyedCount = 0
+        var visible by mutableStateOf(true)
+
+        // NOTE: TextureView only destroys the surface when TextureView is detached from
+        // the window, and only creates when it gets attached to the window
+        rule.setContent {
+            if (visible) {
+                EmbeddedGraphicsSurface(modifier = Modifier.size(size)) {
+                    onSurface { surface, _, _ ->
+                        surfaceCreatedCount++
+                        surface.onDestroyed {
+                            surfaceDestroyedCount++
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, surfaceCreatedCount)
+            assertEquals(0, surfaceDestroyedCount)
+            visible = false
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, surfaceCreatedCount)
+            assertEquals(1, surfaceDestroyedCount)
+            visible = true
+        }
+
+        rule.runOnIdle {
+            assertEquals(2, surfaceCreatedCount)
+            assertEquals(1, surfaceDestroyedCount)
+        }
+    }
+
+    @Test
+    fun testRender() {
+        var surfaceRef: Surface? = null
+        var expectedSize = 0
+
+        rule.setContent {
+            expectedSize = with(LocalDensity.current) {
+                size.toPx().roundToInt()
+            }
+            EmbeddedGraphicsSurface(modifier = Modifier.size(size)) {
+                onSurface { surface, _, _ ->
+                    surfaceRef = surface
+                    surface.lockHardwareCanvas().apply {
+                        drawColor(Color.Blue.toArgb())
+                        surface.unlockCanvasAndPost(this)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertNotNull(surfaceRef)
+        }
+
+        surfaceRef!!
+            .captureToImage(expectedSize, expectedSize)
+            .assertPixels { Color.Blue }
+    }
+
+    @Test
+    fun testNotOpaque() {
+        val translucentRed = Color(1.0f, 0.0f, 0.0f, 0.5f).toArgb()
+
+        rule.setContent {
+            Box(modifier = Modifier.size(size)) {
+                Canvas(modifier = Modifier.size(size)) {
+                    drawRect(Color.White)
+                }
+                EmbeddedGraphicsSurface(
+                    modifier = Modifier
+                        .size(size)
+                        .testTag("EmbeddedGraphicSurface"),
+                    isOpaque = false
+                ) {
+                    onSurface { surface, _, _ ->
+                        surface.lockHardwareCanvas().apply {
+                            drawColor(0x00000000, PorterDuff.Mode.CLEAR)
+                            drawColor(translucentRed)
+                            surface.unlockCanvasAndPost(this)
+                        }
+                    }
+                }
+            }
+        }
+
+        val expectedColor = Color(ColorUtils.compositeColors(translucentRed, Color.White.toArgb()))
+
+        rule
+            .onNodeWithTag("EmbeddedGraphicSurface")
+            .captureToImage()
+            .assertPixels { expectedColor }
+    }
+
+    @Test
+    fun testOpaque() {
+        rule.setContent {
+            Box(modifier = Modifier.size(size)) {
+                Canvas(modifier = Modifier.size(size)) {
+                    drawRect(Color.Green)
+                }
+                EmbeddedGraphicsSurface(
+                    modifier = Modifier
+                        .size(size)
+                        .testTag("EmbeddedGraphicSurface")
+                ) {
+                    onSurface { surface, _, _ ->
+                        surface.lockHardwareCanvas().apply {
+                            drawColor(Color.Red.toArgb())
+                            surface.unlockCanvasAndPost(this)
+                        }
+                    }
+                }
+            }
+        }
+
+        rule
+            .onNodeWithTag("EmbeddedGraphicSurface")
+            .captureToImage()
+            .assertPixels { Color.Red }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusGroupTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FocusGroupTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusGroupTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FocusGroupTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableBoundsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FocusableBoundsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableBoundsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FocusableBoundsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FocusableTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FocusableTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FoundationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FoundationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FoundationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FoundationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/GoldenCommon.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/GoldenCommon.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/GoldenCommon.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/GoldenCommon.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/GraphicsSurfaceTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/GraphicsSurfaceTest.kt
new file mode 100644
index 0000000..6fc417c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/GraphicsSurfaceTest.kt
@@ -0,0 +1,589 @@
+/*
+ * Copyright 2020 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
+
+import android.graphics.Bitmap
+import android.graphics.PorterDuff
+import android.graphics.Rect
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.view.Choreographer
+import android.view.PixelCopy
+import android.view.Surface
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.ViewRootForTest
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.unit.dp
+import androidx.concurrent.futures.ResolvableFuture
+import androidx.core.graphics.ColorUtils
+import androidx.core.graphics.createBitmap
+import androidx.test.core.internal.os.HandlerExecutor
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+@RunWith(AndroidJUnit4::class)
+class GraphicsSurfaceTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    val size = 48.dp
+
+    @Test
+    fun testOnSurface() {
+        var surfaceRef: Surface? = null
+        var surfaceWidth = 0
+        var surfaceHeight = 0
+        var expectedSize = 0
+
+        rule.setContent {
+            expectedSize = with(LocalDensity.current) {
+                size.toPx().roundToInt()
+            }
+
+            GraphicsSurface(modifier = Modifier.size(size)) {
+                onSurface { surface, width, height ->
+                    surfaceRef = surface
+                    surfaceWidth = width
+                    surfaceHeight = height
+                }
+            }
+        }
+
+        rule.onRoot()
+            .assertWidthIsEqualTo(size)
+            .assertHeightIsEqualTo(size)
+            .assertIsDisplayed()
+
+        rule.runOnIdle {
+            assertNotNull(surfaceRef)
+            assertEquals(expectedSize, surfaceWidth)
+            assertEquals(expectedSize, surfaceHeight)
+        }
+    }
+
+    @Test
+    fun testOnSurfaceChanged() {
+        var surfaceWidth = 0
+        var surfaceHeight = 0
+        var expectedSize = 0
+
+        var desiredSize by mutableStateOf(size)
+
+        rule.setContent {
+            expectedSize = with(LocalDensity.current) {
+                desiredSize.toPx().roundToInt()
+            }
+
+            GraphicsSurface(modifier = Modifier.size(desiredSize)) {
+                onSurface { surface, _, _ ->
+                    surface.onChanged { newWidth, newHeight ->
+                        surfaceWidth = newWidth
+                        surfaceHeight = newHeight
+                    }
+                }
+            }
+        }
+
+        rule.onRoot()
+            .assertWidthIsEqualTo(desiredSize)
+            .assertHeightIsEqualTo(desiredSize)
+
+        // onChanged() hasn't been called yet
+        rule.runOnIdle {
+            assertEquals(0, surfaceWidth)
+            assertEquals(0, surfaceHeight)
+        }
+
+        desiredSize = size * 2
+        val prevSurfaceWidth = surfaceWidth
+        val prevSurfaceHeight = surfaceHeight
+
+        rule.onRoot()
+            .assertWidthIsEqualTo(desiredSize)
+            .assertHeightIsEqualTo(desiredSize)
+
+        rule.runOnIdle {
+            assertNotEquals(prevSurfaceWidth, surfaceWidth)
+            assertNotEquals(prevSurfaceHeight, surfaceHeight)
+            assertEquals(expectedSize, surfaceWidth)
+            assertEquals(expectedSize, surfaceHeight)
+        }
+    }
+
+    @Test
+    fun testOnSurfaceDestroyed() {
+        var surfaceRef: Surface? = null
+        var visible by mutableStateOf(true)
+
+        rule.setContent {
+            if (visible) {
+                GraphicsSurface(modifier = Modifier.size(size)) {
+                    onSurface { surface, _, _ ->
+                        surfaceRef = surface
+
+                        surface.onDestroyed {
+                            surfaceRef = null
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertNotNull(surfaceRef)
+        }
+
+        visible = false
+
+        rule.runOnIdle {
+            assertNull(surfaceRef)
+        }
+    }
+
+    @Test
+    fun testOnSurfaceRecreated() {
+        var surfaceCreatedCount = 0
+        var surfaceDestroyedCount = 0
+
+        var view: View? = null
+
+        rule.setContent {
+            view = LocalView.current
+            GraphicsSurface(modifier = Modifier.size(size)) {
+                onSurface { surface, _, _ ->
+                    surfaceCreatedCount++
+                    surface.onDestroyed {
+                        surfaceDestroyedCount++
+                    }
+                }
+            }
+        }
+
+        // NOTE: SurfaceView only triggers a Surface destroy/create cycle on visibility
+        // change if its *own* visibility or the visibility of the window changes. Here
+        // we change the visibility of the window by setting the visibility of the root
+        // view (the host view in ViewRootImpl).
+        rule.runOnIdle {
+            assertEquals(1, surfaceCreatedCount)
+            assertEquals(0, surfaceDestroyedCount)
+            view?.rootView?.visibility = View.INVISIBLE
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, surfaceCreatedCount)
+            assertEquals(1, surfaceDestroyedCount)
+            view?.rootView?.visibility = View.VISIBLE
+        }
+
+        rule.runOnIdle {
+            assertEquals(2, surfaceCreatedCount)
+            assertEquals(1, surfaceDestroyedCount)
+        }
+    }
+
+    @Test
+    fun testRender() {
+        var surfaceRef: Surface? = null
+        var expectedSize = 0
+
+        rule.setContent {
+            expectedSize = with(LocalDensity.current) {
+                size.toPx().roundToInt()
+            }
+            GraphicsSurface(modifier = Modifier.size(size)) {
+                onSurface { surface, _, _ ->
+                    surfaceRef = surface
+                    surface.lockHardwareCanvas().apply {
+                        drawColor(Color.Blue.toArgb())
+                        surface.unlockCanvasAndPost(this)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertNotNull(surfaceRef)
+        }
+
+        surfaceRef!!
+            .captureToImage(expectedSize, expectedSize)
+            .assertPixels { Color.Blue }
+    }
+
+    @Test
+    fun testZOrderDefault() {
+        val frameCount = 4
+        val latch = CountDownLatch(frameCount)
+
+        rule.setContent {
+            Box(modifier = Modifier.size(size)) {
+                GraphicsSurface(
+                    modifier = Modifier
+                        .size(size)
+                        .testTag("GraphicSurface")
+                ) {
+                    onSurface { surface, _, _ ->
+                        // Draw > 3 frames to make sure the screenshot copy will pick up
+                        // a SurfaceFlinger composition that includes our Surface
+                        repeat(frameCount) {
+                            withFrameNanos {
+                                surface.lockHardwareCanvas().apply {
+                                    drawColor(Color.Blue.toArgb())
+                                    surface.unlockCanvasAndPost(this)
+                                }
+                                latch.countDown()
+                            }
+                        }
+                    }
+                }
+                Canvas(modifier = Modifier.size(size)) {
+                    drawRect(Color.Green)
+                }
+            }
+        }
+
+        if (!latch.await(5, TimeUnit.SECONDS)) {
+            throw AssertionError("Failed waiting for render")
+        }
+
+        rule
+            .onNodeWithTag("GraphicSurface")
+            .screenshotToImage()!!
+            .assertPixels { Color.Green }
+    }
+
+    @Test
+    fun testZOrderMediaOverlay() {
+        val frameCount = 4
+        val latch = CountDownLatch(frameCount)
+
+        rule.setContent {
+            Box(modifier = Modifier.size(size)) {
+                GraphicsSurface(
+                    modifier = Modifier.size(size),
+                    zOrder = GraphicsSurfaceZOrder.Behind
+                ) {
+                    onSurface { surface, _, _ ->
+                        // Draw > 3 frames to make sure the screenshot copy will pick up
+                        // a SurfaceFlinger composition that includes our Surface
+                        repeat(frameCount) {
+                            withFrameNanos {
+                                surface.lockHardwareCanvas().apply {
+                                    drawColor(Color.Blue.toArgb())
+                                    surface.unlockCanvasAndPost(this)
+                                }
+                                latch.countDown()
+                            }
+                        }
+                    }
+                }
+                GraphicsSurface(
+                    modifier = Modifier
+                        .size(size)
+                        .testTag("GraphicSurface"),
+                    zOrder = GraphicsSurfaceZOrder.MediaOverlay
+                ) {
+                    onSurface { surface, _, _ ->
+                        surface.lockHardwareCanvas().apply {
+                            drawColor(Color.Red.toArgb())
+                            surface.unlockCanvasAndPost(this)
+                        }
+                        latch.countDown()
+                    }
+                }
+            }
+        }
+
+        if (!latch.await(5, TimeUnit.SECONDS)) {
+            throw AssertionError("Failed waiting for render")
+        }
+
+        rule
+            .onNodeWithTag("GraphicSurface")
+            .screenshotToImage()!!
+            .assertPixels { Color.Red }
+    }
+
+    @Test
+    fun testZOrderOnTop() {
+        val frameCount = 4
+        val latch = CountDownLatch(frameCount)
+
+        rule.setContent {
+            Box(modifier = Modifier.size(size)) {
+                GraphicsSurface(
+                    modifier = Modifier
+                        .size(size)
+                        .testTag("GraphicSurface"),
+                    zOrder = GraphicsSurfaceZOrder.OnTop
+                ) {
+                    onSurface { surface, _, _ ->
+                        // Draw > 3 frames to make sure the screenshot copy will pick up
+                        // a SurfaceFlinger composition that includes our Surface
+                        repeat(frameCount) {
+                            withFrameNanos {
+                                surface.lockHardwareCanvas().apply {
+                                    drawColor(Color.Blue.toArgb())
+                                    surface.unlockCanvasAndPost(this)
+                                }
+                                latch.countDown()
+                            }
+                        }
+                    }
+                }
+                Canvas(modifier = Modifier.size(size)) {
+                    drawRect(Color.Green)
+                }
+            }
+        }
+
+        if (!latch.await(5, TimeUnit.SECONDS)) {
+            throw AssertionError("Failed waiting for render")
+        }
+
+        rule
+            .onNodeWithTag("GraphicSurface")
+            .screenshotToImage()!!
+            .assertPixels { Color.Blue }
+    }
+
+    @Test
+    fun testNotOpaque() {
+        val frameCount = 4
+        val latch = CountDownLatch(frameCount)
+        val translucentRed = Color(1.0f, 0.0f, 0.0f, 0.5f).toArgb()
+
+        rule.setContent {
+            Box(modifier = Modifier.size(size)) {
+                GraphicsSurface(
+                    modifier = Modifier
+                        .size(size)
+                        .testTag("GraphicSurface"),
+                    isOpaque = false,
+                    zOrder = GraphicsSurfaceZOrder.OnTop
+                ) {
+                    onSurface { surface, _, _ ->
+                        // Draw > 3 frames to make sure the screenshot copy will pick up
+                        // a SurfaceFlinger composition that includes our Surface
+                        repeat(frameCount) {
+                            withFrameNanos {
+                                surface.lockHardwareCanvas().apply {
+                                    // Since we are drawing a translucent color we need to
+                                    // clear first
+                                    drawColor(0x00000000, PorterDuff.Mode.CLEAR)
+                                    drawColor(translucentRed)
+                                    surface.unlockCanvasAndPost(this)
+                                }
+                                latch.countDown()
+                            }
+                        }
+                    }
+                }
+                Canvas(modifier = Modifier.size(size)) {
+                    drawRect(Color.White)
+                }
+            }
+        }
+
+        if (!latch.await(5, TimeUnit.SECONDS)) {
+            throw AssertionError("Failed waiting for render")
+        }
+
+        val expectedColor = Color(ColorUtils.compositeColors(translucentRed, Color.White.toArgb()))
+
+        rule
+            .onNodeWithTag("GraphicSurface")
+            .screenshotToImage()!!
+            .assertPixels { expectedColor }
+    }
+
+    @Test
+    fun testSecure() {
+        val frameCount = 4
+        val latch = CountDownLatch(frameCount)
+
+        rule.setContent {
+            GraphicsSurface(
+                modifier = Modifier
+                    .size(size)
+                    .testTag("GraphicSurface"),
+                isSecure = true
+            ) {
+                onSurface { surface, _, _ ->
+                    // Draw > 3 frames to make sure the screenshot copy will pick up
+                    // a SurfaceFlinger composition that includes our Surface
+                    repeat(frameCount) {
+                        withFrameNanos {
+                            surface.lockHardwareCanvas().apply {
+                                drawColor(Color.Blue.toArgb())
+                                surface.unlockCanvasAndPost(this)
+                            }
+                            latch.countDown()
+                        }
+                    }
+                }
+            }
+        }
+
+        if (!latch.await(5, TimeUnit.SECONDS)) {
+            throw AssertionError("Failed waiting for render")
+        }
+
+        val screen = rule
+            .onNodeWithTag("GraphicSurface")
+            .screenshotToImage(true)
+
+        // Before API 33 taking a screenshot with a secure surface returns null
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            assertNull(screen)
+        } else {
+            screen!!.assertPixels { Color.Black }
+        }
+    }
+}
+
+/**
+ * Returns an ImageBitmap containing a screenshot of the device. On API < 33,
+ * a secure surface present on screen can cause this function to return null.
+ */
+private fun SemanticsNodeInteraction.screenshotToImage(
+    hasSecureSurfaces: Boolean = false
+): ImageBitmap? {
+    val instrumentation = InstrumentationRegistry.getInstrumentation()
+    instrumentation.waitForIdleSync()
+
+    val uiAutomation = instrumentation.uiAutomation
+
+    val node = fetchSemanticsNode()
+    val view = (node.root as ViewRootForTest).view
+
+    val bitmapFuture: ResolvableFuture<Bitmap> = ResolvableFuture.create()
+
+    val mainExecutor = HandlerExecutor(Handler(Looper.getMainLooper()))
+    mainExecutor.execute {
+        Choreographer.getInstance().postFrameCallback {
+            val location = IntArray(2)
+            view.getLocationOnScreen(location)
+
+            val bounds = node.boundsInRoot.translate(
+                location[0].toFloat(),
+                location[1].toFloat()
+            )
+
+            // do multiple retries of uiAutomation.takeScreenshot because it is known to return null
+            // on API 31+ b/257274080
+            var bitmap: Bitmap? = null
+            var i = 0
+            while (i < 3 && bitmap == null) {
+                bitmap = uiAutomation.takeScreenshot()
+                i++
+            }
+
+            if (bitmap != null) {
+                bitmap = Bitmap.createBitmap(
+                    bitmap,
+                    bounds.left.toInt(),
+                    bounds.top.toInt(),
+                    bounds.width.toInt(),
+                    bounds.height.toInt()
+                )
+                bitmapFuture.set(bitmap)
+            } else {
+                if (hasSecureSurfaces) {
+                    // may be null on older API levels when a secure surface is showing
+                    bitmapFuture.set(null)
+                }
+                // if we don't show secure surfaces, let the future timeout on get()
+            }
+        }
+    }
+
+    return try {
+        bitmapFuture.get(5, TimeUnit.SECONDS)?.asImageBitmap()
+    } catch (e: ExecutionException) {
+        null
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal fun Surface.captureToImage(width: Int, height: Int): ImageBitmap {
+    val bitmap = createBitmap(width, height)
+
+    val latch = CountDownLatch(1)
+    var copyResult = 0
+    val onCopyFinished = PixelCopy.OnPixelCopyFinishedListener { result ->
+        copyResult = result
+        latch.countDown()
+        android.util.Log.d("Test", Thread.currentThread().toString())
+    }
+
+    PixelCopy.request(
+        this,
+        Rect(0, 0, width, height),
+        bitmap,
+        onCopyFinished,
+        Handler(Looper.getMainLooper())
+    )
+
+    if (!latch.await(1, TimeUnit.SECONDS)) {
+        throw AssertionError("Failed waiting for PixelCopy!")
+    }
+
+    if (copyResult != PixelCopy.SUCCESS) {
+        throw AssertionError("PixelCopy failed!")
+    }
+
+    return bitmap.asImageBitmap()
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/HoverableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/HoverableTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/HoverableTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/HoverableTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ImageTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ImageTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/IndicationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/IndicationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/InteractionSourceTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/InteractionSourceTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/InteractionSourceTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/InteractionSourceTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/LazyListFocusableInteractionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/LazyListFocusableInteractionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/LazyListFocusableInteractionTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/LazyListFocusableInteractionTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/MagnifierTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/MagnifierTest.kt
new file mode 100644
index 0000000..c19d2d2
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/MagnifierTest.kt
@@ -0,0 +1,764 @@
+/*
+ * Copyright 2021 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
+
+import android.annotation.SuppressLint
+import android.view.View
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.InspectableModifier
+import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.ValueElement
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.toSize
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SuppressLint("NewApi")
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class MagnifierTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Before
+    fun setUp() {
+        isDebugInspectorInfoEnabled = true
+    }
+
+    @After
+    fun tearDown() {
+        isDebugInspectorInfoEnabled = false
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun magnifier_inspectorValue_whenSupported() {
+        val sourceCenterLambda: Density.() -> Offset = { Offset(42f, 42f) }
+        val magnifierCenterLambda: Density.() -> Offset = { Offset(42f, 42f) }
+        val modifier = Modifier.magnifier(
+            sourceCenter = sourceCenterLambda,
+            magnifierCenter = magnifierCenterLambda
+        ).findInspectableValue()!!
+        assertThat(modifier.nameFallback).isEqualTo("magnifier")
+        assertThat(modifier.valueOverride).isNull()
+        assertThat(modifier.inspectableElements.toList()).containsExactly(
+            ValueElement("sourceCenter", sourceCenterLambda),
+            ValueElement("magnifierCenter", magnifierCenterLambda),
+            ValueElement("zoom", Float.NaN),
+            ValueElement("size", DpSize.Unspecified),
+            ValueElement("cornerRadius", Dp.Unspecified),
+            ValueElement("elevation", Dp.Unspecified),
+            ValueElement("clippingEnabled", true),
+        )
+    }
+
+    @SdkSuppress(maxSdkVersion = 27)
+    @Test
+    fun magnifier_inspectorValue_whenNotSupported() {
+        val sourceCenterLambda: Density.() -> Offset = { Offset(42f, 42f) }
+        val magnifierCenterLambda: Density.() -> Offset = { Offset(42f, 42f) }
+        val modifier = Modifier.magnifier(
+            sourceCenter = sourceCenterLambda,
+            magnifierCenter = magnifierCenterLambda
+        ).findInspectableValue()!!
+        assertThat(modifier.nameFallback).isEqualTo("magnifier (not supported)")
+        assertThat(modifier.valueOverride).isNull()
+        assertThat(modifier.inspectableElements.toList()).containsExactly(
+            ValueElement("sourceCenter", sourceCenterLambda),
+            ValueElement("magnifierCenter", magnifierCenterLambda),
+            ValueElement("zoom", Float.NaN),
+            ValueElement("size", DpSize.Unspecified),
+            ValueElement("cornerRadius", Dp.Unspecified),
+            ValueElement("elevation", Dp.Unspecified),
+            ValueElement("clippingEnabled", true),
+        )
+    }
+
+    @SdkSuppress(maxSdkVersion = 27)
+    @Test
+    fun magnifier_returnsEmptyModifier_whenNotSupported() {
+        val modifier = Modifier.magnifier(sourceCenter = { Offset.Zero })
+        val elements: List<Modifier.Element> =
+            modifier.foldIn(emptyList()) { elements, element -> elements + element }
+
+        // Modifier.magnifier doesn't have its own modifier class, so instead of checking for the
+        // absence of the actual modifier we just check that the only modifier returned is the
+        // InspectableValue (which actually has two elements).
+        assertThat(elements).hasSize(2)
+        assertThat(elements.first()).isInstanceOf(InspectableValue::class.java)
+        assertThat(elements.last()).isInstanceOf(InspectableModifier.End::class.java)
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_recreatesMagnifier_whenDensityChanged() {
+        val magnifierFactory = CountingPlatformMagnifierFactory()
+        var density by mutableStateOf(Density(1f))
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides density) {
+                Box(
+                    Modifier.magnifier(
+                        sourceCenter = { Offset.Zero },
+                        magnifierCenter = { Offset.Unspecified },
+                        zoom = Float.NaN,
+                        onSizeChanged = null,
+                        platformMagnifierFactory = magnifierFactory
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(magnifierFactory.creationCount).isEqualTo(1)
+        }
+
+        density = Density(density.density * 2)
+
+        rule.runOnIdle {
+            assertThat(magnifierFactory.creationCount).isEqualTo(2)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_recreatesMagnifier_whenConfigurationChanged() {
+        val magnifierFactory = CountingPlatformMagnifierFactory()
+        var elevation by mutableStateOf(1.dp)
+        rule.setContent {
+            Box(
+                Modifier.magnifier(
+                    sourceCenter = { Offset.Zero },
+                    magnifierCenter = { Offset.Unspecified },
+                    zoom = Float.NaN,
+                    elevation = elevation,
+                    onSizeChanged = null,
+                    platformMagnifierFactory = magnifierFactory
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(magnifierFactory.creationCount).isEqualTo(1)
+        }
+
+        elevation *= 2
+
+        rule.runOnIdle {
+            assertThat(magnifierFactory.creationCount).isEqualTo(2)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_recreatesMagnifier_whenConfigurationChangedToText() {
+        val magnifierFactory = CountingPlatformMagnifierFactory()
+        var useTextDefault by mutableStateOf(false)
+        rule.setContent {
+            Box(
+                Modifier.magnifier(
+                    sourceCenter = { Offset.Zero },
+                    magnifierCenter = { Offset.Unspecified },
+                    zoom = Float.NaN,
+                    useTextDefault = useTextDefault,
+                    onSizeChanged = null,
+                    platformMagnifierFactory = magnifierFactory
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(magnifierFactory.creationCount).isEqualTo(1)
+        }
+
+        useTextDefault = true
+
+        rule.runOnIdle {
+            assertThat(magnifierFactory.creationCount).isEqualTo(2)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_recreatesMagnifier_whenCannotUpdateZoom() {
+        val magnifierFactory = CountingPlatformMagnifierFactory()
+        var zoom by mutableStateOf(1f)
+        rule.setContent {
+            Box(
+                Modifier.magnifier(
+                    sourceCenter = { Offset.Zero },
+                    magnifierCenter = { Offset.Unspecified },
+                    zoom = zoom,
+                    onSizeChanged = null,
+                    platformMagnifierFactory = magnifierFactory
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(magnifierFactory.creationCount).isEqualTo(1)
+        }
+
+        zoom += 2
+
+        rule.runOnIdle {
+            assertThat(magnifierFactory.creationCount).isEqualTo(2)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_doesNotRecreateMagnifier_whenCanUpdateZoom() {
+        val magnifierFactory = CountingPlatformMagnifierFactory(canUpdateZoom = true)
+        var zoom by mutableStateOf(1f)
+        rule.setContent {
+            Box(
+                Modifier.magnifier(
+                    sourceCenter = { Offset.Zero },
+                    magnifierCenter = { Offset.Unspecified },
+                    zoom = zoom,
+                    onSizeChanged = null,
+                    platformMagnifierFactory = magnifierFactory
+
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(magnifierFactory.creationCount).isEqualTo(1)
+        }
+
+        zoom += 2
+
+        rule.runOnIdle {
+            assertThat(magnifierFactory.creationCount).isEqualTo(1)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_updatesContent_whenLayerRedrawn() {
+        var drawTrigger by mutableStateOf(0)
+        val platformMagnifier = CountingPlatformMagnifier()
+        rule.setContent {
+            Box(
+                Modifier
+                    // If the node has zero size, it won't draw and this test won't work.
+                    .fillMaxSize()
+                    .drawBehind {
+                        // Read this state to trigger re-draw when it changes.
+                        drawCircle(Color.Black, radius = drawTrigger.toFloat())
+                    }
+                    .magnifier(
+                        sourceCenter = { Offset.Zero },
+                        magnifierCenter = { Offset.Unspecified },
+                        zoom = Float.NaN,
+                        onSizeChanged = null,
+                        platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
+                    )
+            )
+        }
+
+        rule.runOnIdle {
+            // This will always happen once right away, since there's always an immediate draw pass.
+            assertThat(platformMagnifier.contentUpdateCount).isEqualTo(1)
+        }
+
+        drawTrigger++
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.contentUpdateCount).isEqualTo(2)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_doesNotUpdateProperties_whenLayerRedrawn() {
+        var drawTrigger by mutableStateOf(0)
+        val platformMagnifier = CountingPlatformMagnifier()
+        rule.setContent {
+            Box(
+                Modifier
+                    // If the node has zero size, it won't draw and this test won't work.
+                    .fillMaxSize()
+                    .drawBehind {
+                        // Read this state to trigger re-draw when it changes.
+                        drawCircle(Color.Black, radius = drawTrigger.toFloat())
+                    }
+                    .magnifier(
+                        sourceCenter = { Offset.Zero },
+                        magnifierCenter = { Offset.Unspecified },
+                        zoom = Float.NaN,
+                        onSizeChanged = null,
+                        platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
+                    )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
+        }
+
+        drawTrigger++
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_updatesProperties_whenPlacementChanged() {
+        var layoutOffset by mutableStateOf(IntOffset.Zero)
+        val platformMagnifier = CountingPlatformMagnifier()
+        rule.setContent {
+            Box(
+                Modifier
+                    .offset { layoutOffset }
+                    .magnifier(
+                        sourceCenter = { Offset.Zero },
+                        magnifierCenter = { Offset.Unspecified },
+                        zoom = Float.NaN,
+                        onSizeChanged = null,
+                        platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
+                    )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
+        }
+
+        layoutOffset += IntOffset(10, 1)
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(2)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_updatesProperties_whenSourceCenterChanged() {
+        var sourceCenter by mutableStateOf(Offset(1f, 1f))
+        val platformMagnifier = CountingPlatformMagnifier()
+        rule.setContent {
+            Box(
+                Modifier.magnifier(
+                    sourceCenter = { sourceCenter },
+                    magnifierCenter = { Offset.Unspecified },
+                    zoom = Float.NaN,
+                    onSizeChanged = null,
+                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
+        }
+
+        sourceCenter += Offset(1f, 1f)
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(2)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_updatesProperties_whenMagnifierCenterChanged() {
+        var magnifierCenter by mutableStateOf(Offset(1f, 1f))
+        val platformMagnifier = CountingPlatformMagnifier()
+        rule.setContent {
+            Box(
+                Modifier.magnifier(
+                    sourceCenter = { Offset.Zero },
+                    magnifierCenter = { magnifierCenter },
+                    zoom = Float.NaN,
+                    onSizeChanged = null,
+                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
+        }
+
+        magnifierCenter += Offset(1f, 1f)
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(2)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @Test
+    fun platformMagnifierModifier_updatesProperties_whenZoomChanged() {
+        var zoom by mutableStateOf(1f)
+        val platformMagnifier = CountingPlatformMagnifier()
+        rule.setContent {
+            Box(
+                Modifier.magnifier(
+                    sourceCenter = { Offset.Zero },
+                    magnifierCenter = { Offset.Unspecified },
+                    zoom = zoom,
+                    onSizeChanged = null,
+                    platformMagnifierFactory = PlatformMagnifierFactory(
+                        platformMagnifier,
+                        canUpdateZoom = true
+                    )
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
+        }
+
+        zoom += 1f
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(2)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_dismissesMagnifier_whenRemovedFromComposition() {
+        var showMagnifier by mutableStateOf(true)
+        val platformMagnifier = CountingPlatformMagnifier()
+        rule.setContent {
+            Box(
+                if (showMagnifier) {
+                    Modifier.magnifier(
+                        sourceCenter = { Offset.Zero },
+                        magnifierCenter = { Offset.Unspecified },
+                        zoom = Float.NaN,
+                        onSizeChanged = null,
+                        platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
+                    )
+                } else {
+                    Modifier
+                }
+            )
+        }
+
+        val initialDismissCount = rule.runOnIdle { platformMagnifier.dismissCount }
+
+        showMagnifier = false
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.dismissCount).isEqualTo(initialDismissCount + 1)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_dismissesMagnifier_whenCenterUnspecified() {
+        // Show the magnifier initially and then hide it, to ensure that it's actually dismissed vs
+        // just never shown.
+        var sourceCenter by mutableStateOf(Offset.Zero)
+        val platformMagnifier = CountingPlatformMagnifier()
+        rule.setContent {
+            Box(
+                Modifier.magnifier(
+                    sourceCenter = { sourceCenter },
+                    magnifierCenter = { Offset.Unspecified },
+                    zoom = Float.NaN,
+                    onSizeChanged = null,
+                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
+        }
+        val initialDismissCount = rule.runOnIdle { platformMagnifier.dismissCount }
+
+        // Now update with an unspecified sourceCenter to hide it.
+        sourceCenter = Offset.Unspecified
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.propertyUpdateCount).isEqualTo(1)
+            assertThat(platformMagnifier.dismissCount).isEqualTo(initialDismissCount + 1)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_dismissesMagnifier_whenMagnifierRecreated() {
+        var elevation by mutableStateOf(1.dp)
+        val platformMagnifier = CountingPlatformMagnifier()
+        rule.setContent {
+            Box(
+                Modifier.magnifier(
+                    sourceCenter = { Offset.Zero },
+                    magnifierCenter = { Offset.Unspecified },
+                    zoom = Float.NaN,
+                    elevation = elevation,
+                    onSizeChanged = null,
+                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
+                )
+            )
+        }
+
+        val initialDismissCount = rule.runOnIdle { platformMagnifier.dismissCount }
+
+        elevation += 1.dp
+
+        rule.runOnIdle {
+            assertThat(platformMagnifier.dismissCount).isEqualTo(initialDismissCount + 1)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_firesOnSizeChanged_initially() {
+        val magnifierSize = IntSize(10, 11)
+        val sizeEvents = mutableListOf<DpSize>()
+        val platformMagnifier = CountingPlatformMagnifier().apply {
+            size = magnifierSize
+        }
+        rule.setContent {
+            Box(
+                Modifier.magnifier(
+                    sourceCenter = { Offset.Zero },
+                    magnifierCenter = { Offset.Unspecified },
+                    zoom = Float.NaN,
+                    onSizeChanged = { sizeEvents += it },
+                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(sizeEvents).containsExactly(
+                with(rule.density) {
+                    magnifierSize.toSize().toDpSize()
+                }
+            )
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_firesOnSizeChanged_initially_whenSourceCenterUnspecified() {
+        val magnifierSize = IntSize(10, 11)
+        val sizeEvents = mutableListOf<DpSize>()
+        val platformMagnifier = CountingPlatformMagnifier().apply {
+            size = magnifierSize
+        }
+        rule.setContent {
+            Box(
+                Modifier.magnifier(
+                    sourceCenter = { Offset.Unspecified },
+                    magnifierCenter = { Offset.Unspecified },
+                    zoom = Float.NaN,
+                    onSizeChanged = { sizeEvents += it },
+                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(sizeEvents).containsExactly(
+                with(rule.density) {
+                    magnifierSize.toSize().toDpSize()
+                }
+            )
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_firesOnSizeChanged_whenNewSizeRequested() {
+        val size1 = IntSize(10, 11)
+        val size2 = size1 * 2
+        var magnifierSize by mutableStateOf(size1)
+        val magnifierDpSize by derivedStateOf {
+            with(rule.density) {
+                magnifierSize.toSize().toDpSize()
+            }
+        }
+        val sizeEvents = mutableListOf<DpSize>()
+        val platformMagnifier = CountingPlatformMagnifier().apply {
+            size = magnifierSize
+        }
+        rule.setContent {
+            Box(
+                Modifier.magnifier(
+                    sourceCenter = { Offset.Zero },
+                    magnifierCenter = { Offset.Unspecified },
+                    zoom = Float.NaN,
+                    size = magnifierDpSize,
+                    onSizeChanged = { sizeEvents += it },
+                    platformMagnifierFactory = PlatformMagnifierFactory(platformMagnifier)
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            // Need to update the fake magnifier so it reports the right size when asked…
+            platformMagnifier.size = size2
+            // …and update the mutable state to trigger a recomposition.
+            magnifierSize = size2
+        }
+
+        rule.runOnIdle {
+            assertThat(sizeEvents).containsExactlyElementsIn(
+                listOf(size1, size2).map {
+                    with(rule.density) { it.toSize().toDpSize() }
+                }
+            ).inOrder()
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun platformMagnifierModifier_reportsSemantics() {
+        var magnifierOffset by mutableStateOf(Offset.Zero)
+        rule.setContent {
+            Box(Modifier.magnifier(sourceCenter = { magnifierOffset }))
+        }
+        val getPosition = rule.onNode(keyIsDefined(MagnifierPositionInRoot))
+            .fetchSemanticsNode()
+            .config[MagnifierPositionInRoot]
+
+        rule.runOnIdle {
+            assertThat(getPosition()).isEqualTo(magnifierOffset)
+        }
+
+        // Move the modifier, same function should return new value.
+        magnifierOffset = Offset(42f, 24f)
+
+        rule.runOnIdle {
+            assertThat(getPosition()).isEqualTo(magnifierOffset)
+        }
+    }
+
+    private fun PlatformMagnifierFactory(
+        platformMagnifier: PlatformMagnifier,
+        canUpdateZoom: Boolean = false
+    ) = object : PlatformMagnifierFactory {
+        override val canUpdateZoom: Boolean = canUpdateZoom
+        override fun create(
+            view: View,
+            useTextDefault: Boolean,
+            size: DpSize,
+            cornerRadius: Dp,
+            elevation: Dp,
+            clippingEnabled: Boolean,
+            density: Density,
+            initialZoom: Float
+        ): PlatformMagnifier {
+            return platformMagnifier
+        }
+    }
+
+    private fun Modifier.findInspectableValue(): InspectableValue? =
+        foldIn<InspectableValue?>(null) { acc, element -> acc ?: element as? InspectableValue }
+
+    private class CountingPlatformMagnifierFactory(
+        override val canUpdateZoom: Boolean = false
+    ) : PlatformMagnifierFactory {
+        var creationCount = 0
+
+        override fun create(
+            view: View,
+            useTextDefault: Boolean,
+            size: DpSize,
+            cornerRadius: Dp,
+            elevation: Dp,
+            clippingEnabled: Boolean,
+            density: Density,
+            initialZoom: Float
+        ): PlatformMagnifier {
+            creationCount++
+            return NoopPlatformMagnifier
+        }
+    }
+
+    private object NoopPlatformMagnifier : PlatformMagnifier {
+        override val size: IntSize = IntSize.Zero
+
+        override fun updateContent() {
+        }
+
+        override fun update(
+            sourceCenter: Offset,
+            magnifierCenter: Offset,
+            zoom: Float
+        ) {
+        }
+
+        override fun dismiss() {
+        }
+    }
+
+    private class CountingPlatformMagnifier : PlatformMagnifier {
+        var contentUpdateCount = 0
+        var propertyUpdateCount = 0
+        var dismissCount = 0
+
+        override var size: IntSize = IntSize.Zero
+
+        override fun updateContent() {
+            contentUpdateCount++
+        }
+
+        override fun update(
+            sourceCenter: Offset,
+            magnifierCenter: Offset,
+            zoom: Float
+        ) {
+            propertyUpdateCount++
+        }
+
+        override fun dismiss() {
+            dismissCount++
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollScreenshotTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollScreenshotTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollScreenshotTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollScreenshotTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/PlatformMagnifierTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/PlatformMagnifierTest.kt
new file mode 100644
index 0000000..b2c7a87
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/PlatformMagnifierTest.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2021 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
+
+import android.graphics.Point
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.center
+import androidx.compose.ui.unit.toOffset
+import androidx.compose.ui.unit.toSize
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class PlatformMagnifierTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @SdkSuppress(minSdkVersion = 29)
+    @Test
+    fun androidPlatformMagnifier_showsMagnifier() {
+        val magnifier = createAndroidPlatformMagnifier()
+        rule.runOnIdle {
+            magnifier.update(
+                sourceCenter = Offset.Zero,
+                magnifierCenter = Offset.Unspecified,
+                zoom = Float.NaN
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(magnifier.magnifier.position).isEqualTo(Point(0, 0))
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @Test
+    fun androidPlatformMagnifier_updatesZoom_whenValid() {
+        val magnifier = createAndroidPlatformMagnifier()
+        rule.runOnIdle {
+            magnifier.update(
+                sourceCenter = Offset.Zero,
+                magnifierCenter = Offset.Unspecified,
+                zoom = 1f
+            )
+
+            assertThat(magnifier.magnifier.zoom).isEqualTo(1f)
+
+            magnifier.update(
+                sourceCenter = Offset.Zero,
+                magnifierCenter = Offset.Unspecified,
+                zoom = 2f
+            )
+
+            assertThat(magnifier.magnifier.zoom).isEqualTo(2f)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @Test
+    fun androidPlatformMagnifier_doesNotUpdateZoom_whenNaN() {
+        val magnifier = createAndroidPlatformMagnifier()
+        rule.runOnIdle {
+            magnifier.update(
+                sourceCenter = Offset.Zero,
+                magnifierCenter = Offset.Unspecified,
+                zoom = 1f
+            )
+
+            assertThat(magnifier.magnifier.zoom).isEqualTo(1f)
+
+            magnifier.update(
+                sourceCenter = Offset.Zero,
+                magnifierCenter = Offset.Unspecified,
+                zoom = Float.NaN
+            )
+
+            assertThat(magnifier.magnifier.zoom).isEqualTo(1f)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @Test
+    fun androidPlatformMagnifier_specifiesMagnifierCenter_whenSpecified() {
+        val magnifier = createAndroidPlatformMagnifier()
+        rule.runOnIdle {
+            magnifier.update(
+                sourceCenter = Offset.Zero,
+                magnifierCenter = VIEW_SIZE.center.toOffset(),
+                zoom = Float.NaN
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(magnifier.magnifier.sourcePosition).isEqualTo(Point(0, 0))
+            // position is the top-left of the magnifier so we need to offset it.
+            assertThat(magnifier.magnifier.position!!.x + magnifier.magnifier.width / 2)
+                .isEqualTo(VIEW_SIZE.center.x)
+            assertThat(magnifier.magnifier.position!!.y + magnifier.magnifier.height / 2)
+                .isEqualTo(VIEW_SIZE.center.y)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @Test
+    fun androidPlatformMagnifier_doesNotSpecifyMagnifierCenter_whenNotSpecified() {
+        // To avoid making this test depend on the actual default offset the framework happens to
+        // use for the magnifier, we just record the first magnifier position after placing the
+        // source, then move the source and check the new position and assert that it moved the
+        // same amount.
+        val magnifierDelta = IntOffset(10, 10)
+        val magnifier = createAndroidPlatformMagnifier()
+        rule.runOnIdle {
+            magnifier.update(
+                sourceCenter = VIEW_SIZE.center.toOffset(),
+                magnifierCenter = Offset.Unspecified,
+                zoom = Float.NaN
+            )
+            val initialMagnifierPosition = magnifier.magnifier.position!!.toIntOffset()
+
+            magnifier.update(
+                sourceCenter = (VIEW_SIZE.center + magnifierDelta).toOffset(),
+                magnifierCenter = Offset.Unspecified,
+                zoom = Float.NaN
+            )
+
+            assertThat(magnifier.magnifier.position!!.toIntOffset())
+                .isEqualTo(initialMagnifierPosition + magnifierDelta)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun androidPlatformMagnifier_returnsDefaultSize() {
+        val magnifier = createAndroidPlatformMagnifier()
+        assertThat(magnifier.size.width).isGreaterThan(0)
+        assertThat(magnifier.size.height).isGreaterThan(0)
+    }
+
+    // Size is only configurable on 29+
+    @SdkSuppress(minSdkVersion = 29)
+    @Test
+    fun androidPlatformMagnifier_usesRequestedSize() {
+        val magnifierSize = IntSize(10, 11)
+        val magnifier = with(rule.density) {
+            createAndroidPlatformMagnifier(size = magnifierSize.toSize().toDpSize())
+        }
+        assertThat(magnifier.size).isEqualTo(magnifierSize)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.P)
+    private fun createAndroidPlatformMagnifier(
+        size: DpSize = DpSize.Unspecified
+    ): PlatformMagnifierFactoryApi28Impl.PlatformMagnifierImpl {
+        lateinit var magnifier: PlatformMagnifierFactoryApi28Impl.PlatformMagnifierImpl
+        rule.setContent {
+            val dpSize = with(LocalDensity.current) { VIEW_SIZE.toSize().toDpSize() }
+            // Force the view to measure to non-zero size to give the magnifier room to show.
+            Box(Modifier.requiredSize(dpSize)) {
+                val currentView = LocalView.current
+                val density = LocalDensity.current
+
+                DisposableEffect(Unit) {
+                    magnifier = PlatformMagnifierFactory.getForCurrentPlatform().create(
+                        view = currentView,
+                        density = density,
+                        initialZoom = Float.NaN,
+                        useTextDefault = false,
+                        size = size,
+                        cornerRadius = Dp.Unspecified,
+                        elevation = Dp.Unspecified,
+                        clippingEnabled = true,
+                    ) as PlatformMagnifierFactoryApi28Impl.PlatformMagnifierImpl
+                    onDispose {}
+                }
+            }
+        }
+        return rule.runOnIdle { magnifier }
+    }
+
+    private companion object {
+        val VIEW_SIZE = IntSize(500, 500)
+
+        fun Point.toIntOffset() = IntOffset(x, y)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/PreferKeepClearTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/PreferKeepClearTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/PreferKeepClearTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/PreferKeepClearTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ProgressSemanticsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ProgressSemanticsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ProgressSemanticsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ProgressSemanticsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollAccessibilityTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollAccessibilityTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollAccessibilityTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollAccessibilityTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
new file mode 100644
index 0000000..e05e7d7
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -0,0 +1,3031 @@
+/*
+ * Copyright 2020 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
+
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.rememberSplineBasedDecay
+import androidx.compose.foundation.gestures.DefaultFlingBehavior
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.ModifierLocalScrollableContainer
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.interaction.DragInteraction
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.currentComposer
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.first
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.MotionDurationScale
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.util.VelocityTracker
+import androidx.compose.ui.input.pointer.util.VelocityTrackerAddPointsFix
+import androidx.compose.ui.materialize
+import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.platform.AbstractComposeView
+import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.ScrollWheel
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.performMouseInput
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.test.swipe
+import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastForEach
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.CoordinatesProvider
+import androidx.test.espresso.action.GeneralLocation
+import androidx.test.espresso.action.GeneralSwipeAction
+import androidx.test.espresso.action.Press
+import androidx.test.espresso.action.Swipe
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.math.abs
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.instanceOf
+import org.junit.After
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ScrollableTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val scrollableBoxTag = "scrollableBox"
+
+    private lateinit var scope: CoroutineScope
+
+    private fun ComposeContentTestRule.setContentAndGetScope(content: @Composable () -> Unit) {
+        setContent {
+            val actualScope = rememberCoroutineScope()
+            SideEffect { scope = actualScope }
+            content()
+        }
+    }
+
+    @Before
+    fun before() {
+        isDebugInspectorInfoEnabled = true
+    }
+
+    @After
+    fun after() {
+        isDebugInspectorInfoEnabled = false
+    }
+
+    @Test
+    fun scrollable_horizontalScroll() {
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        setScrollableContent {
+            Modifier.scrollable(
+                state = controller,
+                orientation = Orientation.Horizontal
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x + 100f, this.center.y),
+                durationMillis = 100
+            )
+        }
+
+        val lastTotal = rule.runOnIdle {
+            assertThat(total).isGreaterThan(0)
+            total
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x, this.center.y + 100f),
+                durationMillis = 100
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(total).isEqualTo(lastTotal)
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x - 100f, this.center.y),
+                durationMillis = 100
+            )
+        }
+        rule.runOnIdle {
+            assertThat(total).isLessThan(0.01f)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun scrollable_horizontalScroll_mouseWheel() {
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        setScrollableContent {
+            Modifier.scrollable(
+                state = controller,
+                orientation = Orientation.Horizontal
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(-100f, ScrollWheel.Horizontal)
+        }
+
+        val lastTotal = rule.runOnIdle {
+            assertThat(total).isGreaterThan(0)
+            total
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(-100f, ScrollWheel.Vertical)
+        }
+
+        rule.runOnIdle {
+            assertThat(total).isEqualTo(lastTotal)
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(100f, ScrollWheel.Horizontal)
+        }
+        rule.runOnIdle {
+            assertThat(total).isLessThan(0.01f)
+        }
+    }
+
+    /*
+     * Note: For keyboard scrolling to work (that is, scrolling based on the page up/down keys),
+     * at least one child within the scrollable must be focusable. (This matches the behavior in
+     * Views.)
+     */
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun scrollable_horizontalScroll_keyboardPageUpAndDown() {
+        var scrollAmount = 0f
+
+        val scrollableState = ScrollableState(
+            consumeScrollDelta = {
+                scrollAmount += it
+                it
+            }
+        )
+
+        rule.setContent {
+            Row(
+                Modifier
+                    .fillMaxHeight()
+                    .wrapContentWidth()
+                    .background(Color.Red)
+                    .scrollable(
+                        state = scrollableState,
+                        orientation = Orientation.Horizontal
+                    )
+                    .padding(10.dp)
+            ) {
+                Box(
+                    modifier = Modifier
+                        .fillMaxHeight()
+                        .testTag(scrollableBoxTag)
+                        .width(50.dp)
+                        .background(Color.Blue)
+                        // Required for keyboard scrolling (page up/down keys) to work.
+                        .focusable()
+                        .padding(10.dp)
+                )
+
+                Spacer(modifier = Modifier.size(10.dp))
+
+                for (i in 0 until 40) {
+                    val color = if (i % 2 == 0) {
+                        Color.Yellow
+                    } else {
+                        Color.Green
+                    }
+
+                    Box(
+                        modifier = Modifier
+                            .fillMaxHeight()
+                            .width(50.dp)
+                            .background(color)
+                            .padding(10.dp)
+                    )
+                    Spacer(modifier = Modifier.size(10.dp))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag).requestFocus()
+        rule.onNodeWithTag(scrollableBoxTag).performKeyInput {
+            pressKey(Key.PageDown)
+        }
+
+        rule.runOnIdle {
+            assertThat(scrollAmount).isLessThan(0f)
+        }
+
+        scrollAmount = 0f
+
+        rule.onNodeWithTag(scrollableBoxTag).performKeyInput {
+            pressKey(Key.PageUp)
+        }
+
+        rule.runOnIdle {
+            assertThat(scrollAmount).isGreaterThan(0f)
+        }
+    }
+
+    @Test
+    fun scrollable_horizontalScroll_reverse() {
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        setScrollableContent {
+            Modifier.scrollable(
+                reverseDirection = true,
+                state = controller,
+                orientation = Orientation.Horizontal
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x + 100f, this.center.y),
+                durationMillis = 100
+            )
+        }
+
+        val lastTotal = rule.runOnIdle {
+            assertThat(total).isLessThan(0)
+            total
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x, this.center.y + 100f),
+                durationMillis = 100
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(total).isEqualTo(lastTotal)
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x - 100f, this.center.y),
+                durationMillis = 100
+            )
+        }
+        rule.runOnIdle {
+            assertThat(total).isLessThan(0.01f)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun scrollable_horizontalScroll_reverse_mouseWheel() {
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        setScrollableContent {
+            Modifier.scrollable(
+                reverseDirection = true,
+                state = controller,
+                orientation = Orientation.Horizontal
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(-100f, ScrollWheel.Horizontal)
+        }
+
+        val lastTotal = rule.runOnIdle {
+            assertThat(total).isLessThan(0)
+            total
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(-100f, ScrollWheel.Vertical)
+        }
+
+        rule.runOnIdle {
+            assertThat(total).isEqualTo(lastTotal)
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(100f, ScrollWheel.Horizontal)
+        }
+        rule.runOnIdle {
+            assertThat(total).isLessThan(0.01f)
+        }
+    }
+
+    @Test
+    fun scrollable_verticalScroll() {
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        setScrollableContent {
+            Modifier.scrollable(
+                state = controller,
+                orientation = Orientation.Vertical
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x, this.center.y + 100f),
+                durationMillis = 100
+            )
+        }
+
+        val lastTotal = rule.runOnIdle {
+            assertThat(total).isGreaterThan(0)
+            total
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x + 100f, this.center.y),
+                durationMillis = 100
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(total).isEqualTo(lastTotal)
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x, this.center.y - 100f),
+                durationMillis = 100
+            )
+        }
+        rule.runOnIdle {
+            assertThat(total).isLessThan(0.01f)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun scrollable_verticalScroll_mouseWheel() {
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        setScrollableContent {
+            Modifier.scrollable(
+                state = controller,
+                orientation = Orientation.Vertical
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(-100f, ScrollWheel.Vertical)
+        }
+
+        val lastTotal = rule.runOnIdle {
+            assertThat(total).isGreaterThan(0)
+            total
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(-100f, ScrollWheel.Horizontal)
+        }
+
+        rule.runOnIdle {
+            assertThat(total).isEqualTo(lastTotal)
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(100f, ScrollWheel.Vertical)
+        }
+        rule.runOnIdle {
+            assertThat(total).isLessThan(0.01f)
+        }
+    }
+
+    /*
+     * Note: For keyboard scrolling to work (that is, scrolling based on the page up/down keys),
+     * at least one child within the scrollable must be focusable. (This matches the behavior in
+     * Views.)
+     */
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun scrollable_verticalScroll_keyboardPageUpAndDown() {
+        var scrollAmount = 0f
+
+        val scrollableState = ScrollableState(
+            consumeScrollDelta = {
+                scrollAmount += it
+                it
+            }
+        )
+
+        rule.setContent {
+            Column(
+                Modifier
+                    .fillMaxWidth()
+                    .background(Color.Red)
+                    .scrollable(
+                        state = scrollableState,
+                        orientation = Orientation.Vertical
+                    )
+                    .padding(10.dp)
+            ) {
+                Box(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .testTag(scrollableBoxTag)
+                        .height(50.dp)
+                        .background(Color.Blue)
+                        // Required for keyboard scrolling (page up/down keys) to work.
+                        .focusable()
+                        .padding(10.dp)
+                )
+
+                Spacer(modifier = Modifier.size(10.dp))
+
+                for (i in 0 until 40) {
+                    val color = if (i % 2 == 0) {
+                        Color.Yellow
+                    } else {
+                        Color.Green
+                    }
+
+                    Box(
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .height(50.dp)
+                            .background(color)
+                            .padding(10.dp)
+                    )
+                    Spacer(modifier = Modifier.size(10.dp))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag).requestFocus()
+        rule.onNodeWithTag(scrollableBoxTag).performKeyInput {
+            pressKey(Key.PageDown)
+        }
+
+        rule.runOnIdle {
+            assertThat(scrollAmount).isLessThan(0f)
+        }
+
+        scrollAmount = 0f
+
+        rule.onNodeWithTag(scrollableBoxTag).performKeyInput {
+            pressKey(Key.PageUp)
+        }
+
+        rule.runOnIdle {
+            assertThat(scrollAmount).isGreaterThan(0f)
+        }
+    }
+
+    @Test
+    fun scrollable_verticalScroll_reversed() {
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        setScrollableContent {
+            Modifier.scrollable(
+                reverseDirection = true,
+                state = controller,
+                orientation = Orientation.Vertical
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x, this.center.y + 100f),
+                durationMillis = 100
+            )
+        }
+
+        val lastTotal = rule.runOnIdle {
+            assertThat(total).isLessThan(0)
+            total
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x + 100f, this.center.y),
+                durationMillis = 100
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(total).isEqualTo(lastTotal)
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x, this.center.y - 100f),
+                durationMillis = 100
+            )
+        }
+        rule.runOnIdle {
+            assertThat(total).isLessThan(0.01f)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun scrollable_verticalScroll_reversed_mouseWheel() {
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        setScrollableContent {
+            Modifier.scrollable(
+                reverseDirection = true,
+                state = controller,
+                orientation = Orientation.Vertical
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(-100f, ScrollWheel.Vertical)
+        }
+
+        val lastTotal = rule.runOnIdle {
+            assertThat(total).isLessThan(0)
+            total
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(-100f, ScrollWheel.Horizontal)
+        }
+
+        rule.runOnIdle {
+            assertThat(total).isEqualTo(lastTotal)
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(100f, ScrollWheel.Vertical)
+        }
+        rule.runOnIdle {
+            assertThat(total).isLessThan(0.01f)
+        }
+    }
+
+    @Test
+    fun scrollable_disabledWontCallLambda() {
+        val enabled = mutableStateOf(true)
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        setScrollableContent {
+            Modifier.scrollable(
+                state = controller,
+                orientation = Orientation.Horizontal,
+                enabled = enabled.value
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x + 100f, this.center.y),
+                durationMillis = 100
+            )
+        }
+        val prevTotal = rule.runOnIdle {
+            assertThat(total).isGreaterThan(0f)
+            enabled.value = false
+            total
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x + 100f, this.center.y),
+                durationMillis = 100
+            )
+        }
+        rule.runOnIdle {
+            assertThat(total).isEqualTo(prevTotal)
+        }
+    }
+
+    @Test
+    fun scrollable_startWithoutSlop_ifFlinging() {
+        rule.mainClock.autoAdvance = false
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        setScrollableContent {
+            Modifier.scrollable(
+                state = controller,
+                orientation = Orientation.Horizontal
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            swipeWithVelocity(
+                start = this.center,
+                end = Offset(this.center.x + 200f, this.center.y),
+                durationMillis = 100,
+                endVelocity = 4000f
+            )
+        }
+        assertThat(total).isGreaterThan(0f)
+        val prev = total
+        // pump frames twice to start fling animation
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeByFrame()
+        val prevAfterSomeFling = total
+        assertThat(prevAfterSomeFling).isGreaterThan(prev)
+        // don't advance main clock anymore since we're in the middle of the fling. Now interrupt
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            down(this.center)
+            moveBy(Offset(115f, 0f))
+            up()
+        }
+        val expected = prevAfterSomeFling + 115
+        assertThat(total).isEqualTo(expected)
+    }
+
+    @Test
+    fun scrollable_blocksDownEvents_ifFlingingCaught() {
+        rule.mainClock.autoAdvance = false
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        rule.setContent {
+            Box {
+                Box(
+                    contentAlignment = Alignment.Center,
+                    modifier = Modifier
+                        .size(300.dp)
+                        .scrollable(
+                            orientation = Orientation.Horizontal,
+                            state = controller
+                        )
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .size(300.dp)
+                            .testTag(scrollableBoxTag)
+                            .clickable {
+                                assertWithMessage("Clickable shouldn't click when fling caught")
+                                    .fail()
+                            }
+                    )
+                }
+            }
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            swipeWithVelocity(
+                start = this.center,
+                end = Offset(this.center.x + 200f, this.center.y),
+                durationMillis = 100,
+                endVelocity = 4000f
+            )
+        }
+        assertThat(total).isGreaterThan(0f)
+        val prev = total
+        // pump frames twice to start fling animation
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeByFrame()
+        val prevAfterSomeFling = total
+        assertThat(prevAfterSomeFling).isGreaterThan(prev)
+        // don't advance main clock anymore since we're in the middle of the fling. Now interrupt
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            down(this.center)
+            up()
+        }
+        // shouldn't assert in clickable lambda
+    }
+
+    @Test
+    fun scrollable_snappingScrolling() {
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        setScrollableContent {
+            Modifier.scrollable(
+                orientation = Orientation.Vertical,
+                state = controller
+            )
+        }
+        rule.waitForIdle()
+        assertThat(total).isEqualTo(0f)
+
+        scope.launch {
+            controller.animateScrollBy(1000f)
+        }
+        rule.waitForIdle()
+        assertThat(total).isWithin(0.001f).of(1000f)
+
+        scope.launch {
+            controller.animateScrollBy(-200f)
+        }
+        rule.waitForIdle()
+        assertThat(total).isWithin(0.001f).of(800f)
+    }
+
+    @Test
+    fun scrollable_explicitDisposal() {
+        rule.mainClock.autoAdvance = false
+        val emit = mutableStateOf(true)
+        val expectEmission = mutableStateOf(true)
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                assertWithMessage("Animating after dispose!").that(expectEmission.value).isTrue()
+                total += it
+                it
+            }
+        )
+        setScrollableContent {
+            if (emit.value) {
+                Modifier.scrollable(
+                    orientation = Orientation.Horizontal,
+                    state = controller
+                )
+            } else {
+                Modifier
+            }
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipeWithVelocity(
+                start = this.center,
+                end = Offset(this.center.x + 200f, this.center.y),
+                durationMillis = 100,
+                endVelocity = 4000f
+            )
+        }
+        assertThat(total).isGreaterThan(0f)
+
+        // start the fling for a few frames
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeByFrame()
+        // flip the emission
+        rule.runOnUiThread {
+            emit.value = false
+        }
+        // propagate the emit flip and record the value
+        rule.mainClock.advanceTimeByFrame()
+        val prevTotal = total
+        // make sure we don't receive any deltas
+        rule.runOnUiThread {
+            expectEmission.value = false
+        }
+
+        // pump the clock until idle
+        rule.mainClock.autoAdvance = true
+        rule.waitForIdle()
+
+        // still same and didn't fail in onScrollConsumptionRequested.. lambda
+        assertThat(total).isEqualTo(prevTotal)
+    }
+
+    @Test
+    fun scrollable_nestedDrag() {
+        var innerDrag = 0f
+        var outerDrag = 0f
+        val outerState = ScrollableState(
+            consumeScrollDelta = {
+                outerDrag += it
+                it
+            }
+        )
+        val innerState = ScrollableState(
+            consumeScrollDelta = {
+                innerDrag += it / 2
+                it / 2
+            }
+        )
+
+        rule.setContentAndGetScope {
+            Box {
+                Box(
+                    contentAlignment = Alignment.Center,
+                    modifier = Modifier
+                        .size(300.dp)
+                        .scrollable(
+                            state = outerState,
+                            orientation = Orientation.Horizontal
+                        )
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .testTag(scrollableBoxTag)
+                            .size(300.dp)
+                            .scrollable(
+                                state = innerState,
+                                orientation = Orientation.Horizontal
+                            )
+                    )
+                }
+            }
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipeWithVelocity(
+                start = this.center,
+                end = Offset(this.center.x + 200f, this.center.y),
+                durationMillis = 300,
+                endVelocity = 0f
+            )
+        }
+        val lastEqualDrag = rule.runOnIdle {
+            assertThat(innerDrag).isGreaterThan(0f)
+            assertThat(outerDrag).isGreaterThan(0f)
+            // we consumed half delta in child, so exactly half should go to the parent
+            assertThat(outerDrag).isEqualTo(innerDrag)
+            innerDrag
+        }
+        rule.runOnIdle {
+            // values should be the same since no fling
+            assertThat(innerDrag).isEqualTo(lastEqualDrag)
+            assertThat(outerDrag).isEqualTo(lastEqualDrag)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun scrollable_nestedScroll_childPartialConsumptionForMouseWheel() {
+        var innerDrag = 0f
+        var outerDrag = 0f
+        val outerState = ScrollableState(
+            consumeScrollDelta = {
+                // Since the child has already consumed half, the parent will consume the rest.
+                outerDrag += it
+                it
+            }
+        )
+        val innerState = ScrollableState(
+            consumeScrollDelta = {
+                // Child consumes half, leaving the rest for the parent to consume.
+                innerDrag += it / 2
+                it / 2
+            }
+        )
+
+        rule.setContentAndGetScope {
+            Box {
+                Box(
+                    contentAlignment = Alignment.Center,
+                    modifier = Modifier
+                        .size(300.dp)
+                        .scrollable(
+                            state = outerState,
+                            orientation = Orientation.Horizontal
+                        )
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .testTag(scrollableBoxTag)
+                            .size(300.dp)
+                            .scrollable(
+                                state = innerState,
+                                orientation = Orientation.Horizontal
+                            )
+                    )
+                }
+            }
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performMouseInput {
+            this.scroll(-200f, ScrollWheel.Horizontal)
+        }
+        rule.runOnIdle {
+            assertThat(innerDrag).isGreaterThan(0f)
+            assertThat(outerDrag).isGreaterThan(0f)
+            // Since child (inner) consumes half of the scroll, the parent (outer) consumes the
+            // remainder (which is half as well), so they will be equal.
+            assertThat(innerDrag).isEqualTo(outerDrag)
+            innerDrag
+        }
+    }
+
+    /*
+     * Note: For keyboard scrolling to work (that is, scrolling based on the page up/down keys),
+     * at least one child within the scrollable must be focusable. (This matches the behavior in
+     * Views.)
+     */
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun scrollable_nestedScroll_childPartialConsumptionForKeyboardPageUpAndDown() {
+        var innerDrag = 0f
+        var outerDrag = 0f
+        val outerState = ScrollableState(
+            consumeScrollDelta = {
+                // Since the child has already consumed half, the parent will consume the rest.
+                outerDrag += it
+                it
+            }
+        )
+        val innerState = ScrollableState(
+            consumeScrollDelta = {
+                // Child consumes half, leaving the rest for the parent to consume.
+                innerDrag += it / 2
+                it / 2
+            }
+        )
+
+        rule.setContent {
+            Box {
+                Box(
+                    contentAlignment = Alignment.Center,
+                    modifier = Modifier
+                        .size(300.dp)
+                        .scrollable(
+                            state = outerState,
+                            orientation = Orientation.Horizontal
+                        )
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .size(300.dp)
+                            .scrollable(
+                                state = innerState,
+                                orientation = Orientation.Horizontal
+                            )
+                    ) {
+                        Box(
+                            modifier = Modifier
+                                .testTag(scrollableBoxTag)
+                                // Required for keyboard scrolling (page up/down keys) to work.
+                                .focusable()
+                                .size(300.dp)
+                        )
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag).requestFocus()
+        rule.onNodeWithTag(scrollableBoxTag).performKeyInput {
+            pressKey(Key.PageDown)
+        }
+
+        rule.runOnIdle {
+            assertThat(outerDrag).isLessThan(0f)
+            assertThat(innerDrag).isLessThan(0f)
+            // Since child (inner) consumes half of the scroll, the parent (outer) consumes the
+            // remainder (which is half as well), so they will be equal.
+            assertThat(innerDrag).isEqualTo(outerDrag)
+        }
+
+        outerDrag = 0f
+        innerDrag = 0f
+
+        rule.onNodeWithTag(scrollableBoxTag).performKeyInput {
+            pressKey(Key.PageUp)
+        }
+
+        rule.runOnIdle {
+            assertThat(outerDrag).isGreaterThan(0f)
+            assertThat(innerDrag).isGreaterThan(0f)
+            // Since child (inner) consumes half of the scroll, the parent (outer) consumes the
+            // remainder (which is half as well), so they will be equal.
+            assertThat(innerDrag).isEqualTo(outerDrag)
+        }
+    }
+
+    @Test
+    fun scrollable_nestedFling() {
+        var innerDrag = 0f
+        var outerDrag = 0f
+        val outerState = ScrollableState(
+            consumeScrollDelta = {
+                outerDrag += it
+                it
+            }
+        )
+        val innerState = ScrollableState(
+            consumeScrollDelta = {
+                innerDrag += it / 2
+                it / 2
+            }
+        )
+
+        rule.setContentAndGetScope {
+            Box {
+                Box(
+                    contentAlignment = Alignment.Center,
+                    modifier = Modifier
+                        .size(300.dp)
+                        .scrollable(
+                            state = outerState,
+                            orientation = Orientation.Horizontal
+                        )
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .testTag(scrollableBoxTag)
+                            .size(300.dp)
+                            .scrollable(
+                                state = innerState,
+                                orientation = Orientation.Horizontal
+                            )
+                    )
+                }
+            }
+        }
+
+        // swipe again with velocity
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x + 200f, this.center.y),
+                durationMillis = 300
+            )
+        }
+        assertThat(innerDrag).isGreaterThan(0f)
+        assertThat(outerDrag).isGreaterThan(0f)
+        // we consumed half delta in child, so exactly half should go to the parent
+        assertThat(outerDrag).isEqualTo(innerDrag)
+        val lastEqualDrag = innerDrag
+        rule.runOnIdle {
+            assertThat(innerDrag).isGreaterThan(lastEqualDrag)
+            assertThat(outerDrag).isGreaterThan(lastEqualDrag)
+        }
+    }
+
+    @Test
+    fun scrollable_nestedScrollAbove_respectsPreConsumption() {
+        var value = 0f
+        var lastReceivedPreScrollAvailable = 0f
+        val preConsumeFraction = 0.7f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                val expected = lastReceivedPreScrollAvailable * (1 - preConsumeFraction)
+                assertThat(it - expected).isWithin(0.01f)
+                value += it
+                it
+            }
+        )
+        val preConsumingParent = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                lastReceivedPreScrollAvailable = available.x
+                return available * preConsumeFraction
+            }
+
+            override suspend fun onPreFling(available: Velocity): Velocity {
+                // consume all velocity
+                return available
+            }
+        }
+
+        rule.setContentAndGetScope {
+            Box {
+                Box(
+                    contentAlignment = Alignment.Center,
+                    modifier = Modifier
+                        .size(300.dp)
+                        .nestedScroll(preConsumingParent)
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .size(300.dp)
+                            .testTag(scrollableBoxTag)
+                            .scrollable(
+                                state = controller,
+                                orientation = Orientation.Horizontal
+                            )
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipe(
+                start = this.center,
+                end = Offset(this.center.x + 200f, this.center.y),
+                durationMillis = 300
+            )
+        }
+
+        val preFlingValue = rule.runOnIdle { value }
+        rule.runOnIdle {
+            // if scrollable respects pre-fling consumption, it should fling 0px since we
+            // pre-consume all
+            assertThat(preFlingValue).isEqualTo(value)
+        }
+    }
+
+    @Test
+    fun scrollable_nestedScrollAbove_proxiesPostCycles() {
+        var value = 0f
+        var expectedLeft = 0f
+        val velocityFlung = 5000f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                val toConsume = it * 0.345f
+                value += toConsume
+                expectedLeft = it - toConsume
+                toConsume
+            }
+        )
+        val parent = object : NestedScrollConnection {
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                // we should get in post scroll as much as left in controller callback
+                assertThat(available.x).isEqualTo(expectedLeft)
+                return if (source == NestedScrollSource.Fling) Offset.Zero else available
+            }
+
+            override suspend fun onPostFling(
+                consumed: Velocity,
+                available: Velocity
+            ): Velocity {
+                val expected = velocityFlung - consumed.x
+                assertThat(consumed.x).isLessThan(velocityFlung)
+                assertThat(abs(available.x - expected)).isLessThan(0.1f)
+                return available
+            }
+        }
+
+        rule.setContentAndGetScope {
+            Box {
+                Box(
+                    contentAlignment = Alignment.Center,
+                    modifier = Modifier
+                        .size(300.dp)
+                        .nestedScroll(parent)
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .size(300.dp)
+                            .testTag(scrollableBoxTag)
+                            .scrollable(
+                                state = controller,
+                                orientation = Orientation.Horizontal
+                            )
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipeWithVelocity(
+                start = this.center,
+                end = Offset(this.center.x + 500f, this.center.y),
+                durationMillis = 300,
+                endVelocity = velocityFlung
+            )
+        }
+
+        // all assertions in callback above
+        rule.waitForIdle()
+    }
+
+    @Test
+    fun scrollable_nestedScrollAbove_reversed_proxiesPostCycles() {
+        var value = 0f
+        var expectedLeft = 0f
+        val velocityFlung = 5000f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                val toConsume = it * 0.345f
+                value += toConsume
+                expectedLeft = it - toConsume
+                toConsume
+            }
+        )
+        val parent = object : NestedScrollConnection {
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                // we should get in post scroll as much as left in controller callback
+                assertThat(available.x).isEqualTo(-expectedLeft)
+                return if (source == NestedScrollSource.Fling) Offset.Zero else available
+            }
+
+            override suspend fun onPostFling(
+                consumed: Velocity,
+                available: Velocity
+            ): Velocity {
+                val expected = velocityFlung - consumed.x
+                assertThat(consumed.x).isLessThan(velocityFlung)
+                assertThat(abs(available.x - expected)).isLessThan(0.1f)
+                return available
+            }
+        }
+
+        rule.setContentAndGetScope {
+            Box {
+                Box(
+                    contentAlignment = Alignment.Center,
+                    modifier = Modifier
+                        .size(300.dp)
+                        .nestedScroll(parent)
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .size(300.dp)
+                            .testTag(scrollableBoxTag)
+                            .scrollable(
+                                state = controller,
+                                reverseDirection = true,
+                                orientation = Orientation.Horizontal
+                            )
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipeWithVelocity(
+                start = this.center,
+                end = Offset(this.center.x + 500f, this.center.y),
+                durationMillis = 300,
+                endVelocity = velocityFlung
+            )
+        }
+
+        // all assertions in callback above
+        rule.waitForIdle()
+    }
+
+    @Test
+    fun scrollable_nestedScrollBelow_listensDispatches() {
+        var value = 0f
+        var expectedConsumed = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                expectedConsumed = it * 0.3f
+                value += expectedConsumed
+                expectedConsumed
+            }
+        )
+        val child = object : NestedScrollConnection {}
+        val dispatcher = NestedScrollDispatcher()
+
+        rule.setContentAndGetScope {
+            Box {
+                Box(
+                    modifier = Modifier
+                        .size(300.dp)
+                        .scrollable(
+                            state = controller,
+                            orientation = Orientation.Horizontal
+                        )
+                ) {
+                    Box(
+                        Modifier
+                            .size(200.dp)
+                            .testTag(scrollableBoxTag)
+                            .nestedScroll(child, dispatcher)
+                    )
+                }
+            }
+        }
+
+        val lastValueBeforeFling = rule.runOnIdle {
+            val preScrollConsumed = dispatcher
+                .dispatchPreScroll(Offset(20f, 20f), NestedScrollSource.Drag)
+            // scrollable is not interested in pre scroll
+            assertThat(preScrollConsumed).isEqualTo(Offset.Zero)
+
+            val consumed = dispatcher.dispatchPostScroll(
+                Offset(20f, 20f),
+                Offset(50f, 50f),
+                NestedScrollSource.Drag
+            )
+            assertThat(consumed.x - expectedConsumed).isWithin(0.001f)
+            value
+        }
+
+        scope.launch {
+            val preFlingConsumed = dispatcher.dispatchPreFling(Velocity(50f, 50f))
+            // scrollable won't participate in the pre fling
+            assertThat(preFlingConsumed).isEqualTo(Velocity.Zero)
+        }
+        rule.waitForIdle()
+
+        scope.launch {
+            dispatcher.dispatchPostFling(
+                Velocity(1000f, 1000f),
+                Velocity(2000f, 2000f)
+            )
+        }
+
+        rule.runOnIdle {
+            // catch that scrollable caught our post fling and flung
+            assertThat(value).isGreaterThan(lastValueBeforeFling)
+        }
+    }
+
+    @Test
+    fun scrollable_nestedScroll_allowParentWhenDisabled() {
+        var childValue = 0f
+        var parentValue = 0f
+        val childController = ScrollableState(
+            consumeScrollDelta = {
+                childValue += it
+                it
+            }
+        )
+        val parentController = ScrollableState(
+            consumeScrollDelta = {
+                parentValue += it
+                it
+            }
+        )
+
+        rule.setContentAndGetScope {
+            Box {
+                Box(
+                    modifier = Modifier
+                        .size(300.dp)
+                        .scrollable(
+                            state = parentController,
+                            orientation = Orientation.Horizontal
+                        )
+                ) {
+                    Box(
+                        Modifier
+                            .size(200.dp)
+                            .testTag(scrollableBoxTag)
+                            .scrollable(
+                                enabled = false,
+                                orientation = Orientation.Horizontal,
+                                state = childController
+                            )
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(parentValue).isEqualTo(0f)
+            assertThat(childValue).isEqualTo(0f)
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag)
+            .performTouchInput {
+                swipe(center, center.copy(x = center.x + 100f))
+            }
+
+        rule.runOnIdle {
+            assertThat(childValue).isEqualTo(0f)
+            assertThat(parentValue).isGreaterThan(0f)
+        }
+    }
+
+    @Test
+    fun scrollable_nestedScroll_disabledConnectionNoOp() {
+        var childValue = 0f
+        var parentValue = 0f
+        var selfValue = 0f
+        val childController = ScrollableState(
+            consumeScrollDelta = {
+                childValue += it / 2
+                it / 2
+            }
+        )
+        val middleController = ScrollableState(
+            consumeScrollDelta = {
+                selfValue += it / 2
+                it / 2
+            }
+        )
+        val parentController = ScrollableState(
+            consumeScrollDelta = {
+                parentValue += it / 2
+                it / 2
+            }
+        )
+
+        rule.setContentAndGetScope {
+            Box {
+                Box(
+                    modifier = Modifier
+                        .size(300.dp)
+                        .scrollable(
+                            state = parentController,
+                            orientation = Orientation.Horizontal
+                        )
+                ) {
+                    Box(
+                        Modifier
+                            .size(200.dp)
+                            .scrollable(
+                                enabled = false,
+                                orientation = Orientation.Horizontal,
+                                state = middleController
+                            )
+                    ) {
+                        Box(
+                            Modifier
+                                .size(200.dp)
+                                .testTag(scrollableBoxTag)
+                                .scrollable(
+                                    orientation = Orientation.Horizontal,
+                                    state = childController
+                                )
+                        )
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(parentValue).isEqualTo(0f)
+            assertThat(selfValue).isEqualTo(0f)
+            assertThat(childValue).isEqualTo(0f)
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag)
+            .performTouchInput {
+                swipe(center, center.copy(x = center.x + 100f))
+            }
+
+        rule.runOnIdle {
+            assertThat(childValue).isGreaterThan(0f)
+            // disabled middle node doesn't consume
+            assertThat(selfValue).isEqualTo(0f)
+            // but allow nested scroll to propagate up correctly
+            assertThat(parentValue).isGreaterThan(0f)
+        }
+    }
+
+    @Test
+    fun scrollable_nestedFlingCancellation_shouldPreventDeltasFromPropagating() {
+        var childDeltas = 0f
+        var touchSlop = 0f
+        val childController = ScrollableState {
+            childDeltas += it
+            it
+        }
+        val flingCancellationParent = object : NestedScrollConnection {
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                if (source == NestedScrollSource.Fling && available != Offset.Zero) {
+                    throw CancellationException()
+                }
+                return Offset.Zero
+            }
+        }
+
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            Box(modifier = Modifier.nestedScroll(flingCancellationParent)) {
+                Box(
+                    modifier = Modifier
+                        .size(600.dp)
+                        .testTag("childScrollable")
+                        .scrollable(childController, Orientation.Horizontal)
+                )
+            }
+        }
+
+        // First drag, this won't trigger the cancellation flow.
+        rule.onNodeWithTag("childScrollable").performTouchInput {
+            down(centerLeft)
+            moveBy(Offset(100f, 0f))
+            up()
+        }
+
+        rule.runOnIdle {
+            assertThat(childDeltas).isEqualTo(100f - touchSlop)
+        }
+
+        childDeltas = 0f
+        var dragged = 0f
+        rule.onNodeWithTag("childScrollable").performTouchInput {
+            swipeWithVelocity(centerLeft, centerRight, 2000f)
+            dragged = centerRight.x - centerLeft.x
+        }
+
+        // child didn't receive more deltas after drag, because fling was cancelled by the parent
+        assertThat(childDeltas).isEqualTo(dragged - touchSlop)
+    }
+
+    @Test
+    fun scrollable_bothOrientations_proxiesPostFling() {
+        val velocityFlung = 5000f
+        val outerState = ScrollableState(consumeScrollDelta = { 0f })
+        val innerState = ScrollableState(consumeScrollDelta = { 0f })
+        val innerFlingBehavior = object : FlingBehavior {
+            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+                return initialVelocity
+            }
+        }
+        val parent = object : NestedScrollConnection {
+            override suspend fun onPostFling(
+                consumed: Velocity,
+                available: Velocity
+            ): Velocity {
+                assertThat(consumed.x).isEqualTo(0f)
+                assertThat(available.x).isWithin(0.1f).of(velocityFlung)
+                return available
+            }
+        }
+
+        rule.setContentAndGetScope {
+            Box {
+                Box(
+                    contentAlignment = Alignment.Center,
+                    modifier = Modifier
+                        .size(300.dp)
+                        .nestedScroll(parent)
+                        .scrollable(
+                            state = outerState,
+                            orientation = Orientation.Vertical
+                        )
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .size(300.dp)
+                            .testTag(scrollableBoxTag)
+                            .scrollable(
+                                state = innerState,
+                                flingBehavior = innerFlingBehavior,
+                                orientation = Orientation.Horizontal
+                            )
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            this.swipeWithVelocity(
+                start = this.center,
+                end = Offset(this.center.x + 500f, this.center.y),
+                durationMillis = 300,
+                endVelocity = velocityFlung
+            )
+        }
+
+        // all assertions in callback above
+        rule.waitForIdle()
+    }
+
+    @Test
+    fun scrollable_interactionSource() {
+        val interactionSource = MutableInteractionSource()
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+
+        setScrollableContent {
+            Modifier.scrollable(
+                interactionSource = interactionSource,
+                orientation = Orientation.Horizontal,
+                state = controller
+            )
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag)
+            .performTouchInput {
+                down(Offset(visibleSize.width / 4f, visibleSize.height / 2f))
+                moveBy(Offset(visibleSize.width / 2f, 0f))
+            }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(1)
+            assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag)
+            .performTouchInput {
+                up()
+            }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(2)
+            assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
+            assertThat(interactions[1]).isInstanceOf(DragInteraction.Stop::class.java)
+            assertThat((interactions[1] as DragInteraction.Stop).start)
+                .isEqualTo(interactions[0])
+        }
+    }
+
+    @Test
+    fun scrollable_interactionSource_resetWhenDisposed() {
+        val interactionSource = MutableInteractionSource()
+        var emitScrollableBox by mutableStateOf(true)
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+
+        rule.setContentAndGetScope {
+            Box {
+                if (emitScrollableBox) {
+                    Box(
+                        modifier = Modifier
+                            .testTag(scrollableBoxTag)
+                            .size(100.dp)
+                            .scrollable(
+                                interactionSource = interactionSource,
+                                orientation = Orientation.Horizontal,
+                                state = controller
+                            )
+                    )
+                }
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag)
+            .performTouchInput {
+                down(Offset(visibleSize.width / 4f, visibleSize.height / 2f))
+                moveBy(Offset(visibleSize.width / 2f, 0f))
+            }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(1)
+            assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
+        }
+
+        // Dispose scrollable
+        rule.runOnIdle {
+            emitScrollableBox = false
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(2)
+            assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
+            assertThat(interactions[1]).isInstanceOf(DragInteraction.Cancel::class.java)
+            assertThat((interactions[1] as DragInteraction.Cancel).start)
+                .isEqualTo(interactions[0])
+        }
+    }
+
+    @Test
+    fun scrollable_flingBehaviourCalled_whenVelocity0() {
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        var flingCalled = 0
+        var flingVelocity: Float = Float.MAX_VALUE
+        val flingBehaviour = object : FlingBehavior {
+            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+                flingCalled++
+                flingVelocity = initialVelocity
+                return 0f
+            }
+        }
+        setScrollableContent {
+            Modifier.scrollable(
+                state = controller,
+                flingBehavior = flingBehaviour,
+                orientation = Orientation.Horizontal
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            down(this.center)
+            moveBy(Offset(115f, 0f))
+            up()
+        }
+        assertThat(flingCalled).isEqualTo(1)
+        assertThat(flingVelocity).isLessThan(0.01f)
+        assertThat(flingVelocity).isGreaterThan(-0.01f)
+    }
+
+    @Test
+    fun scrollable_flingBehaviourCalled() {
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        var flingCalled = 0
+        var flingVelocity: Float = Float.MAX_VALUE
+        val flingBehaviour = object : FlingBehavior {
+            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+                flingCalled++
+                flingVelocity = initialVelocity
+                return 0f
+            }
+        }
+        setScrollableContent {
+            Modifier.scrollable(
+                state = controller,
+                flingBehavior = flingBehaviour,
+                orientation = Orientation.Horizontal
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            swipeWithVelocity(
+                this.center,
+                this.center + Offset(115f, 0f),
+                endVelocity = 1000f
+            )
+        }
+        assertThat(flingCalled).isEqualTo(1)
+        assertThat(flingVelocity).isWithin(5f).of(1000f)
+    }
+
+    @Test
+    fun scrollable_flingBehaviourCalled_reversed() {
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        var flingCalled = 0
+        var flingVelocity: Float = Float.MAX_VALUE
+        val flingBehaviour = object : FlingBehavior {
+            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+                flingCalled++
+                flingVelocity = initialVelocity
+                return 0f
+            }
+        }
+        setScrollableContent {
+            Modifier.scrollable(
+                state = controller,
+                reverseDirection = true,
+                flingBehavior = flingBehaviour,
+                orientation = Orientation.Horizontal
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            swipeWithVelocity(
+                this.center,
+                this.center + Offset(115f, 0f),
+                endVelocity = 1000f
+            )
+        }
+        assertThat(flingCalled).isEqualTo(1)
+        assertThat(flingVelocity).isWithin(5f).of(-1000f)
+    }
+
+    @Test
+    fun scrollable_flingBehaviourCalled_correctScope() {
+        var total = 0f
+        var returned = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        val flingBehaviour = object : FlingBehavior {
+            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+                returned = scrollBy(123f)
+                return 0f
+            }
+        }
+        setScrollableContent {
+            Modifier.scrollable(
+                state = controller,
+                flingBehavior = flingBehaviour,
+                orientation = Orientation.Horizontal
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            down(center)
+            moveBy(Offset(x = 100f, y = 0f))
+        }
+
+        val prevTotal = rule.runOnIdle {
+            assertThat(total).isGreaterThan(0f)
+            total
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            up()
+        }
+
+        rule.runOnIdle {
+            assertThat(total).isEqualTo(prevTotal + 123)
+            assertThat(returned).isEqualTo(123f)
+        }
+    }
+
+    @Test
+    fun scrollable_flingBehaviourCalled_reversed_correctScope() {
+        var total = 0f
+        var returned = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        val flingBehaviour = object : FlingBehavior {
+            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+                returned = scrollBy(123f)
+                return 0f
+            }
+        }
+        setScrollableContent {
+            Modifier.scrollable(
+                state = controller,
+                reverseDirection = true,
+                flingBehavior = flingBehaviour,
+                orientation = Orientation.Horizontal
+            )
+        }
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            down(center)
+            moveBy(Offset(x = 100f, y = 0f))
+        }
+
+        val prevTotal = rule.runOnIdle {
+            assertThat(total).isLessThan(0f)
+            total
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            up()
+        }
+
+        rule.runOnIdle {
+            assertThat(total).isEqualTo(prevTotal + 123)
+            assertThat(returned).isEqualTo(123f)
+        }
+    }
+
+    @Test
+    fun scrollable_setsModifierLocalScrollableContainer() {
+        val controller = ScrollableState { it }
+
+        var isOuterInScrollableContainer: Boolean? = null
+        var isInnerInScrollableContainer: Boolean? = null
+        rule.setContent {
+            Box {
+                Box(
+                    modifier = Modifier
+                        .testTag(scrollableBoxTag)
+                        .size(100.dp)
+                        .then(
+                            object : ModifierLocalConsumer {
+                                override fun onModifierLocalsUpdated(
+                                    scope: ModifierLocalReadScope
+                                ) {
+                                    with(scope) {
+                                        isOuterInScrollableContainer =
+                                            ModifierLocalScrollableContainer.current
+                                    }
+                                }
+                            }
+                        )
+                        .scrollable(
+                            state = controller,
+                            orientation = Orientation.Horizontal
+                        )
+                        .then(
+                            object : ModifierLocalConsumer {
+                                override fun onModifierLocalsUpdated(
+                                    scope: ModifierLocalReadScope
+                                ) {
+                                    with(scope) {
+                                        isInnerInScrollableContainer =
+                                            ModifierLocalScrollableContainer.current
+                                    }
+                                }
+                            }
+                        )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(isOuterInScrollableContainer).isFalse()
+            assertThat(isInnerInScrollableContainer).isTrue()
+        }
+    }
+
+    @Test
+    fun scrollable_scrollByWorksWithRepeatableAnimations() {
+        rule.mainClock.autoAdvance = false
+
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        rule.setContentAndGetScope {
+            Box(
+                modifier = Modifier
+                    .size(100.dp)
+                    .scrollable(
+                        state = controller,
+                        orientation = Orientation.Horizontal
+                    )
+            )
+        }
+
+        rule.runOnIdle {
+            scope.launch {
+                controller.animateScrollBy(
+                    100f,
+                    keyframes {
+                        durationMillis = 2500
+                        // emulate a repeatable animation:
+                        0f at 0
+                        100f at 500
+                        100f at 1000
+                        0f at 1500
+                        0f at 2000
+                        100f at 2500
+                    }
+                )
+            }
+        }
+
+        rule.mainClock.advanceTimeBy(250)
+        rule.runOnIdle {
+            // in the middle of the first animation
+            assertThat(total).isGreaterThan(0f)
+            assertThat(total).isLessThan(100f)
+        }
+
+        rule.mainClock.advanceTimeBy(500) // 750 ms
+        rule.runOnIdle {
+            // first animation finished
+            assertThat(total).isEqualTo(100)
+        }
+
+        rule.mainClock.advanceTimeBy(250) // 1250 ms
+        rule.runOnIdle {
+            // in the middle of the second animation
+            assertThat(total).isGreaterThan(0f)
+            assertThat(total).isLessThan(100f)
+        }
+
+        rule.mainClock.advanceTimeBy(500) // 1750 ms
+        rule.runOnIdle {
+            // second animation finished
+            assertThat(total).isEqualTo(0)
+        }
+
+        rule.mainClock.advanceTimeBy(500) // 2250 ms
+        rule.runOnIdle {
+            // in the middle of the third animation
+            assertThat(total).isGreaterThan(0f)
+            assertThat(total).isLessThan(100f)
+        }
+
+        rule.mainClock.advanceTimeBy(500) // 2750 ms
+        rule.runOnIdle {
+            // third animation finished
+            assertThat(total).isEqualTo(100)
+        }
+    }
+
+    @Test
+    fun scrollable_cancellingAnimateScrollUpdatesIsScrollInProgress() {
+        rule.mainClock.autoAdvance = false
+
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        rule.setContentAndGetScope {
+            Box(
+                modifier = Modifier
+                    .size(100.dp)
+                    .scrollable(
+                        state = controller,
+                        orientation = Orientation.Horizontal
+                    )
+            )
+        }
+
+        lateinit var animateJob: Job
+
+        rule.runOnIdle {
+            animateJob = scope.launch {
+                controller.animateScrollBy(
+                    100f,
+                    tween(1000)
+                )
+            }
+        }
+
+        rule.mainClock.advanceTimeBy(500)
+        rule.runOnIdle {
+            assertThat(controller.isScrollInProgress).isTrue()
+        }
+
+        // Stop halfway through the animation
+        animateJob.cancel()
+
+        rule.runOnIdle {
+            assertThat(controller.isScrollInProgress).isFalse()
+        }
+    }
+
+    @Test
+    fun scrollable_preemptingAnimateScrollUpdatesIsScrollInProgress() {
+        rule.mainClock.autoAdvance = false
+
+        var total = 0f
+        val controller = ScrollableState(
+            consumeScrollDelta = {
+                total += it
+                it
+            }
+        )
+        rule.setContentAndGetScope {
+            Box(
+                modifier = Modifier
+                    .size(100.dp)
+                    .scrollable(
+                        state = controller,
+                        orientation = Orientation.Horizontal
+                    )
+            )
+        }
+
+        rule.runOnIdle {
+            scope.launch {
+                controller.animateScrollBy(
+                    100f,
+                    tween(1000)
+                )
+            }
+        }
+
+        rule.mainClock.advanceTimeBy(500)
+        rule.runOnIdle {
+            assertThat(total).isGreaterThan(0f)
+            assertThat(total).isLessThan(100f)
+            assertThat(controller.isScrollInProgress).isTrue()
+            scope.launch {
+                controller.animateScrollBy(
+                    -100f,
+                    tween(1000)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(controller.isScrollInProgress).isTrue()
+        }
+
+        rule.mainClock.advanceTimeBy(1000)
+        rule.mainClock.advanceTimeByFrame()
+
+        rule.runOnIdle {
+            assertThat(total).isGreaterThan(-75f)
+            assertThat(total).isLessThan(0f)
+            assertThat(controller.isScrollInProgress).isFalse()
+        }
+    }
+
+    @Test
+    fun scrollable_multiDirectionsShouldPropagateOrthogonalAxisToNextParentWithSameDirection() {
+        var innerDelta = 0f
+        var middleDelta = 0f
+        var outerDelta = 0f
+
+        val outerStateController = ScrollableState {
+            outerDelta += it
+            it
+        }
+
+        val middleController = ScrollableState {
+            middleDelta += it
+            it / 2
+        }
+
+        val innerController = ScrollableState {
+            innerDelta += it
+            it / 2
+        }
+
+        rule.setContentAndGetScope {
+            Box(
+                modifier = Modifier
+                    .testTag("outerScrollable")
+                    .size(300.dp)
+                    .scrollable(
+                        outerStateController,
+                        orientation = Orientation.Horizontal
+                    )
+
+            ) {
+                Box(
+                    modifier = Modifier
+                        .testTag("middleScrollable")
+                        .size(300.dp)
+                        .scrollable(
+                            middleController,
+                            orientation = Orientation.Vertical
+                        )
+
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .testTag("innerScrollable")
+                            .size(300.dp)
+                            .scrollable(
+                                innerController,
+                                orientation = Orientation.Horizontal
+                            )
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("innerScrollable").performTouchInput {
+            down(center)
+            moveBy(Offset(100f, 0f))
+            up()
+        }
+
+        rule.runOnIdle {
+            assertThat(innerDelta).isGreaterThan(0)
+            assertThat(middleDelta).isEqualTo(0)
+            assertThat(outerDelta).isEqualTo(innerDelta / 2f)
+        }
+    }
+
+    @Test
+    fun nestedScrollable_shouldImmediateScrollIfChildIsFlinging() {
+        var innerDelta = 0f
+        var middleDelta = 0f
+        var outerDelta = 0f
+        var touchSlop = 0f
+
+        val outerStateController = ScrollableState {
+            outerDelta += it
+            0f
+        }
+
+        val middleController = ScrollableState {
+            middleDelta += it
+            0f
+        }
+
+        val innerController = ScrollableState {
+            innerDelta += it
+            it / 2f
+        }
+
+        rule.setContentAndGetScope {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            Box(
+                modifier = Modifier
+                    .testTag("outerScrollable")
+                    .size(600.dp)
+                    .background(Color.Red)
+                    .scrollable(
+                        outerStateController,
+                        orientation = Orientation.Vertical
+                    ),
+                contentAlignment = Alignment.BottomStart
+            ) {
+                Box(
+                    modifier = Modifier
+                        .testTag("middleScrollable")
+                        .size(300.dp)
+                        .background(Color.Blue)
+                        .scrollable(
+                            middleController,
+                            orientation = Orientation.Vertical
+                        ),
+                    contentAlignment = Alignment.BottomStart
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .testTag("innerScrollable")
+                            .size(50.dp)
+                            .background(Color.Yellow)
+                            .scrollable(
+                                innerController,
+                                orientation = Orientation.Vertical
+                            )
+                    )
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("innerScrollable").performTouchInput {
+            swipeUp()
+        }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeByFrame()
+
+        val previousOuter = outerDelta
+
+        rule.onNodeWithTag("outerScrollable").performTouchInput {
+            down(topCenter)
+            // Move less than touch slop, should start immediately
+            moveBy(Offset(0f, touchSlop / 2))
+        }
+
+        rule.mainClock.autoAdvance = true
+
+        rule.runOnIdle {
+            assertThat(outerDelta).isEqualTo(previousOuter + touchSlop / 2)
+        }
+    }
+
+    // b/179417109 Double checks that in a nested scroll cycle, the parent post scroll
+    // consumption is taken into consideration.
+    @Test
+    fun dispatchScroll_shouldReturnConsumedDeltaInNestedScrollChain() {
+        var consumedInner = 0f
+        var consumedOuter = 0f
+        var touchSlop = 0f
+
+        var preScrollAvailable = Offset.Zero
+        var consumedPostScroll = Offset.Zero
+        var postScrollAvailable = Offset.Zero
+
+        val outerStateController = ScrollableState {
+            consumedOuter += it
+            it
+        }
+
+        val innerController = ScrollableState {
+            consumedInner += it / 2
+            it / 2
+        }
+
+        val connection = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                preScrollAvailable += available
+                return Offset.Zero
+            }
+
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                consumedPostScroll += consumed
+                postScrollAvailable += available
+                return Offset.Zero
+            }
+        }
+
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            Box(modifier = Modifier.nestedScroll(connection)) {
+                Box(
+                    modifier = Modifier
+                        .testTag("outerScrollable")
+                        .size(300.dp)
+                        .scrollable(
+                            outerStateController,
+                            orientation = Orientation.Horizontal
+                        )
+
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .testTag("innerScrollable")
+                            .size(300.dp)
+                            .scrollable(
+                                innerController,
+                                orientation = Orientation.Horizontal
+                            )
+                    )
+                }
+            }
+        }
+
+        val scrollDelta = 200f
+
+        rule.onRoot().performTouchInput {
+            down(center)
+            moveBy(Offset(scrollDelta, 0f))
+            up()
+        }
+
+        rule.runOnIdle {
+            assertThat(consumedInner).isGreaterThan(0)
+            assertThat(consumedOuter).isGreaterThan(0)
+            assertThat(touchSlop).isGreaterThan(0)
+            assertThat(postScrollAvailable.x).isEqualTo(0f)
+            assertThat(consumedPostScroll.x).isEqualTo(scrollDelta - touchSlop)
+            assertThat(preScrollAvailable.x).isEqualTo(scrollDelta - touchSlop)
+            assertThat(scrollDelta).isEqualTo(consumedInner + consumedOuter + touchSlop)
+        }
+    }
+
+    @Test
+    fun testInspectorValue() {
+        val controller = ScrollableState(
+            consumeScrollDelta = { it }
+        )
+        rule.setContentAndGetScope {
+            val modifier =
+                Modifier.scrollable(controller, Orientation.Vertical).first() as InspectableValue
+            assertThat(modifier.nameFallback).isEqualTo("scrollable")
+            assertThat(modifier.valueOverride).isNull()
+            assertThat(modifier.inspectableElements.map { it.name }.asIterable()).containsExactly(
+                "orientation",
+                "state",
+                "overscrollEffect",
+                "enabled",
+                "reverseDirection",
+                "flingBehavior",
+                "interactionSource",
+                "scrollableBringIntoViewConfig",
+            )
+        }
+    }
+
+    @OptIn(ExperimentalFoundationApi::class)
+    @Test
+    fun producingEqualMaterializedModifierAfterRecomposition() {
+        val state = ScrollableState { it }
+        val counter = mutableStateOf(0)
+        var materialized: Modifier? = null
+
+        rule.setContent {
+            counter.value // just to trigger recomposition
+            materialized = currentComposer.materialize(
+                Modifier.scrollable(
+                    state,
+                    Orientation.Vertical,
+                    NoOpOverscrollEffect
+                )
+            )
+        }
+
+        lateinit var first: Modifier
+        rule.runOnIdle {
+            first = requireNotNull(materialized)
+            materialized = null
+            counter.value++
+        }
+
+        rule.runOnIdle {
+            val second = requireNotNull(materialized)
+            assertThat(first).isEqualTo(second)
+        }
+    }
+
+    @Test
+    fun focusStaysInScrollableEvenThoughThereIsACloserItemOutside() {
+        lateinit var focusManager: FocusManager
+        val initialFocus = FocusRequester()
+        var nextItemIsFocused = false
+        rule.setContent {
+            focusManager = LocalFocusManager.current
+            Column {
+                Column(
+                    Modifier
+                        .size(10.dp)
+                        .verticalScroll(rememberScrollState())
+                ) {
+                    Box(
+                        Modifier
+                            .size(10.dp)
+                            .focusRequester(initialFocus)
+                            .focusable()
+                    )
+                    Box(Modifier.size(10.dp))
+                    Box(
+                        Modifier
+                            .size(10.dp)
+                            .onFocusChanged { nextItemIsFocused = it.isFocused }
+                            .focusable())
+                }
+                Box(
+                    Modifier
+                        .size(10.dp)
+                        .focusable()
+                )
+            }
+        }
+
+        rule.runOnIdle { initialFocus.requestFocus() }
+        rule.runOnIdle { focusManager.moveFocus(FocusDirection.Down) }
+
+        rule.runOnIdle { assertThat(nextItemIsFocused).isTrue() }
+    }
+
+    @Test
+    fun verticalScrollable_assertVelocityCalculationIsSimilarInsideOutsideVelocityTracker() {
+        // arrange
+        val tracker = VelocityTracker()
+        var velocity = Velocity.Zero
+        val capturingScrollConnection = object : NestedScrollConnection {
+            override suspend fun onPreFling(available: Velocity): Velocity {
+                velocity += available
+                return Velocity.Zero
+            }
+        }
+        val controller = ScrollableState { _ -> 0f }
+
+        setScrollableContent {
+            Modifier
+                .pointerInput(Unit) {
+                    savePointerInputEvents(tracker, this)
+                }
+                .nestedScroll(capturingScrollConnection)
+                .scrollable(controller, Orientation.Vertical)
+        }
+
+        // act
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            swipeUp()
+        }
+
+        // assert
+        rule.runOnIdle {
+            val diff = abs((velocity - tracker.calculateVelocity()).y)
+            assertThat(diff).isLessThan(VelocityTrackerCalculationThreshold)
+        }
+        tracker.resetTracking()
+        velocity = Velocity.Zero
+
+        // act
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            swipeDown()
+        }
+
+        // assert
+        rule.runOnIdle {
+            val diff = abs((velocity - tracker.calculateVelocity()).y)
+            assertThat(diff).isLessThan(VelocityTrackerCalculationThreshold)
+        }
+    }
+
+    @Test
+    fun horizontalScrollable_assertVelocityCalculationIsSimilarInsideOutsideVelocityTracker() {
+        // arrange
+        val tracker = VelocityTracker()
+        var velocity = Velocity.Zero
+        val capturingScrollConnection = object : NestedScrollConnection {
+            override suspend fun onPreFling(available: Velocity): Velocity {
+                velocity += available
+                return Velocity.Zero
+            }
+        }
+        val controller = ScrollableState { _ -> 0f }
+
+        setScrollableContent {
+            Modifier
+                .pointerInput(Unit) {
+                    savePointerInputEvents(tracker, this)
+                }
+                .nestedScroll(capturingScrollConnection)
+                .scrollable(controller, Orientation.Horizontal)
+        }
+
+        // act
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            swipeLeft()
+        }
+
+        // assert
+        rule.runOnIdle {
+            val diff = abs((velocity - tracker.calculateVelocity()).x)
+            assertThat(diff).isLessThan(VelocityTrackerCalculationThreshold)
+        }
+        tracker.resetTracking()
+        velocity = Velocity.Zero
+
+        // act
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            swipeRight()
+        }
+
+        // assert
+        rule.runOnIdle {
+            val diff = abs((velocity - tracker.calculateVelocity()).x)
+            assertThat(diff).isLessThan(VelocityTrackerCalculationThreshold)
+        }
+    }
+
+    @Test
+    fun offsetsScrollable_velocityCalculationShouldConsiderLocalPositions() {
+        // arrange
+        var velocity = Velocity.Zero
+        val fullScreen = mutableStateOf(false)
+        lateinit var scrollState: LazyListState
+        val capturingScrollConnection = object : NestedScrollConnection {
+            override suspend fun onPreFling(available: Velocity): Velocity {
+                velocity += available
+                return Velocity.Zero
+            }
+        }
+        rule.setContent {
+            scrollState = rememberLazyListState()
+            Column(modifier = Modifier.nestedScroll(capturingScrollConnection)) {
+                if (!fullScreen.value) {
+                    Box(
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .background(Color.Black)
+                            .height(400.dp)
+                    )
+                }
+
+                LazyColumn(state = scrollState) {
+                    items(100) {
+                        Box(
+                            modifier = Modifier
+                                .padding(10.dp)
+                                .background(Color.Red)
+                                .fillMaxWidth()
+                                .height(50.dp)
+                        )
+                    }
+                }
+            }
+        }
+        // act
+        // Register generated velocity with offset
+        composeViewSwipeUp()
+        rule.waitForIdle()
+        val previousVelocity = velocity
+        velocity = Velocity.Zero
+        // Remove offset and restart scroll
+        fullScreen.value = true
+        rule.runOnIdle {
+            runBlocking {
+                scrollState.scrollToItem(0)
+            }
+        }
+        rule.waitForIdle()
+        // Register generated velocity without offset, should be larger as there was more
+        // screen to cover.
+        composeViewSwipeUp()
+
+        // assert
+        rule.runOnIdle {
+            assertThat(abs(previousVelocity.y)).isNotEqualTo(abs(velocity.y))
+        }
+    }
+
+    @Test
+    fun disableSystemAnimations_defaultFlingBehaviorShouldContinueToWork() {
+
+        val controller = ScrollableState { 0f }
+        var defaultFlingBehavior: DefaultFlingBehavior? = null
+        setScrollableContent {
+            defaultFlingBehavior = ScrollableDefaults.flingBehavior() as? DefaultFlingBehavior
+            Modifier.scrollable(
+                state = controller,
+                orientation = Orientation.Horizontal,
+                flingBehavior = defaultFlingBehavior
+            )
+        }
+
+        scope.launch {
+            controller.scroll {
+                defaultFlingBehavior?.let {
+                    with(it) { performFling(1000f) }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(defaultFlingBehavior?.lastAnimationCycleCount).isGreaterThan(1)
+        }
+
+        // Simulate turning of animation
+        scope.launch {
+            controller.scroll {
+                withContext(TestScrollMotionDurationScale(0f)) {
+                    defaultFlingBehavior?.let {
+                        with(it) { performFling(1000f) }
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(defaultFlingBehavior?.lastAnimationCycleCount).isGreaterThan(1)
+        }
+    }
+
+    @Test
+    fun defaultFlingBehavior_useScrollMotionDurationScale() {
+
+        val controller = ScrollableState { 0f }
+        var defaultFlingBehavior: DefaultFlingBehavior? = null
+        var switchMotionDurationScale by mutableStateOf(true)
+
+        rule.setContentAndGetScope {
+            val flingSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
+            if (switchMotionDurationScale) {
+                defaultFlingBehavior =
+                    DefaultFlingBehavior(flingSpec, TestScrollMotionDurationScale(1f))
+                Box(
+                    modifier = Modifier
+                        .testTag(scrollableBoxTag)
+                        .size(100.dp)
+                        .scrollable(
+                            state = controller,
+                            orientation = Orientation.Horizontal,
+                            flingBehavior = defaultFlingBehavior
+                        )
+                )
+            } else {
+                defaultFlingBehavior =
+                    DefaultFlingBehavior(flingSpec, TestScrollMotionDurationScale(0f))
+                Box(
+                    modifier = Modifier
+                        .testTag(scrollableBoxTag)
+                        .size(100.dp)
+                        .scrollable(
+                            state = controller,
+                            orientation = Orientation.Horizontal,
+                            flingBehavior = defaultFlingBehavior
+                        )
+                )
+            }
+        }
+
+        scope.launch {
+            controller.scroll {
+                defaultFlingBehavior?.let {
+                    with(it) { performFling(1000f) }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(defaultFlingBehavior?.lastAnimationCycleCount).isGreaterThan(1)
+        }
+
+        switchMotionDurationScale = false
+        rule.waitForIdle()
+
+        scope.launch {
+            controller.scroll {
+                defaultFlingBehavior?.let {
+                    with(it) { performFling(1000f) }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(defaultFlingBehavior?.lastAnimationCycleCount).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun scrollable_noMomentum_shouldChangeScrollStateAfterRelease() {
+        val scrollState = ScrollState(0)
+        val delta = 10f
+        var touchSlop = 0f
+        setScrollableContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            Modifier.scrollable(scrollState, Orientation.Vertical)
+        }
+        var previousScrollValue = 0
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            down(center)
+            // generate various move events
+            repeat(30) {
+                moveBy(Offset(0f, delta), delayMillis = 8L)
+                previousScrollValue += delta.toInt()
+            }
+            // stop for a moment
+            advanceEventTime(3000L)
+            up()
+        }
+
+        rule.runOnIdle {
+            Assert.assertEquals((previousScrollValue - touchSlop).toInt(), scrollState.value)
+        }
+    }
+
+    @Test
+    fun defaultScrollableState_scrollByWithNan_shouldFilterOutNan() {
+        val controller = ScrollableState {
+            assertThat(it).isNotNaN()
+            0f
+        }
+
+        val nanGenerator = object : FlingBehavior {
+            override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+                return scrollBy(Float.NaN)
+            }
+        }
+
+        setScrollableContent {
+            Modifier.scrollable(
+                state = controller,
+                orientation = Orientation.Horizontal,
+                flingBehavior = nanGenerator
+            )
+        }
+
+        rule.onNodeWithTag(scrollableBoxTag).performTouchInput {
+            swipeLeft()
+        }
+    }
+
+    private fun setScrollableContent(scrollableModifierFactory: @Composable () -> Modifier) {
+        rule.setContentAndGetScope {
+            Box {
+                val scrollable = scrollableModifierFactory()
+                Box(
+                    modifier = Modifier
+                        .testTag(scrollableBoxTag)
+                        .size(100.dp)
+                        .then(scrollable)
+                )
+            }
+        }
+    }
+}
+
+// Very low tolerance on the difference
+internal val VelocityTrackerCalculationThreshold = 1
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal suspend fun savePointerInputEvents(
+    tracker: VelocityTracker,
+    pointerInputScope: PointerInputScope
+) {
+    if (VelocityTrackerAddPointsFix) {
+        savePointerInputEventsWithFix(tracker, pointerInputScope)
+    } else {
+        savePointerInputEventsLegacy(tracker, pointerInputScope)
+    }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal suspend fun savePointerInputEventsWithFix(
+    tracker: VelocityTracker,
+    pointerInputScope: PointerInputScope
+) {
+    with(pointerInputScope) {
+        coroutineScope {
+            awaitPointerEventScope {
+                while (true) {
+                    var event: PointerInputChange? = awaitFirstDown()
+                    while (event != null && !event.changedToUpIgnoreConsumed()) {
+                        val currentEvent = awaitPointerEvent().changes
+                            .firstOrNull()
+
+                        if (currentEvent != null && !currentEvent.changedToUpIgnoreConsumed()) {
+                            currentEvent.historical.fastForEach {
+                                tracker.addPosition(it.uptimeMillis, it.position)
+                            }
+                            tracker.addPosition(
+                                currentEvent.uptimeMillis,
+                                currentEvent.position
+                            )
+                        }
+
+                        event = currentEvent
+                    }
+                }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal suspend fun savePointerInputEventsLegacy(
+    tracker: VelocityTracker,
+    pointerInputScope: PointerInputScope
+) {
+    with(pointerInputScope) {
+        coroutineScope {
+            awaitPointerEventScope {
+                while (true) {
+                    var event = awaitFirstDown()
+                    tracker.addPosition(event.uptimeMillis, event.position)
+                    while (!event.changedToUpIgnoreConsumed()) {
+                        val currentEvent = awaitPointerEvent().changes
+                            .firstOrNull()
+
+                        if (currentEvent != null) {
+                            currentEvent.historical.fastForEach {
+                                tracker.addPosition(it.uptimeMillis, it.position)
+                            }
+                            tracker.addPosition(
+                                currentEvent.uptimeMillis,
+                                currentEvent.position
+                            )
+                            event = currentEvent
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+internal fun composeViewSwipeUp() {
+    onView(allOf(instanceOf(AbstractComposeView::class.java)))
+        .perform(
+            espressoSwipe(
+                GeneralLocation.CENTER,
+                GeneralLocation.TOP_CENTER
+            )
+        )
+}
+
+internal fun composeViewSwipeDown() {
+    onView(allOf(instanceOf(AbstractComposeView::class.java)))
+        .perform(
+            espressoSwipe(
+                GeneralLocation.CENTER,
+                GeneralLocation.BOTTOM_CENTER
+            )
+        )
+}
+
+internal fun composeViewSwipeLeft() {
+    onView(allOf(instanceOf(AbstractComposeView::class.java)))
+        .perform(
+            espressoSwipe(
+                GeneralLocation.CENTER,
+                GeneralLocation.CENTER_LEFT
+            )
+        )
+}
+
+internal fun composeViewSwipeRight() {
+    onView(allOf(instanceOf(AbstractComposeView::class.java)))
+        .perform(
+            espressoSwipe(
+                GeneralLocation.CENTER,
+                GeneralLocation.CENTER_RIGHT
+            )
+        )
+}
+
+private fun espressoSwipe(
+    start: CoordinatesProvider,
+    end: CoordinatesProvider
+): GeneralSwipeAction {
+    return GeneralSwipeAction(
+        Swipe.FAST, start, end,
+        Press.FINGER
+    )
+}
+
+internal class TestScrollMotionDurationScale(override val scaleFactor: Float) : MotionDurationScale
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/StretchOverscrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/StretchOverscrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/StretchOverscrollTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/StretchOverscrollTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/SystemGestureExclusionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/SystemGestureExclusionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/SystemGestureExclusionTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/SystemGestureExclusionTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TestActivity.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TestActivity.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TestActivity.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TestActivity.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TransformableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TransformableTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TransformableTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TransformableTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableTestValue.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableTestValue.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableTestValue.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableTestValue.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/AwaitEachGestureTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/AwaitEachGestureTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/AwaitEachGestureTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/AwaitEachGestureTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/AwaitTouchEventTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/AwaitTouchEventTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/AwaitTouchEventTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/AwaitTouchEventTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/DragGestureDetectorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/DragGestureDetectorTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/DragGestureDetectorTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/DragGestureDetectorTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/ForEachGestureTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/ForEachGestureTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/ForEachGestureTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/ForEachGestureTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/TapGestureDetectorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/TapGestureDetectorTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/TapGestureDetectorTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/TapGestureDetectorTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/TransformGestureDetectorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/TransformGestureDetectorTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/TransformGestureDetectorTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/TransformGestureDetectorTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyGridSnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyGridSnapFlingBehaviorTest.kt
new file mode 100644
index 0000000..6d04823
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyGridSnapFlingBehaviorTest.kt
@@ -0,0 +1,524 @@
+/*
+ * 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.foundation.gesture.snapping
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
+import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
+import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout.Companion.CenterToCenter
+import androidx.compose.foundation.gestures.snapping.calculateDistanceToDesiredSnapPosition
+import androidx.compose.foundation.gestures.snapping.offsetOnMainAxis
+import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
+import androidx.compose.foundation.gestures.snapping.singleAxisViewportSize
+import androidx.compose.foundation.gestures.snapping.sizeOnMainAxis
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.grid.BaseLazyGridTestWithOrientation
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
+import kotlin.math.abs
+import kotlin.math.absoluteValue
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlinx.coroutines.runBlocking
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+@OptIn(ExperimentalFoundationApi::class)
+class LazyGridSnapFlingBehaviorTest(private val orientation: Orientation) :
+    BaseLazyGridTestWithOrientation(orientation) {
+
+    private val density: Density
+        get() = rule.density
+
+    private lateinit var snapLayoutInfoProvider: SnapLayoutInfoProvider
+    private lateinit var snapFlingBehavior: FlingBehavior
+
+    @Test
+    fun belowThresholdVelocity_lessThanAnItemScroll_shouldStayInSamePage() {
+        var lazyGridState: LazyGridState? = null
+        var stepSize = 0f
+        var velocityThreshold = 0f
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyGridState().also { lazyGridState = it }
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state)
+        }
+
+        // Scroll a bit
+        onMainList().swipeOnMainAxis()
+        rule.waitForIdle()
+        val currentItem = density.getCurrentSnappedItem(lazyGridState)
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(stepSize / 2, velocityThreshold / 2)
+        }
+
+        // assert
+        rule.runOnIdle {
+            val nextItem = density.getCurrentSnappedItem(lazyGridState)
+            assertEquals(currentItem, nextItem)
+        }
+    }
+
+    @Test
+    fun belowThresholdVelocity_moreThanAnItemScroll_shouldGoToNextPage() {
+        var lazyGridState: LazyGridState? = null
+        var stepSize = 0f
+        var velocityThreshold = 0f
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyGridState().also { lazyGridState = it }
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state)
+        }
+
+        // Scroll a bit
+        onMainList().swipeOnMainAxis()
+        rule.waitForIdle()
+        val currentItem = density.getCurrentSnappedItem(lazyGridState)
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                stepSize,
+                velocityThreshold / 2
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            val nextItem = density.getCurrentSnappedItem(lazyGridState)
+            assertEquals(nextItem, currentItem + (lazyGridState?.maxCells() ?: 0))
+        }
+    }
+
+    @Test
+    fun aboveThresholdVelocityForward_notLargeEnoughScroll_shouldGoToNextPage() {
+        var lazyGridState: LazyGridState? = null
+        var stepSize = 0f
+        var velocityThreshold = 0f
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyGridState().also { lazyGridState = it }
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state)
+        }
+
+        // Scroll a bit
+        onMainList().swipeOnMainAxis()
+        rule.waitForIdle()
+        val currentItem = density.getCurrentSnappedItem(lazyGridState)
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                stepSize / 2,
+                velocityThreshold * 2
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            val nextItem = density.getCurrentSnappedItem(lazyGridState)
+            assertEquals(nextItem, currentItem + (lazyGridState?.maxCells() ?: 0))
+        }
+    }
+
+    @Ignore // b/293513475
+    @Test
+    fun aboveThresholdVelocityBackward_notLargeEnoughScroll_shouldGoToPreviousPage() {
+        var lazyGridState: LazyGridState? = null
+        var stepSize = 0f
+        var velocityThreshold = 0f
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyGridState().also { lazyGridState = it }
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state)
+        }
+
+        // Scroll a bit
+        onMainList().swipeOnMainAxis()
+        rule.waitForIdle()
+        val currentItem = density.getCurrentSnappedItem(lazyGridState)
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                stepSize / 2,
+                velocityThreshold * 2,
+                true
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            val nextItem = density.getCurrentSnappedItem(lazyGridState)
+            assertEquals(nextItem, currentItem - (lazyGridState?.maxCells() ?: 0))
+        }
+    }
+
+    @Test
+    fun aboveThresholdVelocity_largeEnoughScroll_shouldGoToNextNextPage() {
+        var lazyGridState: LazyGridState? = null
+        var stepSize = 0f
+        var velocityThreshold = 0f
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyGridState().also { lazyGridState = it }
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state)
+        }
+
+        // Scroll a bit
+        onMainList().swipeOnMainAxis()
+        rule.waitForIdle()
+        val currentItem = density.getCurrentSnappedItem(lazyGridState)
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                1.5f * stepSize,
+                velocityThreshold * 3
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            val nextItem = density.getCurrentSnappedItem(lazyGridState)
+            assertEquals(nextItem, currentItem + 2 * (lazyGridState?.maxCells() ?: 0))
+        }
+    }
+
+    @Test
+    fun performFling_shouldPropagateVelocityIfHitEdges() {
+        var stepSize = 0f
+        var latestAvailableVelocity = Velocity.Zero
+        lateinit var lazyGridState: LazyGridState
+        val inspectingNestedScrollConnection = object : NestedScrollConnection {
+            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+                latestAvailableVelocity = available
+                return Velocity.Zero
+            }
+        }
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            lazyGridState = rememberLazyGridState(180) // almost at the end
+            stepSize = with(density) { ItemSize.toPx() }
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .nestedScroll(inspectingNestedScrollConnection)
+            ) {
+                MainLayout(state = lazyGridState)
+            }
+        }
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                1.5f * stepSize,
+                30000f
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            assertNotEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+        }
+
+        // arrange
+        rule.runOnIdle {
+            runBlocking {
+                lazyGridState.scrollToItem(20) // almost at the start
+            }
+        }
+
+        latestAvailableVelocity = Velocity.Zero
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                -1.5f * stepSize,
+                30000f
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            assertNotEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+        }
+    }
+
+    @Test
+    fun performFling_shouldConsumeAllVelocityIfInTheMiddleOfTheList() {
+        var stepSize = 0f
+        var latestAvailableVelocity = Velocity.Zero
+        lateinit var lazyGridState: LazyGridState
+        val inspectingNestedScrollConnection = object : NestedScrollConnection {
+            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+                latestAvailableVelocity = available
+                return Velocity.Zero
+            }
+        }
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            lazyGridState = rememberLazyGridState(100) // middle of the grid
+            stepSize = with(density) { ItemSize.toPx() }
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .nestedScroll(inspectingNestedScrollConnection)
+            ) {
+                MainLayout(state = lazyGridState)
+            }
+        }
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                1.5f * stepSize,
+                10000f // use a not so high velocity
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            assertEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+        }
+
+        // arrange
+        rule.runOnIdle {
+            runBlocking {
+                lazyGridState.scrollToItem(100) // return to the middle
+            }
+        }
+
+        latestAvailableVelocity = Velocity.Zero
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                -1.5f * stepSize,
+                10000f // use a not so high velocity
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            assertEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+        }
+    }
+
+    @Test
+    fun remainingScrollOffset_shouldFollowAnimationOffsets() {
+        var stepSize = 0f
+        var velocityThreshold = 0f
+        val scrollOffset = mutableListOf<Float>()
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyGridState()
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state, scrollOffset)
+        }
+
+        rule.mainClock.autoAdvance = false
+        // act
+        val velocity = velocityThreshold * 3
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                1.5f * stepSize,
+                velocity
+            )
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        // assert
+        val initialTargetOffset = snapLayoutInfoProvider.calculateApproachOffset(velocity)
+        Truth.assertThat(scrollOffset[1]).isWithin(0.5f)
+            .of(initialTargetOffset)
+
+        // act: wait for remaining offset to grow instead of decay, this indicates the last
+        // snap step will start
+        rule.mainClock.advanceTimeUntil {
+            scrollOffset.size > 2 &&
+                scrollOffset.last() > scrollOffset[scrollOffset.lastIndex - 1]
+        }
+
+        // assert: next calculated offset is the first value emitted by remainingScrollOffset
+        val finalRemainingOffset = snapLayoutInfoProvider.calculateSnappingOffset(10000f)
+        Truth.assertThat(scrollOffset.last()).isWithin(0.5f)
+            .of(finalRemainingOffset)
+        rule.mainClock.autoAdvance = true
+
+        // assert: value settles back to zero
+        rule.runOnIdle {
+            Truth.assertThat(scrollOffset.last()).isEqualTo(0f)
+        }
+    }
+
+    private fun onMainList() = rule.onNodeWithTag(TestTag)
+
+    @Composable
+    fun MainLayout(state: LazyGridState, scrollOffset: MutableList<Float> = mutableListOf()) {
+        snapLayoutInfoProvider = remember(state) { SnapLayoutInfoProvider(state) }
+        val innerFlingBehavior =
+            rememberSnapFlingBehavior(snapLayoutInfoProvider = snapLayoutInfoProvider)
+        snapFlingBehavior = remember(innerFlingBehavior) {
+            QuerySnapFlingBehavior(innerFlingBehavior) { scrollOffset.add(it) }
+        }
+        LazyGrid(
+            cells = GridCells.FixedSize(ItemSize),
+            state = state,
+            modifier = Modifier.testTag(TestTag),
+            flingBehavior = snapFlingBehavior
+        ) {
+            items(200) {
+                Box(
+                    modifier = Modifier
+                        .size(ItemSize)
+                        .background(Color.Yellow)
+                ) {
+                    BasicText(text = it.toString())
+                }
+            }
+        }
+    }
+
+    private fun LazyGridState.maxCells() =
+        if (layoutInfo.orientation == Orientation.Vertical) {
+            layoutInfo.visibleItemsInfo.maxOf { it.column }
+        } else {
+            layoutInfo.visibleItemsInfo.maxOf { it.row }
+        } + 1
+
+    private fun SemanticsNodeInteraction.swipeOnMainAxis() {
+        performTouchInput {
+            if (orientation == Orientation.Vertical) {
+                swipeUp()
+            } else {
+                swipeLeft()
+            }
+        }
+    }
+
+    private fun Density.getCurrentSnappedItem(state: LazyGridState?): Int {
+        var itemIndex = -1
+        if (state == null) return -1
+        var minDistance = Float.POSITIVE_INFINITY
+        val layoutInfo = state.layoutInfo
+        (state.layoutInfo.visibleItemsInfo).forEach {
+            val distance = calculateDistanceToDesiredSnapPosition(
+                mainAxisViewPortSize = layoutInfo.singleAxisViewportSize,
+                beforeContentPadding = layoutInfo.beforeContentPadding,
+                afterContentPadding = layoutInfo.afterContentPadding,
+                itemSize = it.sizeOnMainAxis(orientation = layoutInfo.orientation),
+                itemOffset = it.offsetOnMainAxis(orientation = layoutInfo.orientation),
+                itemIndex = it.index,
+                snapPositionInLayout = CenterToCenter
+            )
+            if (abs(distance) < minDistance) {
+                minDistance = abs(distance)
+                itemIndex = it.index
+            }
+        }
+        return itemIndex
+    }
+
+    private fun TouchInjectionScope.swipeMainAxisWithVelocity(
+        scrollSize: Float,
+        endVelocity: Float,
+        reversed: Boolean = false
+    ) {
+        val (start, end) = if (orientation == Orientation.Vertical) {
+            bottomCenter to bottomCenter.copy(y = bottomCenter.y - scrollSize)
+        } else {
+            centerRight to centerRight.copy(x = centerRight.x - scrollSize)
+        }
+        swipeWithVelocity(
+            if (reversed) end else start,
+            if (reversed) start else end,
+            endVelocity
+        )
+    }
+
+    private fun Velocity.toAbsoluteFloat(): Float {
+        return (if (orientation == Orientation.Vertical) y else x).absoluteValue
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+
+        val ItemSize = 200.dp
+        const val TestTag = "MainList"
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyGridSnapLayoutInfoProviderTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyGridSnapLayoutInfoProviderTest.kt
new file mode 100644
index 0000000..f0c6d32
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyGridSnapLayoutInfoProviderTest.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.gesture.snapping
+
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.splineBasedDecay
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
+import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.grid.BaseLazyGridTestWithOrientation
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import kotlin.math.absoluteValue
+import kotlin.math.floor
+import kotlin.math.sign
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyGridSnapLayoutInfoProviderTest(orientation: Orientation) :
+    BaseLazyGridTestWithOrientation(orientation) {
+
+    private val density: Density get() = rule.density
+
+    @Test
+    fun calculateApproachOffset_highVelocity_approachOffsetIsEqualToDecayMinusItemSize() {
+        lateinit var layoutInfoProvider: SnapLayoutInfoProvider
+        val decay = splineBasedDecay<Float>(rule.density)
+        fun calculateTargetOffset(velocity: Float): Float {
+            val offset = decay.calculateTargetValue(0f, velocity)
+            val itemSize = with(density) { 200.dp.toPx() }
+            val estimatedNumberOfItemsInDecay = floor(offset.absoluteValue / itemSize)
+            val approachOffset = estimatedNumberOfItemsInDecay * itemSize - itemSize
+            return approachOffset.coerceAtLeast(0f) * velocity.sign
+        }
+        rule.setContent {
+            val state = rememberLazyGridState()
+            layoutInfoProvider = remember(state) { createLayoutInfo(state) }
+            LazyGrid(
+                cells = GridCells.Fixed(3),
+                state = state,
+                flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
+            ) {
+                items(200) {
+                    Box(modifier = Modifier.size(200.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(
+                layoutInfoProvider.calculateApproachOffset(10000f),
+                calculateTargetOffset(10000f)
+            )
+            assertEquals(
+                layoutInfoProvider.calculateApproachOffset(-10000f),
+                calculateTargetOffset(-10000f)
+            )
+        }
+    }
+
+    @Test
+    fun calculateApproachOffset_lowVelocity_approachOffsetIsEqualToZero() {
+        lateinit var layoutInfoProvider: SnapLayoutInfoProvider
+        rule.setContent {
+            val state = rememberLazyGridState()
+            layoutInfoProvider = remember(state) { createLayoutInfo(state) }
+            LazyGrid(
+                cells = GridCells.Fixed(3),
+                state = state,
+                flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
+            ) {
+                items(200) {
+                    Box(modifier = Modifier.size(200.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(
+                layoutInfoProvider.calculateApproachOffset(1000f),
+                0f
+            )
+            assertEquals(
+                layoutInfoProvider.calculateApproachOffset(-1000f),
+                0f
+            )
+        }
+    }
+
+    @Composable
+    private fun MainLayout(
+        state: LazyGridState,
+        layoutInfo: SnapLayoutInfoProvider,
+        items: Int,
+        itemSizeProvider: (Int) -> Dp,
+        gridItem: @Composable (Int) -> Unit = { Box(Modifier.size(itemSizeProvider(it))) }
+    ) {
+        LazyGrid(
+            cells = GridCells.Fixed(3),
+            state = state,
+            flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider = layoutInfo)
+        ) {
+            items(items) { gridItem(it) }
+        }
+    }
+
+    private fun createLayoutInfo(
+        state: LazyGridState,
+    ): SnapLayoutInfoProvider {
+        return SnapLayoutInfoProvider(state)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+
+        val FixedItemSize = 200.dp
+        val DynamicItemSizes = (200..500).map { it.dp }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
new file mode 100644
index 0000000..4a1a839
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
@@ -0,0 +1,519 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.gesture.snapping
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
+import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
+import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
+import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout.Companion.CenterToCenter
+import androidx.compose.foundation.gestures.snapping.calculateDistanceToDesiredSnapPosition
+import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
+import androidx.compose.foundation.gestures.snapping.singleAxisViewportSize
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.list.BaseLazyListTestWithOrientation
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
+import kotlin.math.abs
+import kotlin.math.absoluteValue
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+@OptIn(ExperimentalFoundationApi::class)
+class LazyListSnapFlingBehaviorTest(private val orientation: Orientation) :
+    BaseLazyListTestWithOrientation(orientation) {
+
+    private val density: Density
+        get() = rule.density
+
+    private lateinit var snapLayoutInfoProvider: SnapLayoutInfoProvider
+    private lateinit var snapFlingBehavior: FlingBehavior
+
+    @Test
+    fun belowThresholdVelocity_lessThanAnItemScroll_shouldStayInSamePage() {
+        var lazyListState: LazyListState? = null
+        var stepSize = 0f
+        var velocityThreshold = 0f
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyListState().also { lazyListState = it }
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state)
+        }
+
+        // Scroll a bit
+        onMainList().swipeOnMainAxis()
+        rule.waitForIdle()
+        val currentItem = density.getCurrentSnappedItem(lazyListState)
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(stepSize / 2, velocityThreshold / 2)
+        }
+
+        // assert
+        rule.runOnIdle {
+            val nextItem = density.getCurrentSnappedItem(lazyListState)
+            assertEquals(currentItem, nextItem)
+        }
+    }
+
+    @Test
+    fun belowThresholdVelocity_moreThanAnItemScroll_shouldGoToNextPage() {
+        var lazyListState: LazyListState? = null
+        var stepSize = 0f
+        var velocityThreshold = 0f
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyListState().also { lazyListState = it }
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state)
+        }
+
+        // Scroll a bit
+        onMainList().swipeOnMainAxis()
+        rule.waitForIdle()
+        val currentItem = density.getCurrentSnappedItem(lazyListState)
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                stepSize,
+                velocityThreshold / 2
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            val nextItem = density.getCurrentSnappedItem(lazyListState)
+            assertEquals(currentItem + 1, nextItem)
+        }
+    }
+
+    @Test
+    fun aboveThresholdVelocityForward_notLargeEnoughScroll_shouldGoToNextPage() {
+        var lazyListState: LazyListState? = null
+        var stepSize = 0f
+        var velocityThreshold = 0f
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyListState().also { lazyListState = it }
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state)
+        }
+
+        // Scroll a bit
+        onMainList().swipeOnMainAxis()
+        rule.waitForIdle()
+        val currentItem = density.getCurrentSnappedItem(lazyListState)
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                stepSize / 2,
+                velocityThreshold * 2
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            val nextItem = density.getCurrentSnappedItem(lazyListState)
+            assertEquals(currentItem + 1, nextItem)
+        }
+    }
+
+    @Test
+    fun aboveThresholdVelocityBackward_notLargeEnoughScroll_shouldGoToPreviousPage() {
+        var lazyListState: LazyListState? = null
+        var stepSize = 0f
+        var velocityThreshold = 0f
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyListState().also { lazyListState = it }
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state)
+        }
+
+        // Scroll a bit
+        onMainList().swipeOnMainAxis()
+        rule.waitForIdle()
+        val currentItem = density.getCurrentSnappedItem(lazyListState)
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                stepSize / 2,
+                velocityThreshold * 2,
+                true
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            val nextItem = density.getCurrentSnappedItem(lazyListState)
+            assertEquals(currentItem - 1, nextItem)
+        }
+    }
+
+    @Test
+    fun aboveThresholdVelocity_largeEnoughScroll_shouldGoToNextNextPage() {
+        var lazyListState: LazyListState? = null
+        var stepSize = 0f
+        var velocityThreshold = 0f
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyListState().also { lazyListState = it }
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state)
+        }
+
+        // Scroll a bit
+        onMainList().swipeOnMainAxis()
+        rule.waitForIdle()
+        val currentItem = density.getCurrentSnappedItem(lazyListState)
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                1.5f * stepSize,
+                velocityThreshold * 3
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            val nextItem = density.getCurrentSnappedItem(lazyListState)
+            assertEquals(currentItem + 2, nextItem)
+        }
+    }
+
+    @Test
+    fun performFling_shouldPropagateVelocityIfHitEdges() {
+        var stepSize = 0f
+        var latestAvailableVelocity = Velocity.Zero
+        lateinit var lazyListState: LazyListState
+        val inspectingNestedScrollConnection = object : NestedScrollConnection {
+            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+                latestAvailableVelocity = available
+                return Velocity.Zero
+            }
+        }
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            lazyListState = rememberLazyListState(180) // almost at the end
+            stepSize = with(density) { ItemSize.toPx() }
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .nestedScroll(inspectingNestedScrollConnection)
+            ) {
+                MainLayout(state = lazyListState)
+            }
+        }
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                1.5f * stepSize,
+                30000f
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            assertNotEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+        }
+
+        // arrange
+        rule.runOnIdle {
+            runBlocking {
+                lazyListState.scrollToItem(20) // almost at the start
+            }
+        }
+
+        latestAvailableVelocity = Velocity.Zero
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                -1.5f * stepSize,
+                30000f
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            assertNotEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+        }
+    }
+
+    @Test
+    fun performFling_shouldConsumeAllVelocityIfInTheMiddleOfTheList() {
+        var stepSize = 0f
+        var latestAvailableVelocity = Velocity.Zero
+        lateinit var lazyListState: LazyListState
+        val inspectingNestedScrollConnection = object : NestedScrollConnection {
+            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+                latestAvailableVelocity = available
+                return Velocity.Zero
+            }
+        }
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            lazyListState = rememberLazyListState(100) // middle of the list
+            stepSize = with(density) { ItemSize.toPx() }
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .nestedScroll(inspectingNestedScrollConnection)
+            ) {
+                MainLayout(state = lazyListState)
+            }
+        }
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                1.5f * stepSize,
+                10000f // use a not so high velocity
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            assertEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+        }
+
+        // arrange
+        rule.runOnIdle {
+            runBlocking {
+                lazyListState.scrollToItem(100) // return to the middle
+            }
+        }
+
+        latestAvailableVelocity = Velocity.Zero
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                -1.5f * stepSize,
+                10000f // use a not so high velocity
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            assertEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+        }
+    }
+
+    @Test
+    fun remainingScrollOffset_shouldFollowAnimationOffsets() {
+        var stepSize = 0f
+        var velocityThreshold = 0f
+        val scrollOffset = mutableListOf<Float>()
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyListState()
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state, scrollOffset)
+        }
+
+        rule.mainClock.autoAdvance = false
+        // act
+        val velocity = velocityThreshold * 3
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                1.5f * stepSize,
+                velocity
+            )
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        // assert
+        val initialTargetOffset =
+            snapLayoutInfoProvider.calculateApproachOffset(velocity)
+        Truth.assertThat(scrollOffset[1]).isWithin(0.5f)
+            .of(initialTargetOffset)
+
+        // act: wait for remaining offset to grow instead of decay, this indicates the last
+        // snap step will start
+        rule.mainClock.advanceTimeUntil {
+            scrollOffset.size > 2 &&
+                scrollOffset.last() > scrollOffset[scrollOffset.lastIndex - 1]
+        }
+
+        // assert: next calculated offset is the first value emitted by remainingScrollOffset
+        val finalRemainingOffset = snapLayoutInfoProvider.calculateSnappingOffset(10000f)
+        Truth.assertThat(scrollOffset.last()).isWithin(0.5f)
+            .of(finalRemainingOffset)
+        rule.mainClock.autoAdvance = true
+
+        // assert: value settles back to zero
+        rule.runOnIdle {
+            Truth.assertThat(scrollOffset.last()).isEqualTo(0f)
+        }
+    }
+
+    private fun onMainList() = rule.onNodeWithTag(TestTag)
+
+    @Composable
+    fun MainLayout(state: LazyListState, scrollOffset: MutableList<Float> = mutableListOf()) {
+        snapLayoutInfoProvider = remember(state) { SnapLayoutInfoProvider(state) }
+        val innerFlingBehavior =
+            rememberSnapFlingBehavior(snapLayoutInfoProvider = snapLayoutInfoProvider)
+        snapFlingBehavior = remember(innerFlingBehavior) {
+            QuerySnapFlingBehavior(innerFlingBehavior) {
+                scrollOffset.add(it)
+            }
+        }
+        LazyColumnOrRow(
+            state = state,
+            modifier = Modifier.testTag(TestTag),
+            flingBehavior = snapFlingBehavior
+        ) {
+            items(200) {
+                Box(modifier = Modifier.size(ItemSize))
+            }
+        }
+    }
+
+    private fun SemanticsNodeInteraction.swipeOnMainAxis() {
+        performTouchInput {
+            if (orientation == Orientation.Vertical) {
+                swipeUp()
+            } else {
+                swipeLeft()
+            }
+        }
+    }
+
+    private fun Density.getCurrentSnappedItem(state: LazyListState?): Int {
+        var itemIndex = -1
+        if (state == null) return -1
+        var minDistance = Float.POSITIVE_INFINITY
+        val layoutInfo = state.layoutInfo
+        (state.layoutInfo.visibleItemsInfo).forEach {
+            val distance = calculateDistanceToDesiredSnapPosition(
+                mainAxisViewPortSize = layoutInfo.singleAxisViewportSize,
+                beforeContentPadding = layoutInfo.beforeContentPadding,
+                afterContentPadding = layoutInfo.afterContentPadding,
+                itemSize = it.size,
+                itemOffset = it.offset,
+                itemIndex = it.index,
+                snapPositionInLayout = CenterToCenter
+            )
+            if (abs(distance) < minDistance) {
+                minDistance = abs(distance)
+                itemIndex = it.index
+            }
+        }
+        return itemIndex
+    }
+
+    private fun TouchInjectionScope.swipeMainAxisWithVelocity(
+        scrollSize: Float,
+        endVelocity: Float,
+        reversed: Boolean = false
+    ) {
+        val (start, end) = if (orientation == Orientation.Vertical) {
+            bottomCenter to bottomCenter.copy(y = bottomCenter.y - scrollSize)
+        } else {
+            centerRight to centerRight.copy(x = centerRight.x - scrollSize)
+        }
+        swipeWithVelocity(
+            if (reversed) end else start,
+            if (reversed) start else end,
+            endVelocity
+        )
+    }
+
+    private fun Velocity.toAbsoluteFloat(): Float {
+        return (if (orientation == Orientation.Vertical) y else x).absoluteValue
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+
+        val ItemSize = 200.dp
+        const val TestTag = "MainList"
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class QuerySnapFlingBehavior(
+    val snapFlingBehavior: SnapFlingBehavior,
+    val onAnimationStep: (Float) -> Unit
+) : FlingBehavior {
+    override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+        return with(snapFlingBehavior) {
+            performFling(initialVelocity, onAnimationStep)
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt
new file mode 100644
index 0000000..a91fcb2
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.gesture.snapping
+
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.splineBasedDecay
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
+import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.list.BaseLazyListTestWithOrientation
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import kotlin.math.absoluteValue
+import kotlin.math.floor
+import kotlin.math.sign
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyListSnapLayoutInfoProviderTest(orientation: Orientation) :
+    BaseLazyListTestWithOrientation(orientation) {
+
+    private val density: Density
+        get() = rule.density
+
+    @Test
+    fun calculateApproachOffset_highVelocity_approachOffsetIsEqualToDecayMinusItemSize() {
+        lateinit var layoutInfoProvider: SnapLayoutInfoProvider
+        val decay = splineBasedDecay<Float>(rule.density)
+        fun calculateTargetOffset(velocity: Float): Float {
+            val offset = decay.calculateTargetValue(0f, velocity)
+            val itemSize = with(density) { 200.dp.toPx() }
+            val estimatedNumberOfItemsInDecay = floor(offset.absoluteValue / itemSize)
+            val approachOffset = estimatedNumberOfItemsInDecay * itemSize - itemSize
+            return approachOffset.coerceAtLeast(0f) * velocity.sign
+        }
+        rule.setContent {
+            val state = rememberLazyListState()
+            layoutInfoProvider = remember(state) { createLayoutInfo(state) }
+            LazyColumnOrRow(
+                state = state,
+                flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
+            ) {
+                items(200) {
+                    Box(modifier = Modifier.size(200.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(
+                layoutInfoProvider.calculateApproachOffset(10000f),
+                calculateTargetOffset(10000f)
+            )
+            assertEquals(
+                layoutInfoProvider.calculateApproachOffset(-10000f),
+                calculateTargetOffset(-10000f)
+            )
+        }
+    }
+
+    @Test
+    fun calculateApproachOffset_lowVelocity_approachOffsetIsEqualToZero() {
+        lateinit var layoutInfoProvider: SnapLayoutInfoProvider
+        rule.setContent {
+            val state = rememberLazyListState()
+            layoutInfoProvider = remember(state) { createLayoutInfo(state) }
+            LazyColumnOrRow(
+                state = state,
+                flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
+            ) {
+                items(200) {
+                    Box(modifier = Modifier.size(200.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(
+                layoutInfoProvider.calculateApproachOffset(1000f),
+                0f
+            )
+            assertEquals(
+                layoutInfoProvider.calculateApproachOffset(-1000f),
+                0f
+            )
+        }
+    }
+
+    @Composable
+    private fun MainLayout(
+        state: LazyListState,
+        layoutInfo: SnapLayoutInfoProvider,
+        items: Int,
+        itemSizeProvider: (Int) -> Dp,
+        listItem: @Composable (Int) -> Unit = { Box(Modifier.size(itemSizeProvider(it))) }
+    ) {
+        LazyColumnOrRow(
+            state = state,
+            flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider = layoutInfo)
+        ) {
+            items(items) { listItem(it) }
+        }
+    }
+
+    private fun createLayoutInfo(state: LazyListState): SnapLayoutInfoProvider {
+        return SnapLayoutInfoProvider(state)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+
+        val FixedItemSize = 200.dp
+        val DynamicItemSizes = (200..500).map { it.dp }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
new file mode 100644
index 0000000..38e7560
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
@@ -0,0 +1,561 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.gesture.snapping
+
+import androidx.compose.animation.SplineBasedFloatDecayAnimationSpec
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.FloatDecayAnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.TwoWayConverter
+import androidx.compose.animation.core.VectorizedAnimationSpec
+import androidx.compose.animation.core.generateDecayAnimationSpec
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.TestScrollMotionDurationScale
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
+import androidx.compose.foundation.gestures.snapping.NoVelocity
+import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
+import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
+import androidx.compose.foundation.gestures.snapping.calculateFinalOffset
+import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.text.BasicText
+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.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalFoundationApi::class)
+class SnapFlingBehaviorTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val inspectSpringAnimationSpec = InspectSpringAnimationSpec(spring())
+    private val inspectTweenAnimationSpec = InspectSpringAnimationSpec(tween(easing = LinearEasing))
+
+    private val density: Density
+        get() = rule.density
+
+    @Test
+    fun performFling_whenVelocityIsBelowThreshold_shouldShortSnap() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider()
+        rule.setContent {
+            val testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+            VelocityEffect(testFlingBehavior, calculateVelocityThreshold() - 1)
+        }
+
+        rule.runOnIdle {
+            assertEquals(0, testLayoutInfoProvider.calculateApproachOffsetCount)
+        }
+    }
+
+    @Test
+    fun performFling_whenVelocityIsAboveThreshold_shouldLongSnap() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider()
+        rule.setContent {
+            val testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+            VelocityEffect(testFlingBehavior, calculateVelocityThreshold() + 1)
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, testLayoutInfoProvider.calculateApproachOffsetCount)
+        }
+    }
+
+    @Test
+    fun remainingScrollOffset_whenVelocityIsBelowThreshold_shouldRepresentShortSnapOffsets() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider()
+        lateinit var testFlingBehavior: SnapFlingBehavior
+        val scrollOffset = mutableListOf<Float>()
+        rule.setContent {
+            testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+            VelocityEffect(
+                testFlingBehavior,
+                calculateVelocityThreshold() - 1
+            ) { remainingScrollOffset ->
+                scrollOffset.add(remainingScrollOffset)
+            }
+        }
+
+        // Will Snap Back
+        rule.runOnIdle {
+            assertEquals(scrollOffset.first(), testLayoutInfoProvider.minOffset)
+            assertEquals(scrollOffset.last(), 0f)
+        }
+    }
+
+    @Test
+    fun remainingScrollOffset_whenVelocityIsAboveThreshold_shouldRepresentLongSnapOffsets() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider()
+        lateinit var testFlingBehavior: SnapFlingBehavior
+        val scrollOffset = mutableListOf<Float>()
+        rule.setContent {
+            testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+            VelocityEffect(
+                testFlingBehavior,
+                calculateVelocityThreshold() + 1
+            ) { remainingScrollOffset ->
+                scrollOffset.add(remainingScrollOffset)
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(scrollOffset.first { it != 0f }, testLayoutInfoProvider.maxOffset)
+            assertEquals(scrollOffset.last(), 0f)
+        }
+    }
+
+    @Test
+    fun remainingScrollOffset_longSnap_targetShouldChangeInAccordanceWithAnimation() {
+        // Arrange
+        val initialOffset = 250f
+        val testLayoutInfoProvider = TestLayoutInfoProvider(approachOffset = initialOffset)
+        lateinit var testFlingBehavior: SnapFlingBehavior
+        val scrollOffset = mutableListOf<Float>()
+        rule.mainClock.autoAdvance = false
+        rule.setContent {
+            testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+            VelocityEffect(
+                testFlingBehavior,
+                calculateVelocityThreshold() + 1
+            ) { remainingScrollOffset ->
+                scrollOffset.add(remainingScrollOffset)
+            }
+        }
+
+        // assert the initial value emitted by remainingScrollOffset was the one provider by the
+        // snap layout info provider
+        assertEquals(scrollOffset.first(), initialOffset)
+
+        // Act: Advance until remainingScrollOffset grows again
+        rule.mainClock.advanceTimeUntil {
+            scrollOffset.size > 2 &&
+                scrollOffset.last() > scrollOffset[scrollOffset.lastIndex - 1]
+        }
+
+        assertEquals(scrollOffset.last(), testLayoutInfoProvider.maxOffset)
+
+        rule.mainClock.autoAdvance = true
+        // Assert
+        rule.runOnIdle {
+            assertEquals(scrollOffset.last(), 0f)
+        }
+    }
+
+    @Test
+    fun performFling_afterSnappingVelocity_everythingWasConsumed_shouldReturnNoVelocity() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider()
+        var afterFlingVelocity = 0f
+        rule.setContent {
+            val scrollableState = rememberScrollableState(consumeScrollDelta = { it })
+            val testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+
+            LaunchedEffect(Unit) {
+                scrollableState.scroll {
+                    afterFlingVelocity = with(testFlingBehavior) {
+                        performFling(50000f)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(NoVelocity, afterFlingVelocity)
+        }
+    }
+
+    @Test
+    fun performFling_afterSnappingVelocity_didNotConsumeAllScroll_shouldReturnRemainingVelocity() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider()
+        var afterFlingVelocity = 0f
+        rule.setContent {
+            // Consume only half
+            val scrollableState = rememberScrollableState(consumeScrollDelta = { it / 2f })
+            val testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+
+            LaunchedEffect(Unit) {
+                scrollableState.scroll {
+                    afterFlingVelocity = with(testFlingBehavior) {
+                        performFling(50000f)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertNotEquals(NoVelocity, afterFlingVelocity)
+        }
+    }
+
+    @Test
+    fun findClosestOffset_noFlingDirection_shouldReturnAbsoluteDistance() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider()
+        val offset = testLayoutInfoProvider.calculateSnappingOffset(0f)
+        assertEquals(offset, MinOffset)
+    }
+
+    @Test
+    fun findClosestOffset_flingDirection_shouldReturnCorrectBound() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider()
+        val forwardOffset = testLayoutInfoProvider.calculateSnappingOffset(1f)
+        val backwardOffset = testLayoutInfoProvider.calculateSnappingOffset(-1f)
+        assertEquals(forwardOffset, MaxOffset)
+        assertEquals(backwardOffset, MinOffset)
+    }
+
+    @Test
+    fun approach_cannotDecay_useLowVelocityApproachAndSnap() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider(approachOffset = SnapStep * 5)
+        var inspectSplineAnimationSpec: InspectSplineAnimationSpec? = null
+        rule.setContent {
+            val splineAnimationSpec = rememberInspectSplineAnimationSpec().also {
+                inspectSplineAnimationSpec = it
+            }
+            val testFlingBehavior = rememberSnapFlingBehavior(
+                snapLayoutInfoProvider = testLayoutInfoProvider,
+                highVelocityApproachSpec = splineAnimationSpec.generateDecayAnimationSpec(),
+                lowVelocityApproachSpec = inspectTweenAnimationSpec,
+                snapAnimationSpec = inspectSpringAnimationSpec
+            )
+            VelocityEffect(testFlingBehavior, calculateVelocityThreshold() * 2)
+        }
+
+        rule.runOnIdle {
+            assertEquals(0, inspectSplineAnimationSpec?.animationWasExecutions)
+            assertEquals(1, inspectTweenAnimationSpec.animationWasExecutions)
+            assertEquals(1, inspectSpringAnimationSpec.animationWasExecutions)
+        }
+    }
+
+    @Test
+    fun approach_canDecay_decayAndSnap() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider(maxOffset = 100f)
+        var inspectSplineAnimationSpec: InspectSplineAnimationSpec? = null
+        rule.setContent {
+            val splineAnimationSpec = rememberInspectSplineAnimationSpec().also {
+                inspectSplineAnimationSpec = it
+            }
+            val testFlingBehavior = rememberSnapFlingBehavior(
+                snapLayoutInfoProvider = testLayoutInfoProvider,
+                highVelocityApproachSpec = splineAnimationSpec.generateDecayAnimationSpec(),
+                lowVelocityApproachSpec = inspectTweenAnimationSpec,
+                snapAnimationSpec = inspectSpringAnimationSpec
+            )
+            VelocityEffect(testFlingBehavior, calculateVelocityThreshold() * 5)
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, inspectSplineAnimationSpec?.animationWasExecutions)
+            assertEquals(1, inspectSpringAnimationSpec.animationWasExecutions)
+            assertEquals(0, inspectTweenAnimationSpec.animationWasExecutions)
+        }
+    }
+
+    @Test
+    fun disableSystemAnimations_defaultFlingBehaviorShouldContinueToWork() {
+
+        lateinit var defaultFlingBehavior: SnapFlingBehavior
+        lateinit var scope: CoroutineScope
+        val state = LazyListState()
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            defaultFlingBehavior = rememberSnapFlingBehavior(state) as SnapFlingBehavior
+
+            LazyRow(
+                modifier = Modifier.fillMaxWidth(),
+                state = state,
+                flingBehavior = defaultFlingBehavior as FlingBehavior
+            ) {
+                items(200) { Box(modifier = Modifier.size(20.dp)) }
+            }
+        }
+
+        // Act: Stop clock and fling, one frame should not settle immediately.
+        rule.mainClock.autoAdvance = false
+        scope.launch {
+            state.scroll {
+                with(defaultFlingBehavior) { performFling(10000f) }
+            }
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert
+        rule.runOnIdle {
+            Truth.assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+
+        rule.mainClock.autoAdvance = true
+
+        val previousIndex = state.firstVisibleItemIndex
+
+        // Simulate turning off system wide animation
+        scope.launch {
+            state.scroll {
+                withContext(TestScrollMotionDurationScale(0f)) {
+                    with(defaultFlingBehavior) { performFling(10000f) }
+                }
+            }
+        }
+
+        // Act: Stop clock and fling, one frame should not settle immediately.
+        rule.mainClock.autoAdvance = false
+        scope.launch {
+            state.scroll {
+                with(defaultFlingBehavior) { performFling(10000f) }
+            }
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert
+        rule.runOnIdle {
+            Truth.assertThat(state.firstVisibleItemIndex).isEqualTo(previousIndex)
+        }
+
+        rule.mainClock.autoAdvance = true
+
+        // Assert: let it settle
+        rule.runOnIdle {
+            Truth.assertThat(state.firstVisibleItemIndex).isNotEqualTo(previousIndex)
+        }
+    }
+
+    @Test
+    fun defaultFlingBehavior_useScrollMotionDurationScale() {
+        // Arrange
+        var switchMotionDurationScale by mutableStateOf(false)
+        lateinit var defaultFlingBehavior: SnapFlingBehavior
+        lateinit var scope: CoroutineScope
+        val state = LazyListState()
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            defaultFlingBehavior = rememberSnapFlingBehavior(state) as SnapFlingBehavior
+
+            LazyRow(
+                modifier = Modifier
+                    .testTag("snappingList")
+                    .fillMaxSize(),
+                state = state,
+                flingBehavior = defaultFlingBehavior as FlingBehavior
+            ) {
+                items(200) {
+                    Box(modifier = Modifier.size(150.dp)) {
+                        BasicText(text = it.toString())
+                    }
+                }
+            }
+
+            if (switchMotionDurationScale) {
+                defaultFlingBehavior.motionScaleDuration = TestScrollMotionDurationScale(1f)
+            } else {
+                defaultFlingBehavior.motionScaleDuration = TestScrollMotionDurationScale(0f)
+            }
+        }
+
+        // Act: Stop clock and fling, one frame should settle immediately.
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("snappingList").performTouchInput {
+            swipeWithVelocity(centerRight, center, 10000f)
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert
+        rule.runOnIdle {
+            Truth.assertThat(state.firstVisibleItemIndex).isGreaterThan(0)
+        }
+
+        // Arrange
+        rule.mainClock.autoAdvance = true
+        switchMotionDurationScale = true // Let animations run normally
+        rule.waitForIdle()
+
+        val previousIndex = state.firstVisibleItemIndex
+        // Act: Stop clock and fling, one frame should not settle.
+        rule.mainClock.autoAdvance = false
+        scope.launch {
+            state.scroll {
+                with(defaultFlingBehavior) { performFling(10000f) }
+            }
+        }
+
+        // Assert: First index hasn't changed because animation hasn't started
+        rule.mainClock.advanceTimeByFrame()
+        rule.runOnIdle {
+            Truth.assertThat(state.firstVisibleItemIndex).isEqualTo(previousIndex)
+        }
+        rule.mainClock.autoAdvance = true
+
+        // Wait for settling
+        rule.runOnIdle {
+            Truth.assertThat(state.firstVisibleItemIndex).isNotEqualTo(previousIndex)
+        }
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun VelocityEffect(
+    testFlingBehavior: FlingBehavior,
+    velocity: Float,
+    onSettlingDistanceUpdated: (Float) -> Unit = {}
+) {
+    val scrollableState = rememberScrollableState(consumeScrollDelta = { it })
+    LaunchedEffect(Unit) {
+        scrollableState.scroll {
+            with(testFlingBehavior as SnapFlingBehavior) {
+                performFling(velocity, onSettlingDistanceUpdated)
+            }
+        }
+    }
+}
+
+private class InspectSpringAnimationSpec(
+    private val animation: AnimationSpec<Float>
+) : AnimationSpec<Float> {
+
+    var animationWasExecutions = 0
+
+    override fun <V : AnimationVector> vectorize(
+        converter: TwoWayConverter<Float, V>
+    ): VectorizedAnimationSpec<V> {
+        animationWasExecutions++
+        return animation.vectorize(converter)
+    }
+}
+
+private class InspectSplineAnimationSpec(
+    private val splineBasedFloatDecayAnimationSpec: SplineBasedFloatDecayAnimationSpec
+) : FloatDecayAnimationSpec by splineBasedFloatDecayAnimationSpec {
+
+    private var valueFromNanosCalls = 0
+    val animationWasExecutions: Int
+        get() = valueFromNanosCalls / 2
+
+    override fun getValueFromNanos(
+        playTimeNanos: Long,
+        initialValue: Float,
+        initialVelocity: Float
+    ): Float {
+
+        if (playTimeNanos == 0L) {
+            valueFromNanosCalls++
+        }
+
+        return splineBasedFloatDecayAnimationSpec.getValueFromNanos(
+            playTimeNanos,
+            initialValue,
+            initialVelocity
+        )
+    }
+}
+
+@Composable
+private fun rememberInspectSplineAnimationSpec(): InspectSplineAnimationSpec {
+    val density = LocalDensity.current
+    return remember {
+        InspectSplineAnimationSpec(
+            SplineBasedFloatDecayAnimationSpec(density)
+        )
+    }
+}
+
+@Composable
+private fun calculateVelocityThreshold(): Float {
+    val density = LocalDensity.current
+    return with(density) { MinFlingVelocityDp.toPx() }
+}
+
+private const val SnapStep = 250f
+private const val MinOffset = -200f
+private const val MaxOffset = 300f
+
+@OptIn(ExperimentalFoundationApi::class)
+
+private class TestLayoutInfoProvider(
+    val minOffset: Float = MinOffset,
+    val maxOffset: Float = MaxOffset,
+    val snapStep: Float = SnapStep,
+    val approachOffset: Float = 0f
+) : SnapLayoutInfoProvider {
+    var calculateApproachOffsetCount = 0
+
+    override fun calculateSnappingOffset(currentVelocity: Float): Float {
+        return calculateFinalOffset(currentVelocity, minOffset, maxOffset)
+    }
+
+    override fun calculateApproachOffset(initialVelocity: Float): Float {
+        calculateApproachOffsetCount++
+        return approachOffset
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun rememberSnapFlingBehavior(
+    snapLayoutInfoProvider: SnapLayoutInfoProvider,
+    highVelocityApproachSpec: DecayAnimationSpec<Float>,
+    lowVelocityApproachSpec: AnimationSpec<Float>,
+    snapAnimationSpec: AnimationSpec<Float>
+): FlingBehavior {
+    val density = LocalDensity.current
+    return remember(
+        snapLayoutInfoProvider,
+        highVelocityApproachSpec
+    ) {
+        SnapFlingBehavior(
+            snapLayoutInfoProvider = snapLayoutInfoProvider,
+            lowVelocityAnimationSpec = lowVelocityApproachSpec,
+            highVelocityAnimationSpec = highVelocityApproachSpec,
+            snapAnimationSpec = snapAnimationSpec,
+            shortSnapVelocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+        )
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyCustomKeysTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyCustomKeysTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyCustomKeysTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyCustomKeysTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
new file mode 100644
index 0000000..7f12de1
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
@@ -0,0 +1,634 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.PlacementComparator
+import androidx.compose.foundation.lazy.list.TrackPlacedElement
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.BeyondBoundsLayout
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
+import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
+import androidx.compose.ui.modifier.modifierLocalConsumer
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.LayoutDirection.Ltr
+import androidx.compose.ui.unit.LayoutDirection.Rtl
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalComposeUiApi::class)
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyGridBeyondBoundsTest(param: Param) {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    // We need to wrap the inline class parameter in another class because Java can't instantiate
+    // the inline class.
+    class Param(
+        val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
+        val reverseLayout: Boolean,
+        val layoutDirection: LayoutDirection,
+    ) {
+        override fun toString() = "beyondBoundsLayoutDirection=$beyondBoundsLayoutDirection " +
+            "reverseLayout=$reverseLayout " +
+            "layoutDirection=$layoutDirection"
+    }
+
+    private val beyondBoundsLayoutDirection = param.beyondBoundsLayoutDirection
+    private val reverseLayout = param.reverseLayout
+    private val layoutDirection = param.layoutDirection
+    private val placedItems = sortedMapOf<Int, Rect>()
+    private var beyondBoundsLayout: BeyondBoundsLayout? = null
+    private lateinit var lazyGridState: LazyGridState
+    private val placementComparator =
+        PlacementComparator(beyondBoundsLayoutDirection, layoutDirection, reverseLayout)
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters() = buildList {
+            for (beyondBoundsLayoutDirection in listOf(Left, Right, Above, Below, Before, After)) {
+                for (reverseLayout in listOf(false, true)) {
+                    for (layoutDirection in listOf(Ltr, Rtl)) {
+                        add(Param(beyondBoundsLayoutDirection, reverseLayout, layoutDirection))
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun onlyOneVisibleItemIsPlaced() {
+        // Arrange.
+        rule.setLazyContent(size = 10.toDp(), firstVisibleItem = 0) {
+            items(100) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(0)
+            assertThat(visibleItems).containsExactly(0)
+        }
+    }
+
+    @Test
+    fun onlyTwoVisibleItemsArePlaced() {
+        // Arrange.
+        rule.setLazyContent(size = 20.toDp(), firstVisibleItem = 0) {
+            items(100) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(0, 1)
+            assertThat(visibleItems).containsExactly(0, 1)
+        }
+    }
+
+    @Test
+    fun onlyThreeVisibleItemsArePlaced() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
+            items(100) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(0, 1, 2)
+            assertThat(visibleItems).containsExactly(0, 1, 2)
+        }
+    }
+
+    @Test
+    fun emptyLazyList_doesNotCrash() {
+        // Arrange.
+        var addItems by mutableStateOf(true)
+        lateinit var beyondBoundsLayoutRef: BeyondBoundsLayout
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
+            if (addItems) {
+                item {
+                    Box(
+                        Modifier.modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                    )
+                }
+            }
+        }
+        rule.runOnIdle {
+            beyondBoundsLayoutRef = beyondBoundsLayout!!
+            addItems = false
+        }
+
+        // Act.
+        val hasMoreContent = rule.runOnIdle {
+            beyondBoundsLayoutRef.layout(beyondBoundsLayoutDirection) {
+                hasMoreContent
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(hasMoreContent).isFalse()
+        }
+    }
+
+    @Test
+    fun oneExtraItemBeyondVisibleBounds() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(Modifier
+                    .size(10.toDp())
+                    .trackPlaced(5)
+                    .modifierLocalConsumer {
+                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                    }
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Assert that the beyond bounds items are present.
+                if (expectedExtraItemsBeforeVisibleBounds()) {
+                    assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
+                } else {
+                    assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
+                }
+                assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                assertThat(placedItems.values).isInOrder(placementComparator)
+
+                // Just return true so that we stop as soon as we run this once.
+                // This should result in one extra item being added.
+                true
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+    }
+
+    @Test
+    fun oneExtraItemBeyondVisibleBounds_multipleCells() {
+        val itemSize = 50
+        val itemSizeDp = itemSize.toDp()
+        // Arrange.
+        rule.setLazyContent(cells = 2, size = itemSizeDp * 3, firstVisibleItem = 10) {
+            // item | item  | x5
+            // item | local | x1
+            // item | item  | x5
+            items(11) { index ->
+                Box(
+                    Modifier
+                        .size(itemSizeDp)
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(Modifier
+                    .size(itemSizeDp)
+                    .trackPlaced(11)
+                    .modifierLocalConsumer {
+                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                    }
+                )
+            }
+            items(10) { index ->
+                Box(
+                    Modifier
+                        .size(itemSizeDp)
+                        .trackPlaced(index + 12)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Assert that the beyond bounds items are present.
+                if (expectedExtraItemsBeforeVisibleBounds()) {
+                    assertThat(placedItems.keys).containsExactly(9, 10, 11, 12, 13, 14, 15)
+                } else {
+                    assertThat(placedItems.keys).containsExactly(10, 11, 12, 13, 14, 15, 16)
+                }
+                assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
+
+                assertThat(placedItems.values).isInOrder(placementComparator)
+
+                // Just return true so that we stop as soon as we run this once.
+                // This should result in one extra item being added.
+                true
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(10, 11, 12, 13, 14, 15)
+            assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
+        }
+    }
+
+    @Test
+    fun twoExtraItemsBeyondVisibleBounds() {
+        // Arrange.
+        var extraItemCount = 2
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(5)
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                if (--extraItemCount > 0) {
+                    // Return null to continue the search.
+                    null
+                } else {
+                    // Assert that the beyond bounds items are present.
+                    if (expectedExtraItemsBeforeVisibleBounds()) {
+                        assertThat(placedItems.keys).containsExactly(3, 4, 5, 6, 7)
+                    } else {
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9)
+                    }
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                    assertThat(placedItems.values).isInOrder(placementComparator)
+
+                    // Return true to stop the search.
+                    true
+                }
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+    }
+
+    @Test
+    fun allBeyondBoundsItemsInSpecifiedDirection() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                        .trackPlaced(5)
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                if (hasMoreContent) {
+                    // Just return null so that we keep adding more items till we reach the end.
+                    null
+                } else {
+                    // Assert that the beyond bounds items are present.
+                    if (expectedExtraItemsBeforeVisibleBounds()) {
+                        assertThat(placedItems.keys).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
+                    } else {
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9, 10)
+                    }
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                    assertThat(placedItems.values).isInOrder(placementComparator)
+
+                    // Return true to end the search.
+                    true
+                }
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+        }
+    }
+
+    @Test
+    fun beyondBoundsLayoutRequest_inDirectionPerpendicularToLazyListOrientation() {
+        // Arrange.
+        var beyondBoundsLayoutCount = 0
+        rule.setLazyContentInPerpendicularDirection(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(5)
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                beyondBoundsLayoutCount++
+                when (beyondBoundsLayoutDirection) {
+                    Left, Right, Above, Below -> {
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
+                    }
+                    Before, After -> {
+                        if (expectedExtraItemsBeforeVisibleBounds()) {
+                            assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
+                            assertThat(visibleItems).containsExactly(5, 6, 7)
+                        } else {
+                            assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
+                            assertThat(visibleItems).containsExactly(5, 6, 7)
+                        }
+                    }
+                }
+                // Just return true so that we stop as soon as we run this once.
+                // This should result in one extra item being added.
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            when (beyondBoundsLayoutDirection) {
+                Left, Right, Above, Below -> {
+                    assertThat(beyondBoundsLayoutCount).isEqualTo(0)
+                }
+                Before, After -> {
+                    assertThat(beyondBoundsLayoutCount).isEqualTo(1)
+
+                    // Assert that the beyond bounds items are removed.
+                    assertThat(placedItems.keys).containsExactly(5, 6, 7)
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+                }
+                else -> error("Unsupported BeyondBoundsLayoutDirection")
+            }
+        }
+    }
+
+    @Test
+    fun returningNullDoesNotCauseInfiniteLoop() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                        .trackPlaced(5)
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        var count = 0
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Assert that we don't keep iterating when there is no ending condition.
+                assertThat(count++).isLessThan(lazyGridState.layoutInfo.totalItemsCount)
+                // Always return null to continue the search.
+                null
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+    }
+
+    private fun ComposeContentTestRule.setLazyContent(
+        size: Dp,
+        firstVisibleItem: Int,
+        cells: Int = 1,
+        content: LazyGridScope.() -> Unit
+    ) {
+        setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                lazyGridState = rememberLazyGridState(firstVisibleItem)
+                when (beyondBoundsLayoutDirection) {
+                    Left, Right, Before, After ->
+                        LazyHorizontalGrid(
+                            rows = GridCells.Fixed(cells),
+                            modifier = Modifier.size(size),
+                            state = lazyGridState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    Above, Below ->
+                        LazyVerticalGrid(
+                            columns = GridCells.Fixed(cells),
+                            modifier = Modifier.size(size),
+                            state = lazyGridState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    else -> unsupportedDirection()
+                }
+            }
+        }
+    }
+
+    private fun ComposeContentTestRule.setLazyContentInPerpendicularDirection(
+        size: Dp,
+        firstVisibleItem: Int,
+        content: LazyGridScope.() -> Unit
+    ) {
+        setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                lazyGridState = rememberLazyGridState(firstVisibleItem)
+                when (beyondBoundsLayoutDirection) {
+                    Left, Right, Before, After ->
+                        LazyVerticalGrid(
+                            columns = GridCells.Fixed(1),
+                            modifier = Modifier.size(size),
+                            state = lazyGridState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    Above, Below ->
+                        LazyHorizontalGrid(
+                            rows = GridCells.Fixed(1),
+                            modifier = Modifier.size(size),
+                            state = lazyGridState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    else -> unsupportedDirection()
+                }
+            }
+        }
+    }
+
+    private fun Int.toDp(): Dp = with(rule.density) { toDp() }
+
+    private val visibleItems: List<Int>
+        get() = lazyGridState.layoutInfo.visibleItemsInfo.map { it.index }
+
+    private fun expectedExtraItemsBeforeVisibleBounds() = when (beyondBoundsLayoutDirection) {
+        Right -> if (layoutDirection == Ltr) reverseLayout else !reverseLayout
+        Left -> if (layoutDirection == Ltr) !reverseLayout else reverseLayout
+        Above -> !reverseLayout
+        Below -> reverseLayout
+        After -> false
+        Before -> true
+        else -> error("Unsupported BeyondBoundsDirection")
+    }
+
+    private fun unsupportedDirection(): Nothing = error(
+        "Lazy list does not support beyond bounds layout for the specified direction"
+    )
+
+    private fun Modifier.trackPlaced(index: Int): Modifier =
+        this then TrackPlacedElement(index, placedItems)
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazySemanticsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazySemanticsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazySemanticsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazySemanticsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutStateRestorationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutStateRestorationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutStateRestorationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutStateRestorationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyArrangementsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyArrangementsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyArrangementsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyArrangementsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyCustomKeysTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyCustomKeysTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyCustomKeysTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyCustomKeysTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt
new file mode 100644
index 0000000..01d26d2
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.BeyondBoundsLayout
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
+import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
+import androidx.compose.ui.modifier.modifierLocalConsumer
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyListBeyondBoundsAndExtraItemsTest(val config: Config) :
+    BaseLazyListTestWithOrientation(config.orientation) {
+
+    private val beyondBoundsLayoutDirection = config.beyondBoundsLayoutDirection
+    private val reverseLayout = config.reverseLayout
+    private val layoutDirection = config.layoutDirection
+    private val placedItems = sortedMapOf<Int, Rect>()
+    private val placementComparator =
+        PlacementComparator(beyondBoundsLayoutDirection, layoutDirection, reverseLayout)
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun verifyItemsArePlacedBeforeBeyondBoundsItems_oneBeyondBoundItem() {
+        // Arrange
+        var beyondBoundsLayout: BeyondBoundsLayout? = null
+        val firstVisibleItemIndex = 5
+        var lazyListState = LazyListState()
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                lazyListState = rememberLazyListState(firstVisibleItemIndex)
+                LazyColumnOrRow(
+                    modifier = Modifier.size(30.dp),
+                    state = lazyListState,
+                    beyondBoundsItemCount = 1,
+                    reverseLayout = reverseLayout
+                ) {
+                    items(5) { index ->
+                        Box(
+                            Modifier
+                                .size(10.dp)
+                                .trackPlaced(index)
+                        )
+                    }
+                    item {
+                        Box(
+                            Modifier
+                                .size(10.dp)
+                                .trackPlaced(5)
+                                .modifierLocalConsumer {
+                                    beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                                }
+                        )
+                    }
+                    items(5) { index ->
+                        Box(
+                            Modifier
+                                .size(10.dp)
+                                .trackPlaced(index + 6)
+                        )
+                    }
+                }
+            }
+        }
+
+        // Act
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Beyond bounds items are present.
+                if (expectedExtraItemsBeforeVisibleBounds()) {
+                    assertThat(placedItems.keys).containsAtLeast(3, 4, 5, 6, 7, 8)
+                } else {
+                    assertThat(placedItems.keys).containsAtLeast(4, 5, 6, 7, 8, 9)
+                }
+                assertThat(lazyListState.visibleItems).containsAtLeast(5, 6, 7)
+
+                assertThat(placedItems.values).isInOrder(placementComparator)
+
+                true
+            }
+        }
+
+        // Beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsAtLeast(4, 5, 6, 7, 8)
+            assertThat(lazyListState.visibleItems).containsAtLeast(5, 6, 7)
+        }
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun verifyItemsArePlacedBeforeBeyondBoundsItems_twoBeyondBoundItem() {
+        // Arrange
+        var beyondBoundsLayout: BeyondBoundsLayout? = null
+        val firstVisibleItemIndex = 5
+        var lazyListState = LazyListState()
+        var extraItemCount = 2
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                lazyListState = rememberLazyListState(firstVisibleItemIndex)
+                LazyColumnOrRow(
+                    modifier = Modifier.size(30.dp),
+                    state = lazyListState,
+                    beyondBoundsItemCount = 1,
+                    reverseLayout = reverseLayout
+                ) {
+                    items(5) { index ->
+                        Box(
+                            Modifier
+                                .size(10.dp)
+                                .trackPlaced(index)
+                        )
+                    }
+                    item {
+                        Box(
+                            Modifier
+                                .size(10.dp)
+                                .trackPlaced(5)
+                                .modifierLocalConsumer {
+                                    beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                                }
+                        )
+                    }
+                    items(5) { index ->
+                        Box(
+                            Modifier
+                                .size(10.dp)
+                                .trackPlaced(index + 6)
+                        )
+                    }
+                }
+            }
+        }
+
+        // Act
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                if (--extraItemCount > 0) {
+                    // Return null to continue the search.
+                    null
+                } else {
+                    // Beyond bounds items are present.
+                    if (expectedExtraItemsBeforeVisibleBounds()) {
+                        assertThat(placedItems.keys).containsAtLeast(2, 3, 4, 5, 6, 7, 8)
+                    } else {
+                        assertThat(placedItems.keys).containsAtLeast(4, 5, 6, 7, 8, 9, 10)
+                    }
+                    assertThat(lazyListState.visibleItems).containsAtLeast(5, 6, 7)
+
+                    assertThat(placedItems.values).isInOrder(placementComparator)
+
+                    true
+                }
+            }
+        }
+
+        // Beyond bounds items are removed
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsAtLeast(4, 5, 6, 7, 8)
+            assertThat(lazyListState.visibleItems).containsAtLeast(5, 6, 7)
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = buildList {
+            for (orientation in listOf(Orientation.Horizontal, Orientation.Vertical)) {
+                for (beyondBoundsLayoutDirection in listOf(
+                    Left,
+                    Right,
+                    Above,
+                    Below,
+                    Before,
+                    After
+                )) {
+                    for (reverseLayout in listOf(false, true)) {
+                        for (layoutDirection in listOf(LayoutDirection.Ltr, LayoutDirection.Rtl)) {
+                            add(
+                                Config(
+                                    orientation,
+                                    beyondBoundsLayoutDirection,
+                                    reverseLayout,
+                                    layoutDirection
+                                )
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        class Config(
+            val orientation: Orientation,
+            val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
+            val reverseLayout: Boolean,
+            val layoutDirection: LayoutDirection
+        ) {
+            override fun toString(): String {
+                return "orientation=$orientation " +
+                    "beyondBoundsLayoutDirection=$beyondBoundsLayoutDirection " +
+                    "reverseLayout=$reverseLayout " +
+                    "layoutDirection=$layoutDirection"
+            }
+        }
+    }
+
+    private val LazyListState.visibleItems: List<Int>
+        get() = layoutInfo.visibleItemsInfo.map { it.index }
+
+    private fun expectedExtraItemsBeforeVisibleBounds() = when (beyondBoundsLayoutDirection) {
+        Right -> if (layoutDirection == LayoutDirection.Ltr) reverseLayout else !reverseLayout
+        Left -> if (layoutDirection == LayoutDirection.Ltr) !reverseLayout else reverseLayout
+        Above -> !reverseLayout
+        Below -> reverseLayout
+        After -> false
+        Before -> true
+        else -> error("Unsupported BeyondBoundsDirection")
+    }
+
+    private fun Modifier.trackPlaced(index: Int): Modifier =
+        this then TrackPlacedElement(index, placedItems)
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsItemCountTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsItemCountTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsItemCountTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsItemCountTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
new file mode 100644
index 0000000..afd0020
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
@@ -0,0 +1,633 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.list
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.BeyondBoundsLayout
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
+import androidx.compose.ui.layout.findRootCoordinates
+import androidx.compose.ui.modifier.modifierLocalConsumer
+import androidx.compose.ui.node.LayoutAwareModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.LayoutDirection.Ltr
+import androidx.compose.ui.unit.LayoutDirection.Rtl
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalComposeUiApi::class)
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyListBeyondBoundsTest(param: Param) {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    // We need to wrap the inline class parameter in another class because Java can't instantiate
+    // the inline class.
+    class Param(
+        val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
+        val reverseLayout: Boolean,
+        val layoutDirection: LayoutDirection,
+    ) {
+        override fun toString() = "beyondBoundsLayoutDirection=$beyondBoundsLayoutDirection " +
+            "reverseLayout=$reverseLayout " +
+            "layoutDirection=$layoutDirection"
+    }
+
+    private val beyondBoundsLayoutDirection = param.beyondBoundsLayoutDirection
+    private val reverseLayout = param.reverseLayout
+    private val layoutDirection = param.layoutDirection
+    private val placedItems = sortedMapOf<Int, Rect>()
+    private var beyondBoundsLayout: BeyondBoundsLayout? = null
+    private lateinit var lazyListState: LazyListState
+    private val placementComparator =
+        PlacementComparator(beyondBoundsLayoutDirection, layoutDirection, reverseLayout)
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters() = buildList {
+            for (beyondBoundsLayoutDirection in listOf(Left, Right, Above, Below, Before, After)) {
+                for (reverseLayout in listOf(false, true)) {
+                    for (layoutDirection in listOf(Ltr, Rtl)) {
+                        add(Param(beyondBoundsLayoutDirection, reverseLayout, layoutDirection))
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun onlyOneVisibleItemIsPlaced() {
+        // Arrange.
+        rule.setLazyContent(size = 10.toDp(), firstVisibleItem = 0) {
+            items(100) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(0)
+            assertThat(visibleItems).containsExactly(0)
+        }
+    }
+
+    @Test
+    fun onlyTwoVisibleItemsArePlaced() {
+        // Arrange.
+        rule.setLazyContent(size = 20.toDp(), firstVisibleItem = 0) {
+            items(100) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(0, 1)
+            assertThat(visibleItems).containsExactly(0, 1)
+        }
+    }
+
+    @Test
+    fun onlyThreeVisibleItemsArePlaced() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
+            items(100) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(0, 1, 2)
+            assertThat(visibleItems).containsExactly(0, 1, 2)
+        }
+    }
+
+    @Test
+    fun emptyLazyList_doesNotCrash() {
+        // Arrange.
+        var addItems by mutableStateOf(true)
+        lateinit var beyondBoundsLayoutRef: BeyondBoundsLayout
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
+            if (addItems) {
+                item {
+                    Box(
+                        Modifier.modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                    )
+                }
+            }
+        }
+        rule.runOnIdle {
+            beyondBoundsLayoutRef = beyondBoundsLayout!!
+            addItems = false
+        }
+
+        // Act.
+        val hasMoreContent = rule.runOnIdle {
+            beyondBoundsLayoutRef.layout(beyondBoundsLayoutDirection) {
+                hasMoreContent
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(hasMoreContent).isFalse()
+        }
+    }
+
+    @Test
+    fun oneExtraItemBeyondVisibleBounds() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(5)
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Assert that the beyond bounds items are present.
+                if (expectedExtraItemsBeforeVisibleBounds()) {
+                    assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
+                } else {
+                    assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
+                }
+                assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                assertThat(placedItems.values).isInOrder(placementComparator)
+
+                // Just return true so that we stop as soon as we run this once.
+                // This should result in one extra item being added.
+                true
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+    }
+
+    @Test
+    fun twoExtraItemsBeyondVisibleBounds() {
+        // Arrange.
+        var extraItemCount = 2
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(5)
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                if (--extraItemCount > 0) {
+                    // Return null to continue the search.
+                    null
+                } else {
+                    // Assert that the beyond bounds items are present.
+                    if (expectedExtraItemsBeforeVisibleBounds()) {
+                        assertThat(placedItems.keys).containsExactly(3, 4, 5, 6, 7)
+                    } else {
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9)
+                    }
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                    assertThat(placedItems.values).isInOrder(placementComparator)
+
+                    // Return true to stop the search.
+                    true
+                }
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+    }
+
+    @Test
+    fun allBeyondBoundsItemsInSpecifiedDirection() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                        .trackPlaced(5)
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                if (hasMoreContent) {
+                    // Just return null so that we keep adding more items till we reach the end.
+                    null
+                } else {
+                    // Assert that the beyond bounds items are present.
+                    if (expectedExtraItemsBeforeVisibleBounds()) {
+                        assertThat(placedItems.keys).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
+                    } else {
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9, 10)
+                    }
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                    assertThat(placedItems.values).isInOrder(placementComparator)
+
+                    // Return true to end the search.
+                    true
+                }
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+        }
+    }
+
+    @Test
+    fun beyondBoundsLayoutRequest_inDirectionPerpendicularToLazyListOrientation() {
+        // Arrange.
+        var beyondBoundsLayoutCount = 0
+        rule.setLazyContentInPerpendicularDirection(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(5)
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                beyondBoundsLayoutCount++
+                when (beyondBoundsLayoutDirection) {
+                    Left, Right, Above, Below -> {
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
+                    }
+                    Before, After -> {
+                        if (expectedExtraItemsBeforeVisibleBounds()) {
+                            assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
+                            assertThat(visibleItems).containsExactly(5, 6, 7)
+                        } else {
+                            assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
+                            assertThat(visibleItems).containsExactly(5, 6, 7)
+                        }
+                    }
+                }
+                // Just return true so that we stop as soon as we run this once.
+                // This should result in one extra item being added.
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            when (beyondBoundsLayoutDirection) {
+                Left, Right, Above, Below -> {
+                    assertThat(beyondBoundsLayoutCount).isEqualTo(0)
+                }
+                Before, After -> {
+                    assertThat(beyondBoundsLayoutCount).isEqualTo(1)
+
+                    // Assert that the beyond bounds items are removed.
+                    assertThat(placedItems.keys).containsExactly(5, 6, 7)
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+                }
+                else -> error("Unsupported BeyondBoundsLayoutDirection")
+            }
+        }
+    }
+
+    @Test
+    fun returningNullDoesNotCauseInfiniteLoop() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                        .trackPlaced(5)
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        var count = 0
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Assert that we don't keep iterating when there is no ending condition.
+                assertThat(count++).isLessThan(lazyListState.layoutInfo.totalItemsCount)
+                // Always return null to continue the search.
+                null
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+    }
+
+    private fun ComposeContentTestRule.setLazyContent(
+        size: Dp,
+        firstVisibleItem: Int,
+        content: LazyListScope.() -> Unit
+    ) {
+        setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                lazyListState = rememberLazyListState(firstVisibleItem)
+                when (beyondBoundsLayoutDirection) {
+                    Left, Right, Before, After ->
+                        LazyRow(
+                            modifier = Modifier.size(size),
+                            state = lazyListState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    Above, Below ->
+                        LazyColumn(
+                            modifier = Modifier.size(size),
+                            state = lazyListState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    else -> unsupportedDirection()
+                }
+            }
+        }
+    }
+
+    private fun ComposeContentTestRule.setLazyContentInPerpendicularDirection(
+        size: Dp,
+        firstVisibleItem: Int,
+        content: LazyListScope.() -> Unit
+    ) {
+        setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                lazyListState = rememberLazyListState(firstVisibleItem)
+                when (beyondBoundsLayoutDirection) {
+                    Left, Right, Before, After ->
+                        LazyColumn(
+                            modifier = Modifier.size(size),
+                            state = lazyListState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    Above, Below ->
+                        LazyRow(
+                            modifier = Modifier.size(size),
+                            state = lazyListState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    else -> unsupportedDirection()
+                }
+            }
+        }
+    }
+
+    private fun Int.toDp(): Dp = with(rule.density) { toDp() }
+
+    private val visibleItems: List<Int>
+        get() = lazyListState.layoutInfo.visibleItemsInfo.map { it.index }
+
+    private fun expectedExtraItemsBeforeVisibleBounds() = when (beyondBoundsLayoutDirection) {
+        Right -> if (layoutDirection == Ltr) reverseLayout else !reverseLayout
+        Left -> if (layoutDirection == Ltr) !reverseLayout else reverseLayout
+        Above -> !reverseLayout
+        Below -> reverseLayout
+        After -> false
+        Before -> true
+        else -> error("Unsupported BeyondBoundsDirection")
+    }
+
+    private fun unsupportedDirection(): Nothing = error(
+        "Lazy list does not support beyond bounds layout for the specified direction"
+    )
+
+    private fun Modifier.trackPlaced(index: Int): Modifier =
+        this then TrackPlacedElement(index, placedItems)
+}
+
+internal data class TrackPlacedElement(
+    var index: Int,
+    var placedItems: MutableMap<Int, Rect>
+) : ModifierNodeElement<TrackPlacedNode>() {
+    override fun create() = TrackPlacedNode(index, placedItems)
+
+    override fun update(node: TrackPlacedNode) {
+        node.index = index
+        node.placedItems = placedItems
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "trackPlaced"
+        properties["index"] = index
+        properties["placedItems"] = placedItems
+    }
+}
+
+internal class TrackPlacedNode(
+    var index: Int,
+    var placedItems: MutableMap<Int, Rect>
+) : LayoutAwareModifierNode, Modifier.Node() {
+    override fun onPlaced(coordinates: LayoutCoordinates) {
+        placedItems[index] =
+            coordinates.findRootCoordinates().localBoundingBoxOf(coordinates, false)
+    }
+
+    override fun onDetach() {
+        placedItems.remove(index)
+    }
+}
+
+internal class PlacementComparator(
+    val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
+    val layoutDirection: LayoutDirection,
+    val reverseLayout: Boolean
+) : Comparator<Rect> {
+    private fun itemsInReverseOrder() = when (beyondBoundsLayoutDirection) {
+        Above, Below -> reverseLayout
+        else -> if (layoutDirection == Ltr) reverseLayout else !reverseLayout
+    }
+
+    private fun compareOffset(o1: Float, o2: Float): Int {
+        return if (itemsInReverseOrder()) o2.compareTo(o1) else o1.compareTo(o2)
+    }
+
+    override fun compare(o1: Rect?, o2: Rect?): Int {
+        if (o1 == null || o2 == null) return 0
+        return when (beyondBoundsLayoutDirection) {
+            Above, Below -> compareOffset(o1.top, o2.top)
+            else -> compareOffset(o1.left, o2.left)
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListHeadersTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListHeadersTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListHeadersTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListHeadersTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsIndexedTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsIndexedTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsIndexedTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsIndexedTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyNestedScrollingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyNestedScrollingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyNestedScrollingTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyNestedScrollingTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyRowTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyRowTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyRowTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyRowTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazySemanticsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazySemanticsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazySemanticsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazySemanticsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateItemPlacementTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateItemPlacementTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateItemPlacementTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateItemPlacementTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridArrangementsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridArrangementsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridArrangementsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridArrangementsTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt
new file mode 100644
index 0000000..fda112e
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt
@@ -0,0 +1,701 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.staggeredgrid
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.PlacementComparator
+import androidx.compose.foundation.lazy.list.TrackPlacedElement
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.BeyondBoundsLayout
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
+import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
+import androidx.compose.ui.modifier.modifierLocalConsumer
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.LayoutDirection.Ltr
+import androidx.compose.ui.unit.LayoutDirection.Rtl
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalComposeUiApi::class)
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyStaggeredGridBeyondBoundsTest(param: Param) {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    // We need to wrap the inline class parameter in another class because Java can't instantiate
+    // the inline class.
+    class Param(
+        val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
+        val reverseLayout: Boolean,
+        val layoutDirection: LayoutDirection,
+    ) {
+        override fun toString() = "beyondBoundsLayoutDirection=$beyondBoundsLayoutDirection " +
+            "reverseLayout=$reverseLayout " +
+            "layoutDirection=$layoutDirection"
+    }
+
+    private val beyondBoundsLayoutDirection = param.beyondBoundsLayoutDirection
+    private val reverseLayout = param.reverseLayout
+    private val layoutDirection = param.layoutDirection
+    private val placedItems = sortedMapOf<Int, Rect>()
+    private var beyondBoundsLayout: BeyondBoundsLayout? = null
+    private lateinit var lazyStaggeredGridState: LazyStaggeredGridState
+    private val placementComparator =
+        PlacementComparator(beyondBoundsLayoutDirection, layoutDirection, reverseLayout)
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters() = buildList {
+            for (beyondBoundsLayoutDirection in listOf(Left, Right, Above, Below, Before, After)) {
+                for (reverseLayout in listOf(false, true)) {
+                    for (layoutDirection in listOf(Ltr, Rtl)) {
+                        add(Param(beyondBoundsLayoutDirection, reverseLayout, layoutDirection))
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun onlyOneVisibleItemIsPlaced() {
+        // Arrange.
+        rule.setLazyContent(size = 10.toDp(), firstVisibleItem = 0) {
+            items(100) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(0)
+            assertThat(visibleItems).containsExactly(0)
+        }
+    }
+
+    @Test
+    fun onlyTwoVisibleItemsArePlaced() {
+        // Arrange.
+        rule.setLazyContent(size = 20.toDp(), firstVisibleItem = 0) {
+            items(100) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(0, 1)
+            assertThat(visibleItems).containsExactly(0, 1)
+        }
+    }
+
+    @Test
+    fun onlyThreeVisibleItemsArePlaced() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
+            items(100) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(0, 1, 2)
+            assertThat(visibleItems).containsExactly(0, 1, 2)
+        }
+    }
+
+    @Test
+    fun emptyLazyList_doesNotCrash() {
+        // Arrange.
+        var addItems by mutableStateOf(true)
+        lateinit var beyondBoundsLayoutRef: BeyondBoundsLayout
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
+            if (addItems) {
+                item {
+                    Box(
+                        Modifier.modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                    )
+                }
+            }
+        }
+        rule.runOnIdle {
+            beyondBoundsLayoutRef = beyondBoundsLayout!!
+            addItems = false
+        }
+
+        // Act.
+        val hasMoreContent = rule.runOnIdle {
+            beyondBoundsLayoutRef.layout(beyondBoundsLayoutDirection) {
+                hasMoreContent
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(hasMoreContent).isFalse()
+        }
+    }
+
+    @Test
+    fun oneExtraItemBeyondVisibleBounds() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(Modifier
+                    .size(10.toDp())
+                    .trackPlaced(5)
+                    .modifierLocalConsumer {
+                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                    }
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Assert that the beyond bounds items are present.
+                if (expectedExtraItemsBeforeVisibleBounds()) {
+                    assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
+                } else {
+                    assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
+                }
+                assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                assertThat(placedItems.values).isInOrder(placementComparator)
+
+                // Just return true so that we stop as soon as we run this once.
+                // This should result in one extra item being added.
+                true
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+    }
+
+    @Test
+    fun oneExtraItemBeyondVisibleBounds_multipleCells() {
+        val itemSize = 50
+        val itemSizeDp = itemSize.toDp()
+        // Arrange.
+        rule.setLazyContent(cells = 2, size = itemSizeDp * 3, firstVisibleItem = 10) {
+            // item | item  | x5
+            // item | local | x1
+            // item | item  | x5
+            items(11) { index ->
+                Box(
+                    Modifier
+                        .size(itemSizeDp)
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(Modifier
+                    .size(itemSizeDp)
+                    .trackPlaced(11)
+                    .modifierLocalConsumer {
+                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                    }
+                )
+            }
+            items(10) { index ->
+                Box(
+                    Modifier
+                        .size(itemSizeDp)
+                        .trackPlaced(index + 12)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Assert that the beyond bounds items are present.
+                if (expectedExtraItemsBeforeVisibleBounds()) {
+                    assertThat(placedItems.keys).containsExactly(9, 10, 11, 12, 13, 14, 15)
+                } else {
+                    assertThat(placedItems.keys).containsExactly(10, 11, 12, 13, 14, 15, 16)
+                }
+                assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
+
+                assertThat(placedItems.values).isInOrder(placementComparator)
+
+                // Just return true so that we stop as soon as we run this once.
+                // This should result in one extra item being added.
+                true
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(10, 11, 12, 13, 14, 15)
+            assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
+        }
+    }
+
+    @Test
+    fun oneExtraItemBeyondVisibleBounds_multipleCells_staggered() {
+        val itemSize = 50
+        val itemSizeDp = itemSize.toDp()
+        // Arrange.
+        rule.setLazyContent(cells = 3, size = itemSizeDp * 2, firstVisibleItem = 4) {
+            // -------------
+            // |   | 1 |   |
+            // | 0 |---| 2 |
+            // |   | 3 |   |
+            // |-----------|
+            // |     4     |
+            // |-----------|
+            // |   | 6 |   |
+            // | 5 |---| 7 |
+            // |   | 8 |   |
+            // -------------
+            items(4) { index ->
+                Box(
+                    Modifier
+                        .size(itemSizeDp * if (index % 2 == 0) 2f else 1f)
+                        .trackPlaced(index)
+                )
+            }
+            item(span = StaggeredGridItemSpan.FullLine) {
+                Box(Modifier
+                    .size(itemSizeDp)
+                    .trackPlaced(4)
+                    .modifierLocalConsumer {
+                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                    }
+                )
+            }
+            items(4) { index ->
+                Box(
+                    Modifier
+                        .size(itemSizeDp * if (index % 2 == 0) 2f else 1f)
+                        .trackPlaced(index + 5)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Assert that the beyond bounds items are present.
+                if (expectedExtraItemsBeforeVisibleBounds()) {
+                    assertThat(placedItems.keys).containsExactly(3, 4, 5, 6, 7)
+                    assertThat(visibleItems).containsExactly(4, 5, 6, 7)
+                } else {
+                    assertThat(placedItems.keys).containsExactly(4, 5, 6, 7, 8)
+                    assertThat(visibleItems).containsExactly(4, 5, 6, 7)
+                }
+                // Just return true so that we stop as soon as we run this once.
+                // This should result in one extra item being added.
+                true
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
+            assertThat(visibleItems).containsExactly(4, 5, 6, 7)
+        }
+    }
+
+    @Test
+    fun twoExtraItemsBeyondVisibleBounds() {
+        // Arrange.
+        var extraItemCount = 2
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(5)
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                if (--extraItemCount > 0) {
+                    // Return null to continue the search.
+                    null
+                } else {
+                    // Assert that the beyond bounds items are present.
+                    if (expectedExtraItemsBeforeVisibleBounds()) {
+                        assertThat(placedItems.keys).containsExactly(3, 4, 5, 6, 7)
+                    } else {
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9)
+                    }
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                    assertThat(placedItems.values).isInOrder(placementComparator)
+
+                    // Return true to stop the search.
+                    true
+                }
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+    }
+
+    @Test
+    fun allBeyondBoundsItemsInSpecifiedDirection() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                        .trackPlaced(5)
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                if (hasMoreContent) {
+                    // Just return null so that we keep adding more items till we reach the end.
+                    null
+                } else {
+                    // Assert that the beyond bounds items are present.
+                    if (expectedExtraItemsBeforeVisibleBounds()) {
+                        assertThat(placedItems.keys).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
+                    } else {
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9, 10)
+                    }
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                    // Verify if the placed item offsets are in order.
+                    assertThat(placedItems.toSortedMap().values).isInOrder(placementComparator)
+
+                    // Return true to end the search.
+                    true
+                }
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+        }
+    }
+
+    @Test
+    fun beyondBoundsLayoutRequest_inDirectionPerpendicularToLazyListOrientation() {
+        // Arrange.
+        var beyondBoundsLayoutCount = 0
+        rule.setLazyContentInPerpendicularDirection(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(5)
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                beyondBoundsLayoutCount++
+                when (beyondBoundsLayoutDirection) {
+                    Left, Right, Above, Below -> {
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
+                    }
+                    Before, After -> {
+                        if (expectedExtraItemsBeforeVisibleBounds()) {
+                            assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
+                            assertThat(visibleItems).containsExactly(5, 6, 7)
+                        } else {
+                            assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
+                            assertThat(visibleItems).containsExactly(5, 6, 7)
+                        }
+                    }
+                }
+                // Just return true so that we stop as soon as we run this once.
+                // This should result in one extra item being added.
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            when (beyondBoundsLayoutDirection) {
+                Left, Right, Above, Below -> {
+                    assertThat(beyondBoundsLayoutCount).isEqualTo(0)
+                }
+                Before, After -> {
+                    assertThat(beyondBoundsLayoutCount).isEqualTo(1)
+
+                    // Assert that the beyond bounds items are removed.
+                    assertThat(placedItems.keys).containsExactly(5, 6, 7)
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+                }
+                else -> error("Unsupported BeyondBoundsLayoutDirection")
+            }
+        }
+    }
+
+    @Test
+    fun returningNullDoesNotCauseInfiniteLoop() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                        .trackPlaced(5)
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        var count = 0
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Assert that we don't keep iterating when there is no ending condition.
+                assertThat(count++).isLessThan(lazyStaggeredGridState.layoutInfo.totalItemsCount)
+                // Always return null to continue the search.
+                null
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+    }
+
+    private fun ComposeContentTestRule.setLazyContent(
+        size: Dp,
+        firstVisibleItem: Int,
+        cells: Int = 1,
+        content: LazyStaggeredGridScope.() -> Unit
+    ) {
+        setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                lazyStaggeredGridState = rememberLazyStaggeredGridState(firstVisibleItem)
+                when (beyondBoundsLayoutDirection) {
+                    Left, Right, Before, After ->
+                        LazyHorizontalStaggeredGrid(
+                            rows = StaggeredGridCells.Fixed(cells),
+                            modifier = Modifier.size(size),
+                            state = lazyStaggeredGridState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    Above, Below ->
+                        LazyVerticalStaggeredGrid(
+                            columns = StaggeredGridCells.Fixed(cells),
+                            modifier = Modifier.size(size),
+                            state = lazyStaggeredGridState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    else -> unsupportedDirection()
+                }
+            }
+        }
+    }
+
+    private fun ComposeContentTestRule.setLazyContentInPerpendicularDirection(
+        size: Dp,
+        firstVisibleItem: Int,
+        content: LazyStaggeredGridScope.() -> Unit
+    ) {
+        setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                lazyStaggeredGridState = rememberLazyStaggeredGridState(firstVisibleItem)
+                when (beyondBoundsLayoutDirection) {
+                    Left, Right, Before, After ->
+                        LazyVerticalStaggeredGrid(
+                            columns = StaggeredGridCells.Fixed(1),
+                            modifier = Modifier.size(size),
+                            state = lazyStaggeredGridState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    Above, Below ->
+                        LazyHorizontalStaggeredGrid(
+                            rows = StaggeredGridCells.Fixed(1),
+                            modifier = Modifier.size(size),
+                            state = lazyStaggeredGridState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    else -> unsupportedDirection()
+                }
+            }
+        }
+    }
+
+    private fun Int.toDp(): Dp = with(rule.density) { toDp() }
+
+    private val visibleItems: List<Int>
+        get() = lazyStaggeredGridState.layoutInfo.visibleItemsInfo.map { it.index }
+
+    private fun expectedExtraItemsBeforeVisibleBounds() = when (beyondBoundsLayoutDirection) {
+        Right -> if (layoutDirection == Ltr) reverseLayout else !reverseLayout
+        Left -> if (layoutDirection == Ltr) !reverseLayout else reverseLayout
+        Above -> !reverseLayout
+        Below -> reverseLayout
+        After -> false
+        Before -> true
+        else -> error("Unsupported BeyondBoundsDirection")
+    }
+
+    private fun unsupportedDirection(): Nothing = error(
+        "Lazy list does not support beyond bounds layout for the specified direction"
+    )
+
+    private fun Modifier.trackPlaced(index: Int): Modifier =
+        this then TrackPlacedElement(index, placedItems)
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridCustomKeysTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridCustomKeysTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridCustomKeysTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridCustomKeysTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLayoutInfoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLayoutInfoTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLayoutInfoTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLayoutInfoTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridReverseLayoutTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridReverseLayoutTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridReverseLayoutTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridReverseLayoutTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemanticTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemanticTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemanticTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemanticTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PageSizeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PageSizeTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PageSizeTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PageSizeTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerAccessibilityTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerAccessibilityTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerAccessibilityTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerAccessibilityTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerContentPaddingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerContentPaddingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerContentPaddingTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerContentPaddingTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerContentTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerContentTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerContentTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerContentTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerCrossAxisTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerCrossAxisTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerCrossAxisTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerCrossAxisTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerCustomKeyTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerCustomKeyTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerCustomKeyTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerCustomKeyTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerGestureTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerGestureTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerGestureTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerGestureTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerLayoutInfoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerLayoutInfoTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerLayoutInfoTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerLayoutInfoTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerNestedScrollContentTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerNestedScrollContentTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerNestedScrollContentTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerNestedScrollContentTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerOffscreenPageLimitPlacingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerOffscreenPageLimitPlacingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerOffscreenPageLimitPlacingTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerOffscreenPageLimitPlacingTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerPinnableContainerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPinnableContainerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerPinnableContainerTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPinnableContainerTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerPrefetcherTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPrefetcherTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerPrefetcherTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPrefetcherTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
new file mode 100644
index 0000000..e6321c8
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
@@ -0,0 +1,717 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+class PagerScrollingTest(
+    val config: ParamConfig
+) : BasePagerTest(config) {
+
+    @Before
+    fun setUp() {
+        rule.mainClock.autoAdvance = false
+    }
+
+    @Test
+    fun swipeWithLowVelocity_positionalThresholdLessThanDefaultThreshold_shouldBounceBack() {
+        // Arrange
+        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
+        val swipeValue = 0.4f
+        val delta = pagerSize * swipeValue * scrollForwardSign
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta * -1
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun swipeWithLowVelocity_positionalThresholdLessThanLowThreshold_shouldBounceBack() {
+        // Arrange
+        createPager(
+            initialPage = 5,
+            modifier = Modifier.fillMaxSize(),
+            snapPositionalThreshold = 0.2f
+        )
+        val swipeValue = 0.1f
+        val delta = pagerSize * swipeValue * scrollForwardSign
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta * -1
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun swipeWithLowVelocity_positionalThresholdLessThanHighThreshold_shouldBounceBack() {
+        // Arrange
+        createPager(
+            initialPage = 5,
+            modifier = Modifier.fillMaxSize(),
+            snapPositionalThreshold = 0.8f
+        )
+        val swipeValue = 0.6f
+        val delta = pagerSize * swipeValue * scrollForwardSign
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta * -1
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun swipeWithLowVelocity_positionalThresholdLessThanDefault_customPageSize_shouldBounceBack() {
+        // Arrange
+        createPager(initialPage = 2, modifier = Modifier.fillMaxSize(), pageSize = {
+            PageSize.Fixed(200.dp)
+        })
+
+        val delta = (2.4f * pageSize) * scrollForwardSign // 2.4 pages
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("4").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(4)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta * -1
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("2").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(2)
+    }
+
+    @Test
+    fun swipeWithLowVelocity_positionalThresholdOverDefaultThreshold_shouldGoToNextPage() {
+        // Arrange
+        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
+        val swipeValue = 0.51f
+        val delta = pagerSize * swipeValue * scrollForwardSign
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("6").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(6)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta * -1
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun swipeWithLowVelocity_positionalThresholdOverLowThreshold_shouldGoToNextPage() {
+        // Arrange
+        createPager(
+            initialPage = 5,
+            modifier = Modifier.fillMaxSize(),
+            snapPositionalThreshold = 0.2f
+        )
+        val swipeValue = 0.21f
+        val delta = pagerSize * swipeValue * scrollForwardSign
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("6").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(6)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta * -1
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun swipeWithLowVelocity_onEdgeOfList_smallDeltas_shouldGoToClosestPage_backward() {
+        // Arrange
+        createPager(modifier = Modifier.fillMaxSize())
+        val delta = 10f * scrollForwardSign * -1
+
+        onPager().performTouchInput {
+            down(center)
+            // series of backward delta on edge
+            moveBy(Offset(if (vertical) 0.0f else delta, if (vertical) delta else 0.0f))
+            moveBy(Offset(if (vertical) 0.0f else delta, if (vertical) delta else 0.0f))
+            moveBy(Offset(if (vertical) 0.0f else delta, if (vertical) delta else 0.0f))
+
+            // single delta on opposite direction
+            moveBy(
+                Offset(
+                    if (vertical) 0.0f else -delta,
+                    if (vertical) -delta else 0.0f
+                )
+            )
+            up()
+        }
+        rule.mainClock.advanceTimeUntil { pagerState.isScrollInProgress == false }
+
+        // Assert
+        rule.onNodeWithTag("0").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(0)
+    }
+
+    @Test
+    fun swipeWithLowVelocity_onEdgeOfList_smallDeltas_shouldGoToClosestPage_forward() {
+        // Arrange
+        createPager(modifier = Modifier.fillMaxSize(), initialPage = DefaultPageCount - 1)
+        val delta = 10f * scrollForwardSign
+
+        onPager().performTouchInput {
+            down(center)
+            // series of backward delta on edge
+            moveBy(Offset(if (vertical) 0.0f else delta, if (vertical) delta else 0.0f))
+            moveBy(Offset(if (vertical) 0.0f else delta, if (vertical) delta else 0.0f))
+            moveBy(Offset(if (vertical) 0.0f else delta, if (vertical) delta else 0.0f))
+
+            // single delta on opposite direction
+            moveBy(
+                Offset(
+                    if (vertical) 0.0f else -delta,
+                    if (vertical) -delta else 0.0f
+                )
+            )
+            up()
+        }
+        rule.mainClock.advanceTimeUntil { pagerState.isScrollInProgress == false }
+
+        // Assert
+        rule.onNodeWithTag("${DefaultPageCount - 1}").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(DefaultPageCount - 1)
+    }
+
+    @Test
+    fun swipeWithLowVelocity_positionalThresholdOverThreshold_customPage_shouldGoToNextPage() {
+        // Arrange
+        createPager(
+            initialPage = 2,
+            modifier = Modifier.fillMaxSize(),
+            pageSize = {
+                PageSize.Fixed(200.dp)
+            }
+        )
+
+        val delta = 2.6f * pageSize * scrollForwardSign
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta * -1
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("2").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(2)
+    }
+
+    @Test
+    fun swipeWithLowVelocity_positionalThresholdOverHighThreshold_shouldGoToNextPage() {
+        // Arrange
+        createPager(
+            initialPage = 5,
+            modifier = Modifier.fillMaxSize(),
+            snapPositionalThreshold = 0.8f
+        )
+        val swipeValue = 0.81f
+        val delta = pagerSize * swipeValue * scrollForwardSign
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("6").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(6)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                    delta * -1
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun swipeWithLowVelocity_customVelocityThreshold_shouldBounceBack() {
+        // Arrange
+        val snapVelocityThreshold = 200.dp
+        createPager(
+            initialPage = 5,
+            modifier = Modifier.fillMaxSize(),
+            snapVelocityThreshold = snapVelocityThreshold
+        )
+        val delta = pagerSize * 0.4f * scrollForwardSign
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * snapVelocityThreshold.toPx() },
+                    delta
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 0.5f * snapVelocityThreshold.toPx() },
+                    delta * -1
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun swipeWithHighVelocity_defaultVelocityThreshold_shouldGoToNextPage() {
+        // Arrange
+        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
+        // make sure the scroll distance is not enough to go to next page
+        val delta = pagerSize * 0.4f * scrollForwardSign
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
+                    delta
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("6").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(6)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
+                    delta * -1
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun swipeWithHighVelocity_customVelocityThreshold_shouldGoToNextPage() {
+        // Arrange
+        val snapVelocityThreshold = 200.dp
+        createPager(
+            initialPage = 5,
+            modifier = Modifier.fillMaxSize(),
+            snapVelocityThreshold = snapVelocityThreshold
+        )
+        // make sure the scroll distance is not enough to go to next page
+        val delta = pagerSize * 0.4f * scrollForwardSign
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 1.1f * snapVelocityThreshold.toPx() },
+                    delta
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("6").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(6)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 1.1f * snapVelocityThreshold.toPx() },
+                    delta * -1
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun swipeWithHighVelocity_overHalfPage_shouldGoToNextPage() {
+        // Arrange
+        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
+        // make sure the scroll distance is not enough to go to next page
+        val delta = pagerSize * 0.8f * scrollForwardSign
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
+                    delta
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("6").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(6)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
+                    delta * -1
+                )
+            }
+        }
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun scrollWithoutVelocity_shouldSettlingInClosestPage() {
+        // Arrange
+        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
+        // This will scroll 1 whole page before flinging
+        val delta = pagerSize * 1.4f * scrollForwardSign
+
+        // Act - forward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(0f, delta)
+            }
+        }
+
+        // Assert
+        assertThat(pagerState.currentPage).isAtMost(7)
+        rule.onNodeWithTag("${pagerState.currentPage}").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(pagerState.currentPage)
+
+        // Act - backward
+        runAndWaitForPageSettling {
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(0f, delta * -1)
+            }
+        }
+
+        // Assert
+        assertThat(pagerState.currentPage).isAtLeast(5)
+        rule.onNodeWithTag("${pagerState.currentPage}").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(pagerState.currentPage)
+    }
+
+    @Test
+    fun scrollWithSameVelocity_shouldYieldSameResult_forward() {
+        // Arrange
+        var initialPage = 1
+        createPager(
+            pageSize = { PageSize.Fixed(200.dp) },
+            initialPage = initialPage,
+            modifier = Modifier.fillMaxSize(),
+            pageCount = { 100 },
+            snappingPage = PagerSnapDistance.atMost(3)
+        )
+        // This will scroll 0.5 page before flinging
+        val delta = pagerSize * 0.5f * scrollForwardSign
+
+        // Act - forward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(2000f, delta)
+        }
+        rule.waitForIdle()
+
+        val pageDisplacement = pagerState.currentPage - initialPage
+
+        // Repeat starting from different places
+        // reset
+        initialPage = 10
+        rule.runOnIdle {
+            runBlocking { pagerState.scrollToPage(initialPage) }
+        }
+
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(2000f, delta)
+        }
+        rule.waitForIdle()
+
+        assertThat(pagerState.currentPage - initialPage).isEqualTo(pageDisplacement)
+
+        initialPage = 50
+        rule.runOnIdle {
+            runBlocking { pagerState.scrollToPage(initialPage) }
+        }
+
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(2000f, delta)
+        }
+        rule.waitForIdle()
+
+        assertThat(pagerState.currentPage - initialPage).isEqualTo(pageDisplacement)
+    }
+
+    @Test
+    fun scrollWithSameVelocity_shouldYieldSameResult_backward() {
+        // Arrange
+        var initialPage = 90
+        createPager(
+            pageSize = { PageSize.Fixed(200.dp) },
+            initialPage = initialPage,
+            modifier = Modifier.fillMaxSize(),
+            pageCount = { 100 },
+            snappingPage = PagerSnapDistance.atMost(3)
+        )
+        // This will scroll 0.5 page before flinging
+        val delta = pagerSize * -0.5f * scrollForwardSign
+
+        // Act - forward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(2000f, delta)
+        }
+        rule.waitForIdle()
+
+        val pageDisplacement = pagerState.currentPage - initialPage
+
+        // Repeat starting from different places
+        // reset
+        initialPage = 70
+        rule.runOnIdle {
+            runBlocking { pagerState.scrollToPage(initialPage) }
+        }
+
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(2000f, delta)
+        }
+        rule.waitForIdle()
+
+        assertThat(pagerState.currentPage - initialPage).isEqualTo(pageDisplacement)
+
+        initialPage = 30
+        rule.runOnIdle {
+            runBlocking { pagerState.scrollToPage(initialPage) }
+        }
+
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(2000f, delta)
+        }
+        rule.waitForIdle()
+
+        assertThat(pagerState.currentPage - initialPage).isEqualTo(pageDisplacement)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = mutableListOf<ParamConfig>().apply {
+            for (orientation in TestOrientation) {
+                for (pageSpacing in TestPageSpacing) {
+                    add(
+                        ParamConfig(
+                            orientation = orientation,
+                            pageSpacing = pageSpacing
+                        )
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt
new file mode 100644
index 0000000..ff647ba
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt
@@ -0,0 +1,385 @@
+/*
+ * 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.foundation.pager
+
+import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
+import kotlin.test.assertFalse
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+class PagerStateNonGestureScrollingTest(val config: ParamConfig) : BasePagerTest(config) {
+    @Test
+    fun pagerStateNotAttached_shouldReturnDefaultValues_andChangeAfterAttached() = runBlocking {
+        // Arrange
+        val state = PagerStateImpl(5, 0.2f) { DefaultPageCount }
+
+        Truth.assertThat(state.currentPage).isEqualTo(5)
+        Truth.assertThat(state.currentPageOffsetFraction).isEqualTo(0.2f)
+
+        val currentPage = derivedStateOf { state.currentPage }
+        val currentPageOffsetFraction = derivedStateOf { state.currentPageOffsetFraction }
+
+        rule.setContent {
+            HorizontalOrVerticalPager(
+                state = state,
+                modifier = Modifier
+                    .fillMaxSize()
+                    .testTag(PagerTestTag)
+                    .onSizeChanged { pagerSize = if (vertical) it.height else it.width },
+                pageSize = PageSize.Fill,
+                reverseLayout = config.reverseLayout,
+                pageSpacing = config.pageSpacing,
+                contentPadding = config.mainAxisContentPadding,
+            ) {
+                Page(index = it)
+            }
+        }
+
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToPage(state.currentPage + 1)
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(currentPage.value).isEqualTo(6)
+            Truth.assertThat(currentPageOffsetFraction.value).isEqualTo(0.0f)
+        }
+    }
+
+    @Test
+    fun initialPageOnPagerState_shouldDisplayThatPageFirst() {
+        // Arrange
+
+        // Act
+        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
+
+        // Assert
+        rule.onNodeWithTag("4").assertDoesNotExist()
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        rule.onNodeWithTag("6").assertDoesNotExist()
+        confirmPageIsInCorrectPosition(pagerState.currentPage)
+    }
+
+    @Test
+    fun testStateRestoration() {
+        // Arrange
+        val tester = StateRestorationTester(rule)
+        lateinit var state: PagerState
+        tester.setContent {
+            state = rememberPagerState(pageCount = { DefaultPageCount })
+            scope = rememberCoroutineScope()
+            HorizontalOrVerticalPager(
+                state = state,
+                modifier = Modifier.fillMaxSize()
+            ) {
+                Page(it)
+            }
+        }
+
+        // Act
+        rule.runOnIdle {
+            scope.launch {
+                state.scrollToPage(5)
+            }
+            runBlocking {
+                state.scroll {
+                    scrollBy(50f)
+                }
+            }
+        }
+
+        val previousPage = state.currentPage
+        val previousOffset = state.currentPageOffsetFraction
+        tester.emulateSavedInstanceStateRestore()
+
+        // Assert
+        rule.runOnIdle {
+            Truth.assertThat(state.currentPage).isEqualTo(previousPage)
+            Truth.assertThat(state.currentPageOffsetFraction).isEqualTo(previousOffset)
+        }
+    }
+
+    @Test
+    fun currentPageOffsetFraction_shouldNeverBeNan() {
+        rule.setContent {
+            val state = rememberPagerState(pageCount = { 10 })
+            // Read state in composition, should never be Nan
+            assertFalse { state.currentPageOffsetFraction.isNaN() }
+            HorizontalOrVerticalPager(state = state) {
+                Page(index = it)
+            }
+        }
+    }
+
+    @Test
+    fun currentPage_pagerWithKeys_shouldBeTheSameAfterDatasetUpdate() {
+        // Arrange
+        class Data(val id: Int, val item: String)
+
+        val data = mutableListOf(
+            Data(3, "A"),
+            Data(4, "B"),
+            Data(5, "C")
+        )
+
+        val extraData = mutableListOf(
+            Data(0, "D"),
+            Data(1, "E"),
+            Data(2, "F")
+        )
+
+        val dataset = mutableStateOf<List<Data>>(data)
+
+        createPager(
+            modifier = Modifier.fillMaxSize(),
+            initialPage = 1,
+            key = { dataset.value[it].id },
+            pageCount = {
+                dataset.value.size
+            }, pageContent = {
+                val item = dataset.value[it]
+                Box(modifier = Modifier.fillMaxSize().testTag(item.item))
+            })
+
+        Truth.assertThat(dataset.value[pagerState.currentPage].item).isEqualTo("B")
+
+        rule.runOnIdle {
+            dataset.value = extraData + data // add new data
+        }
+
+        rule.waitForIdle()
+        Truth.assertThat(pagerState.pageCount).isEqualTo(6) // all data is present
+        rule.onNodeWithTag("B").assertIsDisplayed() // scroll kept
+        Truth.assertThat(pagerState.currentPage).isEqualTo(4)
+        Truth.assertThat(pagerState.currentPageOffsetFraction).isEqualTo(0.0f)
+    }
+
+    @Test
+    fun calculatePageCountOffset_shouldBeBasedOnCurrentPage() {
+        val pageToOffsetCalculations = mutableMapOf<Int, Float>()
+        createPager(modifier = Modifier.fillMaxSize(), pageSize = { PageSize.Fixed(20.dp) }) {
+            pageToOffsetCalculations[it] = pagerState.getOffsetFractionForPage(it)
+            Page(index = it)
+        }
+
+        for ((page, offset) in pageToOffsetCalculations) {
+            val currentPage = pagerState.currentPage
+            val currentPageOffset = pagerState.currentPageOffsetFraction
+            Truth.assertThat(offset).isEqualTo((currentPage - page) + currentPageOffset)
+        }
+    }
+
+    @Test
+    fun scrollToPage_usingLaunchedEffect() {
+
+        createPager(additionalContent = {
+            LaunchedEffect(pagerState) {
+                pagerState.scrollToPage(10)
+            }
+        })
+
+        Truth.assertThat(pagerState.currentPage).isEqualTo(10)
+        confirmPageIsInCorrectPosition(10)
+    }
+
+    @Test
+    fun scrollToPageWithOffset_usingLaunchedEffect() {
+        createPager(additionalContent = {
+            LaunchedEffect(pagerState) {
+                pagerState.scrollToPage(10, 0.4f)
+            }
+        })
+
+        Truth.assertThat(pagerState.currentPage).isEqualTo(10)
+        confirmPageIsInCorrectPosition(10, pageOffset = 0.4f)
+    }
+
+    @Test
+    fun animatedScrollToPage_usingLaunchedEffect() {
+
+        createPager(additionalContent = {
+            LaunchedEffect(pagerState) {
+                pagerState.animateScrollToPage(10)
+            }
+        })
+
+        Truth.assertThat(pagerState.currentPage).isEqualTo(10)
+        confirmPageIsInCorrectPosition(10)
+    }
+
+    @Test
+    fun animatedScrollToPage_emptyPager_shouldNotReact() {
+        createPager(pageCount = { 0 }, additionalContent = {
+            LaunchedEffect(pagerState) {
+                pagerState.animateScrollToPage(10)
+            }
+        })
+        Truth.assertThat(pagerState.currentPage).isEqualTo(0)
+    }
+
+    @Test
+    fun animatedScrollToPageWithOffset_usingLaunchedEffect() {
+
+        createPager(additionalContent = {
+            LaunchedEffect(pagerState) {
+                pagerState.animateScrollToPage(10, 0.4f)
+            }
+        })
+
+        Truth.assertThat(pagerState.currentPage).isEqualTo(10)
+        confirmPageIsInCorrectPosition(10, pageOffset = 0.4f)
+    }
+
+    @Test
+    fun animatedScrollToPage_viewPortNumberOfPages_usingLaunchedEffect_shouldNotPlaceALlPages() {
+
+        createPager(additionalContent = {
+            LaunchedEffect(pagerState) {
+                pagerState.animateScrollToPage(DefaultPageCount - 1)
+            }
+        })
+
+        // Assert
+        rule.runOnIdle {
+            Truth.assertThat(pagerState.currentPage).isEqualTo(DefaultPageCount - 1)
+            Truth.assertThat(placed).doesNotContain(DefaultPageCount / 2 - 1)
+            Truth.assertThat(placed).doesNotContain(DefaultPageCount / 2)
+            Truth.assertThat(placed).doesNotContain(DefaultPageCount / 2 + 1)
+        }
+        confirmPageIsInCorrectPosition(pagerState.currentPage)
+    }
+
+    @Test
+    fun scrollTo_beforeFirstLayout_shouldWaitForStateAndLayoutSetting() {
+        // Arrange
+
+        rule.mainClock.autoAdvance = false
+
+        // Act
+        createPager(modifier = Modifier.fillMaxSize(), additionalContent = {
+            LaunchedEffect(pagerState) {
+                pagerState.scrollToPage(5)
+            }
+        })
+
+        // Assert
+        Truth.assertThat(pagerState.currentPage).isEqualTo(5)
+    }
+
+    @Test
+    fun updateCurrentPage_shouldUpdateCurrentPageImmediately() {
+        createPager(modifier = Modifier.fillMaxSize())
+
+        Truth.assertThat(pagerState.currentPage).isEqualTo(0)
+
+        rule.runOnUiThread {
+            scope.launch {
+                with(pagerState) {
+                    scroll {
+                        updateCurrentPage(5)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(pagerState.currentPage).isEqualTo(5)
+        }
+    }
+
+    @Test
+    fun updateCurrentPage_shouldUpdateCurrentPageOffsetFractionImmediately() {
+        createPager(modifier = Modifier.fillMaxSize())
+
+        Truth.assertThat(pagerState.currentPage).isEqualTo(0)
+
+        rule.runOnUiThread {
+            scope.launch {
+                with(pagerState) {
+                    scroll {
+                        updateCurrentPage(5, 0.3f)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(pagerState.currentPageOffsetFraction).isWithin(0.01f).of(0.3f)
+        }
+    }
+
+    @Test
+    fun updateTargetPage_shouldUpdateTargetPageImmediately_andResetIfNotMoved() {
+        createPager(modifier = Modifier.fillMaxSize())
+
+        Truth.assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage)
+
+        rule.mainClock.autoAdvance = false
+
+        rule.runOnUiThread {
+            scope.launch {
+                with(pagerState) {
+                    scroll {
+                        updateTargetPage(5)
+                        delay(1_000L) // simulate an animation
+                    }
+                }
+            }
+        }
+
+        rule.mainClock.advanceTimeByFrame() // pump a frame
+        Truth.assertThat(pagerState.targetPage).isEqualTo(5) // target page changed
+        rule.mainClock.advanceTimeBy(2_000L) // scroll block finished but we didn't move
+        // target page reset
+        Truth.assertThat(pagerState.currentPage).isEqualTo(0)
+        Truth.assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = mutableListOf<ParamConfig>().apply {
+            for (orientation in TestOrientation) {
+                add(ParamConfig(orientation = orientation))
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
new file mode 100644
index 0000000..1289cf9
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
@@ -0,0 +1,868 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.animate
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertTrue
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+class PagerStateTest(val config: ParamConfig) : BasePagerTest(config) {
+
+    @Test
+    fun scrollToPage_shouldPlacePagesCorrectly() = runBlocking {
+        // Arrange
+        createPager(modifier = Modifier.fillMaxSize())
+
+        // Act and Assert
+        repeat(DefaultAnimationRepetition) {
+            assertThat(pagerState.currentPage).isEqualTo(it)
+            val nextPage = pagerState.currentPage + 1
+            withContext(Dispatchers.Main + AutoTestFrameClock()) {
+                pagerState.scrollToPage(nextPage)
+            }
+            rule.mainClock.advanceTimeUntil { pagerState.currentPage == nextPage }
+            confirmPageIsInCorrectPosition(pagerState.currentPage)
+        }
+    }
+
+    @SdkSuppress(maxSdkVersion = 32) // b/269176638
+    @Test
+    fun scrollToPage_usedOffset_shouldPlacePagesCorrectly() = runBlocking {
+        // Arrange
+
+        suspend fun scrollToPageWithOffset(page: Int, offset: Float) {
+            withContext(Dispatchers.Main + AutoTestFrameClock()) {
+                pagerState.scrollToPage(page, offset)
+            }
+        }
+
+        // Arrange
+        createPager(modifier = Modifier.fillMaxSize())
+
+        // Act
+        scrollToPageWithOffset(10, 0.5f)
+
+        // Assert
+        confirmPageIsInCorrectPosition(pagerState.currentPage, 10, pageOffset = 0.5f)
+
+        // Act
+        scrollToPageWithOffset(4, 0.2f)
+
+        // Assert
+        confirmPageIsInCorrectPosition(pagerState.currentPage, 4, pageOffset = 0.2f)
+
+        // Act
+        scrollToPageWithOffset(12, -0.4f)
+
+        // Assert
+        confirmPageIsInCorrectPosition(pagerState.currentPage, 12, pageOffset = -0.4f)
+
+        // Act
+        scrollToPageWithOffset(DefaultPageCount - 1, 0.5f)
+
+        // Assert
+        confirmPageIsInCorrectPosition(pagerState.currentPage, DefaultPageCount - 1)
+
+        // Act
+        scrollToPageWithOffset(0, -0.5f)
+
+        // Assert
+        confirmPageIsInCorrectPosition(pagerState.currentPage, 0)
+    }
+
+    @Test
+    fun scrollToPage_longSkipShouldNotPlaceIntermediatePages() = runBlocking {
+        // Arrange
+
+        createPager(modifier = Modifier.fillMaxSize())
+
+        // Act
+        assertThat(pagerState.currentPage).isEqualTo(0)
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            pagerState.scrollToPage(DefaultPageCount - 1)
+        }
+
+        // Assert
+        rule.runOnIdle {
+            assertThat(pagerState.currentPage).isEqualTo(DefaultPageCount - 1)
+            assertThat(placed).doesNotContain(DefaultPageCount / 2 - 1)
+            assertThat(placed).doesNotContain(DefaultPageCount / 2)
+            assertThat(placed).doesNotContain(DefaultPageCount / 2 + 1)
+        }
+        confirmPageIsInCorrectPosition(pagerState.currentPage)
+    }
+
+    @Test
+    fun animateScrollToPage_shouldPlacePagesCorrectly() = runBlocking {
+        // Arrange
+
+        createPager(modifier = Modifier.fillMaxSize())
+
+        // Act and Assert
+        repeat(DefaultAnimationRepetition) {
+            assertThat(pagerState.currentPage).isEqualTo(it)
+            val nextPage = pagerState.currentPage + 1
+            withContext(Dispatchers.Main + AutoTestFrameClock()) {
+                pagerState.animateScrollToPage(nextPage)
+            }
+            rule.mainClock.advanceTimeUntil { pagerState.currentPage == nextPage }
+            confirmPageIsInCorrectPosition(pagerState.currentPage)
+        }
+    }
+
+    @Test
+    fun animateScrollToPage_usedOffset_shouldPlacePagesCorrectly() = runBlocking {
+        // Arrange
+
+        suspend fun animateScrollToPageWithOffset(page: Int, offset: Float) {
+            withContext(Dispatchers.Main + AutoTestFrameClock()) {
+                pagerState.animateScrollToPage(page, offset)
+            }
+        }
+
+        // Arrange
+        createPager(modifier = Modifier.fillMaxSize())
+
+        // Act
+        animateScrollToPageWithOffset(10, 0.5f)
+
+        // Assert
+        confirmPageIsInCorrectPosition(pagerState.currentPage, 10, pageOffset = 0.5f)
+
+        // Act
+        animateScrollToPageWithOffset(4, 0.2f)
+
+        // Assert
+        confirmPageIsInCorrectPosition(pagerState.currentPage, 4, pageOffset = 0.2f)
+
+        // Act
+        animateScrollToPageWithOffset(12, -0.4f)
+
+        // Assert
+        confirmPageIsInCorrectPosition(pagerState.currentPage, 12, pageOffset = -0.4f)
+
+        // Act
+        animateScrollToPageWithOffset(DefaultPageCount - 1, 0.5f)
+
+        // Assert
+        confirmPageIsInCorrectPosition(pagerState.currentPage, DefaultPageCount - 1)
+
+        // Act
+        animateScrollToPageWithOffset(0, -0.5f)
+
+        // Assert
+        confirmPageIsInCorrectPosition(pagerState.currentPage, 0)
+    }
+
+    @Test
+    fun animateScrollToPage_longSkipShouldNotPlaceIntermediatePages() = runBlocking {
+        // Arrange
+
+        createPager(modifier = Modifier.fillMaxSize())
+
+        // Act
+        assertThat(pagerState.currentPage).isEqualTo(0)
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            pagerState.animateScrollToPage(DefaultPageCount - 1)
+        }
+
+        // Assert
+        rule.runOnIdle {
+            assertThat(pagerState.currentPage).isEqualTo(DefaultPageCount - 1)
+            assertThat(placed).doesNotContain(DefaultPageCount / 2 - 1)
+            assertThat(placed).doesNotContain(DefaultPageCount / 2)
+            assertThat(placed).doesNotContain(DefaultPageCount / 2 + 1)
+        }
+        confirmPageIsInCorrectPosition(pagerState.currentPage)
+    }
+
+    @Test
+    fun scrollToPage_shouldCoerceWithinRange() = runBlocking {
+        // Arrange
+
+        createPager(modifier = Modifier.fillMaxSize())
+
+        // Act
+        assertThat(pagerState.currentPage).isEqualTo(0)
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            pagerState.scrollToPage(DefaultPageCount)
+        }
+
+        // Assert
+        rule.runOnIdle { assertThat(pagerState.currentPage).isEqualTo(DefaultPageCount - 1) }
+
+        // Act
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            pagerState.scrollToPage(-1)
+        }
+
+        // Assert
+        rule.runOnIdle { assertThat(pagerState.currentPage).isEqualTo(0) }
+    }
+
+    @Test
+    fun animateScrollToPage_shouldCoerceWithinRange() = runBlocking {
+        // Arrange
+
+        createPager(modifier = Modifier.fillMaxSize())
+
+        // Act
+        assertThat(pagerState.currentPage).isEqualTo(0)
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            pagerState.animateScrollToPage(DefaultPageCount)
+        }
+
+        // Assert
+        rule.runOnIdle { assertThat(pagerState.currentPage).isEqualTo(DefaultPageCount - 1) }
+
+        // Act
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            pagerState.animateScrollToPage(-1)
+        }
+
+        // Assert
+        rule.runOnIdle { assertThat(pagerState.currentPage).isEqualTo(0) }
+    }
+
+    @Test
+    fun animateScrollToPage_moveToSamePageWithOffset_shouldScroll() = runBlocking {
+        // Arrange
+        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
+
+        // Act
+        assertThat(pagerState.currentPage).isEqualTo(5)
+
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            pagerState.animateScrollToPage(5, 0.4f)
+        }
+
+        // Assert
+        rule.runOnIdle { assertThat(pagerState.currentPage).isEqualTo(5) }
+        rule.runOnIdle { assertThat(pagerState.currentPageOffsetFraction).isWithin(0.01f).of(0.4f) }
+    }
+
+    @Test
+    fun animateScrollToPage_withPassedAnimation() = runBlocking {
+        // Arrange
+        rule.mainClock.autoAdvance = false
+        createPager(modifier = Modifier.fillMaxSize())
+        val differentAnimation: AnimationSpec<Float> = tween()
+
+        // Act and Assert
+        repeat(DefaultAnimationRepetition) {
+            assertThat(pagerState.currentPage).isEqualTo(it)
+            val nextPage = pagerState.currentPage + 1
+            withContext(Dispatchers.Main + AutoTestFrameClock()) {
+                pagerState.animateScrollToPage(
+                    nextPage,
+                    animationSpec = differentAnimation
+                )
+            }
+            rule.mainClock.advanceTimeUntil { pagerState.currentPage == nextPage }
+            confirmPageIsInCorrectPosition(pagerState.currentPage)
+        }
+    }
+
+    @Test
+    fun currentPage_shouldChangeWhenClosestPageToSnappedPositionChanges() {
+        // Arrange
+
+        createPager(modifier = Modifier.fillMaxSize())
+        var previousCurrentPage = pagerState.currentPage
+
+        // Act
+        // Move less than half an item
+        val firstDelta = (pagerSize * 0.4f) * scrollForwardSign
+        onPager().performTouchInput {
+            down(layoutStart)
+            if (vertical) {
+                moveBy(Offset(0f, firstDelta))
+            } else {
+                moveBy(Offset(firstDelta, 0f))
+            }
+        }
+
+        // Assert
+        rule.runOnIdle {
+            assertThat(pagerState.currentPage).isEqualTo(previousCurrentPage)
+        }
+        // Release pointer
+        onPager().performTouchInput { up() }
+
+        rule.runOnIdle {
+            previousCurrentPage = pagerState.currentPage
+        }
+        confirmPageIsInCorrectPosition(pagerState.currentPage)
+
+        // Arrange
+        // Pass closest to snap position threshold (over half an item)
+        val secondDelta = (pagerSize * 0.6f) * scrollForwardSign
+
+        // Act
+        onPager().performTouchInput {
+            down(layoutStart)
+            if (vertical) {
+                moveBy(Offset(0f, secondDelta))
+            } else {
+                moveBy(Offset(secondDelta, 0f))
+            }
+        }
+
+        // Assert
+        rule.runOnIdle {
+            assertThat(pagerState.currentPage).isEqualTo(previousCurrentPage + 1)
+        }
+
+        onPager().performTouchInput { up() }
+        rule.waitForIdle()
+        confirmPageIsInCorrectPosition(pagerState.currentPage)
+    }
+
+    @Test
+    fun targetPage_performScrollBelowMinThreshold_shouldNotShowNextPage() {
+        // Arrange
+        createPager(
+            modifier = Modifier.fillMaxSize(),
+            snappingPage = PagerSnapDistance.atMost(3)
+        )
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving less than threshold
+        val forwardDelta =
+            scrollForwardSign.toFloat() * with(rule.density) { DefaultPositionThreshold.toPx() / 2 }
+
+        var previousTargetPage = pagerState.targetPage
+
+        onPager().performTouchInput {
+            down(layoutStart)
+            moveBy(Offset(forwardDelta, forwardDelta))
+        }
+
+        // Assert
+        assertThat(pagerState.targetPage).isEqualTo(previousTargetPage)
+
+        // Reset
+        rule.mainClock.autoAdvance = true
+        onPager().performTouchInput { up() }
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+
+        // Act
+        // Moving more than threshold
+        val backwardDelta = scrollForwardSign.toFloat() * with(rule.density) {
+            -DefaultPositionThreshold.toPx() / 2
+        }
+
+        previousTargetPage = pagerState.targetPage
+
+        onPager().performTouchInput {
+            down(layoutStart)
+            moveBy(Offset(backwardDelta, backwardDelta))
+        }
+
+        // Assert
+        assertThat(pagerState.targetPage).isEqualTo(previousTargetPage)
+    }
+
+    @Test
+    fun targetPage_performScroll_shouldShowNextPage() {
+        // Arrange
+        createPager(
+            modifier = Modifier.fillMaxSize(),
+            snappingPage = PagerSnapDistance.atMost(3)
+        )
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving forward
+        val forwardDelta = pagerSize * 0.4f * scrollForwardSign.toFloat()
+        onPager().performTouchInput {
+            down(layoutStart)
+            moveBy(Offset(forwardDelta, forwardDelta))
+        }
+
+        // Assert
+        assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage + 1)
+        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
+
+        // Reset
+        rule.mainClock.autoAdvance = true
+        onPager().performTouchInput { up() }
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+        rule.runOnIdle {
+            runBlocking { pagerState.scrollToPage(5) }
+        }
+
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving backward
+        val backwardDelta = -pagerSize * 0.4f * scrollForwardSign.toFloat()
+        onPager().performTouchInput {
+            down(layoutEnd)
+            moveBy(Offset(backwardDelta, backwardDelta))
+        }
+
+        // Assert
+        assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage - 1)
+        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
+
+        rule.mainClock.autoAdvance = true
+        onPager().performTouchInput { up() }
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+    }
+
+    @Test
+    fun targetPage_performingFling_shouldGoToPredictedPage() {
+        // Arrange
+
+        createPager(
+            modifier = Modifier.fillMaxSize(),
+            snappingPage = PagerSnapDistance.atMost(3)
+        )
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving forward
+        var previousTarget = pagerState.targetPage
+        val forwardDelta = pagerSize * scrollForwardSign.toFloat()
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(20000f, forwardDelta)
+        }
+        rule.mainClock.advanceTimeUntil { pagerState.targetPage != previousTarget }
+        var flingOriginIndex = pagerState.firstVisiblePage
+        // Assert
+        assertThat(pagerState.targetPage).isEqualTo(flingOriginIndex + 3)
+        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
+
+        rule.mainClock.autoAdvance = true
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving backward
+        previousTarget = pagerState.targetPage
+        val backwardDelta = -pagerSize * scrollForwardSign.toFloat()
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(20000f, backwardDelta)
+        }
+        rule.mainClock.advanceTimeUntil { pagerState.targetPage != previousTarget }
+
+        // Assert
+        flingOriginIndex = pagerState.firstVisiblePage + 1
+        assertThat(pagerState.targetPage).isEqualTo(flingOriginIndex - 3)
+        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
+
+        rule.mainClock.autoAdvance = true
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+    }
+
+    @Test
+    fun targetPage_shouldReflectTargetWithAnimation() {
+        // Arrange
+
+        createPager(
+            modifier = Modifier.fillMaxSize()
+        )
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving forward
+        var previousTarget = pagerState.targetPage
+        rule.runOnIdle {
+            scope.launch {
+                pagerState.animateScrollToPage(DefaultPageCount - 1)
+            }
+        }
+        rule.mainClock.advanceTimeUntil { pagerState.targetPage != previousTarget }
+
+        // Assert
+        assertThat(pagerState.targetPage).isEqualTo(DefaultPageCount - 1)
+        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
+
+        rule.mainClock.autoAdvance = true
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+        rule.mainClock.autoAdvance = false
+
+        // Act
+        // Moving backward
+        previousTarget = pagerState.targetPage
+        rule.runOnIdle {
+            scope.launch {
+                pagerState.animateScrollToPage(0)
+            }
+        }
+        rule.mainClock.advanceTimeUntil { pagerState.targetPage != previousTarget }
+
+        // Assert
+        assertThat(pagerState.targetPage).isEqualTo(0)
+        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
+
+        rule.mainClock.autoAdvance = true
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+    }
+
+    @Test
+    fun targetPage_shouldReflectTargetWithCustomAnimation() {
+        // Arrange
+        suspend fun PagerState.customAnimateScrollToPage(page: Int) {
+            scroll {
+                updateTargetPage(page)
+                val targetPageDiff = page - currentPage
+                val distance = targetPageDiff * layoutInfo.pageSize.toFloat()
+                var previousValue = 0.0f
+                animate(
+                    0f,
+                    distance,
+                ) { currentValue, _ ->
+                    previousValue += scrollBy(currentValue - previousValue)
+                }
+            }
+        }
+
+        createPager(
+            modifier = Modifier.fillMaxSize()
+        )
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving forward
+        var previousTarget = pagerState.targetPage
+        rule.runOnIdle {
+            scope.launch {
+                pagerState.customAnimateScrollToPage(DefaultPageCount - 1)
+            }
+        }
+        rule.mainClock.advanceTimeUntil { pagerState.targetPage != previousTarget }
+
+        // Assert
+        assertThat(pagerState.targetPage).isEqualTo(DefaultPageCount - 1)
+        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
+
+        rule.mainClock.autoAdvance = true
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+        rule.mainClock.autoAdvance = false
+
+        // Act
+        // Moving backward
+        previousTarget = pagerState.targetPage
+        rule.runOnIdle {
+            scope.launch {
+                pagerState.customAnimateScrollToPage(0)
+            }
+        }
+        rule.mainClock.advanceTimeUntil { pagerState.targetPage != previousTarget }
+
+        // Assert
+        assertThat(pagerState.targetPage).isEqualTo(0)
+        assertThat(pagerState.targetPage).isNotEqualTo(pagerState.currentPage)
+
+        rule.mainClock.autoAdvance = true
+        rule.runOnIdle { assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) }
+    }
+
+    @Test
+    fun targetPage_valueAfterScrollingAfterMidpoint() {
+        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
+
+        var previousCurrentPage = pagerState.currentPage
+
+        val forwardDelta = (pagerSize * 0.7f) * scrollForwardSign
+        onPager().performTouchInput {
+            down(layoutStart)
+            if (vertical) {
+                moveBy(Offset(0f, forwardDelta))
+            } else {
+                moveBy(Offset(forwardDelta, 0f))
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(pagerState.currentPage).isNotEqualTo(previousCurrentPage)
+            assertThat(pagerState.targetPage).isEqualTo(previousCurrentPage + 1)
+        }
+
+        onPager().performTouchInput { up() }
+
+        rule.runOnIdle {
+            previousCurrentPage = pagerState.currentPage
+        }
+
+        val backwardDelta = (pagerSize * 0.7f) * scrollForwardSign * -1
+        onPager().performTouchInput {
+            down(layoutEnd)
+            if (vertical) {
+                moveBy(Offset(0f, backwardDelta))
+            } else {
+                moveBy(Offset(backwardDelta, 0f))
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(pagerState.currentPage).isNotEqualTo(previousCurrentPage)
+            assertThat(pagerState.targetPage).isEqualTo(previousCurrentPage - 1)
+        }
+
+        onPager().performTouchInput { up() }
+    }
+
+    @Test
+    fun targetPage_valueAfterScrollingForwardAndBackward() {
+        createPager(initialPage = 5, modifier = Modifier.fillMaxSize())
+
+        val startCurrentPage = pagerState.currentPage
+
+        val forwardDelta = (pagerSize * 0.8f) * scrollForwardSign
+        onPager().performTouchInput {
+            down(layoutStart)
+            if (vertical) {
+                moveBy(Offset(0f, forwardDelta))
+            } else {
+                moveBy(Offset(forwardDelta, 0f))
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(pagerState.currentPage).isNotEqualTo(startCurrentPage)
+            assertThat(pagerState.targetPage).isEqualTo(startCurrentPage + 1)
+        }
+
+        val backwardDelta = (pagerSize * 0.2f) * scrollForwardSign * -1
+        onPager().performTouchInput {
+            if (vertical) {
+                moveBy(Offset(0f, backwardDelta))
+            } else {
+                moveBy(Offset(backwardDelta, 0f))
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(pagerState.currentPage).isNotEqualTo(startCurrentPage)
+            assertThat(pagerState.targetPage).isEqualTo(startCurrentPage)
+        }
+
+        onPager().performTouchInput { up() }
+    }
+
+    @Test
+    fun settledPage_onAnimationScroll_shouldChangeOnScrollFinishedOnly() {
+        // Arrange
+        var settledPageChanges = 0
+        createPager(
+            modifier = Modifier.fillMaxSize(),
+            additionalContent = {
+                LaunchedEffect(key1 = pagerState.settledPage) {
+                    settledPageChanges++
+                }
+            }
+        )
+
+        // Settle page changed once for first composition
+        rule.runOnIdle {
+            assertThat(pagerState.settledPage).isEqualTo(pagerState.currentPage)
+            assertTrue { settledPageChanges == 1 }
+        }
+
+        settledPageChanges = 0
+        val previousSettled = pagerState.settledPage
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving forward
+        rule.runOnIdle {
+            scope.launch {
+                pagerState.animateScrollToPage(DefaultPageCount - 1)
+            }
+        }
+
+        // Settled page shouldn't change whilst scroll is in progress.
+        assertTrue { pagerState.isScrollInProgress }
+        assertTrue { settledPageChanges == 0 }
+        assertThat(pagerState.settledPage).isEqualTo(previousSettled)
+
+        rule.mainClock.advanceTimeUntil { settledPageChanges != 0 }
+
+        rule.runOnIdle {
+            assertTrue { !pagerState.isScrollInProgress }
+            assertThat(pagerState.settledPage).isEqualTo(pagerState.currentPage)
+        }
+    }
+
+    @Test
+    fun settledPage_onGestureScroll_shouldChangeOnScrollFinishedOnly() {
+        // Arrange
+        var settledPageChanges = 0
+        createPager(
+            modifier = Modifier.fillMaxSize(),
+            additionalContent = {
+                LaunchedEffect(key1 = pagerState.settledPage) {
+                    settledPageChanges++
+                }
+            }
+        )
+
+        settledPageChanges = 0
+        val previousSettled = pagerState.settledPage
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving forward
+        val forwardDelta = pagerSize / 2f * scrollForwardSign.toFloat()
+        rule.onNodeWithTag(PagerTestTag).performTouchInput {
+            swipeWithVelocityAcrossMainAxis(10000f, forwardDelta)
+        }
+
+        // Settled page shouldn't change whilst scroll is in progress.
+        assertTrue { pagerState.isScrollInProgress }
+        assertTrue { settledPageChanges == 0 }
+        assertThat(pagerState.settledPage).isEqualTo(previousSettled)
+
+        rule.mainClock.advanceTimeUntil { settledPageChanges != 0 }
+
+        rule.runOnIdle {
+            assertTrue { !pagerState.isScrollInProgress }
+            assertThat(pagerState.settledPage).isEqualTo(pagerState.currentPage)
+        }
+    }
+
+    @Test
+    fun currentPageOffset_shouldReflectScrollingOfCurrentPage() {
+        // Arrange
+        createPager(initialPage = DefaultPageCount / 2, modifier = Modifier.fillMaxSize())
+
+        // No offset initially
+        rule.runOnIdle {
+            assertThat(pagerState.currentPageOffsetFraction).isWithin(0.01f).of(0f)
+        }
+
+        // Act
+        // Moving forward
+        onPager().performTouchInput {
+            down(layoutStart)
+            if (vertical) {
+                moveBy(Offset(0f, scrollForwardSign * pagerSize / 4f))
+            } else {
+                moveBy(Offset(scrollForwardSign * pagerSize / 4f, 0f))
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(pagerState.currentPageOffsetFraction).isWithin(0.1f).of(0.25f)
+        }
+
+        onPager().performTouchInput { up() }
+        rule.waitForIdle()
+
+        // Reset
+        rule.runOnIdle {
+            scope.launch {
+                pagerState.scrollToPage(DefaultPageCount / 2)
+            }
+        }
+
+        // No offset initially (again)
+        rule.runOnIdle {
+            assertThat(pagerState.currentPageOffsetFraction).isWithin(0.01f).of(0f)
+        }
+
+        // Act
+        // Moving backward
+        onPager().performTouchInput {
+            down(layoutStart)
+            if (vertical) {
+                moveBy(Offset(0f, -scrollForwardSign * pagerSize / 4f))
+            } else {
+                moveBy(Offset(-scrollForwardSign * pagerSize / 4f, 0f))
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(pagerState.currentPageOffsetFraction).isWithin(0.1f).of(-0.25f)
+        }
+    }
+
+    @Test
+    fun onScroll_shouldNotGenerateExtraMeasurements() {
+        // Arrange
+        var layoutCount = 0
+        createPager(initialPage = 5, modifier = Modifier.layout { measurable, constraints ->
+            layoutCount++
+            val placeables = measurable.measure(constraints)
+            layout(constraints.maxWidth, constraints.maxHeight) {
+                placeables.place(0, 0)
+            }
+        })
+
+        // Act: Scroll.
+        val previousMeasurementCount = layoutCount
+        val previousOffsetFraction = pagerState.currentPageOffsetFraction
+        rule.runOnIdle {
+            runBlocking {
+                pagerState.scrollBy((pageSize * 0.2f) * scrollForwardSign)
+            }
+        }
+        rule.runOnIdle {
+            assertThat(pagerState.currentPageOffsetFraction).isNotEqualTo(previousOffsetFraction)
+            assertThat(layoutCount).isEqualTo(previousMeasurementCount + 1)
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = mutableListOf<ParamConfig>().apply {
+            for (orientation in TestOrientation) {
+                for (reverseLayout in TestReverseLayout) {
+                    for (layoutDirection in TestLayoutDirection) {
+                        add(
+                            ParamConfig(
+                                orientation = orientation,
+                                reverseLayout = reverseLayout,
+                                layoutDirection = layoutDirection
+                            )
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerSwipeEdgeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerSwipeEdgeTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerSwipeEdgeTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerSwipeEdgeTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/FakeScrollable.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/relocation/FakeScrollable.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/FakeScrollable.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/relocation/FakeScrollable.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/selection/SelectableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/selection/SelectableTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/selection/SelectableTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/selection/SelectableTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/selection/ToggleableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/selection/ToggleableTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/selection/ToggleableTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/selection/ToggleableTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/shape/AbsoluteCutCornerShapeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/shape/AbsoluteCutCornerShapeTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/shape/AbsoluteCutCornerShapeTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/shape/AbsoluteCutCornerShapeTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/shape/AbsoluteRoundedCornerShapeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/shape/AbsoluteRoundedCornerShapeTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/shape/AbsoluteRoundedCornerShapeTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/shape/AbsoluteRoundedCornerShapeTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/shape/CornerBasedShapeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/shape/CornerBasedShapeTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/shape/CornerBasedShapeTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/shape/CornerBasedShapeTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/shape/CornerSizeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/shape/CornerSizeTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/shape/CornerSizeTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/shape/CornerSizeTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/shape/CutCornerShapeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/shape/CutCornerShapeTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/shape/CutCornerShapeTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/shape/CutCornerShapeTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/shape/RoundedCornerShapeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/shape/RoundedCornerShapeTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/shape/RoundedCornerShapeTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/shape/RoundedCornerShapeTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextDensityTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextDensityTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextDensityTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextDensityTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextGraphicsLayerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextGraphicsLayerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextGraphicsLayerTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextGraphicsLayerTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextHoverTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextHoverTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextHoverTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextHoverTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextMinMaxLinesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextMinMaxLinesTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextMinMaxLinesTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextMinMaxLinesTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextScreenshotTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextScreenshotTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextScreenshotTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextScreenshotTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextSemanticsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextSemanticsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextSemanticsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextSemanticsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/ClickableTextTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/ClickableTextTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/ClickableTextTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/ClickableTextTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldFocusTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldFocusTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldFocusTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldFocusTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHoverTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHoverTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHoverTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHoverTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSelectionOnBackTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSelectionOnBackTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSelectionOnBackTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSelectionOnBackTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftWrapTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftWrapTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftWrapTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftWrapTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextInlineContentTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextInlineContentTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextInlineContentTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextInlineContentTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DeadKeyCombinerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/DeadKeyCombinerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DeadKeyCombinerTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/DeadKeyCombinerTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DetectDownAndDragGesturesWithObserverInitializationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/DetectDownAndDragGesturesWithObserverInitializationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DetectDownAndDragGesturesWithObserverInitializationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/DetectDownAndDragGesturesWithObserverInitializationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DrawPhaseAttributesToggleTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/DrawPhaseAttributesToggleTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DrawPhaseAttributesToggleTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/DrawPhaseAttributesToggleTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/FontScalingScreenshotTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/FontScalingScreenshotTest.kt
new file mode 100644
index 0000000..f83cf84
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/FontScalingScreenshotTest.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2021 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.text
+
+import android.os.Build
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.GOLDEN_UI
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.drawText
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.rememberTextMeasurer
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.LineHeightStyle.Alignment
+import androidx.compose.ui.text.style.LineHeightStyle.Trim
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.testutils.AndroidFontScaleHelper
+import org.junit.After
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+class FontScalingScreenshotTest {
+    @get:Rule
+    val rule = createAndroidComposeRule<ComponentActivity>()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_UI)
+
+    private val containerTag = "container"
+
+    @After
+    fun teardown() {
+        AndroidFontScaleHelper.resetSystemFontScale(rule.activityRule.scenario)
+    }
+
+    @Test
+    fun fontScaling1x_lineHeightDoubleSp() {
+        AndroidFontScaleHelper.setSystemFontScale(1f, rule.activityRule.scenario)
+        rule.waitForIdle()
+
+        rule.setContent {
+            TestLayout(lineHeight = 28.sp)
+        }
+        rule.onNodeWithTag(containerTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "fontScaling1x_lineHeightDoubleSp")
+    }
+
+    @Test
+    fun fontScaling2x_lineHeightDoubleSp() {
+        AndroidFontScaleHelper.setSystemFontScale(2f, rule.activityRule.scenario)
+        rule.waitForIdle()
+
+        rule.setContent {
+            TestLayout(lineHeight = 28.sp)
+        }
+        rule.onNodeWithTag(containerTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "fontScaling2x_lineHeightDoubleSp")
+    }
+
+    @Test
+    fun fontScaling1x_lineHeightStyleDoubleSp() {
+        AndroidFontScaleHelper.setSystemFontScale(1f, rule.activityRule.scenario)
+        rule.waitForIdle()
+
+        rule.setContent {
+            TestLayout(
+                lineHeight = 28.sp,
+                lineHeightStyle = LineHeightStyle(Alignment.Bottom, Trim.Both)
+            )
+        }
+        rule.onNodeWithTag(containerTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "fontScaling1x_lineHeightStyleDoubleSp")
+    }
+
+    @Test
+    fun fontScaling2x_lineHeightStyleDoubleSp() {
+        AndroidFontScaleHelper.setSystemFontScale(2f, rule.activityRule.scenario)
+        rule.waitForIdle()
+
+        rule.setContent {
+            TestLayout(
+                lineHeight = 28.sp,
+                lineHeightStyle = LineHeightStyle(Alignment.Bottom, Trim.Both)
+            )
+        }
+        rule.onNodeWithTag(containerTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "fontScaling2x_lineHeightStyleDoubleSp")
+    }
+
+    @Test
+    fun fontScaling1x_lineHeightDoubleEm() {
+        AndroidFontScaleHelper.setSystemFontScale(1f, rule.activityRule.scenario)
+        rule.waitForIdle()
+
+        rule.setContent {
+            TestLayout(lineHeight = 2.em)
+        }
+        rule.onNodeWithTag(containerTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "fontScaling1x_lineHeightDoubleEm")
+    }
+
+    @Test
+    fun fontScaling2x_lineHeightDoubleEm() {
+        AndroidFontScaleHelper.setSystemFontScale(2f, rule.activityRule.scenario)
+        rule.waitForIdle()
+
+        rule.setContent {
+            TestLayout(lineHeight = 2.em)
+        }
+        rule.onNodeWithTag(containerTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "fontScaling2x_lineHeightDoubleEm")
+    }
+
+    @Test
+    fun fontScaling1x_drawText() {
+        AndroidFontScaleHelper.setSystemFontScale(1f, rule.activityRule.scenario)
+        rule.waitForIdle()
+
+        rule.setContent {
+            TestDrawTextLayout()
+        }
+        rule.onNodeWithTag(containerTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "fontScaling1x_drawText")
+    }
+
+    @Test
+    fun fontScaling2x_drawText() {
+        AndroidFontScaleHelper.setSystemFontScale(2f, rule.activityRule.scenario)
+        rule.waitForIdle()
+
+        rule.setContent {
+            TestDrawTextLayout()
+        }
+        rule.onNodeWithTag(containerTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "fontScaling2x_drawText")
+    }
+
+    @Composable
+    private fun TestLayout(
+        lineHeight: TextUnit,
+        lineHeightStyle: LineHeightStyle = LineHeightStyle.Default
+    ) {
+        Column(
+            modifier = Modifier.testTag(containerTag),
+        ) {
+            BasicText(
+                text = buildAnnotatedString {
+                    append("Hello ")
+                    pushStyle(SpanStyle(
+                        fontSize = 28.sp,
+                        fontWeight = FontWeight.Bold
+                    ))
+                    append("Accessibility")
+                    pop()
+                },
+                style = TextStyle(
+                    fontSize = 36.sp,
+                    fontStyle = FontStyle.Italic,
+                    fontFamily = FontFamily.Monospace
+                )
+            )
+            BasicText(
+                text = "Here's a subtitle",
+                style = TextStyle(
+                    fontSize = 20.sp
+                )
+            )
+            BasicText(
+                text = sampleText,
+                style = TextStyle(
+                    fontSize = 14.sp,
+                    fontStyle = FontStyle.Italic,
+                    lineHeight = lineHeight,
+                    lineHeightStyle = lineHeightStyle
+                )
+            )
+        }
+    }
+
+    @Composable
+    private fun TestDrawTextLayout() {
+        val textMeasurer = rememberTextMeasurer()
+
+        Column(
+            modifier = Modifier.testTag(containerTag),
+        ) {
+            Canvas(Modifier.fillMaxSize()) {
+                 drawText(
+                     textMeasurer = textMeasurer,
+                     style = TextStyle(
+                        fontSize = 14.sp,
+                        lineHeight = 28.sp
+                     ),
+                     text = sampleText
+                )
+            }
+        }
+    }
+
+    companion object {
+        private val sampleText = """
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
+culpa qui officia deserunt mollit anim id est laborum.
+
+Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
+totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
+dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
+sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro
+quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non
+numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim
+ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid
+ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse
+quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
+    """.trimIndent()
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyEventHelpersTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/KeyEventHelpersTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyEventHelpersTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/KeyEventHelpersTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardActionsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/KeyboardActionsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardActionsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/KeyboardActionsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/PointerMoveDetectorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/PointerMoveDetectorTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/PointerMoveDetectorTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/PointerMoveDetectorTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextDelegateIntegrationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateIntegrationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextDelegateIntegrationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateWidthWithLetterSpacingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextDelegateWidthWithLetterSpacingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateWidthWithLetterSpacingTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextDelegateWidthWithLetterSpacingTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextFieldDelegateIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextFieldDelegateIntegrationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextFieldDelegateIntegrationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextFieldDelegateIntegrationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextFieldInteractionsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextFieldInteractionsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextFieldInteractionsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextFieldInteractionsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutDirectionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextLayoutDirectionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutDirectionTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextLayoutDirectionTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutResultIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextLayoutResultIntegrationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutResultIntegrationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextLayoutResultIntegrationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextLayoutTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextLayoutTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextOverflowTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextOverflowTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextOverflowTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextOverflowTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextPreparedSelectionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextPreparedSelectionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextPreparedSelectionTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextPreparedSelectionTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextStyleInvalidationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextStyleInvalidationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextStyleInvalidationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextStyleInvalidationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextTestExtensions.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextTestExtensions.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextTestExtensions.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextTestExtensions.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextUsingModifierMinMaxLinesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextUsingModifierMinMaxLinesTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextUsingModifierMinMaxLinesTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextUsingModifierMinMaxLinesTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/matchers/BitmapSubject.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/matchers/BitmapSubject.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/matchers/BitmapSubject.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/matchers/BitmapSubject.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/matchers/Matchers.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/matchers/Matchers.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/matchers/Matchers.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/matchers/Matchers.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/BasicTextSemanticsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/BasicTextSemanticsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/BasicTextSemanticsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/BasicTextSemanticsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheWidthWithLetterSpacingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheWidthWithLetterSpacingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheWidthWithLetterSpacingTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheWidthWithLetterSpacingTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/NodeInvalidationTestParent.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/NodeInvalidationTestParent.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/NodeInvalidationTestParent.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/NodeInvalidationTestParent.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/SelectionControllerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/SelectionControllerTest.kt
new file mode 100644
index 0000000..86e71928b
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/SelectionControllerTest.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.foundation.text.modifiers
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.selection.LocalTextSelectionColors
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.text.selection.TextSelectionColors
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.getBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.longClick
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.roundToIntRect
+import androidx.compose.ui.unit.sp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class SelectionControllerTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val boxTag = "boxTag"
+    private val tag = "tag"
+
+    private val highlightColor = Color.Blue
+    private val foregroundColor = Color.Black
+    private val backgroundColor = Color.White
+
+    private val highlightArgb get() = highlightColor.toArgb()
+    private val foregroundArgb get() = foregroundColor.toArgb()
+    private val backgroundArgb get() = backgroundColor.toArgb()
+
+    private val density = Density(1f)
+    private val textSelectionColors = TextSelectionColors(highlightColor, highlightColor)
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26)
+    fun drawWithClip_doesClip() = runDrawWithClipTest(TextOverflow.Clip) { argbSet ->
+        assertThat(argbSet).containsExactly(backgroundArgb)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26)
+    fun drawWithVisible_doesNotClip() = runDrawWithClipTest(TextOverflow.Visible) { argbSet ->
+        // there could be more colors due to anti-aliasing, so check that we have at least the
+        // expected colors, and none of the unexpected colors.
+        assertThat(argbSet).containsAtLeast(highlightArgb, foregroundArgb)
+        assertThat(argbSet).doesNotContain(backgroundArgb)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    private fun runDrawWithClipTest(overflow: TextOverflow, assertBlock: (Set<Int>) -> Unit) {
+        rule.setContent {
+            CompositionLocalProvider(
+                LocalTextSelectionColors provides textSelectionColors,
+                LocalDensity provides density,
+            ) {
+                Box(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .drawBehind { drawRect(backgroundColor) }
+                        .testTag(boxTag),
+                    contentAlignment = Alignment.Center,
+                ) {
+                    SelectionContainer {
+                        BasicText(
+                            modifier = Modifier
+                                .width(10.dp)
+                                .testTag(tag),
+                            text = "OOOOOOO",
+                            overflow = overflow,
+                            softWrap = false,
+                            style = TextStyle(color = foregroundColor, fontSize = 48.sp),
+                        )
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag(tag).performTouchInput { longClick() }
+        rule.waitForIdle()
+
+        with(density) {
+            val bitmap = rule.onNodeWithTag(boxTag).captureToImage().asAndroidBitmap()
+            val bitmapPositionInRoot =
+                rule.onNodeWithTag(boxTag).getBoundsInRoot().toRect().roundToIntRect().topLeft
+            val centerRightOffset =
+                rule.onNodeWithTag(tag).getBoundsInRoot().toRect().roundToIntRect().centerRight
+
+            val centerRightOffsetInRoot = centerRightOffset - bitmapPositionInRoot
+            val (x, y) = centerRightOffsetInRoot
+
+            // grab a row of pixels in the area that may be clipped.
+            val seenColors = (1..50).map { bitmap.getPixel(x + it, y) }.toSet()
+            assertBlock(seenColors)
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNodeInvalidationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNodeInvalidationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNodeInvalidationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNodeInvalidationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/TextLayoutResultIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextLayoutResultIntegrationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/TextLayoutResultIntegrationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextLayoutResultIntegrationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeInvalidationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeInvalidationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeInvalidationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeInvalidationTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MagnifierTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/MagnifierTestUtils.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MagnifierTestUtils.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/MagnifierTestUtils.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MotionEventTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/MotionEventTestUtils.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MotionEventTestUtils.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/MotionEventTestUtils.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlesTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlesTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlesTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldMagnifierTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/TextFieldMagnifierTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldMagnifierTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/TextFieldMagnifierTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldVisualTransformationMagnifierTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/TextFieldVisualTransformationMagnifierTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldVisualTransformationMagnifierTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/TextFieldVisualTransformationMagnifierTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextSelectionColorsScreenshotTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/TextSelectionColorsScreenshotTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextSelectionColorsScreenshotTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/TextSelectionColorsScreenshotTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/AbstractSelectionGesturesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/AbstractSelectionGesturesTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/AbstractSelectionGesturesTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/AbstractSelectionGesturesTest.kt
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
new file mode 100644
index 0000000..4aa35e8
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/LazyColumnMultiTextRegressionTest.kt
@@ -0,0 +1,456 @@
+/*
+ * 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.foundation.text.selection.gestures
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.Handle
+import androidx.compose.foundation.text.selection.Selection
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.text.selection.SelectionHandleInfoKey
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.foundation.text.selection.gestures.util.longPress
+import androidx.compose.foundation.text.selection.isSelectionHandle
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.testutils.TestViewConfiguration
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.ClipboardManager
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalTextToolbar
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.TextToolbarStatus
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.longClick
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipe
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.google.common.truth.Subject
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+class LazyColumnMultiTextRegressionTest {
+    @get:Rule
+    val rule = createComposeRule()
+    private val stateRestorationTester = StateRestorationTester(rule)
+    private val textCount = 20
+
+    // regression - text going out of composition and then returning
+    // resulted in selection not working
+    @Test
+    fun whenTextScrollsOutOfCompositionAndThenBackIn_creatingSelectionStillPossible() = runTest {
+        scrollDown()
+        scrollUp()
+        createSelection(line = 0)
+        assertSelection().isNotNull()
+    }
+
+    @Test
+    fun whenSelectionScrollsOutOfCompositionAndThenBackIn_selectionRemains() = runTest {
+        createSelection(line = 0)
+        assertSelection().isNotNull()
+        val initialSelection = selection
+        scrollDown()
+        assertSelection().isEqualTo(initialSelection)
+        scrollUp()
+        assertSelection().isEqualTo(initialSelection)
+    }
+
+    // Copy currently doesn't work when the text leaves the view of a lazy layout
+    @Ignore("b/298067102")
+    @Test
+    fun whenTextScrollsOutOfLazyLayoutBounds_copyCorrectlySetsClipboard() = runTest {
+        resetClipboard()
+        createSelection(startLine = 0, endLine = 4)
+        assertSelection().isNotNull()
+        scrollDown()
+        performCopy()
+        assertClipboardTextEquals("01234")
+    }
+
+    @Test
+    fun whenScrollingTextOutOfViewUpwards_handlesDisappear() = runTest {
+        var prevStart: Offset? = null
+        var prevEnd: Offset? = null
+
+        fun updateHandlePositions() {
+            prevStart = startHandlePosition
+            prevEnd = endHandlePosition
+        }
+
+        assertHandleNotShown(Handle.SelectionStart)
+        assertHandleNotShown(Handle.SelectionEnd)
+
+        createSelection(startLine = 1, endLine = 3)
+        assertHandleShown(Handle.SelectionStart)
+        assertHandleShown(Handle.SelectionEnd)
+        updateHandlePositions()
+
+        scrollLines(fromLine = 3, toLine = 2)
+        assertHandleShown(Handle.SelectionStart)
+        assertHandleShown(Handle.SelectionEnd)
+        assertPositionMovedUp(prevStart, startHandlePosition)
+        assertPositionMovedUp(prevEnd, endHandlePosition)
+        updateHandlePositions()
+
+        scrollLines(fromLine = 4, toLine = 2)
+        assertHandleNotShown(Handle.SelectionStart)
+        assertHandleShown(Handle.SelectionEnd)
+        assertPositionMovedUp(prevEnd, endHandlePosition)
+
+        scrollLines(fromLine = 6, toLine = 4)
+        assertHandleNotShown(Handle.SelectionStart)
+        assertHandleNotShown(Handle.SelectionEnd)
+        updateHandlePositions()
+
+        scrollLines(fromLine = 6, toLine = 8)
+        assertHandleNotShown(Handle.SelectionStart)
+        assertHandleShown(Handle.SelectionEnd)
+        updateHandlePositions()
+
+        scrollLines(fromLine = 4, toLine = 6)
+        assertHandleShown(Handle.SelectionStart)
+        assertHandleShown(Handle.SelectionEnd)
+        assertHandleMovedDown(prevEnd, endHandlePosition)
+        updateHandlePositions()
+
+        scrollLines(fromLine = 2, toLine = 5)
+        assertHandleShown(Handle.SelectionStart)
+        assertHandleShown(Handle.SelectionEnd)
+        assertHandleMovedDown(prevStart, startHandlePosition)
+        assertHandleMovedDown(prevEnd, endHandlePosition)
+        updateHandlePositions()
+    }
+
+    @Test
+    fun whenScrollingTextOutOfViewUpwards_textToolbarCoercedToTop() = runTest {
+        assertThat(textToolbarShown).isFalse()
+
+        createSelection(startLine = 1, endLine = 3)
+        assertThat(textToolbarShown).isTrue()
+        assertTextToolbarTopAt(boundingBoxForLineInRoot(1).top)
+
+        scrollLines(fromLine = 3, toLine = 1)
+        assertThat(textToolbarShown).isTrue()
+        assertTextToolbarTopAt(pointerAreaRect.top)
+
+        scrollLines(fromLine = 5, toLine = 3)
+        assertThat(textToolbarShown).isFalse()
+
+        scrollLines(fromLine = 5, toLine = 7)
+        assertThat(textToolbarShown).isTrue()
+        assertTextToolbarTopAt(pointerAreaRect.top)
+
+        scrollLines(fromLine = 3, toLine = 5)
+        assertThat(textToolbarShown).isTrue()
+        assertTextToolbarTopAt(boundingBoxForLineInRoot(1).top)
+    }
+
+    // TODO(b/298067619)
+    //  When we support saving selection, this test should instead check that
+    //  the previous and current selection is the same.
+    //  Change test name to reflect this when implemented.
+    @Test
+    fun whenTextIsSavedRestored_clearsSelection() = runTest {
+        createSelection(line = 0)
+        assertSelection().isNotNull()
+        stateRestorationTester.emulateSavedInstanceStateRestore()
+        assertSelection().isNull()
+    }
+
+    private inner class TestScope(
+        private val pointerAreaTag: String,
+        private val selectionState: MutableState<Selection?>,
+        private val clipboardManager: ClipboardManager,
+        private val textToolbar: TextToolbarWrapper,
+    ) {
+        val initialText = "Initial text"
+        val selection: Selection? get() = Snapshot.withoutReadObservation { selectionState.value }
+        val textToolbarRect: Rect? get() = textToolbar.mostRecentRect
+        val textToolbarShown: Boolean get() = textToolbar.shown
+
+        val startHandlePosition get() = handlePosition(Handle.SelectionStart)
+        val endHandlePosition get() = handlePosition(Handle.SelectionEnd)
+
+        fun createSelection(startLine: Int, endLine: Int) {
+            performTouchInput {
+                longPress(positionForLineInPointerArea(startLine))
+                moveTo(positionForLineInPointerArea(endLine))
+                up()
+            }
+        }
+
+        fun createSelection(line: Int) {
+            performTouchInput {
+                longClick(positionForLineInPointerArea(line))
+            }
+        }
+
+        private fun performTouchInput(block: TouchInjectionScope.() -> Unit) {
+            rule.onNodeWithTag(pointerAreaTag).performTouchInput(block)
+            rule.waitForIdle()
+        }
+
+        fun boundingBoxForLineInPointerArea(lineNumber: Int): Rect {
+            val containerPosition = rule.onNodeWithTag(pointerAreaTag).fetchSemanticsNode()
+                .positionInRoot
+            return boundingBoxForLineInRoot(lineNumber).translate(-containerPosition)
+        }
+
+        fun boundingBoxForLineInRoot(lineNumber: Int): Rect {
+            val textTag = lineNumber.toString()
+            val textPosition = rule.onNodeWithTag(textTag).fetchSemanticsNode().positionInRoot
+            val textLayoutResult = rule.onNodeWithTag(textTag).fetchTextLayoutResult()
+            val lineStart = textLayoutResult.getLineStart(0)
+            val lineEnd = textLayoutResult.getLineEnd(0)
+
+            val rect = if (lineStart == lineEnd - 1) {
+                textLayoutResult.getBoundingBox(lineStart)
+            } else {
+                val startRect = textLayoutResult.getBoundingBox(lineStart)
+                val endRect = textLayoutResult.getBoundingBox(lineEnd - 1)
+                Rect(
+                    left = minOf(startRect.left, endRect.left),
+                    top = minOf(startRect.top, endRect.top),
+                    right = maxOf(startRect.right, endRect.right),
+                    bottom = maxOf(startRect.bottom, endRect.bottom),
+                )
+            }
+
+            return rect.translate(textPosition)
+        }
+
+        fun positionForLineInPointerArea(lineNumber: Int): Offset =
+            boundingBoxForLineInPointerArea(lineNumber).center
+
+        @OptIn(ExperimentalTestApi::class)
+        fun performCopy() {
+            rule.onNodeWithTag(pointerAreaTag).performKeyInput {
+                keyDown(Key.CtrlLeft)
+                keyDown(Key.C)
+                keyUp(Key.C)
+                keyUp(Key.CtrlLeft)
+            }
+            rule.waitForIdle()
+        }
+
+        fun resetClipboard() {
+            clipboardManager.setText(AnnotatedString(initialText))
+        }
+
+        fun assertClipboardTextEquals(text: String) {
+            val actualClipboardText = clipboardManager.getText()?.text
+            assertWithMessage("Clipboard contents was not changed.")
+                .that(actualClipboardText)
+                .isNotEqualTo(initialText)
+            assertWithMessage("""Clipboard set to incorrect content: "$actualClipboardText".""")
+                .that(actualClipboardText)
+                .isEqualTo(text)
+            resetClipboard()
+        }
+
+        fun assertSelection(): Subject = assertThat(selection)
+
+        fun scrollDown() {
+            performTouchInput {
+                swipe(bottomCenter - Offset(0f, 1f), topCenter + Offset(0f, 1f))
+            }
+        }
+
+        fun scrollUp() {
+            performTouchInput {
+                swipe(topCenter + Offset(0f, 1f), bottomCenter - Offset(0f, 1f))
+            }
+        }
+
+        fun scrollLines(fromLine: Int, toLine: Int) {
+            performTouchInput {
+                swipe(positionForLineInPointerArea(fromLine), positionForLineInPointerArea(toLine))
+            }
+        }
+
+        fun assertPositionMovedUp(previous: Offset?, current: Offset?) {
+            assertHandleMoved(previous, current, up = true)
+        }
+
+        fun assertHandleMovedDown(previous: Offset?, current: Offset?) {
+            assertHandleMoved(previous, current, up = false)
+        }
+
+        private fun assertHandleMoved(previous: Offset?, current: Offset?, up: Boolean) {
+            assertWithMessage("previous handle position should not be null")
+                .that(previous)
+                .isNotNull()
+
+            assertWithMessage("current handle position should not be null")
+                .that(current)
+                .isNotNull()
+
+            val (x, y) = current!!
+            val (prevX, prevY) = previous!!
+
+            assertWithMessage("x should not change")
+                .that(x)
+                .isWithin(0.1f)
+                .of(prevX)
+
+            assertWithMessage("y should have moved ${if (up) "up" else "down"}")
+                .that(y)
+                .run {
+                    if (up) isLessThan(prevY) else isGreaterThan(prevY)
+                }
+        }
+
+        private fun handlePosition(handle: Handle): Offset? =
+            rule.onAllNodes(isSelectionHandle(handle))
+                .fetchSemanticsNodes()
+                .singleOrNull()
+                ?.config
+                ?.get(SelectionHandleInfoKey)
+                ?.position
+
+        fun assertHandleShown(handle: Handle) {
+            rule.onNode(isSelectionHandle(handle)).assertExists()
+        }
+
+        fun assertHandleNotShown(handle: Handle) {
+            rule.onNode(isSelectionHandle(handle)).assertDoesNotExist()
+        }
+
+        fun assertTextToolbarTopAt(y: Float) {
+            assertThat(textToolbarRect?.top)
+                .isWithin(0.1f)
+                .of(y)
+        }
+
+        val pointerAreaRect: Rect
+            get() = rule.onNodeWithTag(pointerAreaTag).fetchSemanticsNode().boundsInRoot
+    }
+
+    private fun runTest(block: TestScope.() -> Unit) {
+        val tag = "tag"
+        val selection = mutableStateOf<Selection?>(null)
+        val testViewConfiguration = TestViewConfiguration(
+            minimumTouchTargetSize = DpSize.Zero
+        )
+        lateinit var clipboardManager: ClipboardManager
+        lateinit var textToolbar: TextToolbarWrapper
+        stateRestorationTester.setContent {
+            clipboardManager = LocalClipboardManager.current
+            val originalTextToolbar = LocalTextToolbar.current
+            textToolbar = remember(originalTextToolbar) {
+                TextToolbarWrapper(originalTextToolbar)
+            }
+            CompositionLocalProvider(
+                LocalTextToolbar provides textToolbar,
+                LocalViewConfiguration provides testViewConfiguration,
+            ) {
+                Box(
+                    modifier = Modifier.fillMaxSize(),
+                    contentAlignment = Alignment.Center,
+                ) {
+                    SelectionContainer(
+                        modifier = Modifier.height(100.dp),
+                        selection = selection.value,
+                        onSelectionChange = { selection.value = it },
+                    ) {
+                        LazyColumn(
+                            modifier = Modifier.testTag(tag)
+                        ) {
+                            items(count = textCount) {
+                                BasicText(
+                                    text = it.toString(),
+                                    style = TextStyle(
+                                        fontSize = 15.sp,
+                                        textAlign = TextAlign.Center
+                                    ),
+                                    modifier = Modifier
+                                        .fillMaxWidth()
+                                        .testTag(it.toString())
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        val scope = TestScope(tag, selection, clipboardManager, textToolbar)
+        scope.resetClipboard()
+        scope.block()
+    }
+}
+
+private class TextToolbarWrapper(private val delegate: TextToolbar) : TextToolbar {
+    private var _shown: Boolean = false
+    val shown: Boolean get() = _shown
+
+    private var _mostRecentRect: Rect? = null
+    val mostRecentRect: Rect? get() = _mostRecentRect
+
+    override fun showMenu(
+        rect: Rect,
+        onCopyRequested: (() -> Unit)?,
+        onPasteRequested: (() -> Unit)?,
+        onCutRequested: (() -> Unit)?,
+        onSelectAllRequested: (() -> Unit)?
+    ) {
+        _shown = true
+        _mostRecentRect = rect
+        delegate.showMenu(
+            rect,
+            onCopyRequested,
+            onPasteRequested,
+            onCutRequested,
+            onSelectAllRequested
+        )
+    }
+
+    override fun hide() {
+        _shown = false
+        delegate.hide()
+    }
+
+    override val status: TextToolbarStatus
+        get() = delegate.status
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiText2dSelectionGesturesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiText2dSelectionGesturesTest.kt
new file mode 100644
index 0000000..823454b
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiText2dSelectionGesturesTest.kt
@@ -0,0 +1,355 @@
+/*
+ * 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.foundation.text.selection.gestures
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.selection.Selection
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.foundation.text.selection.gestures.util.longPress
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+internal class MultiText2dSelectionGesturesTest : AbstractSelectionGesturesTest() {
+
+    // 3 x 3 grid of texts
+    private val sideLength = 3
+
+    override val pointerAreaTag = "selectionContainer"
+    val line = "Test Text"
+    val text = "$line\n$line"
+
+    private val selection = mutableStateOf<Selection?>(null)
+
+    @Composable
+    override fun Content() {
+        SelectionContainer(
+            selection = selection.value,
+            onSelectionChange = { selection.value = it },
+            modifier = Modifier.testTag(pointerAreaTag)
+        ) {
+            Column(
+                modifier = Modifier.fillMaxSize(),
+                verticalArrangement = Arrangement.Center,
+                horizontalAlignment = Alignment.CenterHorizontally,
+            ) {
+                repeat(sideLength) { i ->
+                    Row(
+                        verticalAlignment = Alignment.CenterVertically,
+                        horizontalArrangement = Arrangement.Center,
+                    ) {
+                        repeat(sideLength) { j ->
+                            BasicText(
+                                text = text,
+                                style = TextStyle(
+                                    fontFamily = fontFamily,
+                                    fontSize = fontSize,
+                                ),
+                                modifier = Modifier
+                                    .padding(24.dp)
+                                    .testTag("${i * sideLength + j + 1}"),
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun dragUpperLeftText() {
+        dragTest(
+            dragPosition = characterPosition(1, 6),
+            selectableId = 1,
+            offset = 5,
+            crossed = true
+        )
+    }
+
+    @Test
+    fun dragUpperCenterText() {
+        dragTest(
+            dragPosition = characterPosition(2, 6),
+            selectableId = 2,
+            offset = 5,
+            crossed = true
+        )
+    }
+
+    @Test
+    fun dragUpperRightText() {
+        dragTest(
+            dragPosition = characterPosition(3, 6),
+            selectableId = 3,
+            offset = 6,
+            crossed = true
+        )
+    }
+
+    @Test
+    fun dragLeftText() {
+        dragTest(
+            dragPosition = characterPosition(4, 6),
+            selectableId = 4,
+            offset = 5,
+            crossed = true
+        )
+    }
+
+    @Test
+    fun dragSameText() {
+        dragTest(
+            dragPosition = characterPosition(5, 10),
+            selectableId = 5,
+            offset = 10,
+            crossed = false
+        )
+    }
+
+    @Test
+    fun dragRightText() {
+        dragTest(
+            dragPosition = characterPosition(6, 5),
+            selectableId = 6,
+            offset = 9,
+            crossed = false
+        )
+    }
+
+    @Test
+    fun dragLowerLeftText() {
+        dragTest(
+            dragPosition = characterPosition(7, 5),
+            selectableId = 7,
+            offset = 5,
+            crossed = false
+        )
+    }
+
+    @Test
+    fun dragLowerCenterText() {
+        dragTest(
+            dragPosition = characterPosition(8, 5),
+            selectableId = 8,
+            offset = 9,
+            crossed = false
+        )
+    }
+
+    @Test
+    fun dragLowerRightText() {
+        dragTest(
+            dragPosition = characterPosition(9, 5),
+            selectableId = 9,
+            offset = 9,
+            crossed = false
+        )
+    }
+
+    @Test
+    fun dragTopContainer() {
+        dragTest(
+            dragPosition = topEnd,
+            selectableId = 1,
+            offset = 0,
+            crossed = true
+        )
+    }
+
+    @Test
+    fun dragBetweenFirstAndSecondRow() {
+        dragTest(
+            dragPosition = betweenSelectables(2, 5),
+            selectableId = 4,
+            offset = 0,
+            crossed = true
+        )
+    }
+
+    @Test
+    fun dragBetweenSecondAndThirdRow() {
+        dragTest(
+            dragPosition = betweenSelectables(5, 8),
+            selectableId = 6,
+            offset = 19,
+            crossed = false
+        )
+    }
+
+    @Test
+    fun dragBottomContainer() {
+        dragTest(
+            dragPosition = bottomStart,
+            selectableId = 9,
+            offset = 19,
+            crossed = false
+        )
+    }
+
+    @Test
+    fun dragLeftContainer() {
+        dragTest(
+            // the offset should fall between lines,
+            // nudge it up so the position is on the upper line
+            dragPosition = centerStart.nudge(yDirection = VerticalDirection.UP),
+            selectableId = 4,
+            offset = 0,
+            crossed = true
+        )
+    }
+
+    @Test
+    fun dragBetweenLeftAndCenterTexts() {
+        dragTest(
+            dragPosition = betweenSelectables(4, 5).nudge(yDirection = VerticalDirection.UP),
+            selectableId = 5,
+            offset = 0,
+            crossed = true
+        )
+    }
+
+    @Test
+    fun dragBetweenCenterAndRightTexts() {
+        dragTest(
+            dragPosition = betweenSelectables(5, 6).nudge(yDirection = VerticalDirection.UP),
+            selectableId = 5,
+            offset = 9,
+            crossed = false
+        )
+    }
+
+    @Test
+    fun dragRightContainer() {
+        dragTest(
+            dragPosition = centerEnd.nudge(yDirection = VerticalDirection.UP),
+            selectableId = 6,
+            offset = 9,
+            crossed = false
+        )
+    }
+
+    private fun dragTest(
+        dragPosition: Offset,
+        selectableId: Int,
+        offset: Int,
+        crossed: Boolean,
+    ) {
+        performTouchGesture {
+            longPress(characterPosition(5, 6))
+        }
+
+        assertSelection(
+            startSelectableId = 5,
+            startOffset = 5,
+            endSelectableId = 5,
+            endOffset = 9,
+            handlesCrossed = false
+        )
+
+        touchDragTo(dragPosition)
+
+        assertSelection(
+            startSelectableId = 5,
+            startOffset = 5,
+            endSelectableId = selectableId,
+            endOffset = offset,
+            handlesCrossed = crossed
+        )
+
+        performTouchGesture {
+            up()
+        }
+
+        assertSelection(
+            startSelectableId = 5,
+            startOffset = 5,
+            endSelectableId = selectableId,
+            endOffset = offset,
+            handlesCrossed = crossed
+        )
+    }
+
+    // selectableIds are
+    //  1  2  3
+    //  4  5  6
+    //  7  8  9
+    private fun characterPosition(selectableId: Int, offset: Int): Offset {
+        val tag = "$selectableId"
+        val pointerAreaPosition =
+            rule.onNodeWithTag(pointerAreaTag).fetchSemanticsNode().positionInRoot
+        val nodePosition = rule.onNodeWithTag(tag).fetchSemanticsNode().positionInRoot
+        val textLayoutResult = rule.onNodeWithTag(tag).fetchTextLayoutResult()
+        return textLayoutResult.getBoundingBox(offset)
+            .translate(nodePosition - pointerAreaPosition)
+            .centerLeft
+            .nudge(HorizontalDirection.END)
+    }
+
+    private fun betweenSelectables(selectableId1: Int, selectableId2: Int): Offset {
+        val pointerAreaPosition =
+            rule.onNodeWithTag(pointerAreaTag).fetchSemanticsNode().positionInRoot
+
+        fun nodeCenter(selectableId: Int): Offset {
+            val tag = "$selectableId"
+            val node = rule.onNodeWithTag(tag).fetchSemanticsNode()
+            return node.boundsInRoot.center - pointerAreaPosition
+        }
+
+        return lerp(nodeCenter(selectableId1), nodeCenter(selectableId2), 0.5f)
+    }
+
+    private fun assertSelection(
+        startSelectableId: Int,
+        startOffset: Int,
+        endSelectableId: Int,
+        endOffset: Int,
+        handlesCrossed: Boolean,
+    ) {
+        assertThat(selection.value)
+            .isEqualTo(
+                Selection(
+                    start = Selection.AnchorInfo(
+                        direction = ResolvedTextDirection.Ltr,
+                        offset = startOffset,
+                        selectableId = startSelectableId.toLong()
+                    ),
+                    end = Selection.AnchorInfo(
+                        direction = ResolvedTextDirection.Ltr,
+                        offset = endOffset,
+                        selectableId = endSelectableId.toLong()
+                    ),
+                    handlesCrossed = handlesCrossed,
+                )
+            )
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesBidiTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesBidiTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesBidiTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesBidiTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesRtlTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesRtlTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesRtlTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesRtlTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextWithSpaceSelectionGesturesRegressionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextWithSpaceSelectionGesturesRegressionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextWithSpaceSelectionGesturesRegressionTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextWithSpaceSelectionGesturesRegressionTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesBidiTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesBidiTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesBidiTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesBidiTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesRtlTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesRtlTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesRtlTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesRtlTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesLtrTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesLtrTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesLtrTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesLtrTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesRtlTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesRtlTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesRtlTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesRtlTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionHandlesGesturesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionHandlesGesturesTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionHandlesGesturesTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionHandlesGesturesTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesBidiTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesBidiTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesBidiTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesBidiTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionHandlesGesturesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionHandlesGesturesTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionHandlesGesturesTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionHandlesGesturesTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/MultiTextSelectionTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/MultiTextSelectionTestUtils.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/MultiTextSelectionTestUtils.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/MultiTextSelectionTestUtils.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/SingleTextSelectionTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/SingleTextSelectionTestUtils.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/SingleTextSelectionTestUtils.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/SingleTextSelectionTestUtils.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/TextFieldSelectionTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/TextFieldSelectionTestUtils.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/TextFieldSelectionTestUtils.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/TextFieldSelectionTestUtils.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/TextSelectionTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/TextSelectionTestUtils.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/TextSelectionTestUtils.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/TextSelectionTestUtils.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicSecureTextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicSecureTextFieldTest.kt
new file mode 100644
index 0000000..44c013b
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicSecureTextFieldTest.kt
@@ -0,0 +1,716 @@
+/*
+ * 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.foundation.text2
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.selection.FakeTextToolbar
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.foundation.text2.input.TextObfuscationMode
+import androidx.compose.foundation.text2.input.internal.selection.FakeClipboardManager
+import androidx.compose.foundation.text2.input.rememberTextFieldState
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalTextToolbar
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.performTextInput
+import androidx.compose.ui.test.performTextInputSelection
+import androidx.compose.ui.test.performTextReplacement
+import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+internal class BasicSecureTextFieldTest {
+
+    // Keyboard shortcut tests for BasicSecureTextField are in TextFieldKeyEventTest
+
+    @get:Rule
+    val rule = createComposeRule().apply {
+        mainClock.autoAdvance = false
+    }
+
+    @get:Rule
+    val immRule = ComposeInputMethodManagerTestRule()
+
+    @get:Rule
+    val inputMethodInterceptor = InputMethodInterceptorRule(rule)
+
+    private val Tag = "BasicSecureTextField"
+    private val imm = FakeInputMethodManager()
+
+    @Before
+    fun setUp() {
+        immRule.setFactory { imm }
+    }
+
+    @Test
+    fun passwordSemanticsAreSet() {
+        rule.setContent {
+            BasicSecureTextField(
+                state = remember {
+                    TextFieldState("Hello", initialSelectionInChars = TextRange(0, 1))
+                },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        rule.onNodeWithTag(Tag).requestFocus()
+        rule.waitForIdle()
+        rule.onNodeWithTag(Tag).assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Password))
+        rule.onNodeWithTag(Tag).assert(SemanticsMatcher.keyIsDefined(SemanticsActions.PasteText))
+        // temporarily define copy and cut actions on BasicSecureTextField but make them no-op
+        rule.onNodeWithTag(Tag).assert(SemanticsMatcher.keyIsDefined(SemanticsActions.CopyText))
+        rule.onNodeWithTag(Tag).assert(SemanticsMatcher.keyIsDefined(SemanticsActions.CutText))
+    }
+
+    @Test
+    fun lastTypedCharacterIsRevealedTemporarily() {
+        rule.setContent {
+            BasicSecureTextField(
+                state = rememberTextFieldState(),
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        with(rule.onNodeWithTag(Tag)) {
+            performTextInput("a")
+            rule.mainClock.advanceTimeBy(200)
+            assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("a")
+            rule.mainClock.advanceTimeBy(1500)
+            assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("\u2022")
+        }
+    }
+
+    @Test
+    fun lastTypedCharacterIsRevealed_hidesAfterAnotherCharacterIsTyped() {
+        rule.setContent {
+            BasicSecureTextField(
+                state = rememberTextFieldState(),
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        with(rule.onNodeWithTag(Tag)) {
+            performTextInput("a")
+            rule.mainClock.advanceTimeBy(200)
+            assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("a")
+            performTextInput("b")
+            rule.mainClock.advanceTimeBy(50)
+            assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("\u2022b")
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun lastTypedCharacterIsRevealed_whenInsertedInMiddle() {
+        rule.setContent {
+            BasicSecureTextField(
+                state = rememberTextFieldState(),
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        with(rule.onNodeWithTag(Tag)) {
+            performTextInput("abc")
+            rule.mainClock.advanceTimeBy(200)
+            assertThat(fetchTextLayoutResult().layoutInput.text.text)
+                .isEqualTo("\u2022\u2022\u2022")
+            performTextInputSelection(TextRange(1))
+            performTextInput("d")
+            rule.mainClock.advanceTimeBy(50)
+            assertThat(fetchTextLayoutResult().layoutInput.text.text)
+                .isEqualTo("\u2022d\u2022\u2022")
+        }
+    }
+
+    @Test
+    fun lastTypedCharacterIsRevealed_hidesAfterFocusIsLost() {
+        rule.setContent {
+            Column {
+                BasicSecureTextField(
+                    state = rememberTextFieldState(),
+                    modifier = Modifier.testTag(Tag)
+                )
+                Box(
+                    modifier = Modifier
+                        .size(1.dp)
+                        .testTag("otherFocusable")
+                        .focusable()
+                )
+            }
+        }
+
+        with(rule.onNodeWithTag(Tag)) {
+            performTextInput("a")
+            rule.mainClock.advanceTimeBy(200)
+            assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("a")
+            rule.onNodeWithTag("otherFocusable")
+                .requestFocus()
+            rule.mainClock.advanceTimeBy(50)
+            assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("\u2022")
+        }
+    }
+
+    @Test
+    fun lastTypedCharacterIsRevealed_hidesAfterAnotherCharacterRemoved() {
+        rule.setContent {
+            BasicSecureTextField(
+                state = rememberTextFieldState(),
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        with(rule.onNodeWithTag(Tag)) {
+            performTextInput("abc")
+            rule.mainClock.advanceTimeBy(200)
+            performTextInput("d")
+            rule.mainClock.advanceTimeBy(50)
+            assertThat(fetchTextLayoutResult().layoutInput.text.text)
+                .isEqualTo("\u2022\u2022\u2022d")
+            performTextReplacement("bcd")
+            assertThat(fetchTextLayoutResult().layoutInput.text.text)
+                .isEqualTo("\u2022\u2022\u2022")
+        }
+    }
+
+    @Test
+    fun obfuscationMethodVisible_doesNotHideAnything() {
+        rule.setContent {
+            BasicSecureTextField(
+                state = rememberTextFieldState(),
+                textObfuscationMode = TextObfuscationMode.Visible,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        with(rule.onNodeWithTag(Tag)) {
+            performTextInput("abc")
+            rule.mainClock.advanceTimeBy(200)
+            assertThat(fetchTextLayoutResult().layoutInput.text.text)
+                .isEqualTo("abc")
+            rule.mainClock.advanceTimeBy(1500)
+            assertThat(fetchTextLayoutResult().layoutInput.text.text)
+                .isEqualTo("abc")
+        }
+    }
+
+    @Test
+    fun obfuscationMethodVisible_revealsEverythingWhenSwitchedTo() {
+        var obfuscationMode by mutableStateOf(TextObfuscationMode.Hidden)
+        rule.setContent {
+            BasicSecureTextField(
+                state = rememberTextFieldState(),
+                textObfuscationMode = obfuscationMode,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        with(rule.onNodeWithTag(Tag)) {
+            performTextInput("abc")
+            rule.mainClock.advanceTimeBy(200)
+            assertThat(fetchTextLayoutResult().layoutInput.text.text)
+                .isEqualTo("\u2022\u2022\u2022")
+            obfuscationMode = TextObfuscationMode.Visible
+            rule.mainClock.advanceTimeByFrame()
+            assertThat(fetchTextLayoutResult().layoutInput.text.text)
+                .isEqualTo("abc")
+        }
+    }
+
+    @Test
+    fun obfuscationMethodHidden_hidesEverything() {
+        rule.setContent {
+            BasicSecureTextField(
+                state = rememberTextFieldState(),
+                textObfuscationMode = TextObfuscationMode.Hidden,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        with(rule.onNodeWithTag(Tag)) {
+            performTextInput("abc")
+            rule.mainClock.advanceTimeByFrame()
+            assertThat(fetchTextLayoutResult().layoutInput.text.text)
+                .isEqualTo("\u2022\u2022\u2022")
+            performTextInput("d")
+            rule.mainClock.advanceTimeByFrame()
+            assertThat(fetchTextLayoutResult().layoutInput.text.text)
+                .isEqualTo("\u2022\u2022\u2022\u2022")
+        }
+    }
+
+    @Test
+    fun obfuscationMethodHidden_hidesEverythingWhenSwitchedTo() {
+        var obfuscationMode by mutableStateOf(TextObfuscationMode.Visible)
+        rule.setContent {
+            BasicSecureTextField(
+                state = rememberTextFieldState(),
+                textObfuscationMode = obfuscationMode,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        with(rule.onNodeWithTag(Tag)) {
+            performTextInput("abc")
+            rule.mainClock.advanceTimeByFrame()
+            assertThat(fetchTextLayoutResult().layoutInput.text.text)
+                .isEqualTo("abc")
+            obfuscationMode = TextObfuscationMode.Hidden
+            rule.mainClock.advanceTimeByFrame()
+            assertThat(fetchTextLayoutResult().layoutInput.text.text)
+                .isEqualTo("\u2022\u2022\u2022")
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun semantics_copy() {
+        val state = TextFieldState("Hello World!")
+        val clipboardManager = FakeClipboardManager("initial")
+        rule.setContent {
+            CompositionLocalProvider(LocalClipboardManager provides clipboardManager) {
+                BasicSecureTextField(
+                    state = state,
+                    modifier = Modifier.testTag(Tag)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0, 5))
+        rule.onNodeWithTag(Tag).performSemanticsAction(SemanticsActions.CopyText)
+
+        rule.runOnIdle {
+            assertThat(clipboardManager.getText()?.toString()).isEqualTo("initial")
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun semantics_cut() {
+        val state = TextFieldState("Hello World!")
+        val clipboardManager = FakeClipboardManager("initial")
+        rule.setContent {
+            CompositionLocalProvider(LocalClipboardManager provides clipboardManager) {
+                BasicSecureTextField(
+                    state = state,
+                    modifier = Modifier.testTag(Tag)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0, 5))
+        rule.onNodeWithTag(Tag).performSemanticsAction(SemanticsActions.CutText)
+
+        rule.runOnIdle {
+            assertThat(clipboardManager.getText()?.toString()).isEqualTo("initial")
+            assertThat(state.text.toString()).isEqualTo("Hello World!")
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun toolbarDoesNotShowCopyOrCut() {
+        var copyOptionAvailable = false
+        var cutOptionAvailable = false
+        var showMenuRequested = false
+        val textToolbar = FakeTextToolbar(
+            onShowMenu = { _, onCopyRequested, _, onCutRequested, _ ->
+                showMenuRequested = true
+                copyOptionAvailable = onCopyRequested != null
+                cutOptionAvailable = onCutRequested != null
+            },
+            onHideMenu = {}
+        )
+        val state = TextFieldState("Hello")
+        rule.setContent {
+            CompositionLocalProvider(LocalTextToolbar provides textToolbar) {
+                BasicSecureTextField(
+                    state = state,
+                    modifier = Modifier.testTag(Tag)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(Tag).requestFocus()
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0, 5))
+
+        rule.runOnIdle {
+            assertThat(showMenuRequested).isTrue()
+            assertThat(copyOptionAvailable).isFalse()
+            assertThat(cutOptionAvailable).isFalse()
+        }
+    }
+
+    @Test
+    fun stringValue_updatesFieldText_whenTextChangedFromCode_whileUnfocused() {
+        var text by mutableStateOf("hello")
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        rule.runOnIdle {
+            text = "world"
+        }
+        // Auto-advance is disabled.
+        rule.mainClock.advanceTimeByFrame()
+
+        assertThat(
+            rule.onNodeWithTag(Tag).fetchSemanticsNode().config[SemanticsProperties.EditableText]
+                .text
+        ).isEqualTo("world")
+    }
+
+    @Test
+    fun textFieldValue_updatesFieldText_whenTextChangedFromCode_whileUnfocused() {
+        var text by mutableStateOf(TextFieldValue("hello"))
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        rule.runOnIdle {
+            text = text.copy(text = "world")
+        }
+        // Auto-advance is disabled.
+        rule.mainClock.advanceTimeByFrame()
+
+        assertThat(
+            rule.onNodeWithTag(Tag).fetchSemanticsNode().config[SemanticsProperties.EditableText]
+                .text
+        ).isEqualTo("world")
+    }
+
+    @Test
+    fun textFieldValue_updatesFieldSelection_whenSelectionChangedFromCode_whileUnfocused() {
+        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(1)))
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        rule.runOnIdle {
+            text = text.copy(selection = TextRange(2))
+        }
+        // Auto-advance is disabled.
+        rule.mainClock.advanceTimeByFrame()
+
+        assertTextSelection(TextRange(2))
+    }
+
+    @Test
+    fun stringValue_doesNotUpdateField_whenTextChangedFromCode_whileFocused() {
+        var text by mutableStateOf("hello")
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus(Tag)
+
+        rule.runOnIdle {
+            text = "world"
+        }
+
+        rule.onNodeWithTag(Tag).assertTextEquals("hello")
+    }
+
+    @Test
+    fun textFieldValue_doesNotUpdateField_whenTextChangedFromCode_whileFocused() {
+        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(1)))
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus(Tag)
+
+        rule.runOnIdle {
+            text = TextFieldValue(text = "world", selection = TextRange(2))
+        }
+
+        rule.onNodeWithTag(Tag).assertTextEquals("hello")
+    }
+
+    @Test
+    fun stringValue_doesNotInvokeCallback_onFocus() {
+        var text by mutableStateOf("")
+        var onValueChangedCount = 0
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = {
+                    text = it
+                    onValueChangedCount++
+                },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        assertThat(onValueChangedCount).isEqualTo(0)
+
+        requestFocus(Tag)
+
+        rule.runOnIdle {
+            assertThat(onValueChangedCount).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun stringValue_doesNotInvokeCallback_whenOnlySelectionChanged() {
+        var text by mutableStateOf("")
+        var onValueChangedCount = 0
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = {
+                    text = it
+                    onValueChangedCount++
+                },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus(Tag)
+        assertThat(onValueChangedCount).isEqualTo(0)
+
+        // Act: wiggle the cursor around a bit.
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0))
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(5))
+
+        rule.runOnIdle {
+            assertThat(onValueChangedCount).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun stringValue_doesNotInvokeCallback_whenOnlyCompositionChanged() {
+        var text by mutableStateOf("")
+        var onValueChangedCount = 0
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = {
+                    text = it
+                    onValueChangedCount++
+                },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus(Tag)
+        assertThat(onValueChangedCount).isEqualTo(0)
+
+        // Act: wiggle the composition around a bit
+        inputMethodInterceptor.withInputConnection { setComposingRegion(0, 0) }
+        inputMethodInterceptor.withInputConnection { setComposingRegion(3, 5) }
+
+        rule.runOnIdle {
+            assertThat(onValueChangedCount).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun stringValue_doesNotInvokeCallback_whenTextChangedFromCode_whileUnfocused() {
+        var text by mutableStateOf("")
+        var onValueChangedCount = 0
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = {
+                    text = it
+                    onValueChangedCount++
+                },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        assertThat(onValueChangedCount).isEqualTo(0)
+
+        rule.runOnIdle {
+            text = "hello"
+        }
+
+        rule.runOnIdle {
+            assertThat(onValueChangedCount).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun stringValue_doesNotInvokeCallback_whenTextChangedFromCode_whileFocused() {
+        var text by mutableStateOf("")
+        var onValueChangedCount = 0
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = {
+                    text = it
+                    onValueChangedCount++
+                },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        assertThat(onValueChangedCount).isEqualTo(0)
+        requestFocus(Tag)
+
+        rule.runOnIdle {
+            text = "hello"
+        }
+
+        rule.runOnIdle {
+            assertThat(onValueChangedCount).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun textFieldValue_usesInitialSelectionFromValue() {
+        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(2)))
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertTextSelection(TextRange(2))
+    }
+
+    @Test
+    fun textFieldValue_reportsSelectionChangesInCallback() {
+        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(1)))
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(2))
+
+        rule.runOnIdle {
+            assertThat(text.selection).isEqualTo(TextRange(2))
+        }
+    }
+
+    @Test
+    fun textFieldValue_reportsCompositionChangesInCallback() {
+        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(1)))
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus(Tag)
+
+        inputMethodInterceptor.withInputConnection { setComposingRegion(0, 0) }
+        rule.runOnIdle {
+            assertWithMessage(
+                "After setting composing region to 0, 0, TextFieldState's composition is:"
+            ).that(text.composition).isNull()
+        }
+
+        inputMethodInterceptor.withInputConnection { setComposingRegion(1, 4) }
+        rule.runOnIdle {
+            assertWithMessage(
+                "After setting composing region to 1, 4, TextFieldState's composition is:"
+            ).that(text.composition).isEqualTo(TextRange(1, 4))
+        }
+    }
+
+    @Test
+    fun inputMethod_doesNotRestart_inResponseToKeyEvents() {
+        val state = TextFieldState("hello", initialSelectionInChars = TextRange(5))
+        rule.setContent {
+            BasicSecureTextField(
+                state = state,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        with(rule.onNodeWithTag(Tag)) {
+            requestFocus()
+            imm.resetCalls()
+
+            performKeyInput { pressKey(Key.Backspace) }
+            performTextInputSelection(TextRange.Zero)
+            performKeyInput { pressKey(Key.Delete) }
+        }
+
+        rule.runOnIdle {
+            imm.expectCall("updateSelection(4, 4, -1, -1)")
+            imm.expectCall("updateSelection(0, 0, -1, -1)")
+            imm.expectNoMoreCalls()
+        }
+    }
+
+    private fun requestFocus(tag: String) =
+        rule.onNodeWithTag(tag).requestFocus()
+
+    private fun assertTextSelection(expected: TextRange) {
+        val selection = rule.onNodeWithTag(Tag).fetchSemanticsNode()
+            .config.getOrNull(SemanticsProperties.TextSelectionRange)
+        assertWithMessage("Expected selection to be $expected")
+            .that(selection).isEqualTo(expected)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicTextField2ImmIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicTextField2ImmIntegrationTest.kt
new file mode 100644
index 0000000..3ebfdee
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicTextField2ImmIntegrationTest.kt
@@ -0,0 +1,396 @@
+/*
+ * 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.foundation.text2
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.text2.input.InputTransformation
+import androidx.compose.foundation.text2.input.TextFieldBuffer
+import androidx.compose.foundation.text2.input.TextFieldCharSequence
+import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.foundation.text2.input.placeCursorAtEnd
+import androidx.compose.foundation.text2.input.selectAll
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(
+    ExperimentalFoundationApi::class,
+    ExperimentalTestApi::class,
+)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class BasicTextField2ImmIntegrationTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val immRule = ComposeInputMethodManagerTestRule()
+
+    @get:Rule
+    val inputMethodInterceptor = InputMethodInterceptorRule(rule)
+
+    private val Tag = "BasicTextField2"
+    private val imm = FakeInputMethodManager()
+
+    @Before
+    fun setUp() {
+        immRule.setFactory { imm }
+    }
+
+    @Test
+    fun becomesTextEditor_whenFocusGained() {
+        val state = TextFieldState()
+        rule.setContent {
+            BasicTextField2(state, Modifier.testTag(Tag))
+        }
+
+        requestFocus(Tag)
+
+        inputMethodInterceptor.withInputConnection {
+            commitText("hello", 0)
+            assertThat(state.text.toString()).isEqualTo("hello")
+        }
+    }
+
+    @Test
+    fun stopsBeingTextEditor_whenFocusLost() {
+        val state = TextFieldState()
+        var focusManager: FocusManager? = null
+        rule.setContent {
+            focusManager = LocalFocusManager.current
+            BasicTextField2(state, Modifier.testTag(Tag))
+        }
+        requestFocus(Tag)
+        rule.runOnIdle {
+            focusManager!!.clearFocus()
+        }
+        inputMethodInterceptor.assertNoSessionActive()
+    }
+
+    @Test
+    fun stopsBeingTextEditor_whenChangedToReadOnly() {
+        val state = TextFieldState()
+        var readOnly by mutableStateOf(false)
+        rule.setContent {
+            BasicTextField2(state, Modifier.testTag(Tag), readOnly = readOnly)
+        }
+        requestFocus(Tag)
+        inputMethodInterceptor.assertSessionActive()
+
+        readOnly = true
+
+        inputMethodInterceptor.assertNoSessionActive()
+    }
+
+    @Test
+    fun stopsBeingTextEditor_whenChangedToDisabled() {
+        val state = TextFieldState()
+        var enabled by mutableStateOf(true)
+        rule.setContent {
+            BasicTextField2(state, Modifier.testTag(Tag), enabled = enabled)
+        }
+        requestFocus(Tag)
+        inputMethodInterceptor.assertSessionActive()
+
+        enabled = false
+
+        inputMethodInterceptor.assertNoSessionActive()
+    }
+
+    @Test
+    fun staysTextEditor_whenFocusTransferred() {
+        val state1 = TextFieldState()
+        val state2 = TextFieldState()
+        rule.setContent {
+            BasicTextField2(state1, Modifier.testTag(Tag + 1))
+            BasicTextField2(state2, Modifier.testTag(Tag + 2))
+        }
+
+        requestFocus(Tag + 1)
+        requestFocus(Tag + 2)
+
+        inputMethodInterceptor.withInputConnection {
+            commitText("hello", 0)
+            endBatchEdit()
+            assertThat(state2.text.toString()).isEqualTo("hello")
+            assertThat(state1.text.toString()).isEmpty()
+        }
+    }
+
+    @Test
+    fun stopsBeingTextEditor_whenRemovedFromCompositionWhileFocused() {
+        val state = TextFieldState()
+        var compose by mutableStateOf(true)
+        rule.setContent {
+            if (compose) {
+                BasicTextField2(state, Modifier.testTag(Tag))
+            }
+        }
+        requestFocus(Tag)
+        rule.runOnIdle {
+            compose = false
+        }
+
+        inputMethodInterceptor.assertNoSessionActive()
+    }
+
+    @Test
+    fun inputRestarted_whenStateInstanceChanged() {
+        val state1 = TextFieldState()
+        val state2 = TextFieldState()
+        var state by mutableStateOf(state1)
+        rule.setContent {
+            BasicTextField2(state, Modifier.testTag(Tag))
+        }
+        requestFocus(Tag)
+
+        state = state2
+
+        inputMethodInterceptor.withInputConnection {
+            commitText("hello", 0)
+            assertThat(state2.text.toString()).isEqualTo("hello")
+            assertThat(state1.text.toString()).isEmpty()
+        }
+    }
+
+    @Test
+    fun immUpdated_whenFilterChangesText_fromInputConnection() {
+        val state = TextFieldState()
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                inputTransformation = { _, new ->
+                    // Force the selection not to change.
+                    val initialSelection = new.selectionInChars
+                    new.append("world")
+                    new.selectCharsIn(initialSelection)
+                }
+            )
+        }
+        requestFocus(Tag)
+        inputMethodInterceptor.withInputConnection {
+            // TODO move this before withInputConnection?
+            imm.resetCalls()
+
+            commitText("hello", 1)
+
+            assertThat(state.text.toString()).isEqualTo("helloworld")
+        }
+
+        rule.runOnIdle {
+            imm.expectCall("restartInput")
+            imm.expectNoMoreCalls()
+        }
+    }
+
+    @Test
+    fun immUpdated_whenFilterChangesText_fromKeyEvent() {
+        val state = TextFieldState()
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                inputTransformation = { _, new ->
+                    val initialSelection = new.selectionInChars
+                    new.append("world")
+                    new.selectCharsIn(initialSelection)
+                }
+            )
+        }
+        requestFocus(Tag)
+        rule.runOnIdle { imm.resetCalls() }
+
+        rule.onNodeWithTag(Tag).performKeyInput { pressKey(Key.A) }
+
+        rule.runOnIdle {
+            imm.expectCall("restartInput")
+            imm.expectNoMoreCalls()
+        }
+    }
+
+    @Test
+    fun immUpdated_whenFilterChangesSelection_fromInputConnection() {
+        val state = TextFieldState()
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                inputTransformation = { _, new -> new.selectAll() }
+            )
+        }
+        requestFocus(Tag)
+        inputMethodInterceptor.withInputConnection {
+            imm.resetCalls()
+            setComposingText("hello", 1)
+        }
+
+        rule.runOnIdle {
+            imm.expectCall("updateSelection(0, 5, 0, 5)")
+            imm.expectNoMoreCalls()
+        }
+    }
+
+    @Test
+    fun immUpdated_whenEditChangesText() {
+        val state = TextFieldState()
+        rule.setContent {
+            BasicTextField2(state, Modifier.testTag(Tag))
+        }
+        requestFocus(Tag)
+        rule.runOnIdle {
+            imm.resetCalls()
+
+            state.edit {
+                append("hello")
+                placeCursorBeforeCharAt(0)
+            }
+        }
+
+        rule.runOnIdle {
+            imm.expectCall("restartInput")
+            imm.expectNoMoreCalls()
+        }
+    }
+
+    @Test
+    fun immUpdated_whenEditChangesSelection() {
+        val state = TextFieldState("hello", initialSelectionInChars = TextRange(0))
+        rule.setContent {
+            BasicTextField2(state, Modifier.testTag(Tag))
+        }
+        requestFocus(Tag)
+        rule.runOnIdle {
+            imm.resetCalls()
+
+            state.edit {
+                placeCursorAtEnd()
+            }
+        }
+
+        rule.runOnIdle {
+            imm.expectCall("updateSelection(5, 5, -1, -1)")
+            imm.expectNoMoreCalls()
+        }
+    }
+
+    @Test
+    fun immUpdated_whenEditChangesTextAndSelection() {
+        val state = TextFieldState()
+        rule.setContent {
+            BasicTextField2(state, Modifier.testTag(Tag))
+        }
+        requestFocus(Tag)
+        rule.runOnIdle {
+            imm.resetCalls()
+
+            state.edit {
+                append("hello")
+                placeCursorAtEnd()
+            }
+        }
+
+        rule.runOnIdle {
+            imm.expectCall("updateSelection(5, 5, -1, -1)")
+            imm.expectCall("restartInput")
+            imm.expectNoMoreCalls()
+        }
+    }
+
+    @Test
+    fun immNotRestarted_whenKeyboardIsConfiguredAsPassword() {
+        val state = TextFieldState()
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
+            )
+        }
+        requestFocus(Tag)
+        rule.runOnIdle { imm.resetCalls() }
+
+        rule.onNodeWithTag(Tag).performKeyInput { pressKey(Key.A) }
+        rule.onNodeWithTag(Tag).performKeyInput { pressKey(Key.Backspace) }
+
+        rule.runOnIdle {
+            imm.expectCall("updateSelection(1, 1, -1, -1)")
+            imm.expectCall("updateSelection(0, 0, -1, -1)")
+            imm.expectNoMoreCalls()
+        }
+    }
+
+    @Test
+    fun immNotRestarted_whenKeyboardIsConfiguredAsPassword_fromTransformation() {
+        val state = TextFieldState()
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                inputTransformation = object : InputTransformation {
+                    override val keyboardOptions: KeyboardOptions =
+                        KeyboardOptions(keyboardType = KeyboardType.Password)
+
+                    override fun transformInput(
+                        originalValue: TextFieldCharSequence,
+                        valueWithChanges: TextFieldBuffer
+                    ) {
+                        valueWithChanges.append('A')
+                    }
+                }
+            )
+        }
+        requestFocus(Tag)
+        rule.runOnIdle { imm.resetCalls() }
+
+        // "" -key-> "A" -filter> "AA"
+        rule.onNodeWithTag(Tag).performKeyInput { pressKey(Key.A) }
+        // "AA" -key-> "A" -filter> "AA"
+        rule.onNodeWithTag(Tag).performKeyInput { pressKey(Key.Backspace) }
+
+        rule.runOnIdle {
+            imm.expectCall("updateSelection(2, 2, -1, -1)")
+            imm.expectCall("updateSelection(2, 2, -1, -1)")
+            imm.expectNoMoreCalls()
+        }
+    }
+
+    private fun requestFocus(tag: String) =
+        rule.onNodeWithTag(tag).requestFocus()
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2SemanticsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicTextField2SemanticsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2SemanticsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicTextField2SemanticsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/ComposeInputMethodManagerTestRule.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/ComposeInputMethodManagerTestRule.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/ComposeInputMethodManagerTestRule.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/ComposeInputMethodManagerTestRule.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/DecorationBoxTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/DecorationBoxTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/DecorationBoxTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/DecorationBoxTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/FakeInputMethodManager.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/FakeInputMethodManager.kt
new file mode 100644
index 0000000..b9913d5
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/FakeInputMethodManager.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.foundation.text2
+
+import android.view.KeyEvent
+import android.view.inputmethod.ExtractedText
+import androidx.compose.foundation.text2.input.internal.ComposeInputMethodManager
+import com.google.common.truth.Truth.assertThat
+
+internal class FakeInputMethodManager : ComposeInputMethodManager {
+    private val calls = mutableListOf<String>()
+
+    fun expectCall(description: String) {
+        assertThat(calls.removeFirst()).isEqualTo(description)
+    }
+
+    fun expectNoMoreCalls() {
+        assertThat(calls).isEmpty()
+    }
+
+    fun resetCalls() {
+        calls.clear()
+    }
+
+    override fun restartInput() {
+        calls += "restartInput"
+    }
+
+    override fun showSoftInput() {
+        calls += "showSoftInput"
+    }
+
+    override fun hideSoftInput() {
+        calls += "hideSoftInput"
+    }
+
+    override fun updateExtractedText(token: Int, extractedText: ExtractedText) {
+        calls += "updateExtractedText"
+    }
+
+    override fun updateSelection(
+        selectionStart: Int,
+        selectionEnd: Int,
+        compositionStart: Int,
+        compositionEnd: Int
+    ) {
+        calls += "updateSelection($selectionStart, $selectionEnd, " +
+            "$compositionStart, $compositionEnd)"
+    }
+
+    override fun sendKeyEvent(event: KeyEvent) {
+        calls += "sendKeyEvent"
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/HeightInLinesModifierTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/HeightInLinesModifierTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/HeightInLinesModifierTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/HeightInLinesModifierTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/InputMethodInterceptorRule.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/InputMethodInterceptorRule.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/InputMethodInterceptorRule.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/InputMethodInterceptorRule.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TestSoftwareKeyboardController.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TestSoftwareKeyboardController.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TestSoftwareKeyboardController.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TestSoftwareKeyboardController.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldCodepointTransformationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldCodepointTransformationTest.kt
new file mode 100644
index 0000000..90f3277
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldCodepointTransformationTest.kt
@@ -0,0 +1,750 @@
+/*
+ * 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.foundation.text2
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.foundation.text2.input.CodepointTransformation
+import androidx.compose.foundation.text2.input.TextFieldLineLimits
+import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.foundation.text2.input.mask
+import androidx.compose.foundation.text2.input.setTextAndPlaceCursorAtEnd
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.performTextInput
+import androidx.compose.ui.test.performTextInputSelection
+import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.test.withKeyDown
+import androidx.compose.ui.text.TextRange
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.test.fail
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class TextFieldCodepointTransformationTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val sessionHandler = InputMethodInterceptorRule(rule)
+
+    private val Tag = "BasicTextField2"
+
+    @Test
+    fun textField_rendersTheResultOf_codepointTransformation() {
+        val state = TextFieldState()
+        state.setTextAndPlaceCursorAtEnd("Hello")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                codepointTransformation = { _, codepoint -> codepoint + 1 },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertLayoutText("Ifmmp") // one character after in lexical order
+    }
+
+    @Test
+    fun textField_rendersTheResultOf_codepointTransformation_codepointIndex() {
+        val state = TextFieldState()
+        state.setTextAndPlaceCursorAtEnd("Hello")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                codepointTransformation = { index, codepoint ->
+                    if (index % 2 == 0) codepoint + 1 else codepoint - 1
+                },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertLayoutText("Idmkp") // one character after and before in lexical order
+    }
+
+    @Test
+    fun textField_toggleCodepointTransformation_affectsNextFrame() {
+        rule.mainClock.autoAdvance = false
+        val state = TextFieldState()
+        state.setTextAndPlaceCursorAtEnd("Hello")
+        var codepointTransformation: CodepointTransformation? by mutableStateOf(null)
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                codepointTransformation = codepointTransformation,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertLayoutText("Hello") // no change
+        codepointTransformation = CodepointTransformation.mask('c')
+
+        rule.mainClock.advanceTimeByFrame()
+        assertLayoutText("ccccc") // all characters turn to c
+    }
+
+    @Test
+    fun textField_statefulCodepointTransformation_reactsToStateChange() {
+        val state = TextFieldState()
+        state.setTextAndPlaceCursorAtEnd("Hello")
+        var mask by mutableStateOf('-')
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                codepointTransformation = CodepointTransformation.mask(mask),
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertLayoutText("-----")
+        mask = '@'
+
+        rule.waitForIdle()
+        assertLayoutText("@@@@@")
+    }
+
+    @Test
+    fun textField_removingCodepointTransformation_rendersTextNormally() {
+        val state = TextFieldState()
+        state.setTextAndPlaceCursorAtEnd("Hello")
+        var codepointTransformation by mutableStateOf<CodepointTransformation?>(
+            CodepointTransformation.mask('*')
+        )
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                codepointTransformation = codepointTransformation,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertLayoutText("*****")
+        codepointTransformation = null
+
+        rule.waitForIdle()
+        assertLayoutText("Hello")
+    }
+
+    @Test
+    fun textField_codepointTransformation_continuesToRenderUpdatedText() {
+        val state = TextFieldState()
+        state.setTextAndPlaceCursorAtEnd("Hello")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                codepointTransformation = CodepointTransformation.mask('*'),
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertLayoutText("*****")
+        rule.waitForIdle()
+        rule.onNodeWithTag(Tag).performTextInput(", World!")
+        assertLayoutText("*".repeat("Hello, World!".length))
+    }
+
+    @Test
+    fun textField_singleLine_removesLineFeedViaCodepointTransformation() {
+        val state = TextFieldState()
+        state.setTextAndPlaceCursorAtEnd("Hello\nWorld")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                lineLimits = TextFieldLineLimits.SingleLine,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertLayoutText("Hello World")
+        rule.onNodeWithTag(Tag).performTextInput("\n")
+        assertLayoutText("Hello World ")
+    }
+
+    @Test
+    fun textField_singleLine_removesCarriageReturnViaCodepointTransformation() {
+        val state = TextFieldState()
+        state.setTextAndPlaceCursorAtEnd("Hello\rWorld")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                lineLimits = TextFieldLineLimits.SingleLine,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertLayoutText("Hello\uFEFFWorld")
+    }
+
+    @Test
+    fun textField_singleLine_doesNotOverrideGivenCodepointTransformation() {
+        val state = TextFieldState()
+        state.setTextAndPlaceCursorAtEnd("Hello\nWorld")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                lineLimits = TextFieldLineLimits.SingleLine,
+                codepointTransformation = { _, codepoint -> codepoint },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertLayoutText("Hello\nWorld")
+    }
+
+    @Test
+    fun surrogateToNonSurrogate_singleCodepoint_isTransformed() {
+        val state = TextFieldState(SingleSurrogateCodepointString)
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithNonSurrogate
+            )
+        }
+
+        assertLayoutText(".")
+    }
+
+    @Test
+    fun surrogateToNonSurrogate_multipleCodepoints_areTransformed() {
+        val state = TextFieldState(SingleSurrogateCodepointString + SingleSurrogateCodepointString)
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithNonSurrogate
+            )
+        }
+
+        assertLayoutText("..")
+    }
+
+    @Test
+    fun surrogateToNonSurrogate_withNonSurrogates_areTransformed() {
+        val state = TextFieldState("a${SingleSurrogateCodepointString}b")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithNonSurrogate
+            )
+        }
+
+        assertLayoutText("...")
+    }
+
+    @Test
+    fun nonSurrogateToSurrogate_singleCodepoint_isTransformed() {
+        val state = TextFieldState("a")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithSurrogate
+            )
+        }
+
+        assertLayoutText(SingleSurrogateCodepointString)
+    }
+
+    @Test
+    fun nonSurrogateToSurrogate_multipleCodepoints_areTransformed() {
+        val state = TextFieldState("ab")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithSurrogate
+            )
+        }
+
+        assertLayoutText(SingleSurrogateCodepointString + SingleSurrogateCodepointString)
+    }
+
+    @Test
+    fun nonSurrogateToSurrogate_withNonSurrogates_areTransformed() {
+        val state = TextFieldState("abc")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = { i, codepoint ->
+                    if (i == 1) SurrogateCodepoint else codepoint
+                }
+            )
+        }
+
+        assertLayoutText("a${SingleSurrogateCodepointString}c")
+    }
+
+    @Test
+    fun surrogateToNonSurrogate_singleCodepoint_selectionIsMappedAroundCodepoint() {
+        val state = TextFieldState(SingleSurrogateCodepointString)
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithNonSurrogate
+            )
+        }
+
+        assertVisualTextLength(1)
+        state.assertSelectionMappings(
+            TextRange(0) to TextRange(0),
+            TextRange(0, 1) to TextRange(0, 2),
+            TextRange(1, 0) to TextRange(2, 0),
+            TextRange(1) to TextRange(2),
+        )
+    }
+
+    @Test
+    fun nonSurrogateToSurrogate_singleCodepoint_selectionIsMappedAroundCodepoint() {
+        val state = TextFieldState("a")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithSurrogate
+            )
+        }
+
+        assertVisualTextLength(2)
+        state.assertSelectionMappings(
+            TextRange(0) to TextRange(0),
+            TextRange(0, 1) to TextRange(0, 1),
+            TextRange(0, 2) to TextRange(0, 1),
+            TextRange(1, 0) to TextRange(1, 0),
+            TextRange(1) to TextRange(0, 1),
+            TextRange(1, 2) to TextRange(0, 1),
+            TextRange(2, 0) to TextRange(1, 0),
+            TextRange(2, 1) to TextRange(1, 0),
+            TextRange(2) to TextRange(1),
+        )
+    }
+
+    @Test
+    fun multipleCodepoints_selectionIsMappedAroundCodepoints() {
+        val state = TextFieldState("a${SingleSurrogateCodepointString}c")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = { i, codepoint ->
+                    when (codepoint) {
+                        'a'.code, 'c'.code -> SurrogateCodepoint
+                        SurrogateCodepoint -> 'b'.code
+                        else -> fail(
+                            "unrecognized codepoint at index $i: " +
+                                String(intArrayOf(codepoint), 0, 1)
+                        )
+                    }
+                }
+            )
+        }
+
+        assertVisualTextLength(5)
+        state.assertSelectionMappings(
+            TextRange(0) to TextRange(0),
+            TextRange(0, 1) to TextRange(0, 1),
+            TextRange(0, 2) to TextRange(0, 1),
+            TextRange(0, 3) to TextRange(0, 3),
+            TextRange(0, 4) to TextRange(0, 4),
+            TextRange(0, 5) to TextRange(0, 4),
+            TextRange(1, 0) to TextRange(1, 0),
+            TextRange(1) to TextRange(0, 1),
+            TextRange(1, 2) to TextRange(0, 1),
+            TextRange(1, 3) to TextRange(0, 3),
+            TextRange(1, 4) to TextRange(0, 4),
+            TextRange(1, 5) to TextRange(0, 4),
+            TextRange(2, 0) to TextRange(1, 0),
+            TextRange(2, 1) to TextRange(1, 0),
+            TextRange(2) to TextRange(1),
+            TextRange(2, 3) to TextRange(1, 3),
+            TextRange(2, 4) to TextRange(1, 4),
+            TextRange(2, 5) to TextRange(1, 4),
+            TextRange(3, 0) to TextRange(3, 0),
+            TextRange(3, 1) to TextRange(3, 0),
+            TextRange(3, 2) to TextRange(3, 1),
+            TextRange(3) to TextRange(3),
+            TextRange(3, 4) to TextRange(3, 4),
+            TextRange(3, 5) to TextRange(3, 4),
+            TextRange(4, 0) to TextRange(4, 0),
+            TextRange(4, 1) to TextRange(4, 0),
+            TextRange(4, 2) to TextRange(4, 1),
+            TextRange(4, 3) to TextRange(4, 3),
+            TextRange(4) to TextRange(3, 4),
+            TextRange(4, 5) to TextRange(3, 4),
+        )
+    }
+
+    @Test
+    fun cursorTraversal_withArrowKeys() {
+        val state = TextFieldState("a${SingleSurrogateCodepointString}c")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = { i, codepoint ->
+                    when (codepoint) {
+                        'a'.code -> SurrogateCodepoint
+                        SurrogateCodepoint -> 'b'.code
+                        'c'.code -> SurrogateCodepoint
+                        else -> fail(
+                            "unrecognized codepoint at index $i: " +
+                                String(intArrayOf(codepoint), 0, 1)
+                        )
+                    }
+                }
+            )
+        }
+
+        rule.onNodeWithTag(Tag).requestFocus()
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0))
+
+        listOf(0, 1, 3, 4).forEachIndexed { i, expectedCursor ->
+            rule.runOnIdle {
+                assertWithMessage("After pressing right arrow $i times")
+                    .that(state.text.selectionInChars).isEqualTo(TextRange(expectedCursor))
+            }
+            rule.onNodeWithTag(Tag).performKeyInput {
+                pressKey(Key.DirectionRight)
+            }
+        }
+    }
+
+    @Test
+    fun expandSelectionForward_withArrowKeys() {
+        val state = TextFieldState("a${SingleSurrogateCodepointString}c")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = { i, codepoint ->
+                    when (codepoint) {
+                        'a'.code -> SurrogateCodepoint
+                        SurrogateCodepoint -> 'b'.code
+                        'c'.code -> SurrogateCodepoint
+                        else -> fail(
+                            "unrecognized codepoint at index $i: " +
+                                String(intArrayOf(codepoint), 0, 1)
+                        )
+                    }
+                }
+            )
+        }
+
+        rule.onNodeWithTag(Tag).requestFocus()
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0))
+
+        listOf(
+            TextRange(0),
+            TextRange(0, 1),
+            TextRange(0, 3),
+            TextRange(0, 4)
+        ).forEachIndexed { i, expectedSelection ->
+            rule.runOnIdle {
+                assertWithMessage("After pressing shift+right arrow $i times")
+                    .that(state.text.selectionInChars).isEqualTo(expectedSelection)
+            }
+            rule.onNodeWithTag(Tag).performKeyInput {
+                withKeyDown(Key.ShiftLeft) {
+                    pressKey(Key.DirectionRight)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun expandSelectionBackward_withArrowKeys() {
+        val state = TextFieldState("a${SingleSurrogateCodepointString}c")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = { i, codepoint ->
+                    when (codepoint) {
+                        'a'.code -> SurrogateCodepoint
+                        SurrogateCodepoint -> 'b'.code
+                        'c'.code -> SurrogateCodepoint
+                        else -> fail(
+                            "unrecognized codepoint at index $i: " +
+                                String(intArrayOf(codepoint), 0, 1)
+                        )
+                    }
+                }
+            )
+        }
+
+        rule.onNodeWithTag(Tag).requestFocus()
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(4))
+
+        listOf(
+            TextRange(4),
+            TextRange(4, 3),
+            TextRange(4, 1),
+            TextRange(4, 0)
+        ).forEachIndexed { i, expectedSelection ->
+            rule.runOnIdle {
+                assertWithMessage("After pressing shift+left arrow $i times")
+                    .that(state.text.selectionInChars).isEqualTo(expectedSelection)
+            }
+            rule.onNodeWithTag(Tag).performKeyInput {
+                withKeyDown(Key.ShiftLeft) {
+                    pressKey(Key.DirectionLeft)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun insertNonSurrogates_intoSurrogateMask_fromKeyEvents() {
+        val state = TextFieldState("a$SingleSurrogateCodepointString")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithSurrogate
+            )
+        }
+        rule.onNodeWithTag(Tag).requestFocus()
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0))
+
+        rule.onNodeWithTag(Tag).performKeyInput {
+            pressKey(Key.X)
+            pressKey(Key.DirectionRight)
+            pressKey(Key.Y)
+            pressKey(Key.DirectionRight)
+            pressKey(Key.Z)
+        }
+
+        rule.runOnIdle {
+            assertThat(state.text.toString()).isEqualTo("xay${SingleSurrogateCodepointString}z")
+        }
+        assertVisualTextLength(10)
+    }
+
+    @Test
+    fun insertNonSurrogates_intoNonSurrogateMask_fromKeyEvents() {
+        val state = TextFieldState("a$SingleSurrogateCodepointString")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithNonSurrogate
+            )
+        }
+        rule.onNodeWithTag(Tag).requestFocus()
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0))
+
+        rule.onNodeWithTag(Tag).performKeyInput {
+            pressKey(Key.X)
+            pressKey(Key.DirectionRight)
+            pressKey(Key.Y)
+            pressKey(Key.DirectionRight)
+            pressKey(Key.Z)
+        }
+
+        rule.runOnIdle {
+            assertThat(state.text.toString()).isEqualTo("xay${SingleSurrogateCodepointString}z")
+        }
+        assertVisualTextLength(5)
+    }
+
+    @Test
+    fun insertText_intoSurrogateMask_fromSemantics() {
+        val state = TextFieldState("a$SingleSurrogateCodepointString")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithSurrogate
+            )
+        }
+        rule.onNodeWithTag(Tag).requestFocus()
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0))
+
+        // Use semantics to actually input the text, just use key events to move the cursor.
+        rule.onNodeWithTag(Tag).performTextInput("x")
+        pressKey(Key.DirectionRight)
+        rule.onNodeWithTag(Tag).performTextInput("y")
+        pressKey(Key.DirectionRight)
+        rule.onNodeWithTag(Tag).performTextInput("z")
+
+        rule.runOnIdle {
+            assertThat(state.text.toString()).isEqualTo("xay${SingleSurrogateCodepointString}z")
+        }
+        assertVisualTextLength(10)
+    }
+
+    @Test
+    fun insertNonSurrogates_intoNonSurrogateMask_fromSemantics() {
+        val state = TextFieldState("a$SingleSurrogateCodepointString")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithNonSurrogate
+            )
+        }
+        rule.onNodeWithTag(Tag).requestFocus()
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0))
+
+        // Use semantics to actually input the text, just use key events to move the cursor.
+        rule.onNodeWithTag(Tag).performTextInput("x")
+        pressKey(Key.DirectionRight)
+        rule.onNodeWithTag(Tag).performTextInput("y")
+        pressKey(Key.DirectionRight)
+        rule.onNodeWithTag(Tag).performTextInput("z")
+
+        rule.runOnIdle {
+            assertThat(state.text.toString()).isEqualTo("xay${SingleSurrogateCodepointString}z")
+        }
+        assertVisualTextLength(5)
+    }
+
+    @Test
+    fun insertText_intoSurrogateMask_fromIme() {
+        val state = TextFieldState("a$SingleSurrogateCodepointString")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithSurrogate
+            )
+        }
+        rule.onNodeWithTag(Tag).requestFocus()
+        sessionHandler.withInputConnection {
+            beginBatchEdit()
+            finishComposingText()
+            setSelection(0, 0)
+            endBatchEdit()
+        }
+
+        sessionHandler.withInputConnection { commitText("x", 1) }
+        pressKey(Key.DirectionRight)
+        sessionHandler.withInputConnection { commitText("y", 1) }
+        pressKey(Key.DirectionRight)
+        sessionHandler.withInputConnection { commitText("z", 1) }
+        pressKey(Key.DirectionRight)
+
+        rule.runOnIdle {
+            assertThat(state.text.toString()).isEqualTo("xay${SingleSurrogateCodepointString}z")
+        }
+        assertVisualTextLength(10)
+    }
+
+    @Test
+    fun insertText_intoNonSurrogateMask_fromIme() {
+        val state = TextFieldState("a$SingleSurrogateCodepointString")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag),
+                codepointTransformation = MaskWithNonSurrogate
+            )
+        }
+        rule.onNodeWithTag(Tag).requestFocus()
+        sessionHandler.withInputConnection {
+            beginBatchEdit()
+            finishComposingText()
+            setSelection(0, 0)
+            endBatchEdit()
+        }
+
+        sessionHandler.withInputConnection { commitText("x", 1) }
+        pressKey(Key.DirectionRight)
+        sessionHandler.withInputConnection { commitText("y", 1) }
+        pressKey(Key.DirectionRight)
+        sessionHandler.withInputConnection { commitText("z", 1) }
+        pressKey(Key.DirectionRight)
+
+        rule.runOnIdle {
+            assertThat(state.text.toString()).isEqualTo("xay${SingleSurrogateCodepointString}z")
+        }
+        assertVisualTextLength(5)
+    }
+
+    private fun assertLayoutText(text: String) {
+        assertThat(rule.onNodeWithTag(Tag).fetchTextLayoutResult().layoutInput.text.text)
+            .isEqualTo(text)
+    }
+
+    private fun assertVisualTextLength(expectedLength: Int) {
+        assertThat(rule.onNodeWithTag(Tag).fetchTextLayoutResult().layoutInput.text.text)
+            .hasLength(expectedLength)
+    }
+
+    private fun TextFieldState.assertSelectionMappings(
+        vararg mappings: Pair<TextRange, TextRange>
+    ) {
+        mappings.forEach { (write, expected) ->
+            val existingSelection = rule.onNodeWithTag(Tag)
+                .fetchSemanticsNode().config[SemanticsProperties.TextSelectionRange]
+            // Setting the selection to the current selection will return false.
+            if (existingSelection != write) {
+                assertWithMessage("Expected to be able to select $write")
+                    .that(performSelectionOnVisualText(write)).isTrue()
+                rule.runOnIdle {
+                    assertWithMessage("Visual selection $write to mapped")
+                        .that(text.selectionInChars).isEqualTo(expected)
+                }
+            }
+        }
+    }
+
+    private fun performSelectionOnVisualText(selection: TextRange): Boolean {
+        rule.onNodeWithTag(Tag).requestFocus()
+        var actionSucceeded = false
+        rule.onNodeWithTag(Tag).performSemanticsAction(SemanticsActions.SetSelection) {
+            actionSucceeded = it(selection.start, selection.end, /* relativeToOriginal= */ false)
+        }
+        return actionSucceeded
+    }
+
+    private fun pressKey(key: Key) {
+        rule.onNodeWithTag(Tag).performKeyInput { pressKey(key) }
+    }
+
+    private companion object {
+        /** This is "𐐷", a surrogate codepoint. */
+        val SurrogateCodepoint = Character.toCodePoint('\uD801', '\uDC37')
+        const val SingleSurrogateCodepointString = "\uD801\uDC37"
+
+        val MaskWithSurrogate = CodepointTransformation { _, _ -> SurrogateCodepoint }
+        val MaskWithNonSurrogate = CodepointTransformation { _, _ -> '.'.code }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldCursorTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldCursorTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldCursorTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldFocusTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldFocusTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldFocusTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldFocusTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldKeyEventTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldKeyEventTest.kt
new file mode 100644
index 0000000..19e905b
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldKeyEventTest.kt
@@ -0,0 +1,752 @@
+/*
+ * 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.foundation.text2
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.text.TEST_FONT_FAMILY
+import androidx.compose.foundation.text2.input.TextFieldLineLimits.MultiLine
+import androidx.compose.foundation.text2.input.TextFieldLineLimits.SingleLine
+import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.foundation.text2.input.internal.selection.FakeClipboardManager
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.ClipboardManager
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.KeyInjectionScope
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.test.withKeyDown
+import androidx.compose.ui.test.withKeysDown
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(
+    ExperimentalFoundationApi::class,
+    ExperimentalTestApi::class
+)
+class TextFieldKeyEventTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val tag = "TextFieldTestTag"
+
+    private var defaultDensity = Density(1f)
+
+    @Test
+    fun textField_typedEvents() {
+        keysSequenceTest {
+            pressKey(Key.H)
+            press(Key.ShiftLeft + Key.I)
+            expectedText("hI")
+        }
+    }
+
+    @Test
+    fun textField_copyPaste() {
+        keysSequenceTest("hello") {
+            withKeyDown(Key.CtrlLeft) {
+                pressKey(Key.A)
+                pressKey(Key.C)
+            }
+            pressKey(Key.DirectionRight)
+            pressKey(Key.Spacebar)
+            press(Key.CtrlLeft + Key.V)
+            expectedText("hello hello")
+        }
+    }
+
+    @Test
+    fun secureTextField_doesNotAllowCopy() {
+        keysSequenceTest("hello", secure = true) {
+            clipboardManager.setText(AnnotatedString("world"))
+            withKeyDown(Key.CtrlLeft) {
+                pressKey(Key.A)
+                pressKey(Key.C)
+            }
+            pressKey(Key.Copy) // also attempt direct copy
+            expectedClipboardText("world")
+        }
+    }
+
+    @Test
+    fun textField_directCopyPaste() {
+        keysSequenceTest("hello") {
+            press(Key.CtrlLeft + Key.A)
+            pressKey(Key.Copy)
+            expectedText("hello")
+            pressKey(Key.DirectionRight)
+            pressKey(Key.Spacebar)
+            pressKey(Key.Paste)
+            expectedText("hello hello")
+        }
+    }
+
+    @Test
+    fun textField_directCutPaste() {
+        keysSequenceTest("hello") {
+            press(Key.CtrlLeft + Key.A)
+            pressKey(Key.Cut)
+            expectedText("")
+            pressKey(Key.Paste)
+            expectedText("hello")
+        }
+    }
+
+    @Test
+    fun secureTextField_doesNotAllowCut() {
+        keysSequenceTest("hello", secure = true) {
+            clipboardManager.setText(AnnotatedString("world"))
+            withKeyDown(Key.CtrlLeft) {
+                pressKey(Key.A)
+                pressKey(Key.X)
+            }
+            pressKey(Key.Cut) // Also attempts direct cut
+            expectedText("hello")
+            expectedClipboardText("world")
+        }
+    }
+
+    @Test
+    fun textField_linesNavigation() {
+        keysSequenceTest("hello\nworld") {
+            pressKey(Key.DirectionDown)
+            pressKey(Key.A)
+            pressKey(Key.DirectionUp)
+            pressKey(Key.A)
+            expectedText("haello\naworld")
+            pressKey(Key.DirectionUp)
+            pressKey(Key.A)
+            expectedText("ahaello\naworld")
+        }
+    }
+
+    @Test
+    fun textField_linesNavigation_cache() {
+        keysSequenceTest("hello\n\nworld") {
+            pressKey(Key.DirectionRight)
+            pressKey(Key.DirectionDown)
+            pressKey(Key.DirectionDown)
+            pressKey(Key.Zero)
+            expectedText("hello\n\nw0orld")
+        }
+    }
+
+    @Test
+    fun textField_newLine() {
+        keysSequenceTest("hello") {
+            pressKey(Key.Enter)
+            expectedText("\nhello")
+        }
+    }
+
+    @Test
+    fun textField_backspace() {
+        keysSequenceTest("hello") {
+            pressKey(Key.DirectionRight)
+            pressKey(Key.DirectionRight)
+            pressKey(Key.Backspace)
+            expectedText("hllo")
+        }
+    }
+
+    @Test
+    fun textField_delete() {
+        keysSequenceTest("hello") {
+            pressKey(Key.Delete)
+            expectedText("ello")
+        }
+    }
+
+    @Test
+    fun textField_delete_atEnd() {
+        keysSequenceTest("hello", TextRange(5)) {
+            pressKey(Key.Delete)
+            expectedText("hello")
+        }
+    }
+
+    @Test
+    fun textField_delete_whenEmpty() {
+        keysSequenceTest {
+            pressKey(Key.Delete)
+            expectedText("")
+        }
+    }
+
+    @Test
+    fun textField_nextWord() {
+        keysSequenceTest("hello world") {
+            press(Key.CtrlLeft + Key.DirectionRight)
+            pressKey(Key.Zero)
+            expectedText("hello0 world")
+            press(Key.CtrlLeft + Key.DirectionRight)
+            pressKey(Key.Zero)
+            expectedText("hello0 world0")
+        }
+    }
+
+    @Test
+    fun textField_nextWord_doubleSpace() {
+        keysSequenceTest("hello  world") {
+            press(Key.CtrlLeft + Key.DirectionRight)
+            pressKey(Key.DirectionRight)
+            press(Key.CtrlLeft + Key.DirectionRight)
+            pressKey(Key.Zero)
+            expectedText("hello  world0")
+        }
+    }
+
+    @Test
+    fun textField_prevWord() {
+        keysSequenceTest("hello world") {
+            withKeyDown(Key.CtrlLeft) {
+                pressKey(Key.DirectionRight)
+                pressKey(Key.DirectionRight)
+                pressKey(Key.DirectionLeft)
+            }
+            pressKey(Key.Zero)
+            expectedText("hello 0world")
+        }
+    }
+
+    @Test
+    fun textField_HomeAndEnd() {
+        keysSequenceTest("hello world") {
+            pressKey(Key.MoveEnd)
+            pressKey(Key.Zero)
+            pressKey(Key.MoveHome)
+            pressKey(Key.Zero)
+            expectedText("0hello world0")
+        }
+    }
+
+    @Test
+    fun textField_byWordSelection() {
+        keysSequenceTest("hello  world\nhi") {
+            withKeysDown(listOf(Key.ShiftLeft, Key.CtrlLeft)) {
+                pressKey(Key.DirectionRight)
+                expectedSelection(TextRange(0, 5))
+                pressKey(Key.DirectionRight)
+                expectedSelection(TextRange(0, 12))
+                pressKey(Key.DirectionRight)
+                expectedSelection(TextRange(0, 15))
+                pressKey(Key.DirectionLeft)
+                expectedSelection(TextRange(0, 13))
+            }
+        }
+    }
+
+    @Test
+    fun textField_lineEndStart() {
+        keysSequenceTest(initText = "hi\nhello world\nhi") {
+            pressKey(Key.MoveEnd)
+            pressKey(Key.DirectionRight)
+            pressKey(Key.Zero)
+            expectedText("hi\n0hello world\nhi")
+            pressKey(Key.MoveEnd)
+            pressKey(Key.Zero)
+            expectedText("hi\n0hello world0\nhi")
+            withKeyDown(Key.ShiftLeft) { pressKey(Key.MoveHome) }
+            expectedSelection(TextRange(16, 3))
+            pressKey(Key.MoveHome)
+            pressKey(Key.DirectionRight)
+            withKeyDown(Key.ShiftLeft) { pressKey(Key.MoveEnd) }
+            expectedSelection(TextRange(4, 16))
+            expectedText("hi\n0hello world0\nhi")
+        }
+    }
+
+    @Test
+    fun textField_altLineLeftRight() {
+        keysSequenceTest(initText = "hi\nhello world\nhi") {
+            withKeyDown(Key.AltLeft) { pressKey(Key.DirectionRight) }
+            pressKey(Key.DirectionRight)
+            pressKey(Key.Zero)
+            expectedText("hi\n0hello world\nhi")
+            withKeyDown(Key.AltLeft) { pressKey(Key.DirectionRight) }
+            pressKey(Key.Zero)
+            expectedText("hi\n0hello world0\nhi")
+            withKeysDown(listOf(Key.ShiftLeft, Key.AltLeft)) { pressKey(Key.DirectionLeft) }
+            expectedSelection(TextRange(16, 3))
+            withKeyDown(Key.AltLeft) { pressKey(Key.DirectionLeft) }
+            pressKey(Key.DirectionRight)
+            withKeysDown(listOf(Key.ShiftLeft, Key.AltLeft)) { pressKey(Key.DirectionRight) }
+            expectedSelection(TextRange(4, 16))
+            expectedText("hi\n0hello world0\nhi")
+        }
+    }
+
+    @Test
+    fun textField_altTop() {
+        keysSequenceTest(initText = "hi\nhello world\nhi") {
+            pressKey(Key.MoveEnd)
+            repeat(3) { pressKey(Key.DirectionRight) }
+            pressKey(Key.Zero)
+            expectedText("hi\nhe0llo world\nhi")
+            withKeyDown(Key.AltLeft) { pressKey(Key.DirectionUp) }
+            pressKey(Key.Zero)
+            expectedText("0hi\nhe0llo world\nhi")
+            pressKey(Key.MoveEnd)
+            repeat(3) { pressKey(Key.DirectionRight) }
+            withKeysDown(listOf(Key.ShiftLeft, Key.AltLeft)) { pressKey(Key.DirectionUp) }
+            expectedSelection(TextRange(6, 0))
+            expectedText("0hi\nhe0llo world\nhi")
+        }
+    }
+
+    @Test
+    fun textField_altBottom() {
+        keysSequenceTest(initText = "hi\nhello world\nhi") {
+            pressKey(Key.MoveEnd)
+            repeat(3) { pressKey(Key.DirectionRight) }
+            pressKey(Key.Zero)
+            expectedText("hi\nhe0llo world\nhi")
+            withKeysDown(listOf(Key.ShiftLeft, Key.AltLeft)) { pressKey(Key.DirectionDown) }
+            expectedSelection(TextRange(6, 18))
+            pressKey(Key.DirectionLeft)
+            pressKey(Key.Zero)
+            expectedText("hi\nhe00llo world\nhi")
+            withKeyDown(Key.AltLeft) { pressKey(Key.DirectionDown) }
+            pressKey(Key.Zero)
+            expectedText("hi\nhe00llo world\nhi0")
+        }
+    }
+
+    @Test
+    fun textField_deleteWords() {
+        keysSequenceTest("hello world\nhi world") {
+            pressKey(Key.MoveEnd)
+            withKeyDown(Key.CtrlLeft) {
+                pressKey(Key.Backspace)
+                expectedText("hello \nhi world")
+                pressKey(Key.Delete)
+            }
+            expectedText("hello  world")
+        }
+    }
+
+    @Test
+    fun textField_deleteToBeginningOfLine() {
+        keysSequenceTest("hello world\nhi world") {
+            press(Key.CtrlLeft + Key.DirectionRight)
+
+            withKeyDown(Key.AltLeft) {
+                pressKey(Key.Backspace)
+                expectedText(" world\nhi world")
+                pressKey(Key.Backspace)
+                expectedText(" world\nhi world")
+            }
+
+            repeat(3) { pressKey(Key.DirectionRight) }
+
+            press(Key.AltLeft + Key.Backspace)
+            expectedText("rld\nhi world")
+            pressKey(Key.DirectionDown)
+            pressKey(Key.MoveEnd)
+
+            withKeyDown(Key.AltLeft) {
+                pressKey(Key.Backspace)
+                expectedText("rld\n")
+                pressKey(Key.Backspace)
+                expectedText("rld\n")
+            }
+        }
+    }
+
+    @Test
+    fun textField_deleteToEndOfLine() {
+        keysSequenceTest("hello world\nhi world") {
+            press(Key.CtrlLeft + Key.DirectionRight)
+            withKeyDown(Key.AltLeft) {
+                pressKey(Key.Delete)
+                expectedText("hello\nhi world")
+                pressKey(Key.Delete)
+                expectedText("hello\nhi world")
+            }
+
+            repeat(3) { pressKey(Key.DirectionRight) }
+
+            press(Key.AltLeft + Key.Delete)
+            expectedText("hello\nhi")
+
+            pressKey(Key.MoveHome)
+            withKeyDown(Key.AltLeft) {
+                pressKey(Key.Delete)
+                expectedText("hello\n")
+                pressKey(Key.Delete)
+                expectedText("hello\n")
+            }
+        }
+    }
+
+    @Test
+    fun textField_paragraphNavigation() {
+        keysSequenceTest("hello world\nhi") {
+            press(Key.CtrlLeft + Key.DirectionDown)
+            pressKey(Key.Zero)
+            expectedText("hello world0\nhi")
+            withKeyDown(Key.CtrlLeft) {
+                pressKey(Key.DirectionDown)
+                pressKey(Key.DirectionUp)
+            }
+            pressKey(Key.Zero)
+            expectedText("hello world0\n0hi")
+            withKeyDown(Key.CtrlLeft) {
+                pressKey(Key.DirectionUp)
+                pressKey(Key.DirectionUp)
+            }
+            pressKey(Key.Zero)
+            expectedText("0hello world0\n0hi")
+        }
+    }
+
+    @Test
+    fun textField_selectionCaret() {
+        keysSequenceTest("hello world") {
+            press(Key.CtrlLeft + Key.ShiftLeft + Key.DirectionRight)
+            expectedSelection(TextRange(0, 5))
+            press(Key.ShiftLeft + Key.DirectionRight)
+            expectedSelection(TextRange(0, 6))
+            press(Key.CtrlLeft + Key.Backslash)
+            expectedSelection(TextRange(6, 6))
+            press(Key.CtrlLeft + Key.ShiftLeft + Key.DirectionLeft)
+            expectedSelection(TextRange(6, 0))
+            press(Key.ShiftLeft + Key.DirectionRight)
+            expectedSelection(TextRange(6, 1))
+        }
+    }
+
+    @Test
+    fun textField_pageNavigationDown() {
+        keysSequenceTest(
+            initText = "A\nB\nC\nD\nE",
+            modifier = Modifier.requiredSize(73.dp)
+        ) {
+            pressKey(Key.PageDown)
+            expectedSelection(TextRange(4))
+        }
+    }
+
+    @Test
+    fun textField_pageNavigationDown_exactFit() {
+        keysSequenceTest(
+            initText = "A\nB\nC\nD\nE",
+            modifier = Modifier.requiredSize(90.dp) // exactly 3 lines fit
+        ) {
+            pressKey(Key.PageDown)
+            expectedSelection(TextRange(6))
+        }
+    }
+
+    @Test
+    fun textField_pageNavigationUp() {
+        keysSequenceTest(
+            initText = "A\nB\nC\nD\nE",
+            initSelection = TextRange(8), // just before 5
+            modifier = Modifier.requiredSize(73.dp)
+        ) {
+            pressKey(Key.PageUp)
+            expectedSelection(TextRange(4))
+        }
+    }
+
+    @Test
+    fun textField_pageNavigationUp_exactFit() {
+        keysSequenceTest(
+            initText = "A\nB\nC\nD\nE",
+            initSelection = TextRange(8), // just before 5
+            modifier = Modifier.requiredSize(90.dp) // exactly 3 lines fit
+        ) {
+            pressKey(Key.PageUp)
+            expectedSelection(TextRange(2))
+        }
+    }
+
+    @Test
+    fun textField_pageNavigationUp_cantGoUp() {
+        keysSequenceTest(
+            initText = "1\n2\n3\n4\n5",
+            initSelection = TextRange(0),
+            modifier = Modifier.requiredSize(90.dp)
+        ) {
+            pressKey(Key.PageUp)
+            expectedSelection(TextRange(0))
+        }
+    }
+
+    @Test
+    fun textField_tabSingleLine() {
+        keysSequenceTest("text", singleLine = true) {
+            pressKey(Key.Tab)
+            expectedText("text") // no change, should try focus change instead
+        }
+    }
+
+    @Test
+    fun textField_tabMultiLine() {
+        keysSequenceTest("text") {
+            pressKey(Key.Tab)
+            expectedText("\ttext")
+        }
+    }
+
+    @Test
+    fun textField_shiftTabSingleLine() {
+        keysSequenceTest("text", singleLine = true) {
+            press(Key.ShiftLeft + Key.Tab)
+            expectedText("text") // no change, should try focus change instead
+        }
+    }
+
+    @Test
+    fun textField_enterSingleLine() {
+        keysSequenceTest("text", singleLine = true) {
+            pressKey(Key.Enter)
+            expectedText("text") // no change, should do ime action instead
+        }
+    }
+
+    @Test
+    fun textField_enterMultiLine() {
+        keysSequenceTest("text") {
+            pressKey(Key.Enter)
+            expectedText("\ntext")
+        }
+    }
+
+    @Test
+    fun textField_withActiveSelection_tabSingleLine() {
+        keysSequenceTest("text", singleLine = true) {
+            pressKey(Key.DirectionRight)
+            withKeyDown(Key.ShiftLeft) {
+                pressKey(Key.DirectionRight)
+                pressKey(Key.DirectionRight)
+            }
+            pressKey(Key.Tab)
+            expectedText("text") // no change, should try focus change instead
+        }
+    }
+
+    @Test
+    fun textField_withActiveSelection_tabMultiLine() {
+        keysSequenceTest("text") {
+            pressKey(Key.DirectionRight)
+            withKeyDown(Key.ShiftLeft) {
+                pressKey(Key.DirectionRight)
+                pressKey(Key.DirectionRight)
+            }
+            pressKey(Key.Tab)
+            expectedText("t\tt")
+        }
+    }
+
+    @Test
+    fun textField_selectToLeft() {
+        keysSequenceTest("hello world hello") {
+            pressKey(Key.MoveEnd)
+            expectedSelection(TextRange(17))
+            withKeyDown(Key.ShiftLeft) {
+                pressKey(Key.DirectionLeft)
+                pressKey(Key.DirectionLeft)
+                pressKey(Key.DirectionLeft)
+            }
+            expectedSelection(TextRange(17, 14))
+        }
+    }
+
+    @Test
+    fun textField_withActiveSelection_shiftTabSingleLine() {
+        keysSequenceTest("text", singleLine = true) {
+            pressKey(Key.DirectionRight)
+            withKeyDown(Key.ShiftLeft) {
+                pressKey(Key.DirectionRight)
+                pressKey(Key.DirectionRight)
+                pressKey(Key.Tab)
+            }
+            expectedText("text") // no change, should try focus change instead
+        }
+    }
+
+    @Test
+    fun textField_withActiveSelection_enterSingleLine() {
+        keysSequenceTest("text", singleLine = true) {
+            pressKey(Key.DirectionRight)
+            withKeyDown(Key.ShiftLeft) {
+                pressKey(Key.DirectionRight)
+                pressKey(Key.DirectionRight)
+            }
+            pressKey(Key.Enter)
+            expectedText("text") // no change, should do ime action instead
+        }
+    }
+
+    @Test
+    fun textField_withActiveSelection_enterMultiLine() {
+        keysSequenceTest("text") {
+            pressKey(Key.DirectionRight)
+            withKeyDown(Key.ShiftLeft) {
+                pressKey(Key.DirectionRight)
+                pressKey(Key.DirectionRight)
+            }
+            pressKey(Key.Enter)
+            expectedText("t\nt")
+        }
+    }
+
+    @Test
+    fun textField_simpleUndo() {
+        keysSequenceTest("hello") {
+            press(Key.CtrlLeft + Key.DirectionRight)
+            pressKey(Key.Spacebar)
+            pressKey(Key.A)
+            pressKey(Key.B)
+            pressKey(Key.C)
+            expectedText("hello abc")
+            press(Key.CtrlLeft + Key.Z)
+            expectedText("hello")
+        }
+    }
+
+    @Test
+    fun textField_simpleRedo() {
+        keysSequenceTest("hello") {
+            press(Key.CtrlLeft + Key.DirectionRight)
+            pressKey(Key.Spacebar)
+            pressKey(Key.A)
+            pressKey(Key.B)
+            pressKey(Key.C)
+            expectedText("hello abc")
+            press(Key.CtrlLeft + Key.Z)
+            expectedText("hello")
+            press(Key.CtrlLeft + Key.ShiftLeft + Key.Z)
+            expectedText("hello abc")
+        }
+    }
+
+    private inner class SequenceScope(
+        val state: TextFieldState,
+        val clipboardManager: ClipboardManager,
+        private val keyInjectionScope: KeyInjectionScope
+    ) : KeyInjectionScope by keyInjectionScope {
+
+        fun press(keys: List<Key>) {
+            require(keys.isNotEmpty()) { "At least one key must be specified for press action" }
+            if (keys.size == 1) {
+                pressKey(keys.first())
+            } else {
+                withKeysDown(keys.dropLast(1)) { pressKey(keys.last()) }
+            }
+        }
+
+        infix operator fun Key.plus(other: Key): MutableList<Key> {
+            return mutableListOf(this, other)
+        }
+
+        fun expectedText(text: String) {
+            rule.runOnIdle {
+                assertThat(state.text.toString()).isEqualTo(text)
+            }
+        }
+
+        fun expectedSelection(selection: TextRange) {
+            rule.runOnIdle {
+                assertThat(state.text.selectionInChars).isEqualTo(selection)
+            }
+        }
+
+        fun expectedClipboardText(text: String) {
+            rule.runOnIdle {
+                assertThat(clipboardManager.getText()?.text).isEqualTo(text)
+            }
+        }
+    }
+
+    private fun keysSequenceTest(
+        initText: String = "",
+        initSelection: TextRange = TextRange.Zero,
+        modifier: Modifier = Modifier.fillMaxSize(),
+        singleLine: Boolean = false,
+        secure: Boolean = false,
+        sequence: SequenceScope.() -> Unit,
+    ) {
+        val state = TextFieldState(initText, initSelection)
+        val focusRequester = FocusRequester()
+        val clipboardManager = FakeClipboardManager("InitialTestText")
+        rule.setContent {
+            CompositionLocalProvider(
+                LocalDensity provides defaultDensity,
+                LocalClipboardManager provides clipboardManager,
+            ) {
+                if (!secure) {
+                    BasicTextField2(
+                        state = state,
+                        textStyle = TextStyle(
+                            fontFamily = TEST_FONT_FAMILY,
+                            fontSize = 30.sp
+                        ),
+                        modifier = modifier
+                            .focusRequester(focusRequester)
+                            .testTag(tag),
+                        lineLimits = if (singleLine) SingleLine else MultiLine(),
+                    )
+                } else {
+                    BasicSecureTextField(
+                        state = state,
+                        textStyle = TextStyle(
+                            fontFamily = TEST_FONT_FAMILY,
+                            fontSize = 30.sp
+                        ),
+                        modifier = modifier
+                            .focusRequester(focusRequester)
+                            .testTag(tag)
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle { focusRequester.requestFocus() }
+
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeBy(1000)
+
+        rule.onNodeWithTag(tag).performKeyInput {
+            sequence(SequenceScope(state, clipboardManager, this@performKeyInput))
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldKeyboardActionsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldKeyboardActionsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldKeyboardActionsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldKeyboardActionsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldScrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldScrollTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldScrollTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldStateRestorationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldStateRestorationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldStateRestorationTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/TextFieldStateRestorationTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSessionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSessionTest.kt
new file mode 100644
index 0000000..695ae0e
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSessionTest.kt
@@ -0,0 +1,243 @@
+/*
+ * 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.foundation.text2.input.internal
+
+import android.text.InputType
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.PlatformTextInputModifierNode
+import androidx.compose.ui.platform.PlatformTextInputSession
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.platform.textInputSession
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.ImeOptions
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFalse
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalFoundationApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AndroidTextInputSessionTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private lateinit var coroutineScope: CoroutineScope
+    private lateinit var hostView: View
+    private lateinit var textInputNode: PlatformTextInputModifierNode
+
+    @Before
+    fun setup() {
+        rule.setContent {
+            coroutineScope = rememberCoroutineScope()
+            hostView = LocalView.current
+            Box(
+                modifier = Modifier
+                    .size(1.dp)
+                    .testTag("tag")
+                    .then(TestTextElement())
+                    .focusable()
+            )
+        }
+        rule.onNodeWithTag("tag").requestFocus()
+        rule.waitForIdle()
+    }
+
+    @Test
+    fun createInputConnection_modifiesEditorInfo() {
+        val state = TextFieldState("hello", initialSelectionInChars = TextRange(0, 5))
+        launchInputSessionWithDefaultsForTest(state)
+        val editorInfo = EditorInfo()
+
+        rule.runOnUiThread {
+            hostView.onCreateInputConnection(editorInfo)
+        }
+
+        assertThat(editorInfo.initialSelStart).isEqualTo(0)
+        assertThat(editorInfo.initialSelEnd).isEqualTo(5)
+        assertThat(editorInfo.inputType).isEqualTo(
+            InputType.TYPE_CLASS_TEXT or
+                InputType.TYPE_TEXT_FLAG_MULTI_LINE or
+                InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
+        )
+        assertThat(editorInfo.imeOptions).isEqualTo(
+            EditorInfo.IME_FLAG_NO_FULLSCREEN or
+                EditorInfo.IME_FLAG_NO_ENTER_ACTION
+        )
+    }
+
+    @Test
+    fun inputConnection_sendsUpdates_toActiveSession() {
+        val state1 = TextFieldState()
+        val state2 = TextFieldState()
+        launchInputSessionWithDefaultsForTest(state1)
+
+        rule.runOnIdle {
+            hostView.onCreateInputConnection(EditorInfo())
+                .commitText("hello", 1)
+
+            assertThat(state1.text.toString()).isEqualTo("hello")
+            assertThat(state2.text.toString()).isEqualTo("")
+        }
+
+        launchInputSessionWithDefaultsForTest(state2)
+
+        rule.runOnIdle {
+            hostView.onCreateInputConnection(EditorInfo())
+                .commitText("world", 1)
+
+            assertThat(state1.text.toString()).isEqualTo("hello")
+            assertThat(state2.text.toString()).isEqualTo("world")
+        }
+    }
+
+    @Test
+    fun inputConnection_sendsEditorAction_toActiveSession() {
+        var imeActionFromOne: ImeAction? = null
+        var imeActionFromTwo: ImeAction? = null
+
+        launchInputSessionWithDefaultsForTest(
+            imeOptions = ImeOptions(imeAction = ImeAction.Done),
+            onImeAction = { imeActionFromOne = it }
+        )
+
+        rule.runOnIdle {
+            hostView.onCreateInputConnection(EditorInfo())
+                .performEditorAction(EditorInfo.IME_ACTION_DONE)
+
+            assertThat(imeActionFromOne).isEqualTo(ImeAction.Done)
+            assertThat(imeActionFromTwo).isNull()
+        }
+
+        launchInputSessionWithDefaultsForTest(
+            imeOptions = ImeOptions(imeAction = ImeAction.Go),
+            onImeAction = { imeActionFromTwo = it }
+        )
+
+        rule.runOnIdle {
+            hostView.onCreateInputConnection(EditorInfo())
+                .performEditorAction(EditorInfo.IME_ACTION_GO)
+
+            assertThat(imeActionFromOne).isEqualTo(ImeAction.Done)
+            assertThat(imeActionFromTwo).isEqualTo(ImeAction.Go)
+        }
+    }
+
+    @Test
+    fun createInputConnection_updatesEditorInfo() {
+        launchInputSessionWithDefaultsForTest(
+            imeOptions = ImeOptions(
+                singleLine = true,
+                keyboardType = KeyboardType.Email,
+                autoCorrect = false,
+                imeAction = ImeAction.Search,
+                capitalization = KeyboardCapitalization.Words
+            )
+        )
+        val editorInfo = EditorInfo()
+
+        rule.runOnIdle {
+            hostView.onCreateInputConnection(editorInfo)
+        }
+
+        assertThat(editorInfo.inputType).isEqualTo(
+            InputType.TYPE_CLASS_TEXT or
+                InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS or
+                InputType.TYPE_TEXT_FLAG_CAP_WORDS
+        )
+        assertThat(editorInfo.imeOptions).isEqualTo(
+            EditorInfo.IME_ACTION_SEARCH or EditorInfo.IME_FLAG_NO_FULLSCREEN
+        )
+    }
+
+    @Test
+    fun debugMode_isDisabled() {
+        // run this in presubmit to check that we are not accidentally enabling logs on prod
+        assertFalse(
+            TIA_DEBUG,
+            "Oops, looks like you accidentally enabled logging. Don't worry, we've all " +
+                "been there. Just remember to turn it off before you deploy your code."
+        )
+    }
+
+    private fun launchInputSessionWithDefaultsForTest(
+        state: TextFieldState = TextFieldState(),
+        imeOptions: ImeOptions = ImeOptions.Default,
+        onImeAction: (ImeAction) -> Unit = {}
+    ) {
+        coroutineScope.launch {
+            textInputNode.textInputSession {
+                inputSessionWithDefaultsForTest(
+                    state,
+                    imeOptions,
+                    onImeAction
+                )
+            }
+        }
+    }
+
+    private suspend fun PlatformTextInputSession.inputSessionWithDefaultsForTest(
+        state: TextFieldState = TextFieldState(),
+        imeOptions: ImeOptions = ImeOptions.Default,
+        onImeAction: (ImeAction) -> Unit = {}
+    ): Nothing = platformSpecificTextInputSession(
+        state = TransformedTextFieldState(
+            textFieldState = state,
+            inputTransformation = null,
+            codepointTransformation = null
+        ),
+        imeOptions = imeOptions,
+        onImeAction = onImeAction
+    )
+
+    private inner class TestTextElement : ModifierNodeElement<TestTextNode>() {
+        override fun create(): TestTextNode = TestTextNode()
+        override fun update(node: TestTextNode) {}
+        override fun hashCode(): Int = 0
+        override fun equals(other: Any?): Boolean = other is TestTextElement
+    }
+
+    private inner class TestTextNode : Modifier.Node(), PlatformTextInputModifierNode {
+        override fun onAttach() {
+            textInputNode = this
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/BackspaceCommandTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/BackspaceCommandTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/BackspaceCommandTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/BackspaceCommandTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/ComposeInputMethodManagerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/ComposeInputMethodManagerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/ComposeInputMethodManagerTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/ComposeInputMethodManagerTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/EditorInfoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/EditorInfoTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/EditorInfoTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/EditorInfoTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/MoveCursorCommandTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/MoveCursorCommandTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/MoveCursorCommandTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/MoveCursorCommandTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnectionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnectionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnectionTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnectionTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldLayoutStateCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldLayoutStateCacheTest.kt
new file mode 100644
index 0000000..4d8908c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldLayoutStateCacheTest.kt
@@ -0,0 +1,778 @@
+/*
+ * 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.foundation.text2.input.internal
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text2.input.CodepointTransformation
+import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.foundation.text2.input.setTextAndPlaceCursorAtEnd
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.ObserverHandle
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.snapshots.SnapshotStateObserver
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.sp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.test.assertNotNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalFoundationApi::class)
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class TextFieldLayoutStateCacheTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var textFieldState = TextFieldState()
+    private var transformedTextFieldState = TransformedTextFieldState(
+        textFieldState,
+        inputTransformation = null,
+        codepointTransformation = null
+    )
+    private var textStyle = TextStyle()
+    private var singleLine = false
+    private var softWrap = false
+    private var cache = TextFieldLayoutStateCache()
+    private var density = Density(1f, 1f)
+    private var layoutDirection = LayoutDirection.Ltr
+    private var fontFamilyResolver =
+        createFontFamilyResolver(InstrumentationRegistry.getInstrumentation().context)
+    private var constraints = Constraints()
+
+    private lateinit var globalWriteObserverHandle: ObserverHandle
+
+    @Before
+    fun setUp() {
+        globalWriteObserverHandle = Snapshot.registerGlobalWriteObserver {
+            Snapshot.sendApplyNotifications()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        globalWriteObserverHandle.dispose()
+    }
+
+    @Test
+    fun updateAllInputs_doesntInvalidateSnapshot_whenNothingChanged() {
+        assertInvalidationsOnChange(0) {
+            updateNonMeasureInputs()
+            updateMeasureInputs()
+        }
+    }
+
+    @Test
+    fun updateNonMeasureInputs_invalidatesSnapshot_whenTextContentChanged() {
+        textFieldState.edit {
+            replace(0, length, "")
+            placeCursorBeforeCharAt(0)
+        }
+        assertInvalidationsOnChange(1) {
+            textFieldState.edit {
+                append("hello")
+                placeCursorBeforeCharAt(0)
+            }
+        }
+    }
+
+    @Test
+    fun updateNonMeasureInputs_invalidatesSnapshot_whenTextSelectionChanged() {
+        textFieldState.edit {
+            append("hello")
+            placeCursorBeforeCharAt(0)
+        }
+        assertInvalidationsOnChange(1) {
+            textFieldState.edit {
+                placeCursorBeforeCharAt(1)
+            }
+        }
+    }
+
+    @Test
+    fun updateNonMeasureInputs_invalidatesSnapshot_whenCodepointTransformationChanged() {
+        assertInvalidationsOnChange(1) {
+            val codepointTransformation = CodepointTransformation { _, codepoint -> codepoint }
+            transformedTextFieldState = TransformedTextFieldState(
+                textFieldState,
+                inputTransformation = null,
+                codepointTransformation
+            )
+            updateNonMeasureInputs()
+        }
+    }
+
+    @Test
+    fun updateNonMeasureInputs_invalidatesSnapshot_whenStyleLayoutAffectingAttrsChanged() {
+        textStyle = TextStyle(fontSize = 12.sp)
+        assertInvalidationsOnChange(1) {
+            textStyle = TextStyle(fontSize = 23.sp)
+            updateNonMeasureInputs()
+        }
+    }
+
+    @Test
+    fun updateNonMeasureInputs_doesntInvalidateSnapshot_whenStyleDrawAffectingAttrsChanged() {
+        textStyle = TextStyle(color = Color.Black)
+        assertInvalidationsOnChange(0) {
+            textStyle = TextStyle(color = Color.Blue)
+            updateNonMeasureInputs()
+        }
+    }
+
+    @Test
+    fun updateNonMeasureInputs_invalidatesSnapshot_whenSingleLineChanged() {
+        assertInvalidationsOnChange(1) {
+            singleLine = !singleLine
+            updateNonMeasureInputs()
+        }
+    }
+
+    @Test
+    fun updateNonMeasureInputs_invalidatesSnapshot_whenSoftWrapChanged() {
+        assertInvalidationsOnChange(1) {
+            softWrap = !softWrap
+            updateNonMeasureInputs()
+        }
+    }
+
+    @Test
+    fun updateMeasureInputs_invalidatesSnapshot_whenDensityInstanceChangedWithDifferentValues() {
+        density = Density(1f, 1f)
+        assertInvalidationsOnChange(1) {
+            density = Density(1f, 2f)
+            updateMeasureInputs()
+        }
+    }
+
+    @Test
+    fun updateMeasureInputs_doesntInvalidateSnapshot_whenDensityInstanceChangedWithSameValues() {
+        density = Density(1f, 1f)
+        assertInvalidationsOnChange(0) {
+            density = Density(1f, 1f)
+            updateMeasureInputs()
+        }
+    }
+
+    @Test
+    fun updateMeasureInputs_invalidatesSnapshot_whenDensityValueChangedWithSameInstance() {
+        var densityValue = 1f
+        density = object : Density {
+            override val density: Float
+                get() = densityValue
+            override val fontScale: Float = 1f
+        }
+        assertInvalidationsOnChange(1) {
+            densityValue = 2f
+            updateMeasureInputs()
+        }
+    }
+
+    @Test
+    fun updateMeasureInputs_invalidatesSnapshot_whenFontScaleChangedWithSameInstance() {
+        var fontScale = 1f
+        density = object : Density {
+            override val density: Float = 1f
+            override val fontScale: Float
+                get() = fontScale
+        }
+        assertInvalidationsOnChange(1) {
+            fontScale = 2f
+            updateMeasureInputs()
+        }
+    }
+
+    @Test
+    fun updateMeasureInputs_invalidatesSnapshot_whenLayoutDirectionChanged() {
+        layoutDirection = LayoutDirection.Ltr
+        assertInvalidationsOnChange(1) {
+            layoutDirection = LayoutDirection.Rtl
+            updateMeasureInputs()
+        }
+    }
+
+    @Test
+    fun updateMeasureInputs_invalidatesSnapshot_whenFontFamilyResolverChanged() {
+        assertInvalidationsOnChange(1) {
+            fontFamilyResolver =
+                createFontFamilyResolver(InstrumentationRegistry.getInstrumentation().context)
+            updateMeasureInputs()
+        }
+    }
+
+    @Ignore("b/294443266: figure out how to make fonts stale for test")
+    @Test
+    fun updateMeasureInputs_invalidatesSnapshot_whenFontFamilyResolverFontChanged() {
+        fontFamilyResolver =
+            createFontFamilyResolver(InstrumentationRegistry.getInstrumentation().context)
+        assertInvalidationsOnChange(1) {
+            TODO("b/294443266: make fonts stale")
+        }
+    }
+
+    @Test
+    fun updateMeasureInputs_invalidatesSnapshot_whenConstraintsChanged() {
+        constraints = Constraints.fixed(5, 5)
+        assertInvalidationsOnChange(1) {
+            constraints = Constraints.fixed(6, 5)
+            updateMeasureInputs()
+        }
+    }
+
+    /**
+     * A scope that reads the layout cache and has a full cache hit – that is, all the inputs match
+     * – will never run the CodepointTransformation, and thus never register a "read" of any of the
+     * state objects the transformation function happens to read. If those inputs change, all scopes
+     * should be invalidated, even the ones that never actually ran the transformation function.
+     *
+     * The first time we compute layout with a transformation function, we invoke the transformation
+     * function. This function is provided externally and may perform zero or more state reads. The
+     * first time the layout is computed, those state reads will be seen by whatever snapshot
+     * observer is observing the layout call, and when they change, that reader will be invalidated.
+     * However, if somewhere else some different code asks for the layout, and none of the inputs
+     * have changed, it will return the cached value without ever running the transformation
+     * function. This means that when states read by the transformation change, that second reader
+     * won't be invalidated since it never observed those reads.
+     *
+     * To fix this, we manually record reads done by the transformation function and re-read them
+     * explicitly when checking for a full cache hit.
+     */
+    @Test
+    fun invalidatesAllReaders_whenTransformationDependenciesChanged_producingSameVisualText() {
+        var transformationState by mutableStateOf(1)
+        var transformationInvocations = 0
+        val codepointTransformation = CodepointTransformation { _, codepoint ->
+            transformationInvocations++
+            @Suppress("UNUSED_EXPRESSION")
+            transformationState
+            codepoint + 1
+        }
+        transformedTextFieldState = TransformedTextFieldState(
+            textFieldState, inputTransformation = null,
+            codepointTransformation
+        )
+        // Transformation isn't applied if there's no text. Keep this at 1 char to make the math
+        // simpler.
+        textFieldState.setTextAndPlaceCursorAtEnd("h")
+        val expectedVisualText = "i"
+
+        fun assertVisualText() {
+            assertThat(cache.value?.layoutInput?.text?.text).isEqualTo(expectedVisualText)
+        }
+
+        updateNonMeasureInputs()
+        updateMeasureInputs()
+        var primaryInvalidations = 0
+        var secondaryInvalidations = 0
+
+        val primaryObserver = SnapshotStateObserver(onChangedExecutor = { it() })
+        val secondaryObserver = SnapshotStateObserver(onChangedExecutor = { it() })
+        try {
+            primaryObserver.start()
+            secondaryObserver.start()
+
+            // This will compute the initial layout.
+            primaryObserver.observeReads(Unit, onValueChangedForScope = {
+                primaryInvalidations++
+                assertVisualText()
+            }) { assertVisualText() }
+            assertThat(transformationInvocations).isEqualTo(1)
+
+            // This should be a full cache hit.
+            secondaryObserver.observeReads(Unit, onValueChangedForScope = {
+                secondaryInvalidations++
+                assertVisualText()
+            }) { assertVisualText() }
+            assertThat(transformationInvocations).isEqualTo(1)
+
+            // Invalidate the transformation.
+            transformationState++
+        } finally {
+            primaryObserver.stop()
+            secondaryObserver.stop()
+        }
+
+        assertVisualText()
+        assertThat(transformationInvocations).isEqualTo(2)
+        assertThat(primaryInvalidations).isEqualTo(1)
+        assertThat(secondaryInvalidations).isEqualTo(1)
+    }
+
+    @Test
+    fun invalidatesAllReaders_whenTransformationDependenciesChanged_producingNewVisualText() {
+        var transformationState by mutableStateOf(1)
+        var transformationInvocations = 0
+        val codepointTransformation = CodepointTransformation { _, codepoint ->
+            transformationInvocations++
+            codepoint + transformationState
+        }
+        transformedTextFieldState = TransformedTextFieldState(
+            textFieldState,
+            inputTransformation = null,
+            codepointTransformation
+        )
+        // Transformation isn't applied if there's no text. Keep this at 1 char to make the math
+        // simpler.
+        textFieldState.setTextAndPlaceCursorAtEnd("h")
+        var expectedVisualText = "i"
+
+        fun assertVisualText() {
+            assertThat(cache.value?.layoutInput?.text?.text).isEqualTo(expectedVisualText)
+        }
+
+        updateNonMeasureInputs()
+        updateMeasureInputs()
+        var primaryInvalidations = 0
+        var secondaryInvalidations = 0
+
+        val primaryObserver = SnapshotStateObserver(onChangedExecutor = { it() })
+        val secondaryObserver = SnapshotStateObserver(onChangedExecutor = { it() })
+        try {
+            primaryObserver.start()
+            secondaryObserver.start()
+
+            // This will compute the initial layout.
+            primaryObserver.observeReads(Unit, onValueChangedForScope = {
+                primaryInvalidations++
+                assertVisualText()
+            }) { assertVisualText() }
+            assertThat(transformationInvocations).isEqualTo(1)
+
+            // This should be a full cache hit.
+            secondaryObserver.observeReads(Unit, onValueChangedForScope = {
+                secondaryInvalidations++
+                assertVisualText()
+            }) { assertVisualText() }
+            assertThat(transformationInvocations).isEqualTo(1)
+
+            // Invalidate the transformation.
+            expectedVisualText = "j"
+            transformationState++
+        } finally {
+            primaryObserver.stop()
+            secondaryObserver.stop()
+        }
+
+        assertVisualText()
+        // Two more reads means two more applications of the transformation.
+        assertThat(transformationInvocations).isEqualTo(2)
+        assertThat(primaryInvalidations).isEqualTo(1)
+        assertThat(secondaryInvalidations).isEqualTo(1)
+    }
+
+    @Test
+    fun value_returnsNewLayout_whenTextContentsChanged() {
+        textFieldState.edit {
+            replace(0, length, "h")
+            placeCursorBeforeCharAt(0)
+        }
+        assertLayoutChange(
+            change = {
+                textFieldState.edit {
+                    replace(0, length, "hello")
+                    placeCursorBeforeCharAt(0)
+                }
+            },
+        ) { old, new ->
+            assertThat(old.layoutInput.text.text).isEqualTo("h")
+            assertThat(new.layoutInput.text.text).isEqualTo("hello")
+        }
+    }
+
+    @Test
+    fun value_returnsCachedLayout_whenTextSelectionChanged() {
+        textFieldState.edit {
+            replace(0, length, "hello")
+            placeCursorBeforeCharAt(0)
+        }
+        assertLayoutChange(
+            change = {
+                textFieldState.edit {
+                    placeCursorBeforeCharAt(1)
+                }
+            }
+        ) { old, new ->
+            assertThat(new).isSameInstanceAs(old)
+        }
+    }
+
+    @Test
+    fun value_returnsNewLayout_whenCodepointTransformationInstanceChangedWithDifferentOutput() {
+        textFieldState.setTextAndPlaceCursorAtEnd("h")
+        var codepointTransformation = CodepointTransformation { _, codepoint -> codepoint }
+        transformedTextFieldState = TransformedTextFieldState(
+            textFieldState,
+            inputTransformation = null,
+            codepointTransformation
+        )
+        assertLayoutChange(
+            change = {
+                codepointTransformation = CodepointTransformation { _, codepoint -> codepoint + 1 }
+                transformedTextFieldState = TransformedTextFieldState(
+                    textFieldState,
+                    inputTransformation = null,
+                    codepointTransformation
+                )
+                updateNonMeasureInputs()
+            }
+        ) { old, new ->
+            assertThat(old.layoutInput.text.text).isEqualTo("h")
+            assertThat(new.layoutInput.text.text).isEqualTo("i")
+        }
+    }
+
+    @Test
+    fun value_returnsCachedLayout_whenCodepointTransformationInstanceChangedWithSameOutput() {
+        textFieldState.setTextAndPlaceCursorAtEnd("h")
+        var codepointTransformation = CodepointTransformation { _, codepoint -> codepoint }
+        transformedTextFieldState = TransformedTextFieldState(
+            textFieldState,
+            inputTransformation = null,
+            codepointTransformation
+        )
+        assertLayoutChange(
+            change = {
+                codepointTransformation = CodepointTransformation { _, codepoint -> codepoint }
+                transformedTextFieldState = TransformedTextFieldState(
+                    textFieldState,
+                    inputTransformation = null,
+                    codepointTransformation
+                )
+                updateNonMeasureInputs()
+            }
+        ) { old, new ->
+            assertThat(new).isSameInstanceAs(old)
+        }
+    }
+
+    @Test
+    fun value_returnsNewLayout_whenStyleLayoutAffectingAttributesChanged() {
+        textStyle = TextStyle(fontSize = 12.sp)
+        assertLayoutChange(
+            change = {
+                textStyle = TextStyle(fontSize = 23.sp)
+                updateNonMeasureInputs()
+            }
+        ) { old, new ->
+            assertThat(old.layoutInput.style.fontSize).isEqualTo(12.sp)
+            assertThat(new.layoutInput.style.fontSize).isEqualTo(23.sp)
+        }
+    }
+
+    @Test
+    fun value_returnsCachedLayout_whenStyleDrawAffectingAttributesChanged() {
+        textStyle = TextStyle(color = Color.Black)
+        assertLayoutChange(
+            change = {
+                textStyle = TextStyle(color = Color.Blue)
+                updateNonMeasureInputs()
+            }
+        ) { old, new ->
+            assertThat(new).isSameInstanceAs(old)
+        }
+    }
+
+    @Test
+    fun value_returnsNewLayout_whenSingleLineChanged() {
+        assertLayoutChange(
+            change = {
+                singleLine = !singleLine
+                updateNonMeasureInputs()
+            }
+        ) { old, new ->
+            assertThat(new).isNotSameInstanceAs(old)
+        }
+    }
+
+    @Test
+    fun value_returnsNewLayout_whenSoftWrapChanged() {
+        assertLayoutChange(
+            change = {
+                softWrap = !softWrap
+                updateNonMeasureInputs()
+            }
+        ) { old, new ->
+            assertThat(old.layoutInput.softWrap).isEqualTo(!softWrap)
+            assertThat(new.layoutInput.softWrap).isEqualTo(softWrap)
+        }
+    }
+
+    @Test
+    fun value_returnsNewLayout_whenDensityValueChangedWithSameInstance() {
+        var densityValue = 1f
+        density = object : Density {
+            override val density: Float
+                get() = densityValue
+            override val fontScale: Float = 1f
+        }
+        assertLayoutChange(
+            change = {
+                densityValue = 2f
+                updateMeasureInputs()
+            }
+        ) { old, new ->
+            assertThat(new).isNotSameInstanceAs(old)
+        }
+    }
+
+    @Test
+    fun value_returnsNewLayout_whenFontScaleChangedWithSameInstance() {
+        var fontScale = 1f
+        density = object : Density {
+            override val density: Float = 1f
+            override val fontScale: Float
+                get() = fontScale
+        }
+        assertLayoutChange(
+            change = {
+                fontScale = 2f
+                updateMeasureInputs()
+            }
+        ) { old, new ->
+            assertThat(new).isNotSameInstanceAs(old)
+        }
+    }
+
+    @Test
+    fun value_returnsCachedLayout_whenDensityInstanceChangedWithSameValues() {
+        density = Density(1f, 1f)
+        assertLayoutChange(
+            change = {
+                density = Density(1f, 1f)
+                updateMeasureInputs()
+            }
+        ) { old, new ->
+            assertThat(new).isSameInstanceAs(old)
+        }
+    }
+
+    @Test
+    fun value_returnsNewLayout_whenLayoutDirectionChanged() {
+        layoutDirection = LayoutDirection.Ltr
+        assertLayoutChange(
+            change = {
+                layoutDirection = LayoutDirection.Rtl
+                updateMeasureInputs()
+            }
+        ) { old, new ->
+            assertThat(old.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Ltr)
+            assertThat(new.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Rtl)
+        }
+    }
+
+    @Test
+    fun value_returnsNewLayout_whenFontFamilyResolverChanged() {
+        assertLayoutChange(
+            change = {
+                fontFamilyResolver =
+                    createFontFamilyResolver(InstrumentationRegistry.getInstrumentation().context)
+                updateMeasureInputs()
+            }
+        ) { old, new ->
+            assertThat(new).isNotSameInstanceAs(old)
+        }
+    }
+
+    @Ignore("b/294443266: figure out how to make fonts stale for test")
+    @Test
+    fun value_returnsNewLayout_whenFontFamilyResolverFontChanged() {
+        fontFamilyResolver =
+            createFontFamilyResolver(InstrumentationRegistry.getInstrumentation().context)
+        assertLayoutChange(
+            change = {
+                TODO("b/294443266: make fonts stale")
+            }
+        ) { old, new ->
+            assertThat(new).isNotSameInstanceAs(old)
+        }
+    }
+
+    @Test
+    fun value_returnsNewLayout_whenConstraintsChanged() {
+        constraints = Constraints.fixed(5, 5)
+        assertLayoutChange(
+            change = {
+                constraints = Constraints.fixed(6, 5)
+                updateMeasureInputs()
+            }
+        ) { old, new ->
+            assertThat(old.layoutInput.constraints).isEqualTo(Constraints.fixed(5, 5))
+            assertThat(new.layoutInput.constraints).isEqualTo(Constraints.fixed(6, 5))
+        }
+    }
+
+    @Test
+    fun cacheUpdateInSnapshot_onlyVisibleToParentSnapshotAfterApply() {
+        layoutDirection = LayoutDirection.Ltr
+        updateNonMeasureInputs()
+        updateMeasureInputs()
+        val initialLayout = cache.value!!
+        val snapshot = Snapshot.takeMutableSnapshot()
+
+        try {
+            snapshot.enter {
+                layoutDirection = LayoutDirection.Rtl
+                updateMeasureInputs()
+
+                val newLayout = cache.value!!
+                assertThat(initialLayout.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Ltr)
+                assertThat(newLayout.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Rtl)
+                assertThat(cache.value!!).isSameInstanceAs(newLayout)
+            }
+
+            // Not visible in parent yet.
+            assertThat(initialLayout.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Ltr)
+            assertThat(cache.value!!).isSameInstanceAs(initialLayout)
+            snapshot.apply().check()
+
+            // Now visible in parent.
+            val newLayout = cache.value!!
+            assertThat(initialLayout.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Ltr)
+            assertThat(newLayout.layoutInput.layoutDirection).isEqualTo(LayoutDirection.Rtl)
+            assertThat(cache.value!!).isSameInstanceAs(newLayout)
+        } finally {
+            snapshot.dispose()
+        }
+    }
+
+    @Test
+    fun cachedValue_recomputed_afterSnapshotWithConflictingInputsApplied() {
+        softWrap = false
+        layoutDirection = LayoutDirection.Ltr
+        updateNonMeasureInputs()
+        updateMeasureInputs()
+        val snapshot = Snapshot.takeMutableSnapshot()
+
+        try {
+            softWrap = true
+            updateNonMeasureInputs()
+            val initialLayout = cache.value!!
+
+            snapshot.enter {
+                layoutDirection = LayoutDirection.Rtl
+                updateMeasureInputs()
+                with(cache.value!!) {
+                    assertThat(layoutInput.softWrap).isEqualTo(false)
+                    assertThat(layoutInput.layoutDirection).isEqualTo(LayoutDirection.Rtl)
+                    assertThat(cache.value!!).isSameInstanceAs(this)
+                }
+            }
+
+            // Parent only sees its update.
+            with(cache.value!!) {
+                assertThat(layoutInput.softWrap).isEqualTo(true)
+                assertThat(layoutInput.layoutDirection).isEqualTo(LayoutDirection.Ltr)
+                assertThat(this).isSameInstanceAs(initialLayout)
+                assertThat(cache.value!!).isSameInstanceAs(this)
+            }
+            snapshot.apply().check()
+
+            // Cache should now reflect merged inputs.
+            with(cache.value!!) {
+                assertThat(layoutInput.softWrap).isEqualTo(true)
+                assertThat(layoutInput.layoutDirection).isEqualTo(LayoutDirection.Rtl)
+                assertThat(cache.value!!).isSameInstanceAs(this)
+            }
+        } finally {
+            snapshot.dispose()
+        }
+    }
+
+    private fun assertLayoutChange(
+        change: () -> Unit,
+        compare: (old: TextLayoutResult, new: TextLayoutResult) -> Unit
+    ) {
+        updateNonMeasureInputs()
+        updateMeasureInputs()
+        val initialLayout = cache.value
+
+        change()
+        val newLayout = cache.value
+
+        assertNotNull(newLayout)
+        assertNotNull(initialLayout)
+        compare(initialLayout, newLayout)
+    }
+
+    private fun assertInvalidationsOnChange(
+        expectedInvalidations: Int,
+        update: () -> Unit,
+    ) {
+        updateNonMeasureInputs()
+        updateMeasureInputs()
+        var invalidations = 0
+
+        observingLayoutCache({ invalidations++ }) {
+            update()
+        }
+
+        assertWithMessage("Expected $expectedInvalidations invalidations")
+            .that(invalidations).isEqualTo(expectedInvalidations)
+    }
+
+    private fun updateNonMeasureInputs() {
+        cache.updateNonMeasureInputs(
+            textFieldState = transformedTextFieldState,
+            textStyle = textStyle,
+            singleLine = singleLine,
+            softWrap = softWrap
+        )
+    }
+
+    private fun updateMeasureInputs() {
+        cache.layoutWithNewMeasureInputs(
+            density = density,
+            layoutDirection = layoutDirection,
+            fontFamilyResolver = fontFamilyResolver,
+            constraints = constraints
+        )
+    }
+
+    private fun observingLayoutCache(
+        onLayoutStateInvalidated: (TextLayoutResult?) -> Unit,
+        block: () -> Unit
+    ) {
+        val observer = SnapshotStateObserver(onChangedExecutor = { it() })
+        observer.start()
+        try {
+            observer.observeReads(Unit, onValueChangedForScope = {
+                onLayoutStateInvalidated(cache.value)
+            }) { cache.value }
+            block()
+        } finally {
+            observer.stop()
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/PressDownTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/PressDownTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/PressDownTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/PressDownTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TapAndDoubleTapTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TapAndDoubleTapTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TapAndDoubleTapTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TapAndDoubleTapTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldClickToMoveCursorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldClickToMoveCursorTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldClickToMoveCursorTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldClickToMoveCursorTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldCursorHandleTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldCursorHandleTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldCursorHandleTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldCursorHandleTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldLongPressTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldLongPressTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldLongPressTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldLongPressTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifierTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifierTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifierTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifierTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionHandlesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionHandlesTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionHandlesTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionHandlesTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionOnBackTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionOnBackTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionOnBackTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionOnBackTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldTextToolbarTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldTextToolbarTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldTextToolbarTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldTextToolbarTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/undo/BasicTextField2UndoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/undo/BasicTextField2UndoTest.kt
new file mode 100644
index 0000000..bf37659
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/undo/BasicTextField2UndoTest.kt
@@ -0,0 +1,353 @@
+/*
+ * 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.foundation.text2.input.internal.undo
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text2.BasicTextField2
+import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.foundation.text2.input.internal.selection.FakeClipboardManager
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.hasSetTextAction
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.performTextClearance
+import androidx.compose.ui.test.performTextInput
+import androidx.compose.ui.test.performTextInputSelection
+import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.text.TextRange
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+internal class BasicTextField2UndoTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun canUndo_imeInsert() {
+        val state = TextFieldState("Hello", TextRange(5))
+
+        rule.setContent {
+            BasicTextField2(state)
+        }
+
+        rule.onNode(hasSetTextAction()).performTextInput(", World")
+        assertThat(state.text.toString()).isEqualTo("Hello, World")
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("Hello")
+        rule.onNode(hasSetTextAction()).assertTextEquals("Hello")
+    }
+
+    @Test
+    fun canRedo_imeInsert() {
+        val state = TextFieldState("Hello", TextRange(5))
+
+        rule.setContent {
+            BasicTextField2(state)
+        }
+
+        rule.onNode(hasSetTextAction()).performTextInput(", World")
+
+        state.undoState.undo()
+        rule.onNode(hasSetTextAction()).assertTextEquals("Hello")
+
+        state.undoState.redo()
+        rule.onNode(hasSetTextAction()).assertTextEquals("Hello, World")
+    }
+
+    @Test
+    fun undoMerges_imeInserts() {
+        val state = TextFieldState("Hello", TextRange(5))
+
+        rule.setContent {
+            BasicTextField2(state)
+        }
+
+        rule.onNode(hasSetTextAction()).typeText(", World")
+        assertThat(state.text.toString()).isEqualTo("Hello, World")
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("Hello")
+        rule.onNode(hasSetTextAction()).assertTextEquals("Hello")
+    }
+
+    @Test
+    fun undoMerges_imeInserts_onlyInForwardsDirection() {
+        val state = TextFieldState("Hello", TextRange(5))
+
+        rule.setContent {
+            BasicTextField2(state)
+        }
+
+        with(rule.onNode(hasSetTextAction())) {
+            performTextInput(", World")
+            performTextInputSelection(TextRange(5))
+            performTextInput(" Compose")
+        }
+        assertThat(state.text.toString()).isEqualTo("Hello Compose, World")
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("Hello, World")
+        rule.onNode(hasSetTextAction()).assertTextEquals("Hello, World")
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("Hello")
+        rule.onNode(hasSetTextAction()).assertTextEquals("Hello")
+    }
+
+    @Test
+    fun undoMerges_deletes() {
+        val state = TextFieldState("Hello, World", TextRange(12))
+
+        rule.setContent {
+            BasicTextField2(state)
+        }
+
+        with(rule.onNode(hasSetTextAction())) {
+            requestFocus()
+            performKeyInput {
+                repeat(12) {
+                    pressKey(Key.Backspace)
+                }
+            }
+        }
+        state.assertTextAndSelection("", TextRange.Zero)
+
+        state.undoState.undo()
+
+        state.assertTextAndSelection("Hello, World", TextRange(12))
+    }
+
+    @Test
+    fun undoDoesNotMerge_deletes_inBothDirections() {
+        val state = TextFieldState("Hello, World", TextRange(6))
+
+        rule.setContent {
+            BasicTextField2(state)
+        }
+
+        with(rule.onNode(hasSetTextAction())) {
+            requestFocus()
+            performKeyInput {
+                repeat(6) {
+                    pressKey(Key.Backspace)
+                }
+                repeat(6) {
+                    pressKey(Key.Delete)
+                }
+            }
+        }
+        state.assertTextAndSelection("", TextRange.Zero)
+
+        state.undoState.undo()
+        state.assertTextAndSelection(" World", TextRange(0))
+
+        state.undoState.undo()
+        state.assertTextAndSelection("Hello, World", TextRange(6))
+    }
+
+    @Test
+    fun undo_revertsSelection() {
+        val state = TextFieldState("Hello", TextRange(5))
+
+        rule.setContent {
+            BasicTextField2(state)
+        }
+
+        with(rule.onNode(hasSetTextAction())) {
+            performTextInputSelection(TextRange(0, 5))
+            performTextInput("a")
+        }
+        assertThat(state.text.toString()).isEqualTo("a")
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("Hello")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(0, 5))
+    }
+
+    @Test
+    fun redo_revertsSelection() {
+        val state = TextFieldState("Hello", TextRange(5))
+
+        rule.setContent {
+            BasicTextField2(state)
+        }
+
+        with(rule.onNode(hasSetTextAction())) {
+            performTextInputSelection(TextRange(2))
+            performTextInput(" abc ")
+        }
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(7))
+
+        state.undoState.undo()
+
+        assertThat(state.text.selectionInChars).isNotEqualTo(TextRange(7))
+
+        state.undoState.redo()
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(7))
+    }
+
+    @Test
+    fun variousEditOperations() {
+        val state = TextFieldState()
+
+        rule.setContent {
+            BasicTextField2(state)
+        }
+
+        with(rule.onNode(hasSetTextAction())) {
+            typeText("abc def")
+            performTextInputSelection(TextRange(4))
+            typeText("123 ")
+            performTextInputSelection(TextRange(0, 3))
+            typeText("ghi")
+            performTextClearance()
+        }
+        state.assertTextAndSelection("", TextRange.Zero)
+        state.undoState.undo()
+        state.assertTextAndSelection("ghi 123 def", TextRange(3))
+        state.undoState.undo()
+        state.assertTextAndSelection("g 123 def", TextRange(1))
+        state.undoState.undo()
+        state.assertTextAndSelection("abc 123 def", TextRange(0, 3))
+        state.undoState.undo()
+        state.assertTextAndSelection("abc def", TextRange(4))
+        state.undoState.undo()
+        state.assertTextAndSelection("", TextRange.Zero)
+        assertThat(state.undoState.canUndo).isFalse()
+    }
+
+    @Test
+    fun clearHistory_removesAllUndoAndRedo() {
+        val state = TextFieldState()
+
+        rule.setContent {
+            BasicTextField2(state)
+        }
+
+        with(rule.onNode(hasSetTextAction())) {
+            typeText("abc def")
+            performTextInputSelection(TextRange(4))
+            typeText("123 ")
+            performTextInputSelection(TextRange(0, 3))
+            typeText("ghi")
+            performTextClearance()
+        }
+        state.undoState.undo()
+        state.undoState.undo()
+        state.undoState.undo()
+
+        assertThat(state.undoState.canUndo).isTrue()
+        assertThat(state.undoState.canRedo).isTrue()
+
+        state.undoState.clearHistory()
+
+        assertThat(state.undoState.canUndo).isFalse()
+        assertThat(state.undoState.canRedo).isFalse()
+    }
+
+    @Test
+    fun paste_neverMerges() {
+        val state = TextFieldState()
+        val clipboardManager = FakeClipboardManager("ghi")
+
+        rule.setContent {
+            CompositionLocalProvider(LocalClipboardManager provides clipboardManager) {
+                BasicTextField2(state)
+            }
+        }
+
+        with(rule.onNode(hasSetTextAction())) {
+            typeText("abc def ")
+            performSemanticsAction(SemanticsActions.PasteText)
+            typeText(" jkl")
+        }
+        state.undoState.undo()
+
+        state.assertTextAndSelection("abc def ghi", TextRange(11))
+
+        state.undoState.undo()
+
+        state.assertTextAndSelection("abc def ", TextRange(8))
+
+        state.undoState.undo()
+
+        state.assertTextAndSelection("", TextRange.Zero)
+    }
+
+    @Test
+    fun cut_neverMerges() {
+        val state = TextFieldState("abc def ghi", TextRange(11))
+
+        rule.setContent {
+            BasicTextField2(state)
+        }
+
+        with(rule.onNode(hasSetTextAction())) {
+            requestFocus()
+            repeat(4) {
+                performKeyInput {
+                    pressKey(Key.Backspace)
+                }
+            }
+            performTextInputSelection(TextRange(4, 7))
+            performSemanticsAction(SemanticsActions.CutText)
+            repeat(4) {
+                performKeyInput {
+                    pressKey(Key.Backspace)
+                }
+            }
+        }
+        state.assertTextAndSelection("", TextRange.Zero)
+
+        state.undoState.undo()
+
+        state.assertTextAndSelection("abc ", TextRange(4))
+
+        state.undoState.undo()
+
+        state.assertTextAndSelection("abc def", TextRange(4, 7))
+
+        state.undoState.undo()
+
+        state.assertTextAndSelection("abc def ghi", TextRange(11))
+    }
+
+    private fun SemanticsNodeInteraction.typeText(text: String) {
+        text.forEach { performTextInput(it.toString()) }
+    }
+
+    private fun TextFieldState.assertTextAndSelection(text: String, selection: TextRange) {
+        assertThat(this.text.toString()).isEqualTo(text)
+        assertThat(this.text.selectionInChars).isEqualTo(selection)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldDefaultWidthTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldDefaultWidthTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldDefaultWidthTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldDefaultWidthTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusCustomDialogTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusCustomDialogTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusCustomDialogTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusCustomDialogTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldOnValueChangeTextFieldValueTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldOnValueChangeTextFieldValueTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldOnValueChangeTextFieldValueTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldOnValueChangeTextFieldValueTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldSelectionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldSelectionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldSelectionTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldSelectionTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldUndoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldUndoTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldUndoTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldUndoTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldVisualTransformationCursorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldVisualTransformationCursorTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldVisualTransformationCursorTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldVisualTransformationCursorTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldVisualTransformationSelectionBoundsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldVisualTransformationSelectionBoundsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldVisualTransformationSelectionBoundsTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldVisualTransformationSelectionBoundsTest.kt
diff --git a/compose/foundation/foundation/src/androidAndroidTest/res/drawable-hdpi/ic_image_test.png b/compose/foundation/foundation/src/androidInstrumentedTest/res/drawable-hdpi/ic_image_test.png
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/res/drawable-hdpi/ic_image_test.png
rename to compose/foundation/foundation/src/androidInstrumentedTest/res/drawable-hdpi/ic_image_test.png
Binary files differ
diff --git a/compose/foundation/foundation/src/androidAndroidTest/res/drawable-nodpi/webp_test.webp b/compose/foundation/foundation/src/androidInstrumentedTest/res/drawable-nodpi/webp_test.webp
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/res/drawable-nodpi/webp_test.webp
rename to compose/foundation/foundation/src/androidInstrumentedTest/res/drawable-nodpi/webp_test.webp
Binary files differ
diff --git a/compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_asset_test.xml b/compose/foundation/foundation/src/androidInstrumentedTest/res/drawable/ic_vector_asset_test.xml
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_asset_test.xml
rename to compose/foundation/foundation/src/androidInstrumentedTest/res/drawable/ic_vector_asset_test.xml
diff --git a/compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_square_asset_test.xml b/compose/foundation/foundation/src/androidInstrumentedTest/res/drawable/ic_vector_square_asset_test.xml
similarity index 100%
rename from compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_square_asset_test.xml
rename to compose/foundation/foundation/src/androidInstrumentedTest/res/drawable/ic_vector_square_asset_test.xml
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/GraphicsSurface.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/GraphicsSurface.kt
new file mode 100644
index 0000000..17a6823
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/GraphicsSurface.kt
@@ -0,0 +1,438 @@
+/*
+ * 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.foundation
+
+import android.graphics.PixelFormat
+import android.graphics.SurfaceTexture
+import android.view.Surface
+import android.view.SurfaceHolder
+import android.view.SurfaceView
+import android.view.TextureView
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.viewinterop.AndroidView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.launch
+
+/**
+ * [SurfaceScope] is a scoped environment provided by [GraphicsSurface] and
+ * [EmbeddedGraphicsSurface] to handle [Surface] lifecycle events.
+ *
+ * @sample androidx.compose.foundation.samples.GraphicsSurfaceColors
+ */
+interface SurfaceScope {
+    /**
+     * Invokes [onChanged] when the surface's geometry (width and height) changes.
+     * Always invoked on the main thread.
+     */
+    @Suppress("PrimitiveInLambda")
+    fun Surface.onChanged(onChanged: Surface.(width: Int, height: Int) -> Unit)
+
+    /**
+     * Invokes [onDestroyed] when the surface is destroyed. All rendering into
+     * the surface should stop immediately after [onDestroyed] is invoked.
+     * Always invoked on the main thread.
+     */
+    fun Surface.onDestroyed(onDestroyed: Surface.() -> Unit)
+}
+
+/**
+ * [SurfaceCoroutineScope] is a scoped environment provided by
+ * [GraphicsSurface] and [EmbeddedGraphicsSurface] when a new [Surface] is
+ * created. This environment is a coroutine scope that also provides access to
+ * a [SurfaceScope] environment which can itself be used to handle other [Surface]
+ * lifecycle events.
+ *
+ * @see SurfaceScope
+ * @see GraphicsSurfaceScope
+ *
+ * @sample androidx.compose.foundation.samples.GraphicsSurfaceColors
+ */
+interface SurfaceCoroutineScope : SurfaceScope, CoroutineScope
+
+/**
+ * [GraphicsSurfaceScope] is a scoped environment provided when a [GraphicsSurface]
+ * or [EmbeddedGraphicsSurface] is first initialized. This environment can be
+ * used to register a lambda to invoke when a new [Surface] associated with the
+ * [GraphicsSurface]/[EmbeddedGraphicsSurface] is created.
+ */
+interface GraphicsSurfaceScope {
+    /**
+     * Invokes [onSurface] when a new [Surface] is created. The [onSurface] lambda
+     * is invoked on the main thread as part of a [SurfaceCoroutineScope] to provide
+     * a coroutine context.
+     *
+     * @param onSurface Callback invoked when a new [Surface] is created. The initial
+     *                  dimensions of the surface are provided.
+     */
+    @Suppress("PrimitiveInLambda")
+    fun onSurface(
+        onSurface: suspend SurfaceCoroutineScope.(surface: Surface, width: Int, height: Int) -> Unit
+    )
+}
+
+/**
+ * Base class for [GraphicsSurface] and [EmbeddedGraphicsSurface] state. This class
+ * provides methods to properly dispatch lifecycle events on [Surface] creation,
+ * change, and destruction. Surface creation is treated as a coroutine launch,
+ * using the specified [scope] as the parent. This scope must be the main thread scope.
+ */
+private abstract class BaseGraphicsSurfaceState(val scope: CoroutineScope) :
+    GraphicsSurfaceScope, SurfaceScope {
+
+    private var onSurface:
+        (suspend SurfaceCoroutineScope.(surface: Surface, width: Int, height: Int) -> Unit)? = null
+    private var onSurfaceChanged: (Surface.(width: Int, height: Int) -> Unit)? = null
+    private var onSurfaceDestroyed: (Surface.() -> Unit)? = null
+
+    private var job: Job? = null
+
+    override fun onSurface(
+        onSurface: suspend SurfaceCoroutineScope.(surface: Surface, width: Int, height: Int) -> Unit
+    ) {
+        this.onSurface = onSurface
+    }
+
+    override fun Surface.onChanged(onChanged: Surface.(width: Int, height: Int) -> Unit) {
+        onSurfaceChanged = onChanged
+    }
+
+    override fun Surface.onDestroyed(onDestroyed: Surface.() -> Unit) {
+        onSurfaceDestroyed = onDestroyed
+    }
+
+    /**
+     * Dispatch a surface creation event by launching a new coroutine in [scope].
+     * Any previous job from a previous surface creation dispatch is cancelled.
+     */
+    fun dispatchSurfaceCreated(surface: Surface, width: Int, height: Int) {
+        if (onSurface != null) {
+            job = scope.launch(start = CoroutineStart.UNDISPATCHED) {
+                job?.cancelAndJoin()
+                val receiver =
+                    object : SurfaceCoroutineScope, SurfaceScope by this@BaseGraphicsSurfaceState,
+                        CoroutineScope by this {}
+                onSurface?.invoke(receiver, surface, width, height)
+            }
+        }
+    }
+
+    /**
+     * Dispatch a surface change event, providing the surface's new width and height.
+     * Must be invoked from the main thread.
+     */
+    fun dispatchSurfaceChanged(surface: Surface, width: Int, height: Int) {
+        onSurfaceChanged?.invoke(surface, width, height)
+    }
+
+    /**
+     * Dispatch a surface destruction event. Any pending job from [dispatchSurfaceCreated]
+     * is cancelled before dispatching the event. Must be invoked from the main thread.
+     */
+    fun dispatchSurfaceDestroyed(surface: Surface) {
+        onSurfaceDestroyed?.invoke(surface)
+        job?.cancel()
+        job = null
+    }
+}
+
+private class GraphicsSurfaceState(scope: CoroutineScope) : BaseGraphicsSurfaceState(scope),
+    SurfaceHolder.Callback {
+
+    var lastWidth = -1
+    var lastHeight = -1
+
+    override fun surfaceCreated(holder: SurfaceHolder) {
+        val frame = holder.surfaceFrame
+        lastWidth = frame.width()
+        lastHeight = frame.height()
+
+        dispatchSurfaceCreated(holder.surface, lastWidth, lastHeight)
+    }
+
+    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
+        if (lastWidth != width || lastHeight != height) {
+            lastWidth = width
+            lastHeight = height
+
+            dispatchSurfaceChanged(holder.surface, width, height)
+        }
+    }
+
+    override fun surfaceDestroyed(holder: SurfaceHolder) {
+        dispatchSurfaceDestroyed(holder.surface)
+    }
+}
+
+@Composable
+private fun rememberGraphicsSurfaceState(): GraphicsSurfaceState {
+    val scope = rememberCoroutineScope()
+    return remember { GraphicsSurfaceState(scope) }
+}
+
+@JvmInline
+value class GraphicsSurfaceZOrder private constructor(val zOrder: Int) {
+    companion object {
+        val Behind = GraphicsSurfaceZOrder(0)
+        val MediaOverlay = GraphicsSurfaceZOrder(1)
+        val OnTop = GraphicsSurfaceZOrder(2)
+    }
+}
+
+/**
+ * Provides a dedicated drawing [Surface] as a separate layer positioned by default behind
+ * the window holding the [GraphicsSurface] composable. Because [GraphicsSurface] uses
+ * a separate window layer, graphics composition is handled by the system compositor which
+ * can bypass the GPU and provide better performance and power usage characteristics compared
+ * to [EmbeddedGraphicsSurface]. It is therefore recommended to use [GraphicsSurface]
+ * whenever possible.
+ *
+ * The z-ordering of the surface can be controlled using the [zOrder] parameter:
+ *
+ * - [GraphicsSurfaceZOrder.Behind]: positions the surface behind the window
+ * - [GraphicsSurfaceZOrder.MediaOverlay]: positions the surface behind the window but
+ *   above other [GraphicsSurfaceZOrder.Behind] surfaces
+ * - [GraphicsSurfaceZOrder.OnTop]: positions the surface above the window
+ *
+ * The drawing surface is opaque by default, which can be controlled with the [isOpaque]
+ * parameter. When the surface is transparent, you may need to change the z-order to
+ * see something behind the surface.
+ *
+ * To start rendering, the caller must first acquire the [Surface] when it's created.
+ * This is achieved by providing the [onInit] lambda, which allows the caller to
+ * register an appropriate [GraphicsSurfaceScope.onSurface] callback. The [onInit]
+ * lambda can also be used to initialize/cache resources needed once a surface is
+ * available.
+ *
+ * After acquiring a surface, the caller can start rendering into it. Rendering into a
+ * surface can be done from any thread.
+ *
+ * It is recommended to register the [SurfaceScope.onChanged] and [SurfaceScope.onDestroyed]
+ * callbacks to properly handle the lifecycle of the surface and react to dimension
+ * changes. You must ensure that the rendering thread stops interacting with the surface
+ * when the [SurfaceScope.onDestroyed] callback is invoked.
+ *
+ * If a [surfaceSize] is specified (set to non-[IntSize.Zero]), the surface will use
+ * the specified size instead of the layout size of this composable. The surface will
+ * be stretched at render time to fit the layout size. This can be used for instance to
+ * render at a lower resolution for performance reasons.
+ *
+ * @param modifier Modifier to be applied to the [GraphicsSurface]
+ * @param isOpaque Whether the managed surface should be opaque or transparent.
+ * @param zOrder Sets the z-order of the surface relative to its parent window.
+ * @param surfaceSize Sets the surface size independently of the layout size of
+ *                    this [GraphicsSurface]. If set to [IntSize.Zero], the surface
+ *                    size will be equal to the [GraphicsSurface] layout size.
+ * @param isSecure Control whether the surface view's content should be treated as
+ *                 secure, preventing it from appearing in screenshots or from being
+ *                 viewed on non-secure displays.
+ * @param onInit Lambda invoked on first composition. This lambda can be used to
+ *               declare a [GraphicsSurfaceScope.onSurface] callback that will be
+ *               invoked when a surface is available.
+ *
+ * @sample androidx.compose.foundation.samples.GraphicsSurfaceColors
+ */
+@Composable
+fun GraphicsSurface(
+    modifier: Modifier = Modifier,
+    isOpaque: Boolean = true,
+    zOrder: GraphicsSurfaceZOrder = GraphicsSurfaceZOrder.Behind,
+    surfaceSize: IntSize = IntSize.Zero,
+    isSecure: Boolean = false,
+    onInit: GraphicsSurfaceScope.() -> Unit
+) {
+    val state = rememberGraphicsSurfaceState()
+
+    AndroidView(
+        factory = { context ->
+            SurfaceView(context).apply {
+                state.onInit()
+                holder.addCallback(state)
+            }
+        },
+        modifier = modifier,
+        onReset = { },
+        update = { view ->
+            if (surfaceSize != IntSize.Zero) {
+                view.holder.setFixedSize(surfaceSize.width, surfaceSize.height)
+            } else {
+                view.holder.setSizeFromLayout()
+            }
+
+            view.holder.setFormat(
+                if (isOpaque) {
+                    PixelFormat.OPAQUE
+                } else {
+                    PixelFormat.TRANSLUCENT
+                }
+            )
+
+            when (zOrder) {
+                GraphicsSurfaceZOrder.Behind -> view.setZOrderOnTop(false)
+                GraphicsSurfaceZOrder.MediaOverlay -> view.setZOrderMediaOverlay(true)
+                GraphicsSurfaceZOrder.OnTop -> view.setZOrderOnTop(true)
+            }
+
+            view.setSecure(isSecure)
+        }
+    )
+}
+
+private class EmbeddedGraphicsSurfaceState(scope: CoroutineScope) : BaseGraphicsSurfaceState(scope),
+    TextureView.SurfaceTextureListener {
+
+    var surfaceSize = IntSize.Zero
+
+    private var surfaceTextureSurface: Surface? = null
+
+    override fun onSurfaceTextureAvailable(
+        surfaceTexture: SurfaceTexture,
+        width: Int,
+        height: Int
+    ) {
+        var w = width
+        var h = height
+
+        if (surfaceSize != IntSize.Zero) {
+            w = surfaceSize.width
+            h = surfaceSize.height
+            surfaceTexture.setDefaultBufferSize(w, h)
+        }
+
+        val surface = Surface(surfaceTexture)
+        surfaceTextureSurface = surface
+
+        dispatchSurfaceCreated(surface, w, h)
+    }
+
+    override fun onSurfaceTextureSizeChanged(
+        surfaceTexture: SurfaceTexture,
+        width: Int,
+        height: Int
+    ) {
+        var w = width
+        var h = height
+
+        if (surfaceSize != IntSize.Zero) {
+            w = surfaceSize.width
+            h = surfaceSize.height
+            surfaceTexture.setDefaultBufferSize(w, h)
+        }
+
+        dispatchSurfaceChanged(surfaceTextureSurface!!, w, h)
+    }
+
+    override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
+        dispatchSurfaceDestroyed(surfaceTextureSurface!!)
+        surfaceTextureSurface = null
+        return true
+    }
+
+    override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
+        // onSurfaceTextureUpdated is called when the content of the SurfaceTexture
+        // has changed, which is not relevant to us since we are the producer here
+    }
+}
+
+@Composable
+private fun rememberEmbeddedGraphicsSurfaceState(): EmbeddedGraphicsSurfaceState {
+    val scope = rememberCoroutineScope()
+    return remember { EmbeddedGraphicsSurfaceState(scope) }
+}
+
+/**
+ * Provides a dedicated drawing [Surface] embedded directly in the UI hierarchy.
+ * Unlike [GraphicsSurface], [EmbeddedGraphicsSurface] positions its surface as a
+ * regular element inside the composable hierarchy. This means that graphics
+ * composition is handled like any other UI widget, using the GPU. This can lead
+ * to increased power and memory bandwidth usage compared to [GraphicsSurface]. It
+ * is therefore recommended to use [GraphicsSurface] when possible.
+ *
+ * [EmbeddedGraphicsSurface] can however be useful when interactions with other widgets
+ * is necessary, for instance if the surface needs to be "sandwiched" between two
+ * other widgets, or if it must participate in visual effects driven by
+ * a `Modifier.graphicsLayer{}`.
+ *
+ * The drawing surface is opaque by default, which can be controlled with the [isOpaque]
+ * parameter.
+ *
+ * To start rendering, the caller must first acquire the [Surface] when it's created.
+ * This is achieved by providing the [onInit] lambda, which allows the caller to
+ * register an appropriate [GraphicsSurfaceScope.onSurface] callback. The [onInit]
+ * lambda can also be used to initialize/cache resources needed once a surface is
+ * available.
+ *
+ * After acquiring a surface, the caller can start rendering into it. Rendering into a
+ * surface can be done from any thread.
+ *
+ * It is recommended to register the [SurfaceScope.onChanged] and [SurfaceScope.onDestroyed]
+ * callbacks to properly handle the lifecycle of the surface and react to dimension
+ * changes. You must ensure that the rendering thread stops interacting with the surface
+ * when the [SurfaceScope.onDestroyed] callback is invoked.
+ *
+ * If a [surfaceSize] is specified (set to non-[IntSize.Zero]), the surface will use
+ * the specified size instead of the layout size of this composable. The surface will
+ * be stretched at render time to fit the layout size. This can be used for instance to
+ * render at a lower resolution for performance reasons.
+ *
+ * @param modifier Modifier to be applied to the [GraphicsSurface]
+ * @param isOpaque Whether the managed surface should be opaque or transparent. If
+ *                 transparent and [isMediaOverlay] is `false`, the surface will
+ *                 be positioned above the parent window.
+ * @param surfaceSize Sets the surface size independently of the layout size of
+ *                    this [GraphicsSurface]. If set to [IntSize.Zero], the surface
+ *                    size will be equal to the [GraphicsSurface] layout size.
+ * @param onInit Lambda invoked on first composition. This lambda can be used to
+ *               declare a [GraphicsSurfaceScope.onSurface] callback that will be
+ *               invoked when a surface is available.
+ *
+ * @sample androidx.compose.foundation.samples.EmbeddedGraphicsSurfaceColors
+ */
+@Composable
+fun EmbeddedGraphicsSurface(
+    modifier: Modifier = Modifier,
+    isOpaque: Boolean = true,
+    surfaceSize: IntSize = IntSize.Zero,
+    onInit: GraphicsSurfaceScope.() -> Unit
+) {
+    val state = rememberEmbeddedGraphicsSurfaceState()
+
+    AndroidView(
+        factory = { context ->
+            TextureView(context).apply {
+                state.surfaceSize = surfaceSize
+                state.onInit()
+                surfaceTextureListener = state
+            }
+        },
+        modifier = modifier,
+        onReset = { },
+        update = { view ->
+            if (surfaceSize != IntSize.Zero) {
+                view.surfaceTexture?.setDefaultBufferSize(surfaceSize.width, surfaceSize.height)
+            }
+            state.surfaceSize = surfaceSize
+            view.isOpaque = isOpaque
+        }
+    )
+}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Magnifier.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Magnifier.kt
index 736e938..6368cb1 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Magnifier.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Magnifier.kt
@@ -20,7 +20,6 @@
 import android.view.View
 import android.widget.Magnifier
 import androidx.annotation.ChecksSdkIntAtLeast
-import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
@@ -62,145 +61,13 @@
     SemanticsPropertyKey<() -> Offset>("MagnifierPositionInRoot")
 
 /**
- * Specifies how a [magnifier] should create the underlying [Magnifier] widget. These properties
- * should not be changed while a magnifier is showing, since the magnifier will be dismissed and
- * recreated with the new properties which will cause it to disappear for at least a frame.
- *
- * Not all magnifier features are supported on all platforms. The [isSupported] property will return
- * false for styles that cannot be fully supported on the given platform.
- *
- * @param size See [Magnifier.Builder.setSize]. Only supported on API 29+.
- * @param cornerRadius See [Magnifier.Builder.setCornerRadius]. Only supported on API 29+.
- * @param elevation See [Magnifier.Builder.setElevation]. Only supported on API 29+.
- * @param clippingEnabled See [Magnifier.Builder.setClippingEnabled]. Only supported on API 29+.
- * @param fishEyeEnabled Configures the magnifier to distort the magnification at the edges to
- * look like a fisheye lens. Not currently supported.
- */
-@ExperimentalFoundationApi
-@Stable
-class MagnifierStyle internal constructor(
-    internal val useTextDefault: Boolean,
-    internal val size: DpSize,
-    internal val cornerRadius: Dp,
-    internal val elevation: Dp,
-    internal val clippingEnabled: Boolean,
-    internal val fishEyeEnabled: Boolean
-) {
-    @ExperimentalFoundationApi
-    constructor(
-        size: DpSize = DpSize.Unspecified,
-        cornerRadius: Dp = Dp.Unspecified,
-        elevation: Dp = Dp.Unspecified,
-        clippingEnabled: Boolean = true,
-        fishEyeEnabled: Boolean = false
-    ) : this(
-        useTextDefault = false,
-        size = size,
-        cornerRadius = cornerRadius,
-        elevation = elevation,
-        clippingEnabled = clippingEnabled,
-        fishEyeEnabled = fishEyeEnabled,
-    )
-
-    /**
-     * Returns true if this style is supported by this version of the platform.
-     * When false is returned, it may be either because the [Magnifier] widget is not supported at
-     * all because the platform is too old, or because a particular style flag (e.g.
-     * [fishEyeEnabled]) is not supported on the current platform.
-     * [Default] and [TextDefault] styles are supported on all platforms with SDK version 28 and
-     * higher.
-     */
-    val isSupported: Boolean
-        get() = isStyleSupported(this)
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is MagnifierStyle) return false
-
-        if (useTextDefault != other.useTextDefault) return false
-        if (size != other.size) return false
-        if (cornerRadius != other.cornerRadius) return false
-        if (elevation != other.elevation) return false
-        if (clippingEnabled != other.clippingEnabled) return false
-        if (fishEyeEnabled != other.fishEyeEnabled) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = useTextDefault.hashCode()
-        result = 31 * result + size.hashCode()
-        result = 31 * result + cornerRadius.hashCode()
-        result = 31 * result + elevation.hashCode()
-        result = 31 * result + clippingEnabled.hashCode()
-        result = 31 * result + fishEyeEnabled.hashCode()
-        return result
-    }
-
-    override fun toString(): String {
-        return if (useTextDefault) {
-            "MagnifierStyle.TextDefault"
-        } else {
-            "MagnifierStyle(" +
-                "size=$size, " +
-                "cornerRadius=$cornerRadius, " +
-                "elevation=$elevation, " +
-                "clippingEnabled=$clippingEnabled, " +
-                "fishEyeEnabled=$fishEyeEnabled" +
-                ")"
-        }
-    }
-
-    companion object {
-        /** A [MagnifierStyle] with all default values. */
-        @ExperimentalFoundationApi
-        val Default = MagnifierStyle()
-
-        /**
-         * A [MagnifierStyle] that uses the system defaults for text magnification.
-         *
-         * Different versions of Android may use different magnifier styles for magnifying text, so
-         * using this configuration ensures that the correct style is used to match the system.
-         */
-        @ExperimentalFoundationApi
-        val TextDefault = MagnifierStyle(
-            useTextDefault = true,
-            size = Default.size,
-            cornerRadius = Default.cornerRadius,
-            elevation = Default.elevation,
-            clippingEnabled = Default.clippingEnabled,
-            fishEyeEnabled = Default.fishEyeEnabled,
-        )
-
-        internal fun isStyleSupported(
-            style: MagnifierStyle,
-            sdkVersion: Int = Build.VERSION.SDK_INT
-        ): Boolean {
-            return if (!isPlatformMagnifierSupported(sdkVersion)) {
-                // Older platform versions don't support magnifier at all.
-                false
-            } else if (style.fishEyeEnabled) {
-                // TODO(b/202451044) Add fisheye support once platform APIs are exposed.
-                false
-            } else if (style.useTextDefault || style == Default) {
-                // Default styles are always available on all platforms that support magnifier.
-                true
-            } else {
-                // Custom styles aren't supported on API 28.
-                sdkVersion >= 29
-            }
-        }
-    }
-}
-
-/**
  * Shows a [Magnifier] widget that shows an enlarged version of the content at [sourceCenter]
  * relative to the current layout node.
  *
  * This function returns a no-op modifier on API levels below P (28), since the framework does not
  * support the [Magnifier] widget on those levels. However, even on higher API levels, not all
- * magnifier features are supported on all platforms. To check whether a given [MagnifierStyle] is
- * supported by the current platform, check the [MagnifierStyle.isSupported] property.
+ * magnifier features are supported on all platforms. Please refer to parameter explanations below
+ * to learn more about supported features on different platform versions.
  *
  * This function does not allow configuration of [source bounds][Magnifier.Builder.setSourceBounds]
  * since the magnifier widget does not support constraining to the bounds of composables.
@@ -215,29 +82,69 @@
  * the layout node this modifier is applied to. If [unspecified][DpOffset.Unspecified], the
  * magnifier widget will be placed at a default offset relative to [sourceCenter]. The value of that
  * offset is specified by the system.
- * @param zoom See [Magnifier.setZoom]. Not supported on SDK levels < Q.
- * @param style The [MagnifierStyle] to use to configure the magnifier widget.
  * @param onSizeChanged An optional callback that will be invoked when the magnifier widget is
- * initialized to report on its actual size. This can be useful if one of the default
- * [MagnifierStyle]s is used to find out what size the system decided to use for the widget.
+ * initialized to report on its actual size. This can be useful when [size] parameter is left
+ * unspecified.
+ * @param zoom See [Magnifier.setZoom]. Only supported on API 29+.
+ * @param size See [Magnifier.Builder.setSize]. Only supported on API 29+.
+ * @param cornerRadius See [Magnifier.Builder.setCornerRadius]. Only supported on API 29+.
+ * @param elevation See [Magnifier.Builder.setElevation]. Only supported on API 29+.
+ * @param clippingEnabled See [Magnifier.Builder.setClippingEnabled]. Only supported on API 29+.
  */
-@ExperimentalFoundationApi
 fun Modifier.magnifier(
     sourceCenter: Density.() -> Offset,
     magnifierCenter: Density.() -> Offset = { Offset.Unspecified },
+    onSizeChanged: ((DpSize) -> Unit)? = null,
     zoom: Float = Float.NaN,
-    style: MagnifierStyle = MagnifierStyle.Default,
-    onSizeChanged: ((DpSize) -> Unit)? = null
+    size: DpSize = DpSize.Unspecified,
+    cornerRadius: Dp = Dp.Unspecified,
+    elevation: Dp = Dp.Unspecified,
+    clippingEnabled: Boolean = true
+): Modifier {
+    return magnifier(
+        sourceCenter = sourceCenter,
+        magnifierCenter = magnifierCenter,
+        onSizeChanged = onSizeChanged,
+        zoom = zoom,
+        useTextDefault = false,
+        size = size,
+        cornerRadius = cornerRadius,
+        elevation = elevation,
+        clippingEnabled = clippingEnabled
+    )
+}
+
+/**
+ * For testing and internal Text usage purposes.
+ *
+ * TextField and SelectionManager uses this internal API to pass `useTextDefault` as true.
+ */
+internal fun Modifier.magnifier(
+    sourceCenter: Density.() -> Offset,
+    magnifierCenter: Density.() -> Offset = { Offset.Unspecified },
+    onSizeChanged: ((DpSize) -> Unit)? = null,
+    zoom: Float = Float.NaN,
+    useTextDefault: Boolean = false,
+    size: DpSize = DpSize.Unspecified,
+    cornerRadius: Dp = Dp.Unspecified,
+    elevation: Dp = Dp.Unspecified,
+    clippingEnabled: Boolean = true,
+    platformMagnifierFactory: PlatformMagnifierFactory? = null
 ): Modifier {
     return if (isPlatformMagnifierSupported()) {
         then(
             MagnifierElement(
                 sourceCenter = sourceCenter,
                 magnifierCenter = magnifierCenter,
-                zoom = zoom,
-                style = style,
                 onSizeChanged = onSizeChanged,
-                platformMagnifierFactory = PlatformMagnifierFactory.getForCurrentPlatform()
+                useTextDefault = useTextDefault,
+                zoom = zoom,
+                size = size,
+                cornerRadius = cornerRadius,
+                elevation = elevation,
+                clippingEnabled = clippingEnabled,
+                platformMagnifierFactory = platformMagnifierFactory
+                    ?: PlatformMagnifierFactory.getForCurrentPlatform() // this doesn't do an alloc
             )
         )
     } else {
@@ -251,50 +158,25 @@
                 properties["sourceCenter"] = sourceCenter
                 properties["magnifierCenter"] = magnifierCenter
                 properties["zoom"] = zoom
-                properties["style"] = style
+                properties["size"] = size
+                properties["cornerRadius"] = cornerRadius
+                properties["elevation"] = elevation
+                properties["clippingEnabled"] = clippingEnabled
             }
         ) { this }
     }
 }
 
-/**
- * For testing purposes.
- */
-@OptIn(ExperimentalFoundationApi::class)
-internal fun Modifier.magnifier(
-    sourceCenter: Density.() -> Offset,
-    magnifierCenter: Density.() -> Offset = { Offset.Unspecified },
-    zoom: Float = Float.NaN,
-    style: MagnifierStyle = MagnifierStyle.Default,
-    onSizeChanged: ((DpSize) -> Unit)? = null,
-    platformMagnifierFactory: PlatformMagnifierFactory
-): Modifier {
-    return if (isPlatformMagnifierSupported()) {
-        then(
-            MagnifierElement(
-                sourceCenter = sourceCenter,
-                magnifierCenter = magnifierCenter,
-                zoom = zoom,
-                style = style,
-                onSizeChanged = onSizeChanged,
-                platformMagnifierFactory = platformMagnifierFactory
-            )
-        )
-    } else {
-        // Magnifier is only supported in >=28. So avoid doing all the work to manage the magnifier
-        // state if it's not needed.
-        // TODO(b/202739980) Investigate supporting Magnifier on earlier versions.
-        Modifier
-    }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
 internal class MagnifierElement(
     private val sourceCenter: Density.() -> Offset,
     private val magnifierCenter: Density.() -> Offset = { Offset.Unspecified },
-    private val zoom: Float = Float.NaN,
-    private val style: MagnifierStyle = MagnifierStyle.Default,
     private val onSizeChanged: ((DpSize) -> Unit)? = null,
+    private val zoom: Float = Float.NaN,
+    private val useTextDefault: Boolean = false,
+    private val size: DpSize = DpSize.Unspecified,
+    private val cornerRadius: Dp = Dp.Unspecified,
+    private val elevation: Dp = Dp.Unspecified,
+    private val clippingEnabled: Boolean = true,
     private val platformMagnifierFactory: PlatformMagnifierFactory
 ) : ModifierNodeElement<MagnifierNode>() {
 
@@ -303,7 +185,11 @@
             sourceCenter = sourceCenter,
             magnifierCenter = magnifierCenter,
             zoom = zoom,
-            style = style,
+            useTextDefault = useTextDefault,
+            size = size,
+            cornerRadius = cornerRadius,
+            elevation = elevation,
+            clippingEnabled = clippingEnabled,
             onSizeChanged = onSizeChanged,
             platformMagnifierFactory = platformMagnifierFactory
         )
@@ -314,7 +200,11 @@
             sourceCenter = sourceCenter,
             magnifierCenter = magnifierCenter,
             zoom = zoom,
-            style = style,
+            useTextDefault = useTextDefault,
+            size = size,
+            cornerRadius = cornerRadius,
+            elevation = elevation,
+            clippingEnabled = clippingEnabled,
             onSizeChanged = onSizeChanged,
             platformMagnifierFactory = platformMagnifierFactory
         )
@@ -327,7 +217,11 @@
         if (sourceCenter != other.sourceCenter) return false
         if (magnifierCenter != other.magnifierCenter) return false
         if (zoom != other.zoom) return false
-        if (style != other.style) return false
+        if (useTextDefault != other.useTextDefault) return false
+        if (size != other.size) return false
+        if (cornerRadius != other.cornerRadius) return false
+        if (elevation != other.elevation) return false
+        if (clippingEnabled != other.clippingEnabled) return false
         if (onSizeChanged != other.onSizeChanged) return false
         if (platformMagnifierFactory != other.platformMagnifierFactory) return false
 
@@ -338,7 +232,11 @@
         var result = sourceCenter.hashCode()
         result = 31 * result + magnifierCenter.hashCode()
         result = 31 * result + zoom.hashCode()
-        result = 31 * result + style.hashCode()
+        result = 31 * result + useTextDefault.hashCode()
+        result = 31 * result + size.hashCode()
+        result = 31 * result + cornerRadius.hashCode()
+        result = 31 * result + elevation.hashCode()
+        result = 31 * result + clippingEnabled.hashCode()
         result = 31 * result + (onSizeChanged?.hashCode() ?: 0)
         result = 31 * result + platformMagnifierFactory.hashCode()
         return result
@@ -349,17 +247,23 @@
         properties["sourceCenter"] = sourceCenter
         properties["magnifierCenter"] = magnifierCenter
         properties["zoom"] = zoom
-        properties["style"] = style
+        properties["size"] = size
+        properties["cornerRadius"] = cornerRadius
+        properties["elevation"] = elevation
+        properties["clippingEnabled"] = clippingEnabled
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 internal class MagnifierNode(
     var sourceCenter: Density.() -> Offset,
     var magnifierCenter: Density.() -> Offset = { Offset.Unspecified },
-    var zoom: Float = Float.NaN,
-    var style: MagnifierStyle = MagnifierStyle.Default,
     var onSizeChanged: ((DpSize) -> Unit)? = null,
+    var zoom: Float = Float.NaN,
+    var useTextDefault: Boolean = false,
+    var size: DpSize = DpSize.Unspecified,
+    var cornerRadius: Dp = Dp.Unspecified,
+    var elevation: Dp = Dp.Unspecified,
+    var clippingEnabled: Boolean = true,
     var platformMagnifierFactory: PlatformMagnifierFactory =
         PlatformMagnifierFactory.getForCurrentPlatform()
 ) : Modifier.Node(),
@@ -406,18 +310,29 @@
         sourceCenter: Density.() -> Offset,
         magnifierCenter: Density.() -> Offset,
         zoom: Float,
-        style: MagnifierStyle,
+        useTextDefault: Boolean,
+        size: DpSize,
+        cornerRadius: Dp,
+        elevation: Dp,
+        clippingEnabled: Boolean,
         onSizeChanged: ((DpSize) -> Unit)?,
         platformMagnifierFactory: PlatformMagnifierFactory
     ) {
         val previousZoom = this.zoom
-        val previousStyle = this.style
+        val previousSize = this.size
+        val previousCornerRadius = this.cornerRadius
+        val previousElevation = this.elevation
+        val previousClippingEnabled = this.clippingEnabled
         val previousPlatformMagnifierFactory = this.platformMagnifierFactory
 
         this.sourceCenter = sourceCenter
         this.magnifierCenter = magnifierCenter
         this.zoom = zoom
-        this.style = style
+        this.useTextDefault = useTextDefault
+        this.size = size
+        this.cornerRadius = cornerRadius
+        this.elevation = elevation
+        this.clippingEnabled = clippingEnabled
         this.onSizeChanged = onSizeChanged
         this.platformMagnifierFactory = platformMagnifierFactory
 
@@ -425,10 +340,15 @@
         // if the zoom changes between recompositions we don't need to recreate the magnifier. On
         // older platforms, the zoom can only be set initially, so we use the zoom itself as a key
         // so the magnifier gets recreated if it changes.
-        if (magnifier == null ||
+        if (
+            magnifier == null ||
             (zoom != previousZoom && !platformMagnifierFactory.canUpdateZoom) ||
-            style != previousStyle ||
-            platformMagnifierFactory != previousPlatformMagnifierFactory) {
+            size != previousSize ||
+            cornerRadius != previousCornerRadius ||
+            elevation != previousElevation ||
+            clippingEnabled != previousClippingEnabled ||
+            platformMagnifierFactory != previousPlatformMagnifierFactory
+        ) {
             recreateMagnifier()
         }
         updateMagnifier()
@@ -462,7 +382,16 @@
         magnifier?.dismiss()
         val view = view ?: return
         val density = density ?: return
-        magnifier = platformMagnifierFactory.create(style, view, density, zoom)
+        magnifier = platformMagnifierFactory.create(
+            view = view,
+            useTextDefault = useTextDefault,
+            size = size,
+            cornerRadius = cornerRadius,
+            elevation = elevation,
+            clippingEnabled = clippingEnabled,
+            density = density,
+            initialZoom = zoom
+        )
         updateSizeIfNecessary()
     }
 
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/PlatformMagnifier.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/PlatformMagnifier.kt
index 91f6658..86ce694 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/PlatformMagnifier.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/PlatformMagnifier.kt
@@ -24,6 +24,8 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.isSpecified
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.IntSize
 import kotlin.math.roundToInt
 
@@ -37,10 +39,13 @@
      */
     val canUpdateZoom: Boolean
 
-    @OptIn(ExperimentalFoundationApi::class)
     fun create(
-        style: MagnifierStyle,
         view: View,
+        useTextDefault: Boolean,
+        size: DpSize,
+        cornerRadius: Dp,
+        elevation: Dp,
+        clippingEnabled: Boolean,
         density: Density,
         initialZoom: Float
     ): PlatformMagnifier
@@ -90,10 +95,13 @@
     override val canUpdateZoom: Boolean = false
 
     @Suppress("DEPRECATION")
-    @OptIn(ExperimentalFoundationApi::class)
     override fun create(
-        style: MagnifierStyle,
         view: View,
+        useTextDefault: Boolean,
+        size: DpSize,
+        cornerRadius: Dp,
+        elevation: Dp,
+        clippingEnabled: Boolean,
         density: Density,
         initialZoom: Float
     ): PlatformMagnifierImpl = PlatformMagnifierImpl(Magnifier(view))
@@ -126,35 +134,45 @@
 internal object PlatformMagnifierFactoryApi29Impl : PlatformMagnifierFactory {
     override val canUpdateZoom: Boolean = true
 
-    @OptIn(ExperimentalFoundationApi::class)
     override fun create(
-        style: MagnifierStyle,
         view: View,
+        useTextDefault: Boolean,
+        size: DpSize,
+        cornerRadius: Dp,
+        elevation: Dp,
+        clippingEnabled: Boolean,
         density: Density,
         initialZoom: Float
     ): PlatformMagnifierImpl {
         with(density) {
             // TODO write test for this branch
-            if (style == MagnifierStyle.TextDefault) {
+            if (useTextDefault) {
                 // This deprecated constructor is the only public API to create a Magnifier that
                 // uses the system text magnifier defaults.
                 @Suppress("DEPRECATION")
                 return PlatformMagnifierImpl(Magnifier(view))
             }
 
-            val size = style.size.toSize()
-            val cornerRadius = style.cornerRadius.toPx()
-            val elevation = style.elevation.toPx()
+            val pixelSize = size.toSize()
+            val pixelCornerRadius = cornerRadius.toPx()
+            val pixelElevation = elevation.toPx()
 
             // When Builder properties are not specified, the widget uses different defaults than it
             // does for the non-builder constructor above.
             val magnifier = Magnifier.Builder(view).run {
-                if (size.isSpecified) setSize(size.width.roundToInt(), size.height.roundToInt())
-                if (!cornerRadius.isNaN()) setCornerRadius(cornerRadius)
-                if (!elevation.isNaN()) setElevation(elevation)
-                if (!initialZoom.isNaN()) setInitialZoom(initialZoom)
-                setClippingEnabled(style.clippingEnabled)
-                // TODO(b/202451044) Support setting fisheye style.
+                if (pixelSize.isSpecified) {
+                    setSize(pixelSize.width.roundToInt(), pixelSize.height.roundToInt())
+                }
+                if (!pixelCornerRadius.isNaN()) {
+                    setCornerRadius(pixelCornerRadius)
+                }
+                if (!pixelElevation.isNaN()) {
+                    setElevation(pixelElevation)
+                }
+                if (!initialZoom.isNaN()) {
+                    setInitialZoom(initialZoom)
+                }
+                setClippingEnabled(clippingEnabled)
                 build()
             }
 
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 cfac198..23e6c8e 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
@@ -16,8 +16,8 @@
 
 package androidx.compose.foundation.text.selection
 
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.MagnifierStyle
+import androidx.compose.foundation.PlatformMagnifierFactory
+import androidx.compose.foundation.isPlatformMagnifierSupported
 import androidx.compose.foundation.magnifier
 import androidx.compose.foundation.text.KeyCommand
 import androidx.compose.foundation.text.platformDefaultKeyMapping
@@ -36,10 +36,9 @@
 
 // We use composed{} to read a local, but don't provide inspector info because the underlying
 // magnifier modifier provides more meaningful inspector info.
-@OptIn(ExperimentalFoundationApi::class)
 internal actual fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier {
     // Avoid tracking animation state on older Android versions that don't support magnifiers.
-    if (!MagnifierStyle.TextDefault.isSupported) {
+    if (!isPlatformMagnifierSupported()) {
         return this
     }
 
@@ -59,7 +58,8 @@
                                 IntSize(size.width.roundToPx(), size.height.roundToPx())
                             }
                         },
-                        style = MagnifierStyle.TextDefault
+                        useTextDefault = true,
+                        platformMagnifierFactory = PlatformMagnifierFactory.getForCurrentPlatform()
                     )
             }
         )
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
index e750b21..c888776 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
@@ -17,7 +17,8 @@
 package androidx.compose.foundation.text.selection
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.MagnifierStyle
+import androidx.compose.foundation.PlatformMagnifierFactory
+import androidx.compose.foundation.isPlatformMagnifierSupported
 import androidx.compose.foundation.magnifier
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -37,7 +38,7 @@
 @OptIn(ExperimentalFoundationApi::class)
 internal actual fun Modifier.textFieldMagnifier(manager: TextFieldSelectionManager): Modifier {
     // Avoid tracking animation state on older Android versions that don't support magnifiers.
-    if (!MagnifierStyle.TextDefault.isSupported) {
+    if (!isPlatformMagnifierSupported()) {
         return this
     }
 
@@ -56,8 +57,8 @@
                             IntSize(size.width.roundToPx(), size.height.roundToPx())
                         }
                     },
-                    // TODO(b/202451044) Support fisheye magnifier for eloquent.
-                    style = MagnifierStyle.TextDefault
+                    useTextDefault = true,
+                    platformMagnifierFactory = PlatformMagnifierFactory.getForCurrentPlatform()
                 )
             }
         )
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSession.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSession.android.kt
index 671b253..58424db 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSession.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSession.android.kt
@@ -26,9 +26,7 @@
 import android.view.inputmethod.InputConnection
 import androidx.annotation.VisibleForTesting
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text2.input.InputTransformation
 import androidx.compose.foundation.text2.input.TextFieldCharSequence
-import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.ui.platform.PlatformTextInputSession
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.ImeOptions
@@ -39,7 +37,6 @@
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
 import org.jetbrains.annotations.TestOnly
 
 /** Enable to print logs during debugging, see [logDebug]. */
@@ -70,10 +67,10 @@
     inputConnectionInterceptor = interceptor
 }
 
+@OptIn(ExperimentalFoundationApi::class)
 internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSession(
-    state: TextFieldState,
+    state: TransformedTextFieldState,
     imeOptions: ImeOptions,
-    filter: InputTransformation?,
     onImeAction: ((ImeAction) -> Unit)?
 ): Nothing {
     val composeImm = ComposeInputMethodManager(view)
@@ -93,7 +90,9 @@
                     )
                 }
 
-                if (!old.contentEquals(new)) {
+                // No need to restart the IME if keyboard type is configured as Password. IME
+                // should not keep an internal input state if the content needs to be secured.
+                if (!old.contentEquals(new) && imeOptions.keyboardType != KeyboardType.Password) {
                     composeImm.restartInput()
                 }
             }
@@ -107,8 +106,7 @@
                     get() = state.text
 
                 override fun requestEdit(block: EditingBuffer.() -> Unit) {
-                    state.editAsUser(
-                        inputTransformation = filter,
+                    state.editUntransformedTextAsUser(
                         notifyImeOfChanges = false,
                         block = block
                     )
@@ -231,21 +229,6 @@
     this.imeOptions = this.imeOptions or EditorInfo.IME_FLAG_NO_FULLSCREEN
 }
 
-/**
- * Adds [notifyImeListener] to this [TextFieldState] and then suspends until cancelled, removing the
- * listener before continuing.
- */
-private suspend inline fun TextFieldState.collectImeNotifications(
-    notifyImeListener: TextFieldState.NotifyImeListener
-): Nothing {
-    suspendCancellableCoroutine<Nothing> { continuation ->
-        addNotifyImeListener(notifyImeListener)
-        continuation.invokeOnCancellation {
-            removeNotifyImeListener(notifyImeListener)
-        }
-    }
-}
-
 private fun hasFlag(bits: Int, flag: Int): Boolean = (bits and flag) == flag
 
 private fun logDebug(tag: String = TAG, content: () -> String) {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldKeyEventHandler.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldKeyEventHandler.android.kt
index 8a1a51d..2e48e57 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldKeyEventHandler.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldKeyEventHandler.android.kt
@@ -22,8 +22,6 @@
 import android.view.KeyEvent.KEYCODE_DPAD_LEFT
 import android.view.KeyEvent.KEYCODE_DPAD_RIGHT
 import android.view.KeyEvent.KEYCODE_DPAD_UP
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.foundation.text2.input.internal.selection.TextFieldSelectionState
 import androidx.compose.ui.focus.FocusDirection
 import androidx.compose.ui.focus.FocusManager
@@ -37,12 +35,11 @@
 internal actual fun createTextFieldKeyEventHandler(): TextFieldKeyEventHandler =
     AndroidTextFieldKeyEventHandler()
 
-@OptIn(ExperimentalFoundationApi::class)
 internal class AndroidTextFieldKeyEventHandler : TextFieldKeyEventHandler() {
 
     override fun onPreKeyEvent(
         event: KeyEvent,
-        textFieldState: TextFieldState,
+        textFieldState: TransformedTextFieldState,
         textFieldSelectionState: TextFieldSelectionState,
         focusManager: FocusManager,
         keyboardController: SoftwareKeyboardController
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/AndroidTextFieldMagnifier.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/AndroidTextFieldMagnifier.kt
index 9edd02f..27250b3 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/AndroidTextFieldMagnifier.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/AndroidTextFieldMagnifier.kt
@@ -20,13 +20,12 @@
 import androidx.compose.animation.core.Animatable
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.MagnifierNode
-import androidx.compose.foundation.MagnifierStyle
 import androidx.compose.foundation.isPlatformMagnifierSupported
 import androidx.compose.foundation.text.selection.MagnifierSpringSpec
 import androidx.compose.foundation.text.selection.OffsetDisplacementThreshold
 import androidx.compose.foundation.text.selection.UnspecifiedSafeOffsetVectorConverter
-import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.foundation.text2.input.internal.TextLayoutState
+import androidx.compose.foundation.text2.input.internal.TransformedTextFieldState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
@@ -43,8 +42,8 @@
 import kotlinx.coroutines.launch
 
 @OptIn(ExperimentalFoundationApi::class)
-internal class TextFieldMagnifierNodeImpl28 constructor(
-    private var textFieldState: TextFieldState,
+internal class TextFieldMagnifierNodeImpl28(
+    private var textFieldState: TransformedTextFieldState,
     private var textFieldSelectionState: TextFieldSelectionState,
     private var textLayoutState: TextLayoutState,
     private var isFocused: Boolean
@@ -73,8 +72,7 @@
                     IntSize(size.width.roundToPx(), size.height.roundToPx())
                 }
             },
-            // TODO(b/202451044) Support fisheye magnifier for eloquent.
-            style = MagnifierStyle.TextDefault
+            useTextDefault = true
         )
     )
 
@@ -85,7 +83,7 @@
     }
 
     override fun update(
-        textFieldState: TextFieldState,
+        textFieldState: TransformedTextFieldState,
         textFieldSelectionState: TextFieldSelectionState,
         textLayoutState: TextLayoutState,
         isFocused: Boolean
@@ -114,7 +112,7 @@
         animationJob = null
         // never start an expensive animation job if we do not have focus or
         // magnifier is not supported.
-        if (!isFocused || !MagnifierStyle.TextDefault.isSupported) return
+        if (!isFocused || !isPlatformMagnifierSupported()) return
         animationJob = coroutineScope.launch {
             val animationScope = this
             snapshotFlow {
@@ -170,9 +168,8 @@
  * whether magnifier is supported.
  */
 @SuppressLint("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
-@OptIn(ExperimentalFoundationApi::class)
 internal actual fun textFieldMagnifierNode(
-    textFieldState: TextFieldState,
+    textFieldState: TransformedTextFieldState,
     textFieldSelectionState: TextFieldSelectionState,
     textLayoutState: TextLayoutState,
     isFocused: Boolean
@@ -187,7 +184,7 @@
     } else {
         object : TextFieldMagnifierNode() {
             override fun update(
-                textFieldState: TextFieldState,
+                textFieldState: TransformedTextFieldState,
                 textFieldSelectionState: TextFieldSelectionState,
                 textLayoutState: TextLayoutState,
                 isFocused: Boolean
diff --git a/compose/foundation/foundation/src/androidMain/res/values-af/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-af/strings.xml
new file mode 100644
index 0000000..3e16881
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-af/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"nutswenk"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"wys nutswenk"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-am/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-am/strings.xml
new file mode 100644
index 0000000..f5faadd
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-am/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"መሣሪያ ጥቆማ"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"መሣሪያ ጥቆማን አሳይ"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ar/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ar/strings.xml
new file mode 100644
index 0000000..b335327
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ar/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"تلميح"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"إظهار التلميح"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-as/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-as/strings.xml
new file mode 100644
index 0000000..1465db9
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-as/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"টুলটিপ"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"টুলটিপ দেখুৱাওক"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-az/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-az/strings.xml
new file mode 100644
index 0000000..3a1a1b9
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-az/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"alət izahı"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"alət izahını göstərin"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-b+sr+Latn/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..0486489
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"objašnjenje"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"prikaži objašnjenje"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-be/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-be/strings.xml
new file mode 100644
index 0000000..b9c7e47
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-be/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"падказка"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"паказаць усплывальную падказку"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-bg/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-bg/strings.xml
new file mode 100644
index 0000000..f28bfc1
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-bg/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"подсказка"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"показване на подсказка"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-bn/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-bn/strings.xml
new file mode 100644
index 0000000..3c52482
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-bn/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"টুলটিপ"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"টুলটিপ দেখুন"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-bs/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-bs/strings.xml
new file mode 100644
index 0000000..ae6e561
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-bs/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"skočni opis"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"prikaži skočni opis"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ca/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ca/strings.xml
new file mode 100644
index 0000000..24b583d
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ca/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"descripció emergent"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"mostra la descripció emergent"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-cs/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-cs/strings.xml
new file mode 100644
index 0000000..521a2d7
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-cs/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"popisek"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"zobrazit popisek"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-da/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-da/strings.xml
new file mode 100644
index 0000000..0326ad43
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-da/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"værktøjstip"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"se værktøjstip"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-de/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-de/strings.xml
new file mode 100644
index 0000000..ef00062
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-de/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"Kurzinfo"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"Kurzinfo anzeigen"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-el/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-el/strings.xml
new file mode 100644
index 0000000..1adc933
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-el/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"επεξήγηση εργαλείου"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"εμφάνιση επεξήγησης εργαλείου"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-en-rAU/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..2cbf75c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-en-rAU/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"tooltip"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"show tooltip"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-en-rCA/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..2cbf75c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-en-rCA/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"tooltip"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"show tooltip"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-en-rGB/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..2cbf75c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-en-rGB/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"tooltip"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"show tooltip"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-en-rIN/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..2cbf75c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-en-rIN/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"tooltip"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"show tooltip"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-en-rXC/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..24c5541a
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-en-rXC/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‎‎‏‎‎‎‎‎‏‏‏‎‏‏‏‎‎‏‎‎‎‎‏‏‎‏‏‏‎‎‎‎‏‎‏‎‏‎‏‏‎‎‎‏‎‎‎‎‏‏‎‏‎‏‎‏‏‎tooltip‎‏‎‎‏‎"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‏‏‎‏‎‏‏‏‎‏‎‏‎‎‏‏‏‏‎‎‎‎‎‏‎‎‎‎‏‎‎‏‏‏‎‎‎‏‏‏‏‎‏‏‎‏‎‎‏‏‏‏‏‎‏‎‎‎‎show tooltip‎‏‎‎‏‎"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-es-rUS/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..c887b53
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-es-rUS/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"cuadro de información"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"mostrar cuadro de información"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-es/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-es/strings.xml
new file mode 100644
index 0000000..6115bee
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-es/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"descripción emergente"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"mostrar descripción emergente"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-et/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-et/strings.xml
new file mode 100644
index 0000000..eccb264
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-et/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"kohtspikker"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"kuva kohtspikker"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-eu/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-eu/strings.xml
new file mode 100644
index 0000000..0950e7f
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-eu/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"aholkua"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"erakutsi aholkua"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-fa/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-fa/strings.xml
new file mode 100644
index 0000000..413efba
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-fa/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"نکته‌ابزار"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"نمایش نکته‌ابزار"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-fi/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-fi/strings.xml
new file mode 100644
index 0000000..6326d6d
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-fi/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"vihjeteksti"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"näytä vihjeteksti"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-fr-rCA/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..592d87f
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-fr-rCA/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"infobulle"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"afficher une infobulle"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-fr/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-fr/strings.xml
new file mode 100644
index 0000000..81bfcff
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-fr/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"Info-bulle"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"Afficher l\'info-bulle"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-gl/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-gl/strings.xml
new file mode 100644
index 0000000..35fa851
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-gl/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"cadro de información"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"mostrar cadro de información"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-gu/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-gu/strings.xml
new file mode 100644
index 0000000..f52583d
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-gu/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"ટૂલટિપ"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"ટૂલટિપ બતાવો"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-hi/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-hi/strings.xml
new file mode 100644
index 0000000..56436fe
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-hi/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"टूलटिप"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"टूलटिप दिखाएं"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-hr/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-hr/strings.xml
new file mode 100644
index 0000000..b670c78
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-hr/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"opis"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"prikaži opis"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-hu/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-hu/strings.xml
new file mode 100644
index 0000000..9fb835c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-hu/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"elemleírás"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"elemleírás megjelenítése"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-hy/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-hy/strings.xml
new file mode 100644
index 0000000..b656c32
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-hy/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"հուշակ"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"ցուցադրել հուշակ"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-in/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-in/strings.xml
new file mode 100644
index 0000000..41cbfde
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-in/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"tooltip"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"tampilkan tooltip"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-is/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-is/strings.xml
new file mode 100644
index 0000000..741c33a
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-is/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"ábending"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"sýna ábendingu"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-it/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-it/strings.xml
new file mode 100644
index 0000000..77b02a1
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-it/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"Descrizione comando"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"Mostra descrizione comando"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-iw/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-iw/strings.xml
new file mode 100644
index 0000000..11dddc3
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-iw/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"הסבר קצר"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"הצגת ההסבר הקצר"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ja/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ja/strings.xml
new file mode 100644
index 0000000..c9e4b933f
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ja/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"ツールチップ"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"ツールチップを表示"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ka/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ka/strings.xml
new file mode 100644
index 0000000..e75c49a
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ka/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"მინიშნება"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"მინიშნების ჩვენება"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-kk/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-kk/strings.xml
new file mode 100644
index 0000000..1692d94
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-kk/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"қалқыма көмек"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"қалқыма көмекті көрсету"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-km/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-km/strings.xml
new file mode 100644
index 0000000..52ea3bf
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-km/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"កំណត់​ពន្យល់"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"បង្ហាញ​កំណត់​ពន្យល់"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-kn/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-kn/strings.xml
new file mode 100644
index 0000000..b269a2b
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-kn/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"ಟೂಲ್‌ಟಿಪ್"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"ಟೂಲ್‌ಟಿಪ್ ಅನ್ನು ತೋರಿಸಿ"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ko/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ko/strings.xml
new file mode 100644
index 0000000..1459c94
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ko/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"도움말"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"도움말 표시"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ky/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ky/strings.xml
new file mode 100644
index 0000000..4fe83e49
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ky/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"калкып чыгуучу кеңеш"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"калкып чыгуучу кеңешти көрсөтүү"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-lo/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-lo/strings.xml
new file mode 100644
index 0000000..5f5a0fb
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-lo/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"ຄຳແນະນຳ"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"ສະແດງຄຳແນະນຳ"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-lt/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-lt/strings.xml
new file mode 100644
index 0000000..a9ea489
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-lt/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"patarimas"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"rodyti patarimą"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-lv/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-lv/strings.xml
new file mode 100644
index 0000000..260837f
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-lv/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"rīka padoms"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"rādīt rīka padomu"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-mk/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-mk/strings.xml
new file mode 100644
index 0000000..5adbad2
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-mk/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"совет за алатка"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"прикажи совет за алатка"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ml/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ml/strings.xml
new file mode 100644
index 0000000..cbc76bb
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ml/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"ടൂൾടിപ്പ്"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"ടൂൾടിപ്പ് കാണിക്കുക"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-mn/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-mn/strings.xml
new file mode 100644
index 0000000..0a2d354
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-mn/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"зөвлөмж"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"зөвлөмж харуулах"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-mr/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-mr/strings.xml
new file mode 100644
index 0000000..9770093
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-mr/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"टूलटिप"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"टूलटिप दाखवा"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ms/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ms/strings.xml
new file mode 100644
index 0000000..d1caacc
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ms/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"tip alat"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"tunjukkan tip alat"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-my/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-my/strings.xml
new file mode 100644
index 0000000..ac6d496
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-my/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"အကြံပြုချက်ပြ ပေါ့အပ် ဝင်းဒိုး"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"အကြံပြုချက်ပြ ပေါ့အပ်ဝင်းဒိုး ပြရန်"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-nb/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-nb/strings.xml
new file mode 100644
index 0000000..122538f
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-nb/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"verktøytips"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"vis verktøytips"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ne/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ne/strings.xml
new file mode 100644
index 0000000..38911d6
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ne/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"टुलटिप"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"टुलटिप देखाइयोस्"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-nl/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-nl/strings.xml
new file mode 100644
index 0000000..e465e96
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-nl/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"tooltip"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"tooltip tonen"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-or/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-or/strings.xml
new file mode 100644
index 0000000..d19d550
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-or/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"ଟୁଲଟିପ"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"ଟୁଲଟିପ ଦେଖାନ୍ତୁ"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-pa/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-pa/strings.xml
new file mode 100644
index 0000000..e20db2a
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-pa/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"ਟੂਲ-ਟਿੱਪ"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"ਟੂਲ-ਟਿੱਪ ਦਿਖਾਓ"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-pl/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-pl/strings.xml
new file mode 100644
index 0000000..501fbbf
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-pl/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"etykietka"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"pokaż etykietkę"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-pt-rBR/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..7495f03
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-pt-rBR/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"dica"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"mostrar dica"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-pt-rPT/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..3d32ef4
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-pt-rPT/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"sugestão"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"mostrar sugestão"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-pt/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-pt/strings.xml
new file mode 100644
index 0000000..7495f03
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-pt/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"dica"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"mostrar dica"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ro/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ro/strings.xml
new file mode 100644
index 0000000..9667645
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ro/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"balon explicativ"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"afișează balonul explicativ"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ru/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ru/strings.xml
new file mode 100644
index 0000000..fbe1ad9
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ru/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"Подсказка"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"Показать подсказку"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-si/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-si/strings.xml
new file mode 100644
index 0000000..631ab52
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-si/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"මෙවලම් ඉඟිය"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"මෙවලම් ඉඟිය පෙන්වන්න"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-sk/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-sk/strings.xml
new file mode 100644
index 0000000..9608516
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-sk/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"popis"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"zobraziť popis"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-sl/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-sl/strings.xml
new file mode 100644
index 0000000..bef1544
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-sl/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"opis orodja"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"pokaži opis orodja"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-sq/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-sq/strings.xml
new file mode 100644
index 0000000..9399161
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-sq/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"këshillë për veglën"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"shfaq këshillën për veglën"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-sr/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-sr/strings.xml
new file mode 100644
index 0000000..75af0ed
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-sr/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"објашњење"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"прикажи објашњење"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-sv/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-sv/strings.xml
new file mode 100644
index 0000000..c19f7920
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-sv/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"beskrivning"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"visa beskrivning"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-sw/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-sw/strings.xml
new file mode 100644
index 0000000..d490050
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-sw/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"kidirisha cha vidokezo"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"onyesha kidirisha cha vidokezo"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ta/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ta/strings.xml
new file mode 100644
index 0000000..e2f25cc
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ta/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"உதவிக்குறிப்பு"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"உதவிக்குறிப்பைக் காட்டும்"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-te/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-te/strings.xml
new file mode 100644
index 0000000..7966cb2
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-te/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"టూల్‌టిప్"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"టూల్‌టిప్‌ను చూపించండి"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-th/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-th/strings.xml
new file mode 100644
index 0000000..e3394d1
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-th/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"เคล็ดลับเครื่องมือ"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"แสดงเคล็ดลับเครื่องมือ"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-tl/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-tl/strings.xml
new file mode 100644
index 0000000..0bc2d13
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-tl/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"tooltip"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"ipakita ang tooltip"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-tr/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-tr/strings.xml
new file mode 100644
index 0000000..94a36c6
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-tr/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"ipucu"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"ipucunu göster"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-uk/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-uk/strings.xml
new file mode 100644
index 0000000..57c494a
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-uk/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"спливаюча підказка"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"показати спливаючу підказку"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-ur/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-ur/strings.xml
new file mode 100644
index 0000000..bc52b15
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-ur/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"ٹول ٹپ"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"ٹول ٹپ دکھائیں"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-uz/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-uz/strings.xml
new file mode 100644
index 0000000..f3f79ad
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-uz/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"qalqib chiquvchi maslahat oynasi"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"maslahat oynasini koʻrsatish"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-vi/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-vi/strings.xml
new file mode 100644
index 0000000..ac56a55
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-vi/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"chú thích"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"hiện chú thích"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-zh-rCN/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..a61906c4
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-zh-rCN/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"提示"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"显示提示"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-zh-rHK/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..1d49506
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-zh-rHK/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"提示"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"顯示提示"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-zh-rTW/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..b54a809
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-zh-rTW/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"工具提示"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"顯示工具提示"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/androidMain/res/values-zu/strings.xml b/compose/foundation/foundation/src/androidMain/res/values-zu/strings.xml
new file mode 100644
index 0000000..abc4159
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/res/values-zu/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="tooltip_description" msgid="3765533235322692011">"ithulithiphu"</string>
+    <string name="tooltip_label" msgid="3124740595719787496">"bonisa ithulithiphu"</string>
+</resources>
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/MutatorMutexTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/MutatorMutexTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/MutatorMutexTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/MutatorMutexTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/BringIntoViewRequestPriorityQueueTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/gestures/BringIntoViewRequestPriorityQueueTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/BringIntoViewRequestPriorityQueueTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/gestures/BringIntoViewRequestPriorityQueueTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DraggableAnchorsTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/gestures/DraggableAnchorsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DraggableAnchorsTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/gestures/DraggableAnchorsTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/UpdatableAnimationStateTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/gestures/UpdatableAnimationStateTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/UpdatableAnimationStateTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/gestures/UpdatableAnimationStateTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/lazy/MutableIntervalListTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/lazy/MutableIntervalListTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/lazy/MutableIntervalListTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/lazy/MutableIntervalListTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/BasicTextPaparazziTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/BasicTextPaparazziTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/BasicTextPaparazziTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/BasicTextPaparazziTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/KeyboardOptionsTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/KeyboardOptionsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/KeyboardOptionsTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/KeyboardOptionsTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextDelegateTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/TextDelegateTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextDelegateTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/TextDelegateTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldBringIntoViewTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/TextFieldBringIntoViewTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldBringIntoViewTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/TextFieldBringIntoViewTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldScrollerPositionTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/TextFieldScrollerPositionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldScrollerPositionTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/TextFieldScrollerPositionTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldStateTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/TextFieldStateTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldStateTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/TextFieldStateTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextLayoutHelperTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/TextLayoutHelperTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextLayoutHelperTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/TextLayoutHelperTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/UndoManagerTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/UndoManagerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/UndoManagerTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/UndoManagerTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/ValidatingOffsetMappingTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/ValidatingOffsetMappingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/ValidatingOffsetMappingTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/ValidatingOffsetMappingTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/InlineAnswer.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/InlineAnswer.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/InlineAnswer.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/InlineAnswer.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/MockCoordinates.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/MockCoordinates.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/MockCoordinates.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/MockCoordinates.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectableInfoTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectableInfoTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectableInfoTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectableInfoTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustmentTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustmentTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustmentTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustmentTest.kt
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
new file mode 100644
index 0000000..df963b9
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt
@@ -0,0 +1,352 @@
+/*
+ * 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.foundation.text.selection
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.MultiParagraph
+import androidx.compose.ui.text.TextLayoutInput
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.packInts
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+
+internal fun getSingleSelectionLayoutFake(
+    text: String = "hello",
+    rawStartHandleOffset: Int = 0,
+    rawEndHandleOffset: Int = 5,
+    rawPreviousHandleOffset: Int = -1,
+    rtlRanges: List<IntRange> = emptyList(),
+    wordBoundaries: List<TextRange> = listOf(),
+    lineBreaks: List<Int> = emptyList(),
+    crossStatus: CrossStatus = when {
+        rawStartHandleOffset < rawEndHandleOffset -> CrossStatus.NOT_CROSSED
+        rawStartHandleOffset > rawEndHandleOffset -> CrossStatus.CROSSED
+        else -> CrossStatus.COLLAPSED
+    },
+    isStartHandle: Boolean = false,
+    previousSelection: Selection? = null,
+    shouldRecomputeSelection: Boolean = true,
+    subSelections: Map<Long, Selection> = emptyMap(),
+): SelectionLayout {
+    return getSelectionLayoutFake(
+        infos = listOf(
+            getSelectableInfoFake(
+                text = text,
+                selectableId = 1,
+                slot = 1,
+                rawStartHandleOffset = rawStartHandleOffset,
+                rawEndHandleOffset = rawEndHandleOffset,
+                rawPreviousHandleOffset = rawPreviousHandleOffset,
+                rtlRanges = rtlRanges,
+                wordBoundaries = wordBoundaries,
+                lineBreaks = lineBreaks,
+            )
+        ),
+        currentInfoIndex = 0,
+        startSlot = 1,
+        endSlot = 1,
+        crossStatus = crossStatus,
+        isStartHandle = isStartHandle,
+        previousSelection = previousSelection,
+        shouldRecomputeSelection = shouldRecomputeSelection,
+        subSelections = subSelections,
+    )
+}
+
+internal fun getTextLayoutResultMock(
+    text: String = "hello",
+    rtlCharRanges: List<IntRange> = emptyList(),
+    rtlLines: Set<Int> = emptySet(),
+    wordBoundaries: List<TextRange> = listOf(),
+    lineBreaks: List<Int> = emptyList(),
+): TextLayoutResult {
+    val annotatedString = AnnotatedString(text)
+
+    val textLayoutInput = TextLayoutInput(
+        text = annotatedString,
+        style = TextStyle.Default,
+        placeholders = emptyList(),
+        maxLines = Int.MAX_VALUE,
+        softWrap = false,
+        overflow = TextOverflow.Visible,
+        density = Density(1f),
+        layoutDirection = LayoutDirection.Ltr,
+        fontFamilyResolver = mock(),
+        constraints = Constraints(0L)
+    )
+
+    fun lineForOffset(offset: Int): Int {
+        var line = 0
+        lineBreaks.fastForEach {
+            if (it > offset) {
+                return line
+            }
+            line++
+        }
+        return line
+    }
+
+    val multiParagraph = mock<MultiParagraph> {
+        on { getBidiRunDirection(any()) }.thenAnswer { invocation ->
+            val offset = invocation.arguments[0] as Int
+            if (rtlCharRanges.any { offset in it })
+                ResolvedTextDirection.Rtl else ResolvedTextDirection.Ltr
+        }
+
+        on { getParagraphDirection(any()) }.thenAnswer { invocation ->
+            val offset = invocation.arguments[0] as Int
+            val line = lineForOffset(offset)
+            if (line in rtlLines) ResolvedTextDirection.Rtl else ResolvedTextDirection.Ltr
+        }
+
+        on { getWordBoundary(any()) }.thenAnswer { invocation ->
+            val offset = invocation.arguments[0] as Int
+            val wordBoundary = wordBoundaries.find { offset in it.start..it.end }
+            // Workaround: Mockito doesn't work with inline class now. The packed Long is
+            // equal to TextRange(start, end).
+            packInts(wordBoundary!!.start, wordBoundary.end)
+        }
+
+        on { getLineForOffset(any()) }.thenAnswer { invocation ->
+            val offset = invocation.arguments[0] as Int
+            lineForOffset(offset)
+        }
+
+        on { getLineStart(any()) }.thenAnswer { invocation ->
+            val lineIndex = invocation.arguments[0] as Int
+            if (lineIndex == 0) 0 else lineBreaks[lineIndex - 1]
+        }
+
+        on { getLineEnd(any(), any()) }.thenAnswer { invocation ->
+            val lineIndex = invocation.arguments[0] as Int
+            if (lineIndex == lineBreaks.size) text.length else lineBreaks[lineIndex] - 1
+        }
+    }
+
+    return TextLayoutResult(textLayoutInput, multiParagraph, IntSize.Zero)
+}
+
+internal fun getSelectableInfoFake(
+    text: String = "hello",
+    selectableId: Long = 1L,
+    slot: Int = 1,
+    rawStartHandleOffset: Int = 0,
+    rawEndHandleOffset: Int = text.length,
+    rawPreviousHandleOffset: Int = -1,
+    rtlRanges: List<IntRange> = emptyList(),
+    wordBoundaries: List<TextRange> = listOf(),
+    lineBreaks: List<Int> = emptyList(),
+): SelectableInfo = SelectableInfo(
+    selectableId = selectableId,
+    slot = slot,
+    rawStartHandleOffset = rawStartHandleOffset,
+    rawEndHandleOffset = rawEndHandleOffset,
+    rawPreviousHandleOffset = rawPreviousHandleOffset,
+    textLayoutResult = getTextLayoutResultMock(
+        text = text,
+        rtlCharRanges = rtlRanges,
+        wordBoundaries = wordBoundaries,
+        lineBreaks = lineBreaks,
+    ),
+)
+
+internal fun getSelectionLayoutFake(
+    infos: List<SelectableInfo>,
+    startSlot: Int,
+    endSlot: Int,
+    currentInfoIndex: Int = 0,
+    crossStatus: CrossStatus = when {
+        startSlot < endSlot -> CrossStatus.NOT_CROSSED
+        startSlot > endSlot -> CrossStatus.CROSSED
+        else -> infos.single().rawCrossStatus
+    },
+    startInfo: SelectableInfo =
+        with(infos) { if (crossStatus == CrossStatus.CROSSED) last() else first() },
+    endInfo: SelectableInfo =
+        with(infos) { if (crossStatus == CrossStatus.CROSSED) first() else last() },
+    firstInfo: SelectableInfo = if (crossStatus == CrossStatus.CROSSED) endInfo else startInfo,
+    lastInfo: SelectableInfo = if (crossStatus == CrossStatus.CROSSED) startInfo else endInfo,
+    middleInfos: List<SelectableInfo> =
+        if (infos.size < 2) emptyList() else infos.subList(1, infos.size - 1),
+    isStartHandle: Boolean = false,
+    previousSelection: Selection? = null,
+    shouldRecomputeSelection: Boolean = true,
+    subSelections: Map<Long, Selection> = emptyMap(),
+): SelectionLayout = FakeSelectionLayout(
+    size = infos.size,
+    crossStatus = crossStatus,
+    startSlot = startSlot,
+    endSlot = endSlot,
+    startInfo = startInfo,
+    endInfo = endInfo,
+    currentInfo = infos[currentInfoIndex],
+    firstInfo = firstInfo,
+    lastInfo = lastInfo,
+    middleInfos = middleInfos,
+    isStartHandle = isStartHandle,
+    previousSelection = previousSelection,
+    shouldRecomputeSelection = shouldRecomputeSelection,
+    subSelections = subSelections,
+)
+
+internal class FakeSelectionLayout(
+    override val size: Int,
+    override val crossStatus: CrossStatus,
+    override val startSlot: Int,
+    override val endSlot: Int,
+    override val startInfo: SelectableInfo,
+    override val endInfo: SelectableInfo,
+    override val currentInfo: SelectableInfo,
+    override val firstInfo: SelectableInfo,
+    override val lastInfo: SelectableInfo,
+    override val isStartHandle: Boolean,
+    override val previousSelection: Selection?,
+    private val middleInfos: List<SelectableInfo>,
+    private val shouldRecomputeSelection: Boolean,
+    private val subSelections: Map<Long, Selection>,
+) : SelectionLayout {
+    override fun createSubSelections(selection: Selection): Map<Long, Selection> = subSelections
+    override fun forEachMiddleInfo(block: (SelectableInfo) -> Unit) {
+        middleInfos.forEach(block)
+    }
+
+    override fun shouldRecomputeSelection(other: SelectionLayout?): Boolean =
+        shouldRecomputeSelection
+}
+
+internal fun getSelection(
+    startOffset: Int = 0,
+    endOffset: Int = 5,
+    startSelectableId: Long = 1L,
+    endSelectableId: Long = 1L,
+    handlesCrossed: Boolean = startSelectableId == endSelectableId && startOffset > endOffset,
+    startLayoutDirection: ResolvedTextDirection = ResolvedTextDirection.Ltr,
+    endLayoutDirection: ResolvedTextDirection = ResolvedTextDirection.Ltr,
+): Selection = Selection(
+    start = Selection.AnchorInfo(
+        direction = startLayoutDirection,
+        offset = startOffset,
+        selectableId = startSelectableId,
+    ),
+    end = Selection.AnchorInfo(
+        direction = endLayoutDirection,
+        offset = endOffset,
+        selectableId = endSelectableId,
+    ),
+    handlesCrossed = handlesCrossed,
+)
+
+internal class FakeSelectable : Selectable {
+    override var selectableId = 0L
+    var getTextCalledTimes = 0
+    var textToReturn: AnnotatedString? = null
+
+    var rawStartHandleOffset = 0
+    var startXHandleDirection = Direction.ON
+    var startYHandleDirection = Direction.ON
+    var rawEndHandleOffset = 0
+    var endXHandleDirection = Direction.ON
+    var endYHandleDirection = Direction.ON
+    var rawPreviousHandleOffset = -1 // -1 = no previous offset
+    var layoutCoordinatesToReturn: LayoutCoordinates? = null
+
+    private val selectableKey = 1L
+    private val fakeSelectAllSelection: Selection = Selection(
+        start = Selection.AnchorInfo(
+            direction = ResolvedTextDirection.Ltr,
+            offset = 0,
+            selectableId = selectableKey
+        ),
+        end = Selection.AnchorInfo(
+            direction = ResolvedTextDirection.Ltr,
+            offset = 10,
+            selectableId = selectableKey
+        )
+    )
+
+    override fun appendSelectableInfoToBuilder(builder: SelectionLayoutBuilder) {
+        builder.appendInfo(
+            selectableKey,
+            rawStartHandleOffset,
+            startXHandleDirection,
+            startYHandleDirection,
+            rawEndHandleOffset,
+            endXHandleDirection,
+            endYHandleDirection,
+            rawPreviousHandleOffset,
+            getTextLayoutResultMock(),
+        )
+    }
+
+    override fun getSelectAllSelection(): Selection {
+        return fakeSelectAllSelection
+    }
+
+    override fun getText(): AnnotatedString {
+        getTextCalledTimes++
+        return textToReturn!!
+    }
+
+    override fun getLayoutCoordinates(): LayoutCoordinates? {
+        return layoutCoordinatesToReturn
+    }
+
+    override fun getHandlePosition(selection: Selection, isStartHandle: Boolean): Offset {
+        return Offset.Zero
+    }
+
+    override fun getBoundingBox(offset: Int): Rect {
+        return Rect.Zero
+    }
+
+    override fun getLineLeft(offset: Int): Float {
+        return 0f
+    }
+
+    override fun getLineRight(offset: Int): Float {
+        return 0f
+    }
+
+    override fun getCenterYForOffset(offset: Int): Float {
+        return 0f
+    }
+
+    override fun getRangeOfLineContaining(offset: Int): TextRange {
+        return TextRange.Zero
+    }
+
+    override fun getLastVisibleOffset(): Int {
+        return 0
+    }
+
+    fun clear() {
+        getTextCalledTimes = 0
+        textToReturn = null
+    }
+}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionLayoutStartSlot2DTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionLayoutStartSlot2DTest.kt
new file mode 100644
index 0000000..619627d
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionLayoutStartSlot2DTest.kt
@@ -0,0 +1,300 @@
+/*
+ * 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.foundation.text.selection
+
+import androidx.compose.foundation.text.selection.Direction.AFTER
+import androidx.compose.foundation.text.selection.Direction.BEFORE
+import androidx.compose.foundation.text.selection.Direction.ON
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.TextRange
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+
+/**
+ * The visual representation of the 2D tests looks like a box with for texts laid out like this:
+ * ```
+ *           LEFT  MIDDLE_LEFT MIDDLE MIDDLE_RIGHT  RIGHT
+ *            |         |         |        |          |
+ *  TOP           ┌───────────────────────────────┐
+ *                │ ┌────────┐         ┌────────┐ │
+ *  MIDDLE_TOP    │ │ text 1 │         │ text 2 │ │
+ *                │ └────────┘         └────────┘ │
+ *  MIDDLE        │                               │
+ *                │ ┌────────┐         ┌────────┐ │
+ *  MIDDLE_BOTTOM │ │ text 3 │         │ text 4 │ │
+ *                │ └────────┘         └────────┘ │
+ *  BOTTOM        └───────────────────────────────┘
+ * ```
+ * The labels on the x and y axis that will be referenced in the below tests.
+ */
+open class SelectionLayout2DTest {
+    enum class TestHorizontal {
+        LEFT, MIDDLE_LEFT, MIDDLE, MIDDLE_RIGHT, RIGHT
+    }
+
+    enum class TestVertical {
+        TOP,
+        MIDDLE_TOP,
+        MIDDLE,
+        MIDDLE_BOTTOM,
+        BOTTOM
+    }
+
+    internal fun getDirectionsForX(horizontal: TestHorizontal): List<Direction> =
+        when (horizontal) {
+            TestHorizontal.LEFT -> listOf(BEFORE, BEFORE, BEFORE, BEFORE)
+            TestHorizontal.MIDDLE_LEFT -> listOf(ON, BEFORE, ON, BEFORE)
+            TestHorizontal.MIDDLE -> listOf(AFTER, BEFORE, AFTER, BEFORE)
+            TestHorizontal.MIDDLE_RIGHT -> listOf(AFTER, ON, AFTER, ON)
+            TestHorizontal.RIGHT -> listOf(AFTER, AFTER, AFTER, AFTER)
+        }
+
+    internal fun getDirectionsForY(vertical: TestVertical): List<Direction> =
+        when (vertical) {
+            TestVertical.TOP -> listOf(BEFORE, BEFORE, BEFORE, BEFORE)
+            TestVertical.MIDDLE_TOP -> listOf(ON, ON, BEFORE, BEFORE)
+            TestVertical.MIDDLE -> listOf(AFTER, AFTER, BEFORE, BEFORE)
+            TestVertical.MIDDLE_BOTTOM -> listOf(AFTER, AFTER, ON, ON)
+            TestVertical.BOTTOM -> listOf(AFTER, AFTER, AFTER, AFTER)
+        }
+
+    /** Calls [getTextFieldSelectionLayout] to get a [SelectionLayout]. */
+    @OptIn(ExperimentalContracts::class)
+    internal fun buildSelectionLayoutForTest(
+        startHandlePosition: Offset = Offset(5f, 5f),
+        endHandlePosition: Offset = Offset(25f, 5f),
+        previousHandlePosition: Offset = Offset.Unspecified,
+        containerCoordinates: LayoutCoordinates = MockCoordinates(),
+        isStartHandle: Boolean = false,
+        previousSelection: Selection? = null,
+        selectableIdOrderingComparator: Comparator<Long> = naturalOrder(),
+        block: SelectionLayoutBuilder.() -> Unit,
+    ): SelectionLayout {
+        contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
+        return SelectionLayoutBuilder(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = previousHandlePosition,
+            containerCoordinates = containerCoordinates,
+            isStartHandle = isStartHandle,
+            previousSelection = previousSelection,
+            selectableIdOrderingComparator = selectableIdOrderingComparator,
+        ).run {
+            block()
+            build()
+        }
+    }
+
+    internal fun SelectionLayoutBuilder.appendInfoForTest(
+        selectableId: Long = 1L,
+        text: String = "hello",
+        rawStartHandleOffset: Int = 0,
+        startXHandleDirection: Direction = ON,
+        startYHandleDirection: Direction = ON,
+        rawEndHandleOffset: Int = 5,
+        endXHandleDirection: Direction = ON,
+        endYHandleDirection: Direction = ON,
+        rawPreviousHandleOffset: Int = -1,
+        rtlRanges: List<IntRange> = emptyList(),
+        wordBoundaries: List<TextRange> = listOf(),
+        lineBreaks: List<Int> = emptyList(),
+    ): SelectableInfo {
+        val layoutResult = getTextLayoutResultMock(
+            text = text,
+            rtlCharRanges = rtlRanges,
+            wordBoundaries = wordBoundaries,
+            lineBreaks = lineBreaks,
+        )
+        return appendInfo(
+            selectableId = selectableId,
+            rawStartHandleOffset = rawStartHandleOffset,
+            startXHandleDirection = startXHandleDirection,
+            startYHandleDirection = startYHandleDirection,
+            rawEndHandleOffset = rawEndHandleOffset,
+            endXHandleDirection = endXHandleDirection,
+            endYHandleDirection = endYHandleDirection,
+            rawPreviousHandleOffset = rawPreviousHandleOffset,
+            textLayoutResult = layoutResult
+        )
+    }
+}
+
+@SmallTest
+@RunWith(Parameterized::class)
+class SelectionLayoutStartSlot2DTest(
+    private val vertical: TestVertical,
+    private val horizontal: TestHorizontal,
+    private val expectedSlot: Int,
+) : SelectionLayout2DTest() {
+    companion object {
+        @JvmStatic
+        @Parameters(name = "verticalPosition={0}, horizontalPosition={1} expectedSlot={2}")
+        fun data(): Collection<Array<Any>> = listOf(
+            arrayOf(TestVertical.TOP, TestHorizontal.LEFT, 0),
+            arrayOf(TestVertical.TOP, TestHorizontal.MIDDLE_LEFT, 0),
+            arrayOf(TestVertical.TOP, TestHorizontal.MIDDLE, 0),
+            arrayOf(TestVertical.TOP, TestHorizontal.MIDDLE_RIGHT, 0),
+            arrayOf(TestVertical.TOP, TestHorizontal.RIGHT, 0),
+            arrayOf(TestVertical.MIDDLE_TOP, TestHorizontal.LEFT, 0),
+            arrayOf(TestVertical.MIDDLE_TOP, TestHorizontal.MIDDLE_LEFT, 1),
+            arrayOf(TestVertical.MIDDLE_TOP, TestHorizontal.MIDDLE, 2),
+            arrayOf(TestVertical.MIDDLE_TOP, TestHorizontal.MIDDLE_RIGHT, 3),
+            arrayOf(TestVertical.MIDDLE_TOP, TestHorizontal.RIGHT, 4),
+            arrayOf(TestVertical.MIDDLE, TestHorizontal.LEFT, 4),
+            arrayOf(TestVertical.MIDDLE, TestHorizontal.MIDDLE_LEFT, 4),
+            arrayOf(TestVertical.MIDDLE, TestHorizontal.MIDDLE, 4),
+            arrayOf(TestVertical.MIDDLE, TestHorizontal.MIDDLE_RIGHT, 4),
+            arrayOf(TestVertical.MIDDLE, TestHorizontal.RIGHT, 4),
+            arrayOf(TestVertical.MIDDLE_BOTTOM, TestHorizontal.LEFT, 4),
+            arrayOf(TestVertical.MIDDLE_BOTTOM, TestHorizontal.MIDDLE_LEFT, 5),
+            arrayOf(TestVertical.MIDDLE_BOTTOM, TestHorizontal.MIDDLE, 6),
+            arrayOf(TestVertical.MIDDLE_BOTTOM, TestHorizontal.MIDDLE_RIGHT, 7),
+            arrayOf(TestVertical.MIDDLE_BOTTOM, TestHorizontal.RIGHT, 8),
+            arrayOf(TestVertical.BOTTOM, TestHorizontal.LEFT, 8),
+            arrayOf(TestVertical.BOTTOM, TestHorizontal.MIDDLE_LEFT, 8),
+            arrayOf(TestVertical.BOTTOM, TestHorizontal.MIDDLE, 8),
+            arrayOf(TestVertical.BOTTOM, TestHorizontal.MIDDLE_RIGHT, 8),
+            arrayOf(TestVertical.BOTTOM, TestHorizontal.RIGHT, 8),
+        )
+    }
+
+    // Test the start slot. end slot handle directions will always point to the 4th selectable.
+    @Test
+    fun startSlot2dTest() {
+        val (xDirection1, xDirection2, xDirection3, xDirection4) = getDirectionsForX(horizontal)
+        val (yDirection1, yDirection2, yDirection3, yDirection4) = getDirectionsForY(vertical)
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startXHandleDirection = xDirection1,
+                startYHandleDirection = yDirection1,
+                endXHandleDirection = AFTER,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startXHandleDirection = xDirection2,
+                startYHandleDirection = yDirection2,
+                endXHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startXHandleDirection = xDirection3,
+                startYHandleDirection = yDirection3,
+                endXHandleDirection = AFTER,
+                endYHandleDirection = ON,
+            )
+            appendInfoForTest(
+                selectableId = 4L,
+                startXHandleDirection = xDirection4,
+                startYHandleDirection = yDirection4,
+                endXHandleDirection = ON,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.endSlot).isEqualTo(7)
+        assertThat(layout.startSlot).isEqualTo(expectedSlot)
+    }
+}
+
+@SmallTest
+@RunWith(Parameterized::class)
+class SelectionLayoutEndSlot2DTest(
+    private val vertical: TestVertical,
+    private val horizontal: TestHorizontal,
+    private val expectedSlot: Int,
+) : SelectionLayout2DTest() {
+    companion object {
+        @JvmStatic
+        @Parameters(name = "verticalPosition={0}, horizontalPosition={1} expectedSlot={2}")
+        fun data(): Collection<Array<Any>> = listOf(
+            arrayOf(TestVertical.TOP, TestHorizontal.LEFT, 0),
+            arrayOf(TestVertical.TOP, TestHorizontal.MIDDLE_LEFT, 0),
+            arrayOf(TestVertical.TOP, TestHorizontal.MIDDLE, 0),
+            arrayOf(TestVertical.TOP, TestHorizontal.MIDDLE_RIGHT, 0),
+            arrayOf(TestVertical.TOP, TestHorizontal.RIGHT, 0),
+            arrayOf(TestVertical.MIDDLE_TOP, TestHorizontal.LEFT, 0),
+            arrayOf(TestVertical.MIDDLE_TOP, TestHorizontal.MIDDLE_LEFT, 1),
+            arrayOf(TestVertical.MIDDLE_TOP, TestHorizontal.MIDDLE, 2),
+            arrayOf(TestVertical.MIDDLE_TOP, TestHorizontal.MIDDLE_RIGHT, 3),
+            arrayOf(TestVertical.MIDDLE_TOP, TestHorizontal.RIGHT, 4),
+            arrayOf(TestVertical.MIDDLE, TestHorizontal.LEFT, 4),
+            arrayOf(TestVertical.MIDDLE, TestHorizontal.MIDDLE_LEFT, 4),
+            arrayOf(TestVertical.MIDDLE, TestHorizontal.MIDDLE, 4),
+            arrayOf(TestVertical.MIDDLE, TestHorizontal.MIDDLE_RIGHT, 4),
+            arrayOf(TestVertical.MIDDLE, TestHorizontal.RIGHT, 4),
+            arrayOf(TestVertical.MIDDLE_BOTTOM, TestHorizontal.LEFT, 4),
+            arrayOf(TestVertical.MIDDLE_BOTTOM, TestHorizontal.MIDDLE_LEFT, 5),
+            arrayOf(TestVertical.MIDDLE_BOTTOM, TestHorizontal.MIDDLE, 6),
+            arrayOf(TestVertical.MIDDLE_BOTTOM, TestHorizontal.MIDDLE_RIGHT, 7),
+            arrayOf(TestVertical.MIDDLE_BOTTOM, TestHorizontal.RIGHT, 8),
+            arrayOf(TestVertical.BOTTOM, TestHorizontal.LEFT, 8),
+            arrayOf(TestVertical.BOTTOM, TestHorizontal.MIDDLE_LEFT, 8),
+            arrayOf(TestVertical.BOTTOM, TestHorizontal.MIDDLE, 8),
+            arrayOf(TestVertical.BOTTOM, TestHorizontal.MIDDLE_RIGHT, 8),
+            arrayOf(TestVertical.BOTTOM, TestHorizontal.RIGHT, 8),
+        )
+    }
+
+    // Test the end slot. start slot handle directions will always point to the 1st selectable.
+    @Test
+    fun endSlot2dTest() {
+        val (xDirection1, xDirection2, xDirection3, xDirection4) = getDirectionsForX(horizontal)
+        val (yDirection1, yDirection2, yDirection3, yDirection4) = getDirectionsForY(vertical)
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startXHandleDirection = ON,
+                startYHandleDirection = ON,
+                endXHandleDirection = xDirection1,
+                endYHandleDirection = yDirection1,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startXHandleDirection = BEFORE,
+                startYHandleDirection = ON,
+                endXHandleDirection = xDirection2,
+                endYHandleDirection = yDirection2,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startXHandleDirection = ON,
+                startYHandleDirection = BEFORE,
+                endXHandleDirection = xDirection3,
+                endYHandleDirection = yDirection3,
+            )
+            appendInfoForTest(
+                selectableId = 4L,
+                startXHandleDirection = BEFORE,
+                startYHandleDirection = BEFORE,
+                endXHandleDirection = xDirection4,
+                endYHandleDirection = yDirection4,
+            )
+        }
+        assertThat(layout.startSlot).isEqualTo(1)
+        assertThat(layout.endSlot).isEqualTo(expectedSlot)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionLayoutTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionLayoutTest.kt
new file mode 100644
index 0000000..3cba9f6
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionLayoutTest.kt
@@ -0,0 +1,1608 @@
+/*
+ * 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.foundation.text.selection
+
+import androidx.compose.foundation.text.selection.Direction.AFTER
+import androidx.compose.foundation.text.selection.Direction.BEFORE
+import androidx.compose.foundation.text.selection.Direction.ON
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.TextRange
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class SelectionLayoutTest {
+    @Test
+    fun layoutBuilderSizeZero_throws() {
+        assertFailsWith(IllegalStateException::class) {
+            buildSelectionLayoutForTest { }
+        }
+    }
+
+    @Test
+    fun singleLayout_verifySimpleParameters() {
+        val selection = getSelection()
+        val layout = getSingleSelectionLayoutForTest(
+            isStartHandle = true,
+            previousSelection = selection,
+        )
+        assertThat(layout.isStartHandle).isTrue()
+        assertThat(layout.previousSelection).isEqualTo(selection)
+    }
+
+    @Test
+    fun layoutBuilder_verifySimpleParameters() {
+        val selection = getSelection()
+        val layout = buildSelectionLayoutForTest(
+            isStartHandle = true,
+            previousSelection = selection,
+        ) {
+            appendInfoForTest()
+        }
+        assertThat(layout.isStartHandle).isTrue()
+        assertThat(layout.previousSelection).isEqualTo(selection)
+    }
+
+    @Test
+    fun singleLayout_sameInfoForAllSelectableInfoFunctions() {
+        val layout = getSingleSelectionLayoutForTest()
+        // since there is only one info, each info function should return the same
+        val info = layout.currentInfo
+        assertThat(layout.startInfo).isSameInstanceAs(info)
+        assertThat(layout.endInfo).isSameInstanceAs(info)
+        assertThat(layout.firstInfo).isSameInstanceAs(info)
+        assertThat(layout.lastInfo).isSameInstanceAs(info)
+    }
+
+    @Test
+    fun size_singleLayout_returnsOne() {
+        val selection = getSingleSelectionLayoutForTest()
+        assertThat(selection.size).isEqualTo(1)
+    }
+
+    @Test
+    fun size_layoutBuilderSizeOne_returnsOne() {
+        val selection = buildSelectionLayoutForTest {
+            appendInfoForTest()
+        }
+        assertThat(selection.size).isEqualTo(1)
+    }
+
+    @Test
+    fun size_layoutBuilderSizeMoreThanOne_returnsSize() {
+        val selection = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startYHandleDirection = BEFORE,
+            )
+        }
+        assertThat(selection.size).isEqualTo(3)
+    }
+
+    @Test
+    fun startSlot_singleLayout_equalsOnlyInfo() {
+        val layout = getSingleSelectionLayoutForTest()
+        // when there is only one info, slot doesn't matter
+        // so, ensure that the slot is equal to the only info's slot
+        assertThat(layout.startSlot).isEqualTo(layout.currentInfo.slot)
+    }
+
+    @Test
+    fun startSlot_layoutBuilder_onBefore_equalsZero() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.startSlot).isEqualTo(0)
+    }
+
+    @Test
+    fun startSlot_layoutBuilder_onFirst_equalsOne() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.startSlot).isEqualTo(1)
+    }
+
+    @Test
+    fun startSlot_layoutBuilder_onMiddle_equalsTwo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.startSlot).isEqualTo(2)
+    }
+
+    @Test
+    fun startSlot_layoutBuilder_onLast_equalsThree() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = ON,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.startSlot).isEqualTo(3)
+    }
+
+    @Test
+    fun startSlot_layoutBuilder_onAfter_equalsFour() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.startSlot).isEqualTo(4)
+    }
+
+    @Test
+    fun endSlot_singleLayout_equalsOnlyInfo() {
+        val layout = getSingleSelectionLayoutForTest()
+        // when there is only one info, slot doesn't matter
+        // so, ensure that the slot is equal to the only info's slot
+        assertThat(layout.endSlot).isEqualTo(layout.currentInfo.slot)
+    }
+
+    @Test
+    fun endSlot_layoutBuilder_onBefore_equalsZero() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = BEFORE,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = BEFORE,
+            )
+        }
+        assertThat(layout.endSlot).isEqualTo(0)
+    }
+
+    @Test
+    fun endSlot_layoutBuilder_onFirst_equalsOne() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = ON,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = BEFORE,
+            )
+        }
+        assertThat(layout.endSlot).isEqualTo(1)
+    }
+
+    @Test
+    fun endSlot_layoutBuilder_onMiddle_equalsTwo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = BEFORE,
+            )
+        }
+        assertThat(layout.endSlot).isEqualTo(2)
+    }
+
+    @Test
+    fun endSlot_layoutBuilder_onLast_equalsThree() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.endSlot).isEqualTo(3)
+    }
+
+    @Test
+    fun endSlot_layoutBuilder_onAfter_equalsFour() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+        }
+        assertThat(layout.endSlot).isEqualTo(4)
+    }
+
+    @Test
+    fun crossStatus_singleLayout_collapsed() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 0,
+        )
+        assertThat(layout.crossStatus).isEqualTo(CrossStatus.COLLAPSED)
+    }
+
+    @Test
+    fun crossStatus_singleLayout_crossed() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 0,
+        )
+        assertThat(layout.crossStatus).isEqualTo(CrossStatus.CROSSED)
+    }
+
+    @Test
+    fun crossStatus_singleLayout_notCrossed() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 1,
+        )
+        assertThat(layout.crossStatus).isEqualTo(CrossStatus.NOT_CROSSED)
+    }
+
+    @Test
+    fun crossStatus_layoutBuilder_collapsed() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = ON,
+                rawStartHandleOffset = 0,
+                rawEndHandleOffset = 0,
+            )
+        }
+        assertThat(layout.crossStatus).isEqualTo(CrossStatus.COLLAPSED)
+    }
+
+    @Test
+    fun crossStatus_layoutBuilder_crossed() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = ON,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = ON,
+                endYHandleDirection = BEFORE,
+            )
+        }
+        assertThat(layout.crossStatus).isEqualTo(CrossStatus.CROSSED)
+    }
+
+    @Test
+    fun crossStatus_layoutBuilder_notCrossed() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.crossStatus).isEqualTo(CrossStatus.NOT_CROSSED)
+    }
+
+    // No startInfo test for singleLayout because it is covered in
+    // singleLayout_sameInfoForAllSelectableInfoFunctions
+
+    @Test
+    fun startInfo_layoutBuilder_onSlotZero_equalsFirstInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.startInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun startInfo_layoutBuilder_onSlotOne_equalsFirstInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.startInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun startInfo_layoutBuilder_onSlotTwo_equalsSecondInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.startInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun startInfo_layoutBuilder_onSlotThree_equalsSecondInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = ON,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.startInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun startInfo_layoutBuilder_onSlotFour_equalsSecondInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.startInfo.selectableId).isEqualTo(2L)
+    }
+
+    // No endInfo test for singleLayout because it is covered in
+    // singleLayout_sameInfoForAllSelectableInfoFunctions
+
+    @Test
+    fun endInfo_layoutBuilder_onSlotZero_equalsFirstInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = BEFORE,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = BEFORE,
+            )
+        }
+        assertThat(layout.endInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun endInfo_layoutBuilder_onSlotOne_equalsFirstInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = ON,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = BEFORE,
+            )
+        }
+        assertThat(layout.endInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun endInfo_layoutBuilder_onSlotTwo_equalsFirstInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = BEFORE,
+            )
+        }
+        assertThat(layout.endInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun endInfo_layoutBuilder_onSlotThree_equalsSecondInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.endInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun endInfo_layoutBuilder_onSlotFour_equalsSecondInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+        }
+        assertThat(layout.endInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun currentInfo_layoutBuilder_currentInfo_startHandle_equalsFirst() {
+        val layout = buildSelectionLayoutForTest(isStartHandle = true) {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.currentInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun currentInfo_layoutBuilder_endHandle_equalsSecond() {
+        val layout = buildSelectionLayoutForTest(isStartHandle = false) {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.currentInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun firstInfo_layoutBuilder_notCrossed_equalsFirst() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.firstInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun firstInfo_layoutBuilder_crossed_equalsFirst() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = ON,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = ON,
+                endYHandleDirection = BEFORE,
+            )
+        }
+        assertThat(layout.firstInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun lastInfo_layoutBuilder_notCrossed_equalsSecond() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.lastInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun lastInfo_layoutBuilder_crossed_equalsSecond() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = ON,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = ON,
+                endYHandleDirection = BEFORE,
+            )
+        }
+        assertThat(layout.lastInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun middleInfos_singleLayout_isEmpty() {
+        val layout = getSingleSelectionLayoutForTest()
+        val infoList = mutableListOf<SelectableInfo>()
+        layout.forEachMiddleInfo { infoList += it }
+        assertThat(infoList).isEmpty()
+    }
+
+    @Test
+    fun middleInfos_layoutBuilder_twoInfos_isEmpty() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+
+        val infoList = mutableListOf<SelectableInfo>()
+        layout.forEachMiddleInfo { infoList += it }
+        assertThat(infoList).isEmpty()
+    }
+
+    @Test
+    fun middleInfos_layoutBuilder_threeInfos_containsOneElement() {
+        val info: SelectableInfo
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            info = appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        val infoList = mutableListOf<SelectableInfo>()
+        layout.forEachMiddleInfo { infoList += it }
+        assertThat(infoList).containsExactly(info)
+    }
+
+    @Test
+    fun middleInfos_layoutBuilder_fourInfos_containsTwoElements() {
+        val infoOne: SelectableInfo
+        val infoTwo: SelectableInfo
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            infoOne = appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+            infoTwo = appendInfoForTest(
+                selectableId = 3L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 4L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        val infoList = mutableListOf<SelectableInfo>()
+        layout.forEachMiddleInfo { infoList += it }
+        assertThat(infoList).containsExactly(infoOne, infoTwo).inOrder()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_otherNull_returnsTrue() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        assertThat(layout.shouldRecomputeSelection(null)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_otherMulti_returnsTrue() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        val otherLayout = buildSelectionLayoutForTest {
+            appendInfoForTest()
+            appendInfoForTest()
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_differentHandle_returnsTrue() {
+        val layout = getSingleSelectionLayoutForTest(
+            isStartHandle = true,
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        val otherLayout = getSingleSelectionLayoutForTest(
+            isStartHandle = false,
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_differentInfo_returnsTrue() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawStartHandleOffset = 0,
+            previousSelection = getSelection()
+        )
+        val otherLayout = getSingleSelectionLayoutForTest(
+            rawStartHandleOffset = 1,
+            previousSelection = getSelection()
+        )
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_noPreviousSelection_returnsTrue() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+        )
+        val otherLayout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_sameLayout_returnsFalse() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        assertThat(layout.shouldRecomputeSelection(layout)).isFalse()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_equalLayout_returnsFalse() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        val otherLayout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isFalse()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_otherNull_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        assertThat(layout.shouldRecomputeSelection(null)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_otherSingle_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        val otherLayout = getSingleSelectionLayoutForTest(
+            previousSelection = getSelection(),
+            rawPreviousHandleOffset = 5
+        )
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_differentStartSlot_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        val otherLayout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_differentEndSlot_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+        }
+        val otherLayout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_differentHandle_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            isStartHandle = true,
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        val otherLayout = buildSelectionLayoutForTest(
+            previousSelection = getSelection(),
+            isStartHandle = false
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_differentSize_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        val otherLayout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_differentInfo_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        val otherLayout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 4, rawPreviousHandleOffset = 5)
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_sameLayout_returnsFalse() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        assertThat(layout.shouldRecomputeSelection(layout)).isFalse()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_equalLayout_returnsFalse() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        val otherLayout = buildSelectionLayoutForTest {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isFalse()
+    }
+
+    @Test
+    fun createSubSelections_singleLayout_missNonCrossedSelection_throws() {
+        val layout = getSingleSelectionLayoutForTest()
+        val selection = getSelection(startOffset = 1, endOffset = 0, handlesCrossed = false)
+        assertFailsWith(IllegalStateException::class) {
+            layout.createSubSelections(selection)
+        }
+    }
+
+    @Test
+    fun createSubSelections_singleLayout_missCrossedSelection_throws() {
+        val layout = getSingleSelectionLayoutForTest()
+        val selection = getSelection(startOffset = 0, endOffset = 1, handlesCrossed = true)
+        assertFailsWith(IllegalStateException::class) {
+            layout.createSubSelections(selection)
+        }
+    }
+
+    @Test
+    fun createSubSelections_singleLayout_validSelection_returnsInputSelection() {
+        val layout = getSingleSelectionLayoutForTest()
+        val selection = getSelection()
+        val actual = layout.createSubSelections(selection)
+        assertThat(actual).hasSize(1)
+        // We don't care about the selectableId since it isn't used anyways
+        assertThat(actual.toList().single().second).isEqualTo(selection)
+    }
+
+    @Test
+    fun createSubSelections_builtSingleLayout_validSelection_returnsInputSelection() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(selectableId = 1L)
+        }
+        val selection = getSelection()
+        assertThat(layout.createSubSelections(selection)).containsExactly(1L, selection)
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_missNonCrossedSingleSelection_throws() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(selectableId = 1L)
+        }
+        val selection = getSelection(startOffset = 1, endOffset = 0, handlesCrossed = false)
+        assertFailsWith(IllegalStateException::class) {
+            layout.createSubSelections(selection)
+        }
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_missCrossedSingleSelection_throws() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(selectableId = 1L)
+        }
+        val selection = getSelection(startOffset = 0, endOffset = 1, handlesCrossed = true)
+        assertFailsWith(IllegalStateException::class) {
+            layout.createSubSelections(selection)
+        }
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_selectionInOneSelectable_returnsInputSelection() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(selectableId = 1L)
+            appendInfoForTest(selectableId = 2L)
+        }
+        val selection = getSelection(startSelectableId = 2L, endSelectableId = 2L)
+        assertThat(layout.createSubSelections(selection)).containsExactly(2L, selection)
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_selectionInTwoSelectables() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        val selection = getSelection(startSelectableId = 1L, endSelectableId = 2L)
+        assertThat(layout.createSubSelections(selection)).containsExactly(
+            1L, getSelection(startSelectableId = 1L, endSelectableId = 1L),
+            2L, getSelection(startSelectableId = 2L, endSelectableId = 2L),
+        )
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_selectionInThreeSelectables() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        val selection = getSelection(startSelectableId = 1L, endSelectableId = 3L)
+        assertThat(layout.createSubSelections(selection)).containsExactly(
+            1L, getSelection(startSelectableId = 1L, endSelectableId = 1L),
+            2L, getSelection(startSelectableId = 2L, endSelectableId = 2L),
+            3L, getSelection(startSelectableId = 3L, endSelectableId = 3L),
+        )
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_selectionInFourSelectables() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = ON,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 4L,
+                startYHandleDirection = BEFORE,
+                endYHandleDirection = ON,
+            )
+        }
+        val selection = getSelection(startSelectableId = 1L, endSelectableId = 4L)
+        assertThat(layout.createSubSelections(selection)).containsExactly(
+            1L, getSelection(startSelectableId = 1L, endSelectableId = 1L),
+            2L, getSelection(startSelectableId = 2L, endSelectableId = 2L),
+            3L, getSelection(startSelectableId = 3L, endSelectableId = 3L),
+            4L, getSelection(startSelectableId = 4L, endSelectableId = 4L),
+        )
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_crossedSelectionInOneSelectable() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = ON,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+        }
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 1L,
+            endOffset = 0,
+            handlesCrossed = true
+        )
+        assertThat(layout.createSubSelections(selection)).containsExactly(1L, selection)
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_crossedSelectionInTwoSelectables() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = ON,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = ON,
+                endYHandleDirection = BEFORE,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+        }
+        val selection = getSelection(
+            startSelectableId = 2L,
+            startOffset = 5,
+            endSelectableId = 1L,
+            endOffset = 0,
+            handlesCrossed = true
+        )
+        assertThat(layout.createSubSelections(selection)).containsExactly(
+            1L,
+            getSelection(
+                startSelectableId = 1L,
+                endSelectableId = 1L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+            2L,
+            getSelection(
+                startSelectableId = 2L,
+                endSelectableId = 2L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+        )
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_crossedSelectionInThreeSelectables() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = ON,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = BEFORE,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startYHandleDirection = ON,
+                endYHandleDirection = BEFORE,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+        }
+        val selection = getSelection(
+            startSelectableId = 3L,
+            startOffset = 5,
+            endSelectableId = 1L,
+            endOffset = 0,
+            handlesCrossed = true
+        )
+        assertThat(layout.createSubSelections(selection)).containsExactly(
+            1L,
+            getSelection(
+                startSelectableId = 1L,
+                endSelectableId = 1L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+            2L,
+            getSelection(
+                startSelectableId = 2L,
+                endSelectableId = 2L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+            3L,
+            getSelection(
+                startSelectableId = 3L,
+                endSelectableId = 3L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+        )
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_crossedSelectionInFourSelectables() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = ON,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = BEFORE,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startYHandleDirection = AFTER,
+                endYHandleDirection = BEFORE,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+            appendInfoForTest(
+                selectableId = 4L,
+                startYHandleDirection = ON,
+                endYHandleDirection = BEFORE,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+        }
+        val selection = getSelection(
+            startSelectableId = 4L,
+            startOffset = 5,
+            endSelectableId = 1L,
+            endOffset = 0,
+            handlesCrossed = true
+        )
+        assertThat(layout.createSubSelections(selection)).containsExactly(
+            1L,
+            getSelection(
+                startSelectableId = 1L,
+                endSelectableId = 1L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+            2L,
+            getSelection(
+                startSelectableId = 2L,
+                endSelectableId = 2L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+            3L,
+            getSelection(
+                startSelectableId = 3L,
+                endSelectableId = 3L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+            4L,
+            getSelection(
+                startSelectableId = 4L,
+                endSelectableId = 4L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+        )
+    }
+
+    @Test
+    fun selection_isCollapsed_nullSelection_returnsTrue() {
+        assertThat(null.isCollapsed(getSingleSelectionLayoutFake())).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_nullLayout_returnsTrue() {
+        assertThat(getSelection().isCollapsed(null)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_singleLayout_empty_returnsTrue() {
+        val selection = getSelection(startOffset = 0, endOffset = 0)
+        val layout = getSingleSelectionLayoutFake(text = "")
+        assertThat(selection.isCollapsed(layout)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_singleLayout_collapsed_returnsTrue() {
+        val selection = getSelection(startOffset = 0, endOffset = 0)
+        val layout = getSingleSelectionLayoutFake(text = "hello")
+        assertThat(selection.isCollapsed(layout)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_singleLayout_notCollapsed_returnsFalse() {
+        val selection = getSelection(startOffset = 0, endOffset = 5)
+        val layout = getSingleSelectionLayoutFake(text = "hello")
+        assertThat(selection.isCollapsed(layout)).isFalse()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_twoLayouts_empty_returnsTrue() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 0,
+            endSelectableId = 2L,
+            endOffset = 0
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = ""),
+                getSelectableInfoFake(selectableId = 2L, text = ""),
+            ),
+            startSlot = 1,
+            endSlot = 3,
+        )
+        assertThat(selection.isCollapsed(layout)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_twoLayouts_collapsed_returnsTrue() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 2L,
+            endOffset = 0
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = "hello"),
+                getSelectableInfoFake(selectableId = 2L, text = "hello"),
+            ),
+            startSlot = 1,
+            endSlot = 3,
+        )
+        assertThat(selection.isCollapsed(layout)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_twoLayouts_notCollapsedInFirst_returnsFalse() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 4,
+            endSelectableId = 2L,
+            endOffset = 0
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = "hello"),
+                getSelectableInfoFake(selectableId = 2L, text = "hello"),
+            ),
+            startSlot = 1,
+            endSlot = 3,
+        )
+        assertThat(selection.isCollapsed(layout)).isFalse()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_twoLayouts_notCollapsedInSecond_returnsFalse() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 2L,
+            endOffset = 1
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = "hello"),
+                getSelectableInfoFake(selectableId = 2L, text = "hello"),
+            ),
+            startSlot = 1,
+            endSlot = 3,
+        )
+        assertThat(selection.isCollapsed(layout)).isFalse()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_threeLayouts_empty_returnsTrue() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 0,
+            endSelectableId = 3L,
+            endOffset = 0
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = ""),
+                getSelectableInfoFake(selectableId = 2L, text = ""),
+                getSelectableInfoFake(selectableId = 3L, text = ""),
+            ),
+            startSlot = 1,
+            endSlot = 5,
+        )
+        assertThat(selection.isCollapsed(layout)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_threeLayouts_collapsed_returnsTrue() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 3L,
+            endOffset = 0
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = "hello"),
+                getSelectableInfoFake(selectableId = 2L, text = ""),
+                getSelectableInfoFake(selectableId = 3L, text = "hello"),
+            ),
+            startSlot = 1,
+            endSlot = 5,
+        )
+        assertThat(selection.isCollapsed(layout)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_threeLayouts_notCollapsed_returnsFalse() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 3L,
+            endOffset = 0
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = "hello"),
+                getSelectableInfoFake(selectableId = 2L, text = "."),
+                getSelectableInfoFake(selectableId = 3L, text = "hello"),
+            ),
+            startSlot = 1,
+            endSlot = 5,
+        )
+        assertThat(selection.isCollapsed(layout)).isFalse()
+    }
+
+    /** Calls [getTextFieldSelectionLayout] to get a [SelectionLayout]. */
+    @OptIn(ExperimentalContracts::class)
+    private fun buildSelectionLayoutForTest(
+        startHandlePosition: Offset = Offset(5f, 5f),
+        endHandlePosition: Offset = Offset(25f, 5f),
+        previousHandlePosition: Offset = Offset.Unspecified,
+        containerCoordinates: LayoutCoordinates = MockCoordinates(),
+        isStartHandle: Boolean = false,
+        previousSelection: Selection? = null,
+        selectableIdOrderingComparator: Comparator<Long> = naturalOrder(),
+        block: SelectionLayoutBuilder.() -> Unit,
+    ): SelectionLayout {
+        contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
+        return SelectionLayoutBuilder(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = previousHandlePosition,
+            containerCoordinates = containerCoordinates,
+            isStartHandle = isStartHandle,
+            previousSelection = previousSelection,
+            selectableIdOrderingComparator = selectableIdOrderingComparator,
+        ).run {
+            block()
+            build()
+        }
+    }
+
+    private fun SelectionLayoutBuilder.appendInfoForTest(
+        selectableId: Long = 1L,
+        text: String = "hello",
+        rawStartHandleOffset: Int = 0,
+        startXHandleDirection: Direction = ON,
+        startYHandleDirection: Direction = ON,
+        rawEndHandleOffset: Int = 5,
+        endXHandleDirection: Direction = ON,
+        endYHandleDirection: Direction = ON,
+        rawPreviousHandleOffset: Int = -1,
+        rtlRanges: List<IntRange> = emptyList(),
+        wordBoundaries: List<TextRange> = listOf(),
+        lineBreaks: List<Int> = emptyList(),
+    ): SelectableInfo {
+        val layoutResult = getTextLayoutResultMock(
+            text = text,
+            rtlCharRanges = rtlRanges,
+            wordBoundaries = wordBoundaries,
+            lineBreaks = lineBreaks,
+        )
+        return appendInfo(
+            selectableId = selectableId,
+            rawStartHandleOffset = rawStartHandleOffset,
+            startXHandleDirection = startXHandleDirection,
+            startYHandleDirection = startYHandleDirection,
+            rawEndHandleOffset = rawEndHandleOffset,
+            endXHandleDirection = endXHandleDirection,
+            endYHandleDirection = endYHandleDirection,
+            rawPreviousHandleOffset = rawPreviousHandleOffset,
+            textLayoutResult = layoutResult
+        )
+    }
+
+    /** Calls [getTextFieldSelectionLayout] to get a [SelectionLayout]. */
+    private fun getSingleSelectionLayoutForTest(
+        text: String = "hello",
+        rawStartHandleOffset: Int = 0,
+        rawEndHandleOffset: Int = 5,
+        rawPreviousHandleOffset: Int = -1,
+        rtlRanges: List<IntRange> = emptyList(),
+        wordBoundaries: List<TextRange> = listOf(),
+        lineBreaks: List<Int> = emptyList(),
+        isStartHandle: Boolean = false,
+        previousSelection: Selection? = null,
+        isStartOfSelection: Boolean = previousSelection == null,
+    ): SelectionLayout {
+        val layoutResult = getTextLayoutResultMock(
+            text = text,
+            rtlCharRanges = rtlRanges,
+            wordBoundaries = wordBoundaries,
+            lineBreaks = lineBreaks,
+        )
+        return getTextFieldSelectionLayout(
+            layoutResult = layoutResult,
+            rawStartHandleOffset = rawStartHandleOffset,
+            rawEndHandleOffset = rawEndHandleOffset,
+            rawPreviousHandleOffset = rawPreviousHandleOffset,
+            previousSelectionRange = previousSelection?.toTextRange() ?: TextRange.Zero,
+            isStartOfSelection = isStartOfSelection,
+            isStartHandle = isStartHandle
+        )
+    }
+}
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
new file mode 100644
index 0000000..295f7af
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
@@ -0,0 +1,920 @@
+/*
+ * Copyright 2021 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.text.selection
+
+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.style.ResolvedTextDirection
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.kotlin.any
+import org.mockito.kotlin.isNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(JUnit4::class)
+class SelectionManagerTest {
+    private val selectionRegistrar = spy(SelectionRegistrarImpl())
+    private val selectable = FakeSelectable()
+    private val selectableId = 1L
+    private val selectionManager = SelectionManager(selectionRegistrar)
+    private var onSelectionChangeCalledTimes = 0
+
+    private val containerLayoutCoordinates = MockCoordinates()
+
+    private val startSelectableId = 2L
+    private val startSelectable = mock<Selectable> {
+        whenever(it.selectableId).thenReturn(startSelectableId)
+    }
+
+    private val endSelectableId = 3L
+    private val endSelectable = mock<Selectable> {
+        whenever(it.selectableId).thenReturn(endSelectableId)
+    }
+
+    private val middleSelectableId = 4L
+    private val middleSelectable = mock<Selectable> {
+        whenever(it.selectableId).thenReturn(middleSelectableId)
+    }
+
+    private val lastSelectableId = 5L
+    private val lastSelectable = mock<Selectable> {
+        whenever(it.selectableId).thenReturn(lastSelectableId)
+    }
+
+    private val fakeSelection =
+        Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = 0,
+                selectableId = startSelectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = 5,
+                selectableId = endSelectableId
+            )
+        )
+
+    private val hapticFeedback = mock<HapticFeedback>()
+    private val clipboardManager = mock<ClipboardManager>()
+    private val textToolbar = mock<TextToolbar>()
+
+    @Before
+    fun setup() {
+        selectable.clear()
+        selectable.selectableId = selectableId
+        selectionRegistrar.subscribe(selectable)
+        selectionRegistrar.subselections = mapOf(
+            selectableId to fakeSelection,
+            startSelectableId to fakeSelection,
+            endSelectableId to fakeSelection
+        )
+        selectionManager.containerLayoutCoordinates = containerLayoutCoordinates
+        selectionManager.hapticFeedBack = hapticFeedback
+        selectionManager.clipboardManager = clipboardManager
+        selectionManager.textToolbar = textToolbar
+        selectionManager.selection = fakeSelection
+        selectionManager.onSelectionChange = { onSelectionChangeCalledTimes++ }
+    }
+
+    @Test
+    fun updateSelection_onInitial_returnsTrue() {
+        val startHandlePosition = Offset(x = 5f, y = 5f)
+        val endHandlePosition = Offset(x = 25f, y = 5f)
+        selectable.apply {
+            textToReturn = AnnotatedString("hello")
+            rawStartHandleOffset = 0
+            rawEndHandleOffset = 5
+        }
+
+        val actual = selectionManager.updateSelection(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = endHandlePosition - Offset(x = 5f, y = 0f),
+            isStartHandle = false,
+            adjustment = SelectionAdjustment.None,
+        )
+
+        assertThat(actual).isTrue()
+        assertThat(onSelectionChangeCalledTimes).isEqualTo(1)
+    }
+
+    @Test
+    fun updateSelection_onNoChange_returnsFalse() {
+        val startHandlePosition = Offset(x = 5f, y = 5f)
+        val endHandlePosition = Offset(x = 25f, y = 5f)
+        selectable.apply {
+            textToReturn = AnnotatedString("hello")
+            rawStartHandleOffset = 0
+            rawEndHandleOffset = 5
+            rawPreviousHandleOffset = 5
+        }
+
+        // run once to set context for the "previous" selection update
+        selectionManager.updateSelection(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = endHandlePosition,
+            isStartHandle = false,
+            adjustment = SelectionAdjustment.None,
+        )
+
+        // run again since we are testing the "no changes" case
+        val actual = selectionManager.updateSelection(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = endHandlePosition,
+            isStartHandle = false,
+            adjustment = SelectionAdjustment.None,
+        )
+
+        assertThat(actual).isFalse()
+        assertThat(onSelectionChangeCalledTimes).isEqualTo(1)
+    }
+
+    @Test
+    fun updateSelection_onChange_returnsTrue() {
+        val startHandlePosition = Offset(x = 5f, y = 5f)
+        val endHandlePosition = Offset(x = 25f, y = 5f)
+        selectable.apply {
+            textToReturn = AnnotatedString("hello")
+            rawStartHandleOffset = 0
+            rawEndHandleOffset = 5
+            rawPreviousHandleOffset = 5
+        }
+
+        // run once to set context for the "previous" selection update
+        selectionManager.updateSelection(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = endHandlePosition,
+            isStartHandle = false,
+            adjustment = SelectionAdjustment.None,
+        )
+
+        // run again with a change in end handle
+        selectable.rawEndHandleOffset = 4
+        val actual = selectionManager.updateSelection(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = endHandlePosition - Offset(x = 5f, y = 0f),
+            isStartHandle = false,
+            adjustment = SelectionAdjustment.None,
+        )
+
+        assertThat(actual).isTrue()
+        assertThat(onSelectionChangeCalledTimes).isEqualTo(2)
+    }
+
+    @Test
+    fun shouldPerformHaptics_notInTouchMode_returnsFalse() {
+        selectionManager.isInTouchMode = false
+        selectable.textToReturn = AnnotatedString("hello")
+        val actual = selectionManager.shouldPerformHaptics()
+        assertThat(actual).isFalse()
+    }
+
+    @Test
+    fun shouldPerformHaptics_allEmptyTextSelectables_returnsFalse() {
+        selectionManager.isInTouchMode = true
+        selectable.textToReturn = AnnotatedString("")
+        val actual = selectionManager.shouldPerformHaptics()
+        assertThat(actual).isFalse()
+    }
+
+    @Test
+    fun shouldPerformHaptics_inTouchModeAndNonEmpty_returnsTrue() {
+        selectionManager.isInTouchMode = true
+        selectable.textToReturn = AnnotatedString("hello")
+        val actual = selectionManager.shouldPerformHaptics()
+        assertThat(actual).isTrue()
+    }
+
+    @Test
+    fun mergeSelections_selectAll() {
+        val anotherSelectableId = 100L
+        val selectableAnother = mock<Selectable>()
+        whenever(selectableAnother.selectableId).thenReturn(anotherSelectableId)
+
+        selectionRegistrar.subscribe(selectableAnother)
+
+        selectionManager.selectAll(
+            selectableId = selectableId,
+            previousSelection = fakeSelection
+        )
+
+        verify(selectableAnother, times(0)).getSelectAllSelection()
+        verify(
+            hapticFeedback,
+            times(1)
+        ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
+    }
+
+    @Test
+    fun isNonEmptySelection_whenNonEmptySelection_sameLine_returnsTrue() {
+        val text = "Text Demo"
+        val annotatedString = AnnotatedString(text)
+        val startOffset = text.indexOf('e')
+        val endOffset = text.indexOf('m')
+        selectable.textToReturn = annotatedString
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = selectableId
+            ),
+            handlesCrossed = false
+        )
+        selectionManager.selection = selection
+        selectionRegistrar.subselections = mapOf(selectableId to selection)
+
+        assertThat(selectionManager.isNonEmptySelection()).isTrue()
+    }
+
+    @Test
+    fun isNonEmptySelection_whenEmptySelection_sameLine_returnsFalse() {
+        val text = "Text Demo"
+        val annotatedString = AnnotatedString(text)
+        val startOffset = text.indexOf('e')
+        selectable.textToReturn = annotatedString
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = selectableId
+            ),
+            handlesCrossed = false
+        )
+        selectionManager.selection = selection
+        selectionRegistrar.subselections = mapOf(selectableId to selection)
+
+        assertThat(selectionManager.isNonEmptySelection()).isFalse()
+    }
+
+    @Test
+    fun isNonEmptySelection_whenNonEmptySelection_multiLine_returnsTrue() {
+        val text = "Text Demo"
+        val annotatedString = AnnotatedString(text = text)
+        val startOffset = text.indexOf('m')
+        val endOffset = text.indexOf('x')
+
+        selectionRegistrar.subscribe(endSelectable)
+        selectionRegistrar.subscribe(middleSelectable)
+        selectionRegistrar.subscribe(startSelectable)
+        selectionRegistrar.subscribe(lastSelectable)
+        selectionRegistrar.sorted = true
+        whenever(startSelectable.getText()).thenReturn(annotatedString)
+        whenever(middleSelectable.getText()).thenReturn(annotatedString)
+        whenever(endSelectable.getText()).thenReturn(annotatedString)
+        selectionManager.selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = startSelectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = endSelectableId
+            ),
+            handlesCrossed = true
+        )
+
+        selectionRegistrar.subselections = mapOf(
+            endSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = annotatedString.length,
+                    selectableId = endSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = endOffset,
+                    selectableId = endSelectableId
+                ),
+                handlesCrossed = true
+            ),
+            middleSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = annotatedString.length,
+                    selectableId = middleSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = middleSelectableId
+                ),
+                handlesCrossed = true
+            ),
+            startSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = startOffset,
+                    selectableId = startSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = startSelectableId
+                ),
+                handlesCrossed = true
+            ),
+        )
+
+        assertThat(selectionManager.isNonEmptySelection()).isTrue()
+    }
+
+    @Test
+    fun isNonEmptySelection_whenEmptySelection_multiLine_returnsFalse() {
+        val text = "Text Demo"
+        val annotatedString = AnnotatedString(text)
+        val startOffset = text.length
+        val endOffset = 0
+
+        selectionRegistrar.subscribe(startSelectable)
+        selectionRegistrar.subscribe(middleSelectable)
+        selectionRegistrar.subscribe(endSelectable)
+        selectionRegistrar.subscribe(lastSelectable)
+        selectionRegistrar.sorted = true
+        whenever(startSelectable.getText()).thenReturn(annotatedString)
+        whenever(middleSelectable.getText()).thenReturn(AnnotatedString(""))
+        whenever(endSelectable.getText()).thenReturn(annotatedString)
+        selectionManager.selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = startSelectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = endSelectableId
+            ),
+            handlesCrossed = false
+        )
+
+        selectionRegistrar.subselections = mapOf(
+            startSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = annotatedString.length,
+                    selectableId = startSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = annotatedString.length,
+                    selectableId = startSelectableId
+                ),
+                handlesCrossed = false
+            ),
+            middleSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = middleSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = middleSelectableId
+                ),
+                handlesCrossed = false
+            ),
+            endSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = endSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = endSelectableId
+                ),
+                handlesCrossed = false
+            ),
+        )
+
+        assertThat(selectionManager.isNonEmptySelection()).isFalse()
+    }
+
+    @Test
+    fun isNonEmptySelection_whenEmptySelection_multiLineCrossed_returnsFalse() {
+        val text = "Text Demo"
+        val annotatedString = AnnotatedString(text)
+        val startOffset = 0
+        val endOffset = text.length
+
+        selectionRegistrar.subscribe(endSelectable)
+        selectionRegistrar.subscribe(middleSelectable)
+        selectionRegistrar.subscribe(startSelectable)
+        selectionRegistrar.subscribe(lastSelectable)
+        selectionRegistrar.sorted = true
+        whenever(startSelectable.getText()).thenReturn(annotatedString)
+        whenever(middleSelectable.getText()).thenReturn(AnnotatedString(""))
+        whenever(endSelectable.getText()).thenReturn(annotatedString)
+        selectionManager.selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = startSelectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = endSelectableId
+            ),
+            handlesCrossed = true
+        )
+
+        selectionRegistrar.subselections = mapOf(
+            startSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = annotatedString.length,
+                    selectableId = startSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = annotatedString.length,
+                    selectableId = startSelectableId
+                ),
+                handlesCrossed = true
+            ),
+            middleSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = middleSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = middleSelectableId
+                ),
+                handlesCrossed = true
+            ),
+            endSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = endSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = endSelectableId
+                ),
+                handlesCrossed = true
+            ),
+        )
+
+        assertThat(selectionManager.isNonEmptySelection()).isFalse()
+    }
+
+    @Test
+    fun getSelectedText_selection_null_return_null() {
+        selectionManager.selection = null
+        selectionRegistrar.subselections = emptyMap()
+
+        assertThat(selectionManager.getSelectedText()).isNull()
+        assertThat(selectable.getTextCalledTimes).isEqualTo(0)
+    }
+
+    @Test
+    fun getSelectedText_not_crossed_single_widget() {
+        val text = "Text Demo"
+        val annotatedString = AnnotatedString(text = text)
+        val startOffset = text.indexOf('e')
+        val endOffset = text.indexOf('m')
+        selectable.textToReturn = annotatedString
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = selectableId
+            ),
+            handlesCrossed = false
+        )
+        selectionManager.selection = selection
+        selectionRegistrar.subselections = mapOf(selectableId to selection)
+
+        assertThat(selectionManager.getSelectedText())
+            .isEqualTo(annotatedString.subSequence(startOffset, endOffset))
+        assertThat(selectable.getTextCalledTimes).isEqualTo(1)
+    }
+
+    @Test
+    fun getSelectedText_crossed_single_widget() {
+        val text = "Text Demo"
+        val annotatedString = AnnotatedString(text = text)
+        val startOffset = text.indexOf('m')
+        val endOffset = text.indexOf('x')
+        selectable.textToReturn = annotatedString
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = selectableId
+            ),
+            handlesCrossed = true
+        )
+        selectionManager.selection = selection
+        selectionRegistrar.subselections = mapOf(selectableId to selection)
+
+        assertThat(selectionManager.getSelectedText())
+            .isEqualTo(annotatedString.subSequence(endOffset, startOffset))
+        assertThat(selectable.getTextCalledTimes).isEqualTo(1)
+    }
+
+    @Test
+    fun getSelectedText_not_crossed_multi_widgets() {
+        val text = "Text Demo"
+        val annotatedString = AnnotatedString(text = text)
+        val startOffset = text.indexOf('m')
+        val endOffset = text.indexOf('x')
+
+        selectionRegistrar.subscribe(startSelectable)
+        selectionRegistrar.subscribe(middleSelectable)
+        selectionRegistrar.subscribe(endSelectable)
+        selectionRegistrar.subscribe(lastSelectable)
+        selectionRegistrar.sorted = true
+        whenever(startSelectable.getText()).thenReturn(annotatedString)
+        whenever(middleSelectable.getText()).thenReturn(annotatedString)
+        whenever(endSelectable.getText()).thenReturn(annotatedString)
+        selectionManager.selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = startSelectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = endSelectableId
+            ),
+            handlesCrossed = false
+        )
+
+        selectionRegistrar.subselections = mapOf(
+            startSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = startOffset,
+                    selectableId = startSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = annotatedString.length,
+                    selectableId = startSelectableId
+                ),
+                handlesCrossed = false
+            ),
+            middleSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = middleSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = annotatedString.length,
+                    selectableId = middleSelectableId
+                ),
+                handlesCrossed = false
+            ),
+            endSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = endSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = endOffset,
+                    selectableId = endSelectableId
+                ),
+                handlesCrossed = false
+            ),
+        )
+
+        val result = annotatedString.subSequence(startOffset, annotatedString.length) +
+            annotatedString + annotatedString.subSequence(0, endOffset)
+        assertThat(selectionManager.getSelectedText()).isEqualTo(result)
+        assertThat(selectable.getTextCalledTimes).isEqualTo(0)
+        verify(startSelectable, times(1)).getText()
+        verify(middleSelectable, times(1)).getText()
+        verify(endSelectable, times(1)).getText()
+        verify(lastSelectable, times(0)).getText()
+    }
+
+    @Test
+    fun getSelectedText_crossed_multi_widgets() {
+        val text = "Text Demo"
+        val annotatedString = AnnotatedString(text = text)
+        val startOffset = text.indexOf('m')
+        val endOffset = text.indexOf('x')
+
+        selectionRegistrar.subscribe(endSelectable)
+        selectionRegistrar.subscribe(middleSelectable)
+        selectionRegistrar.subscribe(startSelectable)
+        selectionRegistrar.subscribe(lastSelectable)
+        selectionRegistrar.sorted = true
+        whenever(startSelectable.getText()).thenReturn(annotatedString)
+        whenever(middleSelectable.getText()).thenReturn(annotatedString)
+        whenever(endSelectable.getText()).thenReturn(annotatedString)
+        selectionManager.selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = startSelectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = endSelectableId
+            ),
+            handlesCrossed = true
+        )
+
+        selectionRegistrar.subselections = mapOf(
+            endSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = annotatedString.length,
+                    selectableId = endSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = endOffset,
+                    selectableId = endSelectableId
+                ),
+                handlesCrossed = true
+            ),
+            middleSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = annotatedString.length,
+                    selectableId = middleSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = middleSelectableId
+                ),
+                handlesCrossed = true
+            ),
+            startSelectableId to Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = startOffset,
+                    selectableId = startSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = startSelectableId
+                ),
+                handlesCrossed = true
+            ),
+        )
+
+        val result = annotatedString.subSequence(endOffset, annotatedString.length) +
+            annotatedString + annotatedString.subSequence(0, startOffset)
+        assertThat(selectionManager.getSelectedText()).isEqualTo(result)
+        assertThat(selectable.getTextCalledTimes).isEqualTo(0)
+        verify(startSelectable, times(1)).getText()
+        verify(middleSelectable, times(1)).getText()
+        verify(endSelectable, times(1)).getText()
+        verify(lastSelectable, times(0)).getText()
+    }
+
+    @Test
+    fun copy_selection_null_not_trigger_clipboardManager() {
+        selectionManager.selection = null
+        selectionRegistrar.subselections = emptyMap()
+
+        selectionManager.copy()
+
+        verify(clipboardManager, times(0)).setText(any())
+    }
+
+    @Test
+    fun copy_selection_not_null_trigger_clipboardManager_setText() {
+        val text = "Text Demo"
+        val annotatedString = AnnotatedString(text = text)
+        val startOffset = text.indexOf('m')
+        val endOffset = text.indexOf('x')
+        selectable.textToReturn = annotatedString
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = selectableId
+            ),
+            handlesCrossed = true
+        )
+        selectionManager.selection = selection
+        selectionRegistrar.subselections = mapOf(selectableId to selection)
+
+        selectionManager.copy()
+
+        verify(clipboardManager, times(1)).setText(
+            annotatedString.subSequence(
+                endOffset,
+                startOffset
+            )
+        )
+    }
+
+    @Test
+    fun showSelectionToolbar_trigger_textToolbar_showMenu() {
+        val text = "Text Demo"
+        val annotatedString = AnnotatedString(text = text)
+        val startOffset = text.indexOf('m')
+        val endOffset = text.indexOf('x')
+        selectable.textToReturn = annotatedString
+        selectable.layoutCoordinatesToReturn = containerLayoutCoordinates
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = selectableId
+            ),
+            handlesCrossed = true
+        )
+        selectionManager.selection = selection
+        selectionRegistrar.subselections = mapOf(selectableId to selection)
+        selectionManager.hasFocus = true
+
+        selectionManager.showToolbar = true
+
+        verify(textToolbar, times(1)).showMenu(
+            any(),
+            any(),
+            isNull(),
+            isNull(),
+            isNull()
+        )
+    }
+
+    @Test
+    fun showSelectionToolbar_withoutFocus_notTrigger_textToolbar_showMenu() {
+        val text = "Text Demo"
+        val annotatedString = AnnotatedString(text = text)
+        val startOffset = text.indexOf('m')
+        val endOffset = text.indexOf('x')
+        selectable.textToReturn = annotatedString
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = selectableId
+            ),
+            handlesCrossed = true
+        )
+        selectionManager.selection = selection
+        selectionRegistrar.subselections = mapOf(selectableId to selection)
+        selectionManager.hasFocus = false
+
+        selectionManager.showToolbar = true
+
+        verify(textToolbar, never()).showMenu(
+            any(),
+            any(),
+            isNull(),
+            isNull(),
+            isNull()
+        )
+    }
+
+    @Test
+    fun onRelease_selectionMap_is_setToEmpty() {
+        val fakeSelection =
+            Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = startSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 5,
+                    selectableId = endSelectableId
+                )
+            )
+        var selection: Selection? = fakeSelection
+        val lambda: (Selection?) -> Unit = { selection = it }
+        val spyLambda = spy(lambda)
+        selectionManager.onSelectionChange = spyLambda
+        selectionManager.selection = fakeSelection
+
+        selectionManager.onRelease()
+
+        verify(selectionRegistrar).subselections = emptyMap()
+
+        assertThat(selection).isNull()
+        verify(spyLambda, times(1)).invoke(null)
+        verify(
+            hapticFeedback,
+            times(1)
+        ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
+    }
+
+    @Test
+    fun notifySelectableChange_clears_selection() {
+        val fakeSelection =
+            Selection(
+                start = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 0,
+                    selectableId = startSelectableId
+                ),
+                end = Selection.AnchorInfo(
+                    direction = ResolvedTextDirection.Ltr,
+                    offset = 5,
+                    selectableId = startSelectableId
+                )
+            )
+        var selection: Selection? = fakeSelection
+        val lambda: (Selection?) -> Unit = { selection = it }
+        val spyLambda = spy(lambda)
+        selectionManager.onSelectionChange = spyLambda
+        selectionManager.selection = fakeSelection
+
+        selectionRegistrar.subselections = mapOf(
+            startSelectableId to fakeSelection
+        )
+        selectionRegistrar.notifySelectableChange(startSelectableId)
+
+        verify(selectionRegistrar).subselections = emptyMap()
+        assertThat(selection).isNull()
+        verify(spyLambda, times(1)).invoke(null)
+        verify(
+            hapticFeedback,
+            times(1)
+        ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
+    }
+}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionModeTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionModeTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionModeTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionModeTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImplTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImplTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImplTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImplTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/StringHelpersTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/StringHelpersTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/StringHelpersTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/StringHelpersTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/AllCapsTransformationTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/AllCapsTransformationTest.kt
new file mode 100644
index 0000000..1f20b17
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/AllCapsTransformationTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.foundation.text2.input
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.intl.Locale
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalFoundationApi::class)
+@RunWith(JUnit4::class)
+class AllCapsTransformationTest {
+
+    @Test
+    fun allCapsTransformation_definesCharacterCapitalizationKeyboardOption() {
+        val transformation = InputTransformation.allCaps(Locale.current)
+        assertThat(transformation.keyboardOptions?.capitalization)
+            .isEqualTo(KeyboardCapitalization.Characters)
+    }
+
+    @Test
+    fun allNewTypedCharacters_convertedToUppercase() {
+        val transformation = InputTransformation.allCaps(Locale("en_US"))
+
+        val originalValue = TextFieldCharSequence("")
+        val buffer = TextFieldBuffer(originalValue).apply {
+            append("hello")
+        }
+
+        transformation.transformInput(originalValue, buffer)
+
+        assertThat(buffer.toString()).isEqualTo("HELLO")
+    }
+
+    @Test
+    fun oldCharacters_areNotConverted() {
+        val transformation = InputTransformation.allCaps(Locale("en_US"))
+
+        val originalValue = TextFieldCharSequence("hello")
+        val buffer = TextFieldBuffer(originalValue).apply {
+            append(" world")
+        }
+
+        transformation.transformInput(originalValue, buffer)
+
+        assertThat(buffer.toString()).isEqualTo("hello WORLD")
+    }
+
+    @Test
+    fun localeDifference_turkishI() {
+        val transformation = InputTransformation.allCaps(Locale("tr"))
+
+        val originalValue = TextFieldCharSequence("")
+        val buffer = TextFieldBuffer(originalValue).apply {
+            append("i")
+        }
+
+        transformation.transformInput(originalValue, buffer)
+
+        assertThat(buffer.toString()).isEqualTo("\u0130") // Turkish dotted capital i
+    }
+
+    @Test
+    fun multipleEdits() {
+        val transformation = InputTransformation.allCaps(Locale("en_US"))
+
+        var originalValue = TextFieldCharSequence("hello")
+        var buffer = TextFieldBuffer(originalValue)
+
+        with(buffer) {
+            delete(0, 3) // lo
+            replace(1, 1, "abc") // lABCo
+        }
+
+        transformation.transformInput(originalValue, buffer)
+
+        originalValue = buffer.toTextFieldCharSequence()
+        buffer = TextFieldBuffer(originalValue)
+
+        with(buffer) {
+            delete(2, 3) // lACo
+            append("xyz") // lACoXYZ
+        }
+
+        transformation.transformInput(originalValue, buffer)
+
+        assertThat(buffer.toString()).isEqualTo("lACoXYZ")
+    }
+}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/CodepointTransformationTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/CodepointTransformationTest.kt
new file mode 100644
index 0000000..21ef966
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/CodepointTransformationTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.foundation.text2.input
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text2.input.internal.OffsetMappingCalculator
+import androidx.compose.ui.text.TextRange
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalFoundationApi::class)
+@RunWith(JUnit4::class)
+class CodepointTransformationTest {
+
+    @Test
+    fun toVisualText_codepointIndices() {
+        val source =
+            TextFieldCharSequence("a${SurrogateCodepointString}c$SurrogateCodepointString")
+        val offsetMapping = OffsetMappingCalculator()
+        val codepointTransformation = CodepointTransformation { i, codepoint ->
+            val expectedCodePoint = when (i) {
+                0 -> 'a'.code
+                1 -> SurrogateCodepoint
+                2 -> 'c'.code
+                3 -> SurrogateCodepoint
+                else -> fail("Invalid codepoint index: $i")
+            }
+            assertThat(codepoint).isEqualTo(expectedCodePoint)
+            codepoint
+        }
+
+        source.toVisualText(codepointTransformation, offsetMapping)
+    }
+
+    @Test
+    fun toVisualText_mapsOffsetsForward() {
+        val source = TextFieldCharSequence("a${SurrogateCodepointString}c")
+        val offsetMapping = OffsetMappingCalculator()
+        val codepointTransformation = CodepointTransformation { i, codepoint ->
+            when (codepoint) {
+                'a'.code, 'c'.code -> SurrogateCodepoint
+                SurrogateCodepoint -> 'b'.code
+                else -> fail(
+                    "codepointIndex=$i, codepoint=\"${
+                        String(intArrayOf(codepoint), 0, 1)
+                    }\""
+                )
+            }
+        }
+        val visual = source.toVisualText(codepointTransformation, offsetMapping)
+
+        assertThat(visual.toString())
+            .isEqualTo("${SurrogateCodepointString}b$SurrogateCodepointString")
+
+        listOf(
+            0 to TextRange(0),
+            1 to TextRange(2),
+            2 to TextRange(2, 3),
+            3 to TextRange(3),
+            4 to TextRange(5),
+        ).forEach { (source, dest) ->
+            assertWithMessage("Mapping from untransformed offset $source")
+                .that(offsetMapping.mapFromSource(source)).isEqualTo(dest)
+        }
+    }
+
+    @Test
+    fun toVisualText_mapsOffsetsBackward() {
+        val source = TextFieldCharSequence("a${SurrogateCodepointString}c")
+        val offsetMapping = OffsetMappingCalculator()
+        val codepointTransformation = CodepointTransformation { i, codepoint ->
+            when (codepoint) {
+                'a'.code, 'c'.code -> SurrogateCodepoint
+                SurrogateCodepoint -> 'b'.code
+                else -> fail(
+                    "codepointIndex=$i, codepoint=\"${
+                        String(intArrayOf(codepoint), 0, 1)
+                    }\""
+                )
+            }
+        }
+        val visual = source.toVisualText(codepointTransformation, offsetMapping)
+
+        assertThat(visual.toString())
+            .isEqualTo("${SurrogateCodepointString}b$SurrogateCodepointString")
+
+        listOf(
+            0 to TextRange(0),
+            1 to TextRange(0, 1),
+            2 to TextRange(1),
+            3 to TextRange(3),
+            4 to TextRange(3, 4),
+            5 to TextRange(4),
+        ).forEach { (dest, source) ->
+            assertWithMessage("Mapping from transformed offset $dest")
+                .that(offsetMapping.mapFromDest(dest)).isEqualTo(source)
+        }
+    }
+
+    private companion object {
+        /** This is "𐐷", a surrogate codepoint. */
+        val SurrogateCodepoint = Character.toCodePoint('\uD801', '\uDC37')
+        const val SurrogateCodepointString = "\uD801\uDC37"
+    }
+}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/InputTransformationTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/InputTransformationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/InputTransformationTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/InputTransformationTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/TextFieldBufferTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/TextFieldBufferTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/TextFieldBufferTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/TextFieldBufferTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/TextFieldCharSequenceTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCharSequenceTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/TextFieldCharSequenceTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCharSequenceTest.kt
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/TextFieldStateSaverTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/TextFieldStateSaverTest.kt
new file mode 100644
index 0000000..ef655cf
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/TextFieldStateSaverTest.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.foundation.text2.input
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text2.input.internal.commitText
+import androidx.compose.runtime.saveable.SaverScope
+import androidx.compose.ui.text.TextRange
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
+import org.junit.Test
+
+@OptIn(ExperimentalFoundationApi::class)
+class TextFieldStateSaverTest {
+
+    @Test
+    fun savesAndRestoresTextAndSelection() {
+        val state = TextFieldState("hello, world", initialSelectionInChars = TextRange(0, 5))
+
+        val saved = with(TextFieldState.Saver) { TestSaverScope.save(state) }
+        assertNotNull(saved)
+        val restoredState = TextFieldState.Saver.restore(saved)
+
+        assertNotNull(restoredState)
+        assertThat(restoredState.text.toString()).isEqualTo("hello, world")
+        assertThat(restoredState.text.selectionInChars).isEqualTo(TextRange(0, 5))
+    }
+
+    @Test
+    fun savesAndRestoresUndo() {
+        val state = TextFieldState("hello, world", initialSelectionInChars = TextRange(0, 5))
+
+        state.editAsUser(null) {
+            commitText("hi", 1)
+        }
+
+        val saved = with(TextFieldState.Saver) { TestSaverScope.save(state) }
+        assertNotNull(saved)
+        val restoredState = TextFieldState.Saver.restore(saved)
+
+        assertNotNull(restoredState)
+        assertThat(restoredState.text.toString()).isEqualTo("hi, world")
+        assertThat(restoredState.undoState.canUndo).isTrue()
+        restoredState.undoState.undo()
+        assertThat(restoredState.text.toString()).isEqualTo("hello, world")
+        assertThat(restoredState.text.selectionInChars).isEqualTo(TextRange(0, 5))
+    }
+
+    private object TestSaverScope : SaverScope {
+        override fun canBeSaved(value: Any): Boolean = true
+    }
+}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/TextFieldStateTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/TextFieldStateTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/TextFieldStateTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/TextFieldStateTest.kt
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/ChangeTrackerTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/ChangeTrackerTest.kt
new file mode 100644
index 0000000..5de9db6
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/ChangeTrackerTest.kt
@@ -0,0 +1,225 @@
+/*
+ * 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.foundation.text2.input.internal
+
+import androidx.compose.ui.text.TextRange
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ChangeTrackerTest {
+
+    @Test
+    fun initialInsert() {
+        val buffer = SimpleBuffer()
+
+        buffer.append("hello")
+
+        assertThat(buffer.toString()).isEqualTo("hello")
+        assertThat(buffer.changes.changeCount).isEqualTo(1)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 5))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0))
+    }
+
+    @Test
+    fun deleteAll() {
+        val buffer = SimpleBuffer("hello")
+
+        buffer.replace("hello", "")
+
+        assertThat(buffer.toString()).isEqualTo("")
+        assertThat(buffer.changes.changeCount).isEqualTo(1)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 0))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 5))
+    }
+
+    @Test
+    fun multipleDiscontinuousChanges() {
+        val buffer = SimpleBuffer("hello world")
+
+        buffer.replace("world", "Compose")
+        buffer.replace("hello", "goodbye")
+
+        assertThat(buffer.toString()).isEqualTo("goodbye Compose")
+        assertThat(buffer.changes.changeCount).isEqualTo(2)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 7))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 5))
+        assertThat(buffer.changes.getRange(1)).isEqualTo(TextRange(8, 15))
+        assertThat(buffer.changes.getOriginalRange(1)).isEqualTo(TextRange(6, 11))
+    }
+
+    @Test
+    fun twoAppends() {
+        val buffer = SimpleBuffer()
+
+        buffer.append("foo")
+        buffer.append("bar")
+
+        assertThat(buffer.toString()).isEqualTo("foobar")
+        assertThat(buffer.changes.changeCount).isEqualTo(1)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 6))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0))
+    }
+
+    @Test
+    fun threeAppends() {
+        val buffer = SimpleBuffer()
+
+        buffer.append("foo")
+        buffer.append("bar")
+        buffer.append("baz")
+
+        assertThat(buffer.toString()).isEqualTo("foobarbaz")
+        assertThat(buffer.changes.changeCount).isEqualTo(1)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 9))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0))
+    }
+
+    @Test
+    fun replaceWithReversedIndices() {
+        val buffer = SimpleBuffer("abcd")
+
+        buffer.replace(2, 0, "e")
+
+        assertThat(buffer.toString()).isEqualTo("ecd")
+        assertThat(buffer.changes.changeCount).isEqualTo(1)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 1))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 2))
+    }
+
+    @Test
+    fun multipleAdjacentReplaces_whenPerformedInOrder_replacementsShorter() {
+        val buffer = SimpleBuffer("abcd")
+
+        buffer.replace("ab", "e") // ecd
+        buffer.replace("cd", "f")
+
+        assertThat(buffer.toString()).isEqualTo("ef")
+        assertThat(buffer.changes.changeCount).isEqualTo(1)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 2))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 4))
+    }
+
+    @Test
+    fun multipleAdjacentReplaces_whenPerformedInOrder_replacementsLonger() {
+        val buffer = SimpleBuffer("abcd")
+
+        buffer.replace("ab", "efg") // efgcd
+        buffer.replace("cd", "hij")
+
+        assertThat(buffer.toString()).isEqualTo("efghij")
+        assertThat(buffer.changes.changeCount).isEqualTo(1)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 6))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 4))
+    }
+
+    @Test
+    fun multipleAdjacentReplaces_whenPerformedInReverseOrder_replacementsShorter() {
+        val buffer = SimpleBuffer("abcd")
+
+        buffer.replace("cd", "f") // abf
+        buffer.replace("ab", "e")
+
+        assertThat(buffer.toString()).isEqualTo("ef")
+        assertThat(buffer.changes.changeCount).isEqualTo(1)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 2))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 4))
+    }
+
+    @Test
+    fun multipleAdjacentReplaces_whenPerformedInReverseOrder_replacementsLonger() {
+        val buffer = SimpleBuffer("abcd")
+
+        buffer.replace("cd", "efg") // abhij
+        buffer.replace("ab", "hij")
+
+        assertThat(buffer.toString()).isEqualTo("hijefg")
+        assertThat(buffer.changes.changeCount).isEqualTo(1)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 6))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 4))
+    }
+
+    @Test
+    fun multiplePartiallyOverlappingChanges_atStart() {
+        val buffer = SimpleBuffer("abcd")
+
+        buffer.replace("bc", "ef") // aefd
+        buffer.replace("ae", "gh")
+
+        assertThat(buffer.toString()).isEqualTo("ghfd")
+        // Overlapping changes are merged.
+        assertThat(buffer.changes.changeCount).isEqualTo(1)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 3))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 3))
+    }
+
+    @Test
+    fun multiplePartiallyOverlappingChanges_atEnd() {
+        val buffer = SimpleBuffer("abcd")
+
+        buffer.replace("bc", "ef") // aefd
+        buffer.replace("fd", "gh")
+
+        assertThat(buffer.toString()).isEqualTo("aegh")
+        // Overlapping changes are merged.
+        assertThat(buffer.changes.changeCount).isEqualTo(1)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(1, 4))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(1, 4))
+    }
+
+    @Test
+    fun multipleFullyOverlappingChanges() {
+        val buffer = SimpleBuffer("abcd")
+
+        buffer.replace("bc", "ef") // aefd
+        buffer.replace("ef", "gh")
+
+        assertThat(buffer.toString()).isEqualTo("aghd")
+        // Overlapping changes are merged.
+        assertThat(buffer.changes.changeCount).isEqualTo(1)
+        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(1, 3))
+        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(1, 3))
+    }
+
+    private class SimpleBuffer(initialText: String = "") {
+        private val builder = StringBuilder(initialText)
+        val changes = ChangeTracker()
+
+        fun append(text: String) {
+            changes.trackChange(builder.length, builder.length, text.length)
+            builder.append(text)
+        }
+
+        fun replace(substring: String, text: String) {
+            val start = builder.indexOf(substring)
+            if (start != -1) {
+                val end = start + substring.length
+                changes.trackChange(start, end, text.length)
+                builder.replace(start, end, text)
+            }
+        }
+
+        fun replace(start: Int, end: Int, text: String) {
+            changes.trackChange(start, end, text.length)
+            builder.replace(minOf(start, end), maxOf(start, end), text)
+        }
+
+        override fun toString(): String = builder.toString()
+    }
+}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/CommitTextCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/CommitTextCommandTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/CommitTextCommandTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/CommitTextCommandTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/DeleteSurroundingTextCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/DeleteSurroundingTextCommandTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/DeleteSurroundingTextCommandTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/DeleteSurroundingTextCommandTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/DeleteSurroundingTextInCodePointsCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/DeleteSurroundingTextInCodePointsCommandTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/DeleteSurroundingTextInCodePointsCommandTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/DeleteSurroundingTextInCodePointsCommandTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferDeleteRangeTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferDeleteRangeTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferDeleteRangeTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferDeleteRangeTest.kt
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferTest.kt
new file mode 100644
index 0000000..6dca492
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferTest.kt
@@ -0,0 +1,483 @@
+/*
+ * 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.foundation.text2.input.internal
+
+import androidx.compose.foundation.text2.input.internal.matchers.assertThat
+import androidx.compose.ui.text.TextRange
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class EditingBufferTest {
+
+    @Test
+    fun insert() {
+        val eb = EditingBuffer("", TextRange.Zero)
+
+        eb.replace(0, 0, "A")
+
+        assertThat(eb).hasChars("A")
+        assertThat(eb.cursor).isEqualTo(1)
+        assertThat(eb.selectionStart).isEqualTo(1)
+        assertThat(eb.selectionEnd).isEqualTo(1)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+
+        // Keep inserting text to the end of string. Cursor should follow.
+        eb.replace(1, 1, "BC")
+        assertThat(eb).hasChars("ABC")
+        assertThat(eb.cursor).isEqualTo(3)
+        assertThat(eb.selectionStart).isEqualTo(3)
+        assertThat(eb.selectionEnd).isEqualTo(3)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+
+        // Insert into middle position. Cursor should be end of inserted text.
+        eb.replace(1, 1, "D")
+        assertThat(eb).hasChars("ADBC")
+        assertThat(eb.cursor).isEqualTo(2)
+        assertThat(eb.selectionStart).isEqualTo(2)
+        assertThat(eb.selectionEnd).isEqualTo(2)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+    }
+
+    @Test
+    fun delete() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.replace(0, 1, "")
+
+        // Delete the left character at the cursor.
+        assertThat(eb).hasChars("BCDE")
+        assertThat(eb.cursor).isEqualTo(0)
+        assertThat(eb.selectionStart).isEqualTo(0)
+        assertThat(eb.selectionEnd).isEqualTo(0)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+
+        // Delete the text before the cursor
+        eb.replace(0, 2, "")
+        assertThat(eb).hasChars("DE")
+        assertThat(eb.cursor).isEqualTo(0)
+        assertThat(eb.selectionStart).isEqualTo(0)
+        assertThat(eb.selectionEnd).isEqualTo(0)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+
+        // Delete end of the text.
+        eb.replace(1, 2, "")
+        assertThat(eb).hasChars("D")
+        assertThat(eb.cursor).isEqualTo(1)
+        assertThat(eb.selectionStart).isEqualTo(1)
+        assertThat(eb.selectionEnd).isEqualTo(1)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+    }
+
+    @Test
+    fun setSelection() {
+        val eb = EditingBuffer("ABCDE", TextRange(0, 3))
+        assertThat(eb).hasChars("ABCDE")
+        assertThat(eb.cursor).isEqualTo(-1)
+        assertThat(eb.selectionStart).isEqualTo(0)
+        assertThat(eb.selectionEnd).isEqualTo(3)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+
+        eb.setSelection(0, 5) // Change the selection
+        assertThat(eb).hasChars("ABCDE")
+        assertThat(eb.cursor).isEqualTo(-1)
+        assertThat(eb.selectionStart).isEqualTo(0)
+        assertThat(eb.selectionEnd).isEqualTo(5)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+
+        eb.replace(0, 3, "X") // replace function cancel the selection and place cursor.
+        assertThat(eb).hasChars("XDE")
+        assertThat(eb.cursor).isEqualTo(1)
+        assertThat(eb.selectionStart).isEqualTo(1)
+        assertThat(eb.selectionEnd).isEqualTo(1)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+
+        eb.setSelection(0, 2) // Set the selection again
+        assertThat(eb).hasChars("XDE")
+        assertThat(eb.cursor).isEqualTo(-1)
+        assertThat(eb.selectionStart).isEqualTo(0)
+        assertThat(eb.selectionEnd).isEqualTo(2)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+    }
+
+    @Test fun setSelection_coerces_whenNegativeStart() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.setSelection(-1, 1)
+
+        assertThat(eb.selectionStart).isEqualTo(0)
+        assertThat(eb.selectionEnd).isEqualTo(1)
+    }
+
+    @Test fun setSelection_coerces_whenNegativeEnd() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.setSelection(1, -1)
+
+        assertThat(eb.selectionStart).isEqualTo(1)
+        assertThat(eb.selectionEnd).isEqualTo(0)
+    }
+
+    @Test
+    fun setSelection_allowReversedSelection() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+        eb.setSelection(4, 2)
+
+        assertThat(eb.selection).isEqualTo(TextRange(4, 2))
+    }
+
+    @Test
+    fun replace_reversedRegion() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+        eb.replace(3, 1, "FGHI")
+
+        assertThat(eb).hasChars("AFGHIDE")
+        assertThat(eb.cursor).isEqualTo(5)
+        assertThat(eb.selectionStart).isEqualTo(5)
+        assertThat(eb.selectionEnd).isEqualTo(5)
+    }
+
+    @Test
+    fun setComposition_and_cancelComposition() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.setComposition(0, 5) // Make all text as composition
+        assertThat(eb).hasChars("ABCDE")
+        assertThat(eb.cursor).isEqualTo(0)
+        assertThat(eb.selectionStart).isEqualTo(0)
+        assertThat(eb.selectionEnd).isEqualTo(0)
+        assertThat(eb.hasComposition()).isTrue()
+        assertThat(eb.compositionStart).isEqualTo(0)
+        assertThat(eb.compositionEnd).isEqualTo(5)
+
+        eb.replace(2, 3, "X") // replace function cancel the composition text.
+        assertThat(eb).hasChars("ABXDE")
+        assertThat(eb.cursor).isEqualTo(3)
+        assertThat(eb.selectionStart).isEqualTo(3)
+        assertThat(eb.selectionEnd).isEqualTo(3)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+
+        eb.setComposition(2, 4) // set composition again
+        assertThat(eb).hasChars("ABXDE")
+        assertThat(eb.cursor).isEqualTo(3)
+        assertThat(eb.selectionStart).isEqualTo(3)
+        assertThat(eb.selectionEnd).isEqualTo(3)
+        assertThat(eb.hasComposition()).isTrue()
+        assertThat(eb.compositionStart).isEqualTo(2)
+        assertThat(eb.compositionEnd).isEqualTo(4)
+    }
+
+    @Test
+    fun setComposition_and_commitComposition() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.setComposition(0, 5) // Make all text as composition
+        assertThat(eb).hasChars("ABCDE")
+        assertThat(eb.cursor).isEqualTo(0)
+        assertThat(eb.selectionStart).isEqualTo(0)
+        assertThat(eb.selectionEnd).isEqualTo(0)
+        assertThat(eb.hasComposition()).isTrue()
+        assertThat(eb.compositionStart).isEqualTo(0)
+        assertThat(eb.compositionEnd).isEqualTo(5)
+
+        eb.replace(2, 3, "X") // replace function cancel the composition text.
+        assertThat(eb).hasChars("ABXDE")
+        assertThat(eb.cursor).isEqualTo(3)
+        assertThat(eb.selectionStart).isEqualTo(3)
+        assertThat(eb.selectionEnd).isEqualTo(3)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+
+        eb.setComposition(2, 4) // set composition again
+        assertThat(eb).hasChars("ABXDE")
+        assertThat(eb.cursor).isEqualTo(3)
+        assertThat(eb.selectionStart).isEqualTo(3)
+        assertThat(eb.selectionEnd).isEqualTo(3)
+        assertThat(eb.hasComposition()).isTrue()
+        assertThat(eb.compositionStart).isEqualTo(2)
+        assertThat(eb.compositionEnd).isEqualTo(4)
+
+        eb.commitComposition() // commit the composition
+        assertThat(eb).hasChars("ABXDE")
+        assertThat(eb.cursor).isEqualTo(3)
+        assertThat(eb.selectionStart).isEqualTo(3)
+        assertThat(eb.selectionEnd).isEqualTo(3)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+    }
+
+    @Test
+    fun setCursor_and_get_cursor() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.cursor = 1
+        assertThat(eb).hasChars("ABCDE")
+        assertThat(eb.cursor).isEqualTo(1)
+        assertThat(eb.selectionStart).isEqualTo(1)
+        assertThat(eb.selectionEnd).isEqualTo(1)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+
+        eb.cursor = 2
+        assertThat(eb).hasChars("ABCDE")
+        assertThat(eb.cursor).isEqualTo(2)
+        assertThat(eb.selectionStart).isEqualTo(2)
+        assertThat(eb.selectionEnd).isEqualTo(2)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+
+        eb.cursor = 5
+        assertThat(eb).hasChars("ABCDE")
+        assertThat(eb.cursor).isEqualTo(5)
+        assertThat(eb.selectionStart).isEqualTo(5)
+        assertThat(eb.selectionEnd).isEqualTo(5)
+        assertThat(eb.hasComposition()).isFalse()
+        assertThat(eb.compositionStart).isEqualTo(-1)
+        assertThat(eb.compositionEnd).isEqualTo(-1)
+    }
+
+    @Test
+    fun delete_preceding_cursor_no_composition() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.delete(1, 2)
+        assertThat(eb).hasChars("ACDE")
+        assertThat(eb.cursor).isEqualTo(0)
+        assertThat(eb.hasComposition()).isFalse()
+    }
+
+    @Test
+    fun delete_trailing_cursor_no_composition() {
+        val eb = EditingBuffer("ABCDE", TextRange(3))
+
+        eb.delete(1, 2)
+        assertThat(eb).hasChars("ACDE")
+        assertThat(eb.cursor).isEqualTo(2)
+        assertThat(eb.hasComposition()).isFalse()
+    }
+
+    @Test
+    fun delete_preceding_selection_no_composition() {
+        val eb = EditingBuffer("ABCDE", TextRange(0, 1))
+
+        eb.delete(1, 2)
+        assertThat(eb).hasChars("ACDE")
+        assertThat(eb.selectionStart).isEqualTo(0)
+        assertThat(eb.selectionEnd).isEqualTo(1)
+        assertThat(eb.hasComposition()).isFalse()
+    }
+
+    @Test
+    fun delete_trailing_selection_no_composition() {
+        val eb = EditingBuffer("ABCDE", TextRange(4, 5))
+
+        eb.delete(1, 2)
+        assertThat(eb).hasChars("ACDE")
+        assertThat(eb.selectionStart).isEqualTo(3)
+        assertThat(eb.selectionEnd).isEqualTo(4)
+        assertThat(eb.hasComposition()).isFalse()
+    }
+
+    @Test
+    fun delete_covered_cursor() {
+        // AB[]CDE
+        val eb = EditingBuffer("ABCDE", TextRange(2, 2))
+
+        eb.delete(1, 3)
+        // A[]DE
+        assertThat(eb).hasChars("ADE")
+        assertThat(eb.selectionStart).isEqualTo(1)
+        assertThat(eb.selectionEnd).isEqualTo(1)
+    }
+
+    @Test
+    fun delete_covered_selection() {
+        // A[BC]DE
+        val eb = EditingBuffer("ABCDE", TextRange(1, 3))
+
+        eb.delete(0, 4)
+        // []E
+        assertThat(eb).hasChars("E")
+        assertThat(eb.selectionStart).isEqualTo(0)
+        assertThat(eb.selectionEnd).isEqualTo(0)
+    }
+
+    @Test
+    fun delete_covered_reversedSelection() {
+        // A[BC]DE
+        val eb = EditingBuffer("ABCDE", TextRange(3, 1))
+
+        eb.delete(0, 4)
+        // []E
+        assertThat(eb).hasChars("E")
+        assertThat(eb.selectionStart).isEqualTo(0)
+        assertThat(eb.selectionEnd).isEqualTo(0)
+    }
+
+    @Test
+    fun delete_intersects_first_half_of_selection() {
+        // AB[CD]E
+        val eb = EditingBuffer("ABCDE", TextRange(2, 4))
+
+        eb.delete(1, 3)
+        // A[D]E
+        assertThat(eb).hasChars("ADE")
+        assertThat(eb.selectionStart).isEqualTo(1)
+        assertThat(eb.selectionEnd).isEqualTo(2)
+    }
+
+    @Test
+    fun delete_intersects_first_half_of_reversedSelection() {
+        // AB[CD]E
+        val eb = EditingBuffer("ABCDE", TextRange(4, 2))
+
+        eb.delete(3, 1)
+        // A[D]E
+        assertThat(eb).hasChars("ADE")
+        assertThat(eb.selectionStart).isEqualTo(1)
+        assertThat(eb.selectionEnd).isEqualTo(2)
+    }
+
+    @Test
+    fun delete_intersects_second_half_of_selection() {
+        // A[BCD]EFG
+        val eb = EditingBuffer("ABCDEFG", TextRange(1, 4))
+
+        eb.delete(3, 5)
+        // A[BC]FG
+        assertThat(eb).hasChars("ABCFG")
+        assertThat(eb.selectionStart).isEqualTo(1)
+        assertThat(eb.selectionEnd).isEqualTo(3)
+    }
+
+    @Test
+    fun delete_intersects_second_half_of_reversedSelection() {
+        // A[BCD]EFG
+        val eb = EditingBuffer("ABCDEFG", TextRange(4, 1))
+
+        eb.delete(5, 3)
+        // A[BC]FG
+        assertThat(eb).hasChars("ABCFG")
+        assertThat(eb.selectionStart).isEqualTo(1)
+        assertThat(eb.selectionEnd).isEqualTo(3)
+    }
+
+    @Test
+    fun delete_preceding_composition_no_intersection() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.setComposition(1, 2)
+        eb.delete(2, 3)
+
+        assertThat(eb).hasChars("ABDE")
+        assertThat(eb.cursor).isEqualTo(0)
+        assertThat(eb.compositionStart).isEqualTo(1)
+        assertThat(eb.compositionEnd).isEqualTo(2)
+    }
+
+    @Test
+    fun delete_trailing_composition_no_intersection() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.setComposition(3, 4)
+        eb.delete(2, 3)
+
+        assertThat(eb).hasChars("ABDE")
+        assertThat(eb.cursor).isEqualTo(0)
+        assertThat(eb.compositionStart).isEqualTo(2)
+        assertThat(eb.compositionEnd).isEqualTo(3)
+    }
+
+    @Test
+    fun delete_preceding_composition_intersection() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.setComposition(1, 3)
+        eb.delete(2, 4)
+
+        assertThat(eb).hasChars("ABE")
+        assertThat(eb.cursor).isEqualTo(0)
+        assertThat(eb.compositionStart).isEqualTo(1)
+        assertThat(eb.compositionEnd).isEqualTo(2)
+    }
+
+    @Test
+    fun delete_trailing_composition_intersection() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.setComposition(3, 5)
+        eb.delete(2, 4)
+
+        assertThat(eb).hasChars("ABE")
+        assertThat(eb.cursor).isEqualTo(0)
+        assertThat(eb.compositionStart).isEqualTo(2)
+        assertThat(eb.compositionEnd).isEqualTo(3)
+    }
+
+    @Test
+    fun delete_composition_contains_delrange() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.setComposition(2, 5)
+        eb.delete(3, 4)
+
+        assertThat(eb).hasChars("ABCE")
+        assertThat(eb.cursor).isEqualTo(0)
+        assertThat(eb.compositionStart).isEqualTo(2)
+        assertThat(eb.compositionEnd).isEqualTo(4)
+    }
+
+    @Test
+    fun delete_delrange_contains_composition() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+
+        eb.setComposition(3, 4)
+        eb.delete(2, 5)
+
+        assertThat(eb).hasChars("AB")
+        assertThat(eb.cursor).isEqualTo(0)
+        assertThat(eb.hasComposition()).isFalse()
+    }
+}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/FinishComposingTextCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/FinishComposingTextCommandTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/FinishComposingTextCommandTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/FinishComposingTextCommandTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/GapBufferTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/GapBufferTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/GapBufferTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/GapBufferTest.kt
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/OffsetMappingCalculatorTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/OffsetMappingCalculatorTest.kt
new file mode 100644
index 0000000..782f6b6
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/OffsetMappingCalculatorTest.kt
@@ -0,0 +1,902 @@
+/*
+ * 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.foundation.text2.input.internal
+
+import androidx.compose.ui.text.TextRange
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class OffsetMappingCalculatorTest {
+
+    @Test
+    fun noChanges() {
+        val builder = TestEditBuffer()
+        builder.assertIdentityMapping()
+    }
+
+    @Test
+    fun insertCharIntoEmpty() {
+        val builder = TestEditBuffer()
+        builder.append("a")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0, 1),
+            1 to TextRange(2),
+            2 to TextRange(3),
+            3 to TextRange(4),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0),
+            2 to TextRange(1),
+        )
+    }
+
+    @Test
+    fun insertCharIntoMiddle() {
+        val builder = TestEditBuffer("ab")
+        builder.insert(1, "c")
+        assertThat(builder.toString()).isEqualTo("acb")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1, 2),
+            2 to TextRange(3),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1),
+            3 to TextRange(2),
+        )
+    }
+
+    @Test
+    fun deleteCharFromMiddle() {
+        val builder = TestEditBuffer("abc")
+        builder.delete(1)
+        assertThat(builder.toString()).isEqualTo("ac")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1),
+            3 to TextRange(2),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1, 2),
+            2 to TextRange(3),
+            3 to TextRange(4),
+        )
+    }
+
+    @Test
+    fun replaceCharInMiddle() {
+        val builder = TestEditBuffer("abc")
+        builder.replace(1, "d")
+        assertThat(builder.toString()).isEqualTo("adc")
+        builder.assertIdentityMapping()
+    }
+
+    @Test
+    fun insertStringIntoEmpty() {
+        val builder = TestEditBuffer("")
+        builder.append("ab")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0, 2),
+            1 to TextRange(3),
+            2 to TextRange(4),
+            3 to TextRange(5),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0),
+            2 to TextRange(0),
+            3 to TextRange(1),
+        )
+    }
+
+    @Test
+    fun insertStringIntoMiddle() {
+        val builder = TestEditBuffer("ab")
+        builder.insert(1, "cd")
+        assertThat(builder.toString()).isEqualTo("acdb")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1, 3),
+            2 to TextRange(4),
+            3 to TextRange(5),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1),
+            3 to TextRange(1),
+            4 to TextRange(2),
+            5 to TextRange(3),
+        )
+    }
+
+    @Test
+    fun deleteStringFromMiddle() {
+        val builder = TestEditBuffer("abcd")
+        builder.delete(1, 3)
+        assertThat(builder.toString()).isEqualTo("ad")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1),
+            3 to TextRange(1),
+            4 to TextRange(2),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1, 3),
+            2 to TextRange(4),
+            3 to TextRange(5),
+        )
+    }
+
+    @Test
+    fun replaceStringWithEqualLengthInMiddle() {
+        val builder = TestEditBuffer("abcd")
+        builder.replace(1, 3, "ef")
+        assertThat(builder.toString()).isEqualTo("aefd")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1, 3),
+            3 to TextRange(3),
+            4 to TextRange(4),
+            5 to TextRange(5),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1, 3),
+            3 to TextRange(3),
+            4 to TextRange(4),
+            5 to TextRange(5),
+        )
+    }
+
+    @Test
+    fun replaceStringWithLongerInMiddle() {
+        val builder = TestEditBuffer("abcd")
+        builder.replace(1, 3, "efg")
+        assertThat(builder.toString()).isEqualTo("aefgd")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1, 4),
+            3 to TextRange(4),
+            4 to TextRange(5),
+            5 to TextRange(6),
+            6 to TextRange(7),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1, 3),
+            3 to TextRange(1, 3),
+            4 to TextRange(3),
+            5 to TextRange(4),
+        )
+    }
+
+    @Test
+    fun replaceStringWithShorterInMiddle() {
+        val builder = TestEditBuffer("abcd")
+        builder.replace(1, 3, "e")
+        assertThat(builder.toString()).isEqualTo("aed")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1, 2),
+            3 to TextRange(2),
+            4 to TextRange(3),
+            5 to TextRange(4),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(3),
+            3 to TextRange(4),
+            4 to TextRange(5),
+            5 to TextRange(6),
+        )
+    }
+
+    @Test
+    fun replaceAllWithEqualLength() {
+        val builder = TestEditBuffer("abcd")
+        builder.replace("efgh")
+        assertThat(builder.toString()).isEqualTo("efgh")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(0, 4),
+            2 to TextRange(0, 4),
+            3 to TextRange(0, 4),
+            4 to TextRange(4),
+            5 to TextRange(5),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0, 4),
+            2 to TextRange(0, 4),
+            3 to TextRange(0, 4),
+            4 to TextRange(4),
+            5 to TextRange(5),
+        )
+    }
+
+    @Test
+    fun replaceAllWithLonger() {
+        val builder = TestEditBuffer("abcd")
+        builder.replace("efghi")
+        assertThat(builder.toString()).isEqualTo("efghi")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(0, 5),
+            2 to TextRange(0, 5),
+            3 to TextRange(0, 5),
+            4 to TextRange(5),
+            5 to TextRange(6),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0, 4),
+            2 to TextRange(0, 4),
+            3 to TextRange(0, 4),
+            4 to TextRange(0, 4),
+            5 to TextRange(4),
+            6 to TextRange(5),
+        )
+    }
+
+    @Test
+    fun replaceAllWithShorter() {
+        val builder = TestEditBuffer("abcd")
+        builder.replace("ef")
+        assertThat(builder.toString()).isEqualTo("ef")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(0, 2),
+            2 to TextRange(0, 2),
+            3 to TextRange(0, 2),
+            4 to TextRange(2),
+            5 to TextRange(3),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0, 4),
+            2 to TextRange(4),
+            3 to TextRange(5),
+            4 to TextRange(6),
+            5 to TextRange(7),
+        )
+    }
+
+    @Test
+    fun prependCharToString() {
+        val builder = TestEditBuffer("a")
+        builder.insert(0, "b")
+        assertThat(builder.toString()).isEqualTo("ba")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0, 1),
+            1 to TextRange(2),
+            2 to TextRange(3),
+            3 to TextRange(4),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0),
+            2 to TextRange(1),
+        )
+    }
+
+    @Test
+    fun prependStringToString() {
+        val builder = TestEditBuffer("a")
+        builder.insert(0, "bc")
+        assertThat(builder.toString()).isEqualTo("bca")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0, 2),
+            1 to TextRange(3),
+            2 to TextRange(4),
+            3 to TextRange(5),
+            4 to TextRange(6),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0),
+            2 to TextRange(0),
+            3 to TextRange(1),
+        )
+    }
+
+    @Test
+    fun appendCharToString() {
+        val builder = TestEditBuffer("a")
+        builder.append("b")
+        assertThat(builder.toString()).isEqualTo("ab")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1, 2),
+            2 to TextRange(3),
+            3 to TextRange(4),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1),
+            3 to TextRange(2),
+        )
+    }
+
+    @Test
+    fun appendStringToString() {
+        val builder = TestEditBuffer("a")
+        builder.append("bc")
+        assertThat(builder.toString()).isEqualTo("abc")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1, 3),
+            2 to TextRange(4),
+            3 to TextRange(5),
+            4 to TextRange(6),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1),
+            3 to TextRange(1),
+            4 to TextRange(2),
+        )
+    }
+
+    @Test
+    fun multiplePrepends() {
+        val builder = TestEditBuffer("ab")
+        builder.insert(0, "c")
+        builder.insert(0, "d")
+        builder.insert(0, "ef")
+        assertThat(builder.toString()).isEqualTo("efdcab")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0, 4),
+            1 to TextRange(5),
+            2 to TextRange(6),
+            3 to TextRange(7),
+            4 to TextRange(8),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0),
+            2 to TextRange(0),
+            3 to TextRange(0),
+            4 to TextRange(0),
+            5 to TextRange(1),
+            6 to TextRange(2),
+            7 to TextRange(3),
+        )
+    }
+
+    @Test
+    fun multipleAppends() {
+        val builder = TestEditBuffer("ab")
+        builder.append("c")
+        builder.append("d")
+        builder.append("ef")
+        assertThat(builder.toString()).isEqualTo("abcdef")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(2, 6),
+            3 to TextRange(7),
+            4 to TextRange(8),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(2),
+            3 to TextRange(2),
+            4 to TextRange(2),
+            5 to TextRange(2),
+            6 to TextRange(2),
+            7 to TextRange(3),
+        )
+    }
+
+    @Test
+    fun multiplePrependsThenDeletesCancellingOut() {
+        val builder = TestEditBuffer("ab")
+        builder.insert(0, "cde") // cdeab
+        builder.delete(2) // cdab
+        builder.delete(0) // dab
+        builder.insert(0, "f") // fdab
+        builder.delete(0, 2)
+        assertThat(builder.toString()).isEqualTo("ab")
+        builder.assertIdentityMapping()
+    }
+
+    @Test
+    fun multipleAppendsThenDeletesCancellingOut() {
+        val builder = TestEditBuffer("ab")
+        builder.append("cde") // abcde
+        builder.delete(2) // abde
+        builder.delete(3) // abd
+        builder.append("f") // abdf
+        builder.delete(2, 4)
+        assertThat(builder.toString()).isEqualTo("ab")
+        builder.assertIdentityMapping()
+    }
+
+    @Test
+    fun multipleInsertsThenDeletesCancellingOut() {
+        val builder = TestEditBuffer("ab")
+        builder.insert(1, "c")
+        builder.insert(2, "de")
+        builder.insert(1, "f")
+        builder.delete(1, 3)
+        builder.delete(1)
+        builder.delete(1)
+        assertThat(builder.toString()).isEqualTo("ab")
+        builder.assertIdentityMapping()
+    }
+
+    @Test
+    fun multipleContinuousDeletesAtStartInOrder() {
+        val builder = TestEditBuffer("abcdef")
+        builder.delete(0)
+        builder.delete(0)
+        builder.delete(0)
+        assertThat(builder.toString()).isEqualTo("def")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(0),
+            2 to TextRange(0),
+            3 to TextRange(0),
+            4 to TextRange(1),
+            5 to TextRange(2),
+            6 to TextRange(3),
+            7 to TextRange(4),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0, 3),
+            1 to TextRange(4),
+            2 to TextRange(5),
+            3 to TextRange(6),
+            4 to TextRange(7),
+        )
+    }
+
+    @Test
+    fun multipleContinuousDeletesAtStartOutOfOrder() {
+        val builder = TestEditBuffer("abcdef")
+        builder.delete(1)
+        builder.delete(1)
+        builder.delete(0)
+        assertThat(builder.toString()).isEqualTo("def")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(0),
+            2 to TextRange(0),
+            3 to TextRange(0),
+            4 to TextRange(1),
+            5 to TextRange(2),
+            6 to TextRange(3),
+            7 to TextRange(4),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0, 3),
+            1 to TextRange(4),
+            2 to TextRange(5),
+            3 to TextRange(6),
+            4 to TextRange(7),
+        )
+    }
+
+    @Test
+    fun multipleContinuousDeletesAtEndInOrder() {
+        val builder = TestEditBuffer("abcdef")
+        builder.delete(builder.length - 1)
+        builder.delete(builder.length - 1)
+        builder.delete(builder.length - 1)
+        assertThat(builder.toString()).isEqualTo("abc")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(2),
+            3 to TextRange(3),
+            4 to TextRange(3),
+            5 to TextRange(3),
+            6 to TextRange(3),
+            7 to TextRange(4),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(2),
+            3 to TextRange(3, 6),
+            4 to TextRange(7),
+            5 to TextRange(8),
+        )
+    }
+
+    @Test
+    fun multipleContinuousDeletesAtEndOutOfOrder() {
+        val builder = TestEditBuffer("abcdef")
+        builder.delete(4)
+        builder.delete(3)
+        builder.delete(3)
+        assertThat(builder.toString()).isEqualTo("abc")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(2),
+            3 to TextRange(3),
+            4 to TextRange(3),
+            5 to TextRange(3),
+            6 to TextRange(3),
+            7 to TextRange(4),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(2),
+            3 to TextRange(3, 6),
+            4 to TextRange(7),
+            5 to TextRange(8),
+        )
+    }
+
+    @Test
+    fun multipleContinuousDeletesInMiddleInOrder() {
+        val builder = TestEditBuffer("abcdef")
+        builder.delete(1)
+        builder.delete(1, 3)
+        builder.delete(1)
+        assertThat(builder.toString()).isEqualTo("af")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1),
+            3 to TextRange(1),
+            4 to TextRange(1),
+            5 to TextRange(1),
+            6 to TextRange(2),
+            7 to TextRange(3),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1, 5),
+            2 to TextRange(6),
+            3 to TextRange(7),
+            4 to TextRange(8),
+            5 to TextRange(9),
+        )
+    }
+
+    @Test
+    fun multipleContinuousDeletesInMiddleOutOfOrder() {
+        val builder = TestEditBuffer("abcdef")
+        builder.delete(2)
+        builder.delete(2, 4)
+        builder.delete(1)
+        assertThat(builder.toString()).isEqualTo("af")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(1),
+            2 to TextRange(1),
+            3 to TextRange(1),
+            4 to TextRange(1),
+            5 to TextRange(1),
+            6 to TextRange(2),
+            7 to TextRange(3),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(1, 5),
+            2 to TextRange(6),
+            3 to TextRange(7),
+            4 to TextRange(8),
+            5 to TextRange(9),
+        )
+    }
+
+    @Test
+    fun discontinuousInsertsAndDeletes() {
+        val builder = TestEditBuffer("ab")
+        builder.insert(1, "cde") // acdeb
+        builder.delete(2) // aceb
+        builder.append("fgh") // acebfgh
+        builder.delete(4) // acebgh
+        builder.insert(0, "ijk") // ijkacebgh
+        builder.delete(2) // ijacebgh
+        assertThat(builder.toString()).isEqualTo("ijacebgh")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0, 2),
+            1 to TextRange(3, 5),
+            2 to TextRange(6, 8),
+            3 to TextRange(9),
+            4 to TextRange(10),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0),
+            2 to TextRange(0),
+            3 to TextRange(1),
+            4 to TextRange(1),
+            5 to TextRange(1),
+            6 to TextRange(2),
+            7 to TextRange(2),
+            8 to TextRange(2),
+            9 to TextRange(3),
+        )
+    }
+
+    @Test
+    fun multipleContinuousOneToOneReplacements() {
+        val builder = TestEditBuffer("abc")
+        builder.replace(0, "f")
+        builder.replace(1, "f")
+        builder.replace(2, "f")
+        assertThat(builder.toString()).isEqualTo("fff")
+        builder.assertIdentityMapping()
+    }
+
+    /** This simulates an expanding codepoint transform. */
+    @Test
+    fun multipleContinuousOneToManyReplacements() {
+        val builder = TestEditBuffer("abc")
+        builder.replace(0, "dd") // ddbc
+        builder.replace(2, "ee") // ddeec
+        builder.replace(4, "ff")
+        assertThat(builder.toString()).isEqualTo("ddeeff")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(2),
+            2 to TextRange(4),
+            3 to TextRange(6),
+            4 to TextRange(7),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0, 1),
+            2 to TextRange(1),
+            3 to TextRange(1, 2),
+            4 to TextRange(2),
+            5 to TextRange(2, 3),
+            6 to TextRange(3),
+            7 to TextRange(4),
+        )
+    }
+
+    @Test
+    fun multipleContinuousOneToManyReplacementsReversed() {
+        val builder = TestEditBuffer("abc")
+        builder.replace(2, "dd") // abdd
+        builder.replace(1, "ee") // aeedd
+        builder.replace(0, "ff")
+        assertThat(builder.toString()).isEqualTo("ffeedd")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(2),
+            2 to TextRange(4),
+            3 to TextRange(6),
+            4 to TextRange(7),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0, 1),
+            2 to TextRange(1),
+            3 to TextRange(1, 2),
+            4 to TextRange(2),
+            5 to TextRange(2, 3),
+            6 to TextRange(3),
+            7 to TextRange(4),
+        )
+    }
+
+    /** This simulates a contracting codepoint transform. */
+    @Test
+    fun multipleContinuousManyToOneReplacements() {
+        val builder = TestEditBuffer("abcdef")
+        builder.replace(0, 2, "g") // gcdef
+        builder.replace(1, 3, "h") // ghef
+        builder.replace(2, 4, "i")
+        assertThat(builder.toString()).isEqualTo("ghi")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(0, 1),
+            2 to TextRange(1),
+            3 to TextRange(1, 2),
+            4 to TextRange(2),
+            5 to TextRange(2, 3),
+            6 to TextRange(3),
+            7 to TextRange(4),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(2),
+            2 to TextRange(4),
+            3 to TextRange(6),
+            4 to TextRange(7),
+        )
+    }
+
+    @Test
+    fun multipleContinuousManyToOneReplacementsReversed() {
+        val builder = TestEditBuffer("abcdef")
+        builder.replace(4, 6, "g") // abcdg
+        builder.replace(2, 4, "h") // abhg
+        builder.replace(0, 2, "i")
+        assertThat(builder.toString()).isEqualTo("ihg")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(0, 1),
+            2 to TextRange(1),
+            3 to TextRange(1, 2),
+            4 to TextRange(2),
+            5 to TextRange(2, 3),
+            6 to TextRange(3),
+            7 to TextRange(4),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(2),
+            2 to TextRange(4),
+            3 to TextRange(6),
+            4 to TextRange(7),
+        )
+    }
+
+    /**
+     * This sequence of operations is basically nonsense and so the mappings don't make much sense
+     * either. This test is just here to ensure the output is consistent and doesn't crash.
+     */
+    @Test
+    fun twoOverlappingReplacements() {
+        val builder = TestEditBuffer("abc")
+        builder.replace(0, 2, "wx") // wxc
+        builder.replace(1, 3, "yz")
+        assertThat(builder.toString()).isEqualTo("wyz")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(0, 3),
+            2 to TextRange(1, 3),
+            3 to TextRange(3),
+            4 to TextRange(4),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0, 2),
+            2 to TextRange(0, 3),
+            3 to TextRange(3),
+            4 to TextRange(4),
+        )
+    }
+
+    /**
+     * This sequence of operations is basically nonsense and so the mappings don't make much sense
+     * either. This test is just here to ensure the output is consistent and doesn't crash.
+     */
+    @Test
+    fun fourOverlappingReplacementsReversed() {
+        val builder = TestEditBuffer("abcde")
+        builder.replace(1, 3, "fg") // afgde
+        builder.replace(2, 4, "hi") // afhie
+        builder.replace(0, 2, "jk") // jkhie
+        builder.replace(3, 5, "lm")
+        assertThat(builder.toString()).isEqualTo("jkhlm")
+        builder.assertMappingsFromSource(
+            0 to TextRange(0),
+            1 to TextRange(0, 2),
+            2 to TextRange(0, 5),
+            3 to TextRange(2, 5),
+            4 to TextRange(3, 5),
+            5 to TextRange(5),
+            6 to TextRange(6),
+            7 to TextRange(7),
+        )
+        builder.assertMappingsFromDest(
+            0 to TextRange(0),
+            1 to TextRange(0, 3),
+            2 to TextRange(1, 3),
+            3 to TextRange(1, 4),
+            4 to TextRange(1, 5),
+            5 to TextRange(5),
+            6 to TextRange(6),
+            7 to TextRange(7),
+        )
+    }
+
+    private fun TestEditBuffer.assertIdentityMapping() {
+        // Check well off the end of the valid index range just to be sure.
+        repeat(length + 2) {
+            assertWithMessage("Mapping from source offset $it")
+                .that(mapFromSource(it)).isEqualTo(TextRange(it))
+            assertWithMessage("Mapping from dest offset $it")
+                .that(mapFromDest(it)).isEqualTo(TextRange(it))
+        }
+    }
+
+    private fun TestEditBuffer.assertMappingsFromSource(
+        vararg expectedMappings: Pair<Int, TextRange>
+    ) {
+        expectedMappings.forEach { (srcOffset, dstRange) ->
+            assertWithMessage("Mapping from source offset $srcOffset")
+                .that(mapFromSource(srcOffset)).isEqualTo(dstRange)
+        }
+    }
+
+    private fun TestEditBuffer.assertMappingsFromDest(
+        vararg expectedMappings: Pair<Int, TextRange>
+    ) {
+        expectedMappings.forEach { (dstOffset, srcRange) ->
+            assertWithMessage("Mapping from dest offset $dstOffset")
+                .that(mapFromDest(dstOffset)).isEqualTo(srcRange)
+        }
+    }
+
+    /**
+     * Basic implementation of a text editing buffer that uses [OffsetMappingCalculator] to make
+     * testing easier.
+     */
+    private class TestEditBuffer private constructor(
+        private val builder: StringBuilder
+    ) : CharSequence by builder {
+        constructor(text: CharSequence = "") : this(StringBuilder(text))
+
+        private val tracker = OffsetMappingCalculator()
+
+        fun append(text: CharSequence) {
+            tracker.recordEditOperation(length, length, text.length)
+            builder.append(text)
+        }
+
+        fun insert(offset: Int, value: CharSequence) {
+            tracker.recordEditOperation(offset, offset, value.length)
+            builder.insert(offset, value)
+        }
+
+        fun delete(start: Int, end: Int = start + 1) {
+            tracker.recordEditOperation(start, end, 0)
+            builder.delete(minOf(start, end), maxOf(start, end))
+        }
+
+        fun replace(value: String) {
+            replace(0, length, value)
+        }
+
+        fun replace(start: Int, value: String) {
+            replace(start, start + 1, value)
+        }
+
+        fun replace(start: Int, end: Int, value: String) {
+            tracker.recordEditOperation(start, end, value.length)
+            builder.replace(minOf(start, end), maxOf(start, end), value)
+        }
+
+        fun mapFromSource(offset: Int): TextRange = tracker.mapFromSource(offset)
+        fun mapFromDest(offset: Int): TextRange = tracker.mapFromDest(offset)
+
+        override fun toString(): String = builder.toString()
+    }
+}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/SetComposingRegionCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/SetComposingRegionCommandTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/SetComposingRegionCommandTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/SetComposingRegionCommandTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/SetComposingTextCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/SetComposingTextCommandTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/SetComposingTextCommandTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/SetComposingTextCommandTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/SetSelectionCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/SetSelectionCommandTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/SetSelectionCommandTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/SetSelectionCommandTest.kt
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldStateInternalBufferTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldStateInternalBufferTest.kt
new file mode 100644
index 0000000..656b1d3
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldStateInternalBufferTest.kt
@@ -0,0 +1,518 @@
+/*
+ * 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.foundation.text2.input.internal
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text2.input.InputTransformation
+import androidx.compose.foundation.text2.input.TextFieldCharSequence
+import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.ui.text.TextRange
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalFoundationApi::class)
+@RunWith(JUnit4::class)
+class TextFieldStateInternalBufferTest {
+
+    @Test
+    fun initializeValue() {
+        val firstValue = TextFieldCharSequence("ABCDE", TextRange.Zero)
+        val state = TextFieldState(firstValue)
+
+        assertThat(state.text).isEqualTo(firstValue)
+    }
+
+    @Test
+    fun apply_commitTextCommand_changesValue() {
+        val firstValue = TextFieldCharSequence("ABCDE", TextRange.Zero)
+        val state = TextFieldState(firstValue)
+
+        var resetCalled = 0
+        state.addNotifyImeListener { _, _ -> resetCalled++ }
+
+        state.editAsUser { commitText("X", 1) }
+        val newState = state.text
+
+        assertThat(newState.toString()).isEqualTo("XABCDE")
+        assertThat(newState.selectionInChars.min).isEqualTo(1)
+        assertThat(newState.selectionInChars.max).isEqualTo(1)
+        // edit command updates should not trigger reset listeners.
+        assertThat(resetCalled).isEqualTo(0)
+    }
+
+    @Test
+    fun apply_setSelectionCommand_changesValue() {
+        val firstValue = TextFieldCharSequence("ABCDE", TextRange.Zero)
+        val state = TextFieldState(firstValue)
+
+        var resetCalled = 0
+        state.addNotifyImeListener { _, _ -> resetCalled++ }
+
+        state.editAsUser { setSelection(0, 2) }
+        val newState = state.text
+
+        assertThat(newState.toString()).isEqualTo("ABCDE")
+        assertThat(newState.selectionInChars.min).isEqualTo(0)
+        assertThat(newState.selectionInChars.max).isEqualTo(2)
+        // edit command updates should not trigger reset listeners.
+        assertThat(resetCalled).isEqualTo(0)
+    }
+
+    @Test
+    fun testNewState_bufferNotUpdated_ifSameModelStructurally() {
+        val state = TextFieldState()
+        var resetCalled = 0
+        state.addNotifyImeListener { _, _ -> resetCalled++ }
+
+        val initialBuffer = state.mainBuffer
+        state.resetStateAndNotifyIme(
+            TextFieldCharSequence("qwerty", TextRange.Zero, TextRange.Zero)
+        )
+        assertThat(state.mainBuffer).isNotSameInstanceAs(initialBuffer)
+
+        val updatedBuffer = state.mainBuffer
+        state.resetStateAndNotifyIme(
+            TextFieldCharSequence("qwerty", TextRange.Zero, TextRange.Zero)
+        )
+        assertThat(state.mainBuffer).isSameInstanceAs(updatedBuffer)
+
+        assertThat(resetCalled).isEqualTo(2)
+    }
+
+    @Test
+    fun testNewState_new_buffer_created_if_text_is_different() {
+        val state = TextFieldState()
+        var resetCalled = 0
+        state.addNotifyImeListener { _, _ -> resetCalled++ }
+
+        val textFieldValue = TextFieldCharSequence("qwerty", TextRange.Zero, TextRange.Zero)
+        state.resetStateAndNotifyIme(textFieldValue)
+        val initialBuffer = state.mainBuffer
+
+        val newTextFieldValue = TextFieldCharSequence("abc")
+        state.resetStateAndNotifyIme(newTextFieldValue)
+
+        assertThat(state.mainBuffer).isNotSameInstanceAs(initialBuffer)
+        assertThat(resetCalled).isEqualTo(2)
+    }
+
+    @Test
+    fun testNewState_buffer_not_recreated_if_selection_is_different() {
+        val state = TextFieldState()
+        var resetCalled = 0
+        state.addNotifyImeListener { _, _ -> resetCalled++ }
+
+        val textFieldValue = TextFieldCharSequence("qwerty", TextRange.Zero, TextRange.Zero)
+        state.resetStateAndNotifyIme(textFieldValue)
+        val initialBuffer = state.mainBuffer
+
+        val newTextFieldValue = TextFieldCharSequence(textFieldValue, selection = TextRange(1))
+        state.resetStateAndNotifyIme(newTextFieldValue)
+
+        assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
+        assertThat(newTextFieldValue.selectionInChars.start)
+            .isEqualTo(state.mainBuffer.selectionStart)
+        assertThat(newTextFieldValue.selectionInChars.end).isEqualTo(
+            state.mainBuffer.selectionEnd
+        )
+        assertThat(resetCalled).isEqualTo(2)
+    }
+
+    @Test
+    fun testNewState_buffer_not_recreated_if_composition_is_different() {
+        val state = TextFieldState()
+        var resetCalled = 0
+        state.addNotifyImeListener { _, _ -> resetCalled++ }
+
+        val textFieldValue = TextFieldCharSequence("qwerty", TextRange.Zero, TextRange(1))
+        state.resetStateAndNotifyIme(textFieldValue)
+        val initialBuffer = state.mainBuffer
+
+        // composition can not be set from app, IME owns it.
+        assertThat(EditingBuffer.NOWHERE).isEqualTo(initialBuffer.compositionStart)
+        assertThat(EditingBuffer.NOWHERE).isEqualTo(initialBuffer.compositionEnd)
+
+        val newTextFieldValue = TextFieldCharSequence(
+            textFieldValue,
+            textFieldValue.selectionInChars,
+            composition = null
+        )
+        state.resetStateAndNotifyIme(newTextFieldValue)
+
+        assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
+        assertThat(EditingBuffer.NOWHERE).isEqualTo(state.mainBuffer.compositionStart)
+        assertThat(EditingBuffer.NOWHERE).isEqualTo(state.mainBuffer.compositionEnd)
+        assertThat(resetCalled).isEqualTo(2)
+    }
+
+    @Test
+    fun testNewState_reversedSelection_setsTheSelection() {
+        val initialSelection = TextRange(2, 1)
+        val textFieldValue = TextFieldCharSequence("qwerty", initialSelection, TextRange(1))
+        val state = TextFieldState(textFieldValue)
+        var resetCalled = 0
+        state.addNotifyImeListener { _, _ -> resetCalled++ }
+
+        val initialBuffer = state.mainBuffer
+
+        assertThat(initialSelection.start).isEqualTo(initialBuffer.selectionStart)
+        assertThat(initialSelection.end).isEqualTo(initialBuffer.selectionEnd)
+
+        val updatedSelection = TextRange(3, 0)
+        val newTextFieldValue = TextFieldCharSequence(textFieldValue, selection = updatedSelection)
+        // set the new selection
+        state.resetStateAndNotifyIme(newTextFieldValue)
+
+        assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
+        assertThat(updatedSelection.start).isEqualTo(initialBuffer.selectionStart)
+        assertThat(updatedSelection.end).isEqualTo(initialBuffer.selectionEnd)
+        assertThat(resetCalled).isEqualTo(1)
+    }
+
+    @Test
+    fun compositionIsCleared_when_textChanged() {
+        val state = TextFieldState()
+        var resetCalled = 0
+        state.addNotifyImeListener { _, _ -> resetCalled++ }
+
+        // set the initial value
+        state.editAsUser {
+            commitText("ab", 0)
+            setComposingRegion(0, 2)
+        }
+
+        // change the text
+        val newValue =
+            TextFieldCharSequence(
+                "cd",
+                state.text.selectionInChars,
+                state.text.compositionInChars
+            )
+        state.resetStateAndNotifyIme(newValue)
+
+        assertThat(state.text.toString()).isEqualTo(newValue.toString())
+        assertThat(state.text.compositionInChars).isNull()
+    }
+
+    @Test
+    fun compositionIsNotCleared_when_textIsSame() {
+        val state = TextFieldState()
+        val composition = TextRange(0, 2)
+
+        // set the initial value
+        state.editAsUser {
+            commitText("ab", 0)
+            setComposingRegion(composition.start, composition.end)
+        }
+
+        // use the same TextFieldValue
+        val newValue =
+            TextFieldCharSequence(
+                state.text,
+                state.text.selectionInChars,
+                state.text.compositionInChars
+            )
+        state.resetStateAndNotifyIme(newValue)
+
+        assertThat(state.text.toString()).isEqualTo(newValue.toString())
+        assertThat(state.text.compositionInChars).isEqualTo(composition)
+    }
+
+    @Test
+    fun compositionIsCleared_when_compositionReset() {
+        val state = TextFieldState()
+
+        // set the initial value
+        state.editAsUser {
+            commitText("ab", 0)
+            setComposingRegion(-1, -1)
+        }
+
+        // change the composition
+        val newValue =
+            TextFieldCharSequence(
+                state.text,
+                state.text.selectionInChars,
+                composition = TextRange(0, 2)
+            )
+        state.resetStateAndNotifyIme(newValue)
+
+        assertThat(state.text.toString()).isEqualTo(newValue.toString())
+        assertThat(state.text.compositionInChars).isNull()
+    }
+
+    @Test
+    fun compositionIsCleared_when_compositionChanged() {
+        val state = TextFieldState()
+
+        // set the initial value
+        state.editAsUser {
+            commitText("ab", 0)
+            setComposingRegion(0, 2)
+        }
+
+        // change the composition
+        val newValue = TextFieldCharSequence(
+            state.text,
+            state.text.selectionInChars,
+            composition = TextRange(0, 1)
+        )
+        state.resetStateAndNotifyIme(newValue)
+
+        assertThat(state.text.toString()).isEqualTo(newValue.toString())
+        assertThat(state.text.compositionInChars).isNull()
+    }
+
+    @Test
+    fun compositionIsNotCleared_when_onlySelectionChanged() {
+        val state = TextFieldState()
+
+        val composition = TextRange(0, 2)
+        val selection = TextRange(0, 2)
+
+        // set the initial value
+        state.editAsUser {
+            commitText("ab", 0)
+            setComposingRegion(composition.start, composition.end)
+            setSelection(selection.start, selection.end)
+        }
+
+        // change selection
+        val newSelection = TextRange(1)
+        val newValue = TextFieldCharSequence(
+            state.text,
+            selection = newSelection,
+            composition = state.text.compositionInChars
+        )
+        state.resetStateAndNotifyIme(newValue)
+
+        assertThat(state.text.toString()).isEqualTo(newValue.toString())
+        assertThat(state.text.compositionInChars).isEqualTo(composition)
+        assertThat(state.text.selectionInChars).isEqualTo(newSelection)
+    }
+
+    @Test
+    fun filterThatDoesNothing_doesNotResetBuffer() {
+        val state = TextFieldState(
+            TextFieldCharSequence(
+                "abc",
+                selection = TextRange(3),
+                composition = TextRange(0, 3)
+            )
+        )
+
+        val initialBuffer = state.mainBuffer
+
+        state.editAsUser { commitText("d", 4) }
+
+        val value = state.text
+
+        assertThat(value.toString()).isEqualTo("abcd")
+        assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
+    }
+
+    @Test
+    fun returningTheEquivalentValueFromFilter_doesNotResetBuffer() {
+        val state = TextFieldState(
+            TextFieldCharSequence(
+                "abc",
+                selection = TextRange(3),
+                composition = TextRange(0, 3)
+            )
+        )
+
+        val initialBuffer = state.mainBuffer
+
+        state.editAsUser { commitText("d", 4) }
+
+        val value = state.text
+
+        assertThat(value.toString()).isEqualTo("abcd")
+        assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
+    }
+
+    @Test
+    fun returningOldValueFromFilter_resetsTheBuffer() {
+        val state = TextFieldState(
+            TextFieldCharSequence(
+                "abc",
+                selection = TextRange(3),
+                composition = TextRange(0, 3)
+            )
+        )
+
+        var resetCalledOld: TextFieldCharSequence? = null
+        var resetCalledNew: TextFieldCharSequence? = null
+        state.addNotifyImeListener { old, new ->
+            resetCalledOld = old
+            resetCalledNew = new
+        }
+
+        val initialBuffer = state.mainBuffer
+
+        state.editAsUser(
+            inputTransformation = { _, new -> new.revertAllChanges() },
+            notifyImeOfChanges = false
+        ) {
+            commitText("d", 4)
+        }
+
+        val value = state.text
+
+        assertThat(value.toString()).isEqualTo("abc")
+        assertThat(state.mainBuffer).isNotSameInstanceAs(initialBuffer)
+        assertThat(resetCalledOld?.toString()).isEqualTo("abcd") // what IME applied
+        assertThat(resetCalledNew?.toString()).isEqualTo("abc") // what is decided by filter
+    }
+
+    @Test
+    fun filterNotRan_whenNoCommands() {
+        val initialValue =
+            TextFieldCharSequence("hello", selection = TextRange(2), composition = null)
+        val state = TextFieldState(initialValue)
+        val inputTransformation = InputTransformation { old, new ->
+            fail("filter ran, old=\"$old\", new=\"$new\"")
+        }
+
+        state.editAsUser(inputTransformation, notifyImeOfChanges = false) {}
+    }
+
+    @Test
+    fun filterNotRan_whenOnlyFinishComposingTextCommand_noComposition() {
+        val initialValue =
+            TextFieldCharSequence("hello", selection = TextRange(2), composition = null)
+        val state = TextFieldState(initialValue)
+        val inputTransformation = InputTransformation { old, new ->
+            fail("filter ran, old=\"$old\", new=\"$new\"")
+        }
+
+        state.editAsUser(
+            inputTransformation = inputTransformation,
+            notifyImeOfChanges = false
+        ) { finishComposingText() }
+    }
+
+    @Test
+    fun filterNotRan_whenOnlyFinishComposingTextCommand_withComposition() {
+        val initialValue =
+            TextFieldCharSequence("hello", selection = TextRange(2), composition = TextRange(0, 5))
+        val state = TextFieldState(initialValue)
+        val inputTransformation = InputTransformation { old, new ->
+            fail("filter ran, old=\"$old\", new=\"$new\"")
+        }
+
+        state.editAsUser(
+            inputTransformation = inputTransformation,
+            notifyImeOfChanges = false
+        ) { finishComposingText() }
+    }
+
+    @Test
+    fun filterNotRan_whenCommandsResultInInitialValue() {
+        val initialValue =
+            TextFieldCharSequence("hello", selection = TextRange(2), composition = TextRange(0, 5))
+        val state = TextFieldState(initialValue)
+        val inputTransformation = InputTransformation { old, new ->
+            fail(
+                "filter ran, old=\"$old\" (${old.selectionInChars}), " +
+                    "new=\"$new\" (${new.selectionInChars})"
+            )
+        }
+
+        state.editAsUser(inputTransformation = inputTransformation, notifyImeOfChanges = false) {
+            setComposingRegion(0, 5)
+            commitText("hello", 1)
+            setSelection(2, 2)
+        }
+    }
+
+    @Test
+    fun filterRan_whenOnlySelectionChanges() {
+        val initialValue =
+            TextFieldCharSequence("hello", selection = TextRange(2), composition = null)
+        var filterRan = false
+        val state = TextFieldState(initialValue)
+        val inputTransformation = InputTransformation { old, new ->
+            // Filter should only run once.
+            assertThat(filterRan).isFalse()
+            filterRan = true
+            assertThat(new.toString()).isEqualTo(old.toString())
+            assertThat(old.selectionInChars).isEqualTo(TextRange(2))
+            assertThat(new.selectionInChars).isEqualTo(TextRange(0, 5))
+        }
+
+        state.editAsUser(
+            inputTransformation = inputTransformation,
+            notifyImeOfChanges = false
+        ) { setSelection(0, 5) }
+    }
+
+    @Test
+    fun filterRan_whenOnlyTextChanges() {
+        val initialValue =
+            TextFieldCharSequence("hello", selection = TextRange(2), composition = null)
+        var filterRan = false
+        val state = TextFieldState(initialValue)
+        val inputTransformation = InputTransformation { old, new ->
+            // Filter should only run once.
+            assertThat(filterRan).isFalse()
+            filterRan = true
+            assertThat(new.selectionInChars).isEqualTo(old.selectionInChars)
+            assertThat(old.toString()).isEqualTo("hello")
+            assertThat(new.toString()).isEqualTo("world")
+        }
+
+        state.editAsUser(inputTransformation = inputTransformation, notifyImeOfChanges = false) {
+            deleteAll()
+            commitText("world", 1)
+            setSelection(2, 2)
+        }
+    }
+
+    @Test
+    fun stateUpdated_whenOnlyCompositionChanges_noFilter() {
+        val initialValue =
+            TextFieldCharSequence("hello", selection = TextRange(5), composition = TextRange(0, 5))
+        val state = TextFieldState(initialValue)
+
+        state.editAsUser { setComposingRegion(2, 3) }
+
+        assertThat(state.text.compositionInChars).isEqualTo(TextRange(2, 3))
+    }
+
+    @Test
+    fun stateUpdated_whenOnlyCompositionChanges_withFilter() {
+        val initialValue =
+            TextFieldCharSequence("hello", selection = TextRange(5), composition = TextRange(0, 5))
+        val state = TextFieldState(initialValue)
+
+        state.editAsUser { setComposingRegion(2, 3) }
+
+        assertThat(state.text.compositionInChars).isEqualTo(TextRange(2, 3))
+    }
+
+    private fun TextFieldState(
+        value: TextFieldCharSequence
+    ) = TextFieldState(value.toString(), value.selectionInChars)
+
+    private fun TextFieldState.editAsUser(block: EditingBuffer.() -> Unit) {
+        editAsUser(inputTransformation = null, notifyImeOfChanges = false, block = block)
+    }
+}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/ToCharArrayTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/ToCharArrayTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/ToCharArrayTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/ToCharArrayTest.kt
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/matchers/EditBufferSubject.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/matchers/EditBufferSubject.kt
similarity index 100%
rename from compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/matchers/EditBufferSubject.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/matchers/EditBufferSubject.kt
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/undo/TextUndoTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/undo/TextUndoTest.kt
new file mode 100644
index 0000000..67d75d4
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/undo/TextUndoTest.kt
@@ -0,0 +1,358 @@
+/*
+ * 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.foundation.text2.input.internal.undo
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text2.input.InputTransformation
+import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.foundation.text2.input.allCaps
+import androidx.compose.foundation.text2.input.internal.commitText
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.intl.Locale
+import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalFoundationApi::class)
+@RunWith(JUnit4::class)
+class TextUndoTest {
+
+    @Test
+    fun insertionFromEndPointCanMerge() {
+        val state = TextFieldState()
+        state.typeAtEnd("a")
+        state.typeAtEnd("b")
+        state.typeAtEnd("c")
+
+        assertThat(state.text.toString()).isEqualTo("abc")
+        assertThat(state.undoState.canUndo).isTrue()
+
+        state.undoState.undo()
+
+        assertThat(state.text.toString()).isEqualTo("")
+    }
+
+    @Test
+    fun insertionFromStartPointCannotMerge() {
+        val state = TextFieldState()
+        state.typeAtStart("a")
+        state.typeAtStart("b")
+        state.typeAtStart("c")
+
+        assertThat(state.text.toString()).isEqualTo("cba")
+        assertThat(state.undoState.canUndo).isTrue()
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("ba")
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("a")
+    }
+
+    @Test
+    fun insertionFromMiddleCannotMerge() {
+        val state = TextFieldState()
+        state.typeAtEnd("a")
+        state.typeAtEnd("c")
+        state.typeAt(1, "b")
+
+        assertThat(state.text.toString()).isEqualTo("abc")
+        assertThat(state.undoState.canUndo).isTrue()
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("ac")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(1))
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("")
+    }
+
+    @Test
+    fun deletionFromEndPointCanMerge() {
+        val state = TextFieldState("abc")
+        state.placeCursorAt(3)
+        state.deleteAt(2)
+        state.deleteAt(1)
+        state.deleteAt(0)
+
+        assertThat(state.text.toString()).isEqualTo("")
+        assertThat(state.undoState.canUndo).isTrue()
+
+        state.undoState.undo()
+
+        assertThat(state.text.toString()).isEqualTo("abc")
+    }
+
+    @Test
+    fun deletionFromStartPointCanMerge() {
+        val state = TextFieldState("abc")
+        state.placeCursorAt(0)
+        state.deleteAt(0)
+        state.deleteAt(0)
+        state.deleteAt(0)
+
+        assertThat(state.text.toString()).isEqualTo("")
+        assertThat(state.undoState.canUndo).isTrue()
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("abc")
+    }
+
+    @Test
+    fun deletionFromMiddleCannotMerge() {
+        val state = TextFieldState("abc")
+        state.placeCursorAt(2) // "ab|c"
+        state.deleteAt(1) // "a|c"
+        state.deleteAt(1) // "a|"
+        state.deleteAt(0) // "|"
+
+        assertThat(state.text.toString()).isEqualTo("")
+        assertThat(state.undoState.canUndo).isTrue()
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("a")
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("ac")
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("abc")
+    }
+
+    @Test
+    fun deletionsWithDifferentDirectionsCannotMerge() {
+        val state = TextFieldState("abcdef")
+        state.placeCursorAt(3) // "abc|def"
+        state.deleteAt(2) // "ab|def"
+        state.deleteAt(2) // "ab|ef"
+
+        assertThat(state.text.toString()).isEqualTo("abef")
+        assertThat(state.undoState.canUndo).isTrue()
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("abdef")
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("abcdef")
+    }
+
+    @Test
+    fun insertionAndDeletionNeverMerge() {
+        val state = TextFieldState()
+        state.typeAtEnd("a") // "a|"
+        state.typeAtEnd("b") // "ab|"
+        state.deleteAt(1) // "a|"
+        state.typeAtStart("c") // "|a" -> "c|a"
+
+        assertThat(state.text.toString()).isEqualTo("ca")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(1))
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("a")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(0))
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("ab")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(2))
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("")
+    }
+
+    @Test
+    fun replaceDoesNotMergeWithInsertion() {
+        val state = TextFieldState("abc")
+        state.typeAtEnd("d") // "abcd|"
+        state.replaceAt(0, 4, "def") // "def|"
+        state.typeAtEnd("g") // "defg|"
+
+        assertThat(state.text.toString()).isEqualTo("defg")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(4))
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("def")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(3))
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("abcd")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(4))
+    }
+
+    @Test
+    fun replaceDoesNotMergeWithDeletion() {
+        val state = TextFieldState("abc")
+        state.placeCursorAt(3)
+        state.deleteAt(2) // "ab|"
+        state.replaceAt(0, 2, "def") // "def|"
+        state.deleteAt(2) // "de|"
+
+        assertThat(state.text.toString()).isEqualTo("de")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(2))
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("def")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(3))
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("ab")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(2))
+    }
+
+    @Test
+    fun newLineInsert_doesNotMerge() {
+        val state = TextFieldState()
+        state.typeAtEnd("a") // "a|"
+        state.typeAtEnd("b") // "ab|"
+        state.typeAtEnd("\n") // "ab\n|"
+        state.typeAtEnd("c") // "ab\nc|"
+
+        assertThat(state.text.toString()).isEqualTo("ab\nc")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(4))
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("ab\n")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(3))
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("ab")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(2))
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("")
+    }
+
+    @Ignore("Enable after EditingBuffer reverse range replace is fixed")
+    @Test
+    fun undoRecoversSelectionState() {
+        val state = TextFieldState("abc")
+        state.select(2, 0) // "|ab|c"
+        state.type("d") // "d|c"
+
+        assertThat(state.text.toString()).isEqualTo("dc")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(1))
+
+        state.undoState.undo()
+        assertThat(state.text.toString()).isEqualTo("abc")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(2, 0))
+    }
+
+    @Ignore("Enable after EditingBuffer reverse range replace is fixed")
+    @Test
+    fun redoDoesNotRecoverSelectionState() {
+        val state = TextFieldState("abc")
+        state.typeAtEnd("d") // "abcd|"
+        state.select(2, 0) // "|ab|cd"
+        state.type("e") // "e|cd"
+
+        assertThat(state.text.toString()).isEqualTo("ecd")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(1))
+
+        state.undoState.undo() // "|ab|cd"
+        state.undoState.undo() // "|abc"
+        state.undoState.redo() // "abcd|"
+
+        assertThat(state.text.toString()).isEqualTo("abcd")
+        assertThat(state.text.selectionInChars).isEqualTo(TextRange(4, 4))
+    }
+
+    @Ignore("Enable after allCapsTransformation is fixed")
+    @Test
+    fun undoHistoryIncludesInputTransformation() {
+        val allCapsTransformation = InputTransformation.allCaps(Locale.current)
+        val state = TextFieldState("abc", TextRange(3))
+
+        // this test also tests for AllCapsTransformation
+        state.editAsUser(inputTransformation = allCapsTransformation) {
+            commitComposition()
+            commitText("d", 1)
+        } // "abcD|"
+        state.editAsUser(inputTransformation = allCapsTransformation) {
+            commitComposition()
+            commitText("e", 1)
+        } // "abcDE|"
+
+        state.undoState.undo() // "abc|"
+        assertThat(state.text.toString()).isEqualTo("abc")
+
+        state.undoState.redo() // "abcDE|"
+        assertThat(state.text.toString()).isEqualTo("abcDE")
+    }
+
+    @Test
+    fun directEditsClearTheUndoHistory() {
+        val state = TextFieldState("abc")
+        state.typeAtEnd("d")
+        state.typeAtStart("e")
+        state.typeAtEnd("f")
+
+        state.edit { replace(0, 1, "x") }
+
+        assertThat(state.undoState.canUndo).isEqualTo(false)
+        assertThat(state.undoState.canRedo).isEqualTo(false)
+    }
+
+    companion object {
+
+        private fun TextFieldState.typeAtEnd(text: String) {
+            placeCursorAt(this.text.length)
+            typeAt(this.text.length, text)
+        }
+
+        private fun TextFieldState.typeAtStart(text: String) {
+            placeCursorAt(0)
+            typeAt(0, text)
+        }
+
+        private fun TextFieldState.typeAt(index: Int, text: String) {
+            placeCursorAt(index)
+            editAsUser(inputTransformation = null) {
+                replace(index, index, text)
+            }
+        }
+
+        private fun TextFieldState.type(text: String) {
+            editAsUser(inputTransformation = null) {
+                commitComposition()
+                commitText(text, 1)
+            }
+        }
+
+        private fun TextFieldState.deleteAt(index: Int) {
+            editAsUser(inputTransformation = null) {
+                delete(index, index + 1)
+            }
+        }
+
+        private fun TextFieldState.placeCursorAt(index: Int) {
+            select(index, index)
+        }
+
+        private fun TextFieldState.select(start: Int, end: Int) {
+            editAsUser(inputTransformation = null) {
+                setSelection(start, end)
+            }
+        }
+
+        private fun TextFieldState.replaceAt(start: Int, end: Int, newText: String) {
+            editAsUser(inputTransformation = null) {
+                replace(start, end, newText)
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/undo/UndoManagerSaverTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/undo/UndoManagerSaverTest.kt
new file mode 100644
index 0000000..750fbe6
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/undo/UndoManagerSaverTest.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.foundation.text2.input.internal.undo
+
+import androidx.compose.runtime.saveable.SaverScope
+import androidx.compose.runtime.saveable.autoSaver
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
+import org.junit.Test
+
+class UndoManagerSaverTest {
+
+    @Test
+    fun savesAndRestoresTextAndSelection() {
+        val undoManager = UndoManager<Int>()
+
+        undoManager.record(1)
+        undoManager.record(2)
+        undoManager.record(3)
+
+        undoManager.undo()
+
+        // undoStack; 1-2 redoStack; 3
+
+        val saver = UndoManager.createSaver(autoSaver<Int>())
+        val saved = with(saver) {
+            TestSaverScope.save(undoManager)
+        }
+        assertNotNull(saved)
+        val restoredState = saver.restore(saved)
+
+        assertNotNull(restoredState)
+        assertThat(restoredState.canUndo).isTrue()
+        assertThat(restoredState.canRedo).isTrue()
+
+        var redoValue = undoManager.redo()
+
+        assertThat(redoValue).isEqualTo(3)
+
+        val undoValues = mutableListOf<Int>()
+        while (undoManager.canUndo) {
+            undoValues += undoManager.undo()
+        }
+
+        assertThat(undoValues).containsExactly(3, 2, 1)
+    }
+
+    private object TestSaverScope : SaverScope {
+        override fun canBeSaved(value: Any): Boolean = true
+    }
+}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/undo/UndoManagerTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/undo/UndoManagerTest.kt
new file mode 100644
index 0000000..cbae21e
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text2/input/internal/undo/UndoManagerTest.kt
@@ -0,0 +1,190 @@
+/*
+ * 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.foundation.text2.input.internal.undo
+
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class UndoManagerTest {
+
+    @Test
+    fun negativeCapacityThrows() {
+        assertFailsWith<IllegalArgumentException>("Capacity must be a positive integer") {
+            UndoManager<Int>(capacity = -1)
+        }
+    }
+
+    @Test
+    fun initialLowCapacityThrows_undoStack() {
+        assertFailsWith<IllegalArgumentException>(
+            getInitialCapacityErrorMessage(
+                capacity = 1,
+                totalStackSize = 2
+            )
+        ) {
+            UndoManager(
+                initialUndoStack = listOf(1, 2),
+                capacity = 1
+            )
+        }
+    }
+
+    @Test
+    fun initialLowCapacityThrows_redoStack() {
+        assertFailsWith<IllegalArgumentException>(
+            getInitialCapacityErrorMessage(
+                capacity = 1,
+                totalStackSize = 2
+            )
+        ) {
+            UndoManager(
+                initialRedoStack = listOf(1, 2),
+                capacity = 1
+            )
+        }
+    }
+
+    @Test
+    fun initialLowCapacityThrows_bothStacks() {
+        assertFailsWith<IllegalArgumentException>(
+            getInitialCapacityErrorMessage(
+                capacity = 3,
+                totalStackSize = 4
+            )
+        ) {
+            UndoManager(
+                initialUndoStack = listOf(1, 2),
+                initialRedoStack = listOf(1, 2),
+                capacity = 3
+            )
+        }
+    }
+
+    @Test
+    fun commitSingleItem_canUndo() {
+        val undoManager = UndoManager<Int>()
+        undoManager.record(1)
+
+        assertThat(undoManager.canUndo).isTrue()
+        assertThat(undoManager.canRedo).isFalse()
+
+        undoManager.undo()
+        assertThat(undoManager.canUndo).isFalse()
+        assertThat(undoManager.canRedo).isTrue()
+    }
+
+    @Test
+    fun cannotRedoWithoutFirstUndo() {
+        val undoManager = UndoManager<Int>()
+        undoManager.record(1)
+        undoManager.record(2)
+        undoManager.record(3)
+
+        assertThat(undoManager.canRedo).isFalse()
+        assertFailsWith<IllegalStateException>(
+            "It's an error to call redo while there is nothing to redo. " +
+                "Please first check `canRedo` value before calling the `redo` function."
+        ) {
+            undoManager.redo()
+        }
+    }
+
+    @Test
+    fun commitItem_clearsRedoStack() {
+        val undoManager = UndoManager<Int>()
+        undoManager.record(1)
+        undoManager.record(2)
+        undoManager.record(3)
+
+        undoManager.undo()
+        assertThat(undoManager.canRedo).isTrue()
+
+        undoManager.record(4)
+        assertThat(undoManager.canRedo).isFalse()
+    }
+
+    @Test
+    fun clearHistoryRemovesUndoAndRedo() {
+        val undoManager = UndoManager<Int>()
+        undoManager.record(1)
+        undoManager.record(2)
+        undoManager.record(3)
+
+        undoManager.undo()
+
+        assertThat(undoManager.canUndo).isTrue()
+        assertThat(undoManager.canRedo).isTrue()
+
+        undoManager.clearHistory()
+
+        assertThat(undoManager.canUndo).isFalse()
+        assertThat(undoManager.canRedo).isFalse()
+    }
+
+    @Test
+    fun capacityOverflow_removesFromTheBottomOfStack() {
+        val undoManager = UndoManager<Int>(capacity = 2)
+        undoManager.record(1)
+        undoManager.record(2)
+        // overflow the capacity, undo history should forget the first item
+        undoManager.record(3)
+
+        var item = undoManager.undo()
+        assertThat(item).isEqualTo(3)
+
+        item = undoManager.undo()
+        assertThat(item).isEqualTo(2)
+
+        assertThat(undoManager.canUndo).isFalse()
+    }
+
+    @Test
+    fun capacityOverflow_shouldRemoveRedoActionsFirst() {
+        val undoManager = UndoManager<Int>(capacity = 20)
+        undoManager.record(1)
+        undoManager.record(2)
+        undoManager.record(3)
+        undoManager.record(4)
+
+        undoManager.undo() // total size does not change  undo; 1-2-3 redo; 4
+        undoManager.undo() // total size does not change  undo; 1-2 redo; 4-3
+
+        // this should not remove anything from the undo stack, auto removed items from redo should
+        // suffice
+        undoManager.record(5)
+
+        assertThat(undoManager.canUndo).isTrue()
+        assertThat(undoManager.canRedo).isFalse()
+
+        var item = undoManager.undo()
+        assertThat(item).isEqualTo(5)
+
+        item = undoManager.undo()
+        assertThat(item).isEqualTo(2)
+
+        item = undoManager.undo()
+        assertThat(item).isEqualTo(1)
+    }
+
+    private fun getInitialCapacityErrorMessage(capacity: Int, totalStackSize: Int) =
+        "Initial list of undo and redo operations have a size=($totalStackSize) greater " +
+        "than the given capacity=($capacity)."
+}
diff --git a/compose/foundation/foundation/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/compose/foundation/foundation/src/androidUnitTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
similarity index 100%
rename from compose/foundation/foundation/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
rename to compose/foundation/foundation/src/androidUnitTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
index 2a770bc..622afb5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation
 
 import androidx.annotation.FloatRange
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Brush
@@ -40,6 +41,7 @@
  * @param color color to paint background with
  * @param shape desired shape of the background
  */
+@Stable
 fun Modifier.background(
     color: Color,
     shape: Shape = RectangleShape
@@ -70,6 +72,7 @@
  * @param alpha Opacity to be applied to the [brush], with `0` being completely transparent and
  * `1` being completely opaque. The value must be between `0` and `1`.
  */
+@Stable
 fun Modifier.background(
     brush: Brush,
     shape: Shape = RectangleShape,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
index 4b77a1d..d173f4a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
@@ -135,6 +135,7 @@
  * Note: this modifier and corresponding APIs are experimental pending some refinements in the API
  * surface, mostly related to customisation params.
  */
+@Stable
 @ExperimentalFoundationApi
 fun Modifier.basicMarquee(
     iterations: Int = DefaultMarqueeIterations,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicTooltip.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicTooltip.kt
index 26deed4..99b38b0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicTooltip.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicTooltip.kt
@@ -20,8 +20,7 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.window.PopupPositionProvider
@@ -79,10 +78,9 @@
     isPersistent: Boolean = true,
     mutatorMutex: MutatorMutex = BasicTooltipDefaults.GlobalMutatorMutex
 ): BasicTooltipState =
-    rememberSaveable(
+    remember(
         isPersistent,
-        mutatorMutex,
-        saver = BasicTooltipStateImpl.Saver
+        mutatorMutex
     ) {
         BasicTooltipStateImpl(
             initialIsVisible = initialIsVisible,
@@ -104,6 +102,7 @@
  * @param mutatorMutex [MutatorMutex] used to ensure that for all of the tooltips associated
  * with the mutator mutex, only one will be shown on the screen at any time.
  */
+@Stable
 fun BasicTooltipState(
     initialIsVisible: Boolean = false,
     isPersistent: Boolean = true,
@@ -179,29 +178,6 @@
     override fun onDispose() {
         job?.cancel()
     }
-
-    companion object {
-        /**
-         * The default [Saver] implementation for [BasicTooltipStateImpl].
-         */
-        val Saver = Saver<BasicTooltipStateImpl, Any>(
-            save = {
-                   listOf(
-                       it.isVisible,
-                       it.isPersistent,
-                       it.mutatorMutex
-                   )
-            },
-            restore = {
-                val (isVisible, isPersistent, mutatorMutex) = it as List<*>
-                BasicTooltipStateImpl(
-                    initialIsVisible = isVisible as Boolean,
-                    isPersistent = isPersistent as Boolean,
-                    mutatorMutex = mutatorMutex as MutatorMutex,
-                )
-            }
-        )
-    }
 }
 
 /**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt
index 0bae435..330d630 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation
 
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.CacheDrawModifierNode
 import androidx.compose.ui.draw.CacheDrawScope
@@ -63,6 +64,7 @@
  * @param border [BorderStroke] class that specifies border appearance, such as size and color
  * @param shape shape of the border
  */
+@Stable
 fun Modifier.border(border: BorderStroke, shape: Shape = RectangleShape) =
     border(width = border.width, brush = border.brush, shape = shape)
 
@@ -76,6 +78,7 @@
  * @param color color to paint the border with
  * @param shape shape of the border
  */
+@Stable
 fun Modifier.border(width: Dp, color: Color, shape: Shape = RectangleShape) =
     border(width, SolidColor(color), shape)
 
@@ -90,6 +93,7 @@
  * @param brush brush to paint the border with
  * @param shape shape of the border
  */
+@Stable
 fun Modifier.border(width: Dp, brush: Brush, shape: Shape) =
     this then BorderModifierNodeElement(width, brush, shape)
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
index d817e37..5a4da47 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation
 
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.geometry.Rect
@@ -33,6 +34,7 @@
  *
  * @param orientation orientation of the scrolling
  */
+@Stable
 fun Modifier.clipScrollableContainer(orientation: Orientation) =
     then(
         if (orientation == Orientation.Vertical) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index 0548c70..88c8618 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -21,6 +21,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.relocation.BringIntoViewRequester
 import androidx.compose.foundation.relocation.BringIntoViewRequesterNode
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusEventModifierNode
 import androidx.compose.ui.focus.FocusProperties
@@ -101,6 +102,7 @@
  *
  * @sample androidx.compose.foundation.samples.FocusableFocusGroupSample
  */
+@Stable
 fun Modifier.focusGroup(): Modifier {
     return this
         .then(focusGroupInspectorInfo)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
index a9b76b7..b2b172a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
@@ -99,7 +99,9 @@
     private var trackingFocusedChild = false
 
     /** The size of the scrollable container. */
-    private var viewportSize = IntSize.Zero
+    internal var viewportSize = IntSize.Zero
+        private set
+
     private var isAnimationRunning = false
     private val animationState =
         UpdatableAnimationState(bringIntoViewSpec.scrollAnimationSpec)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index 4b6c4856..08efb0a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -303,15 +303,18 @@
     private val _canDrag: (PointerInputChange) -> Boolean = { canDrag(it) }
     private val _startDragImmediately: () -> Boolean = { startDragImmediately() }
     private val velocityTracker = VelocityTracker()
+    private var isListeningForEvents = false
 
-    /**
-     * To preserve the original behavior we had (before the Modifier.Node migration) we need to
-     * scope the DragStopped and DragCancel methods to the node's coroutine scope instead of using
-     * the one provided by the pointer input modifier, this is to ensure that even when the pointer
-     * input scope is reset we will continue any coroutine scope scope that we started from these
-     * methods while the pointer input scope was active.
-     */
-    override fun onAttach() {
+    private fun startListeningForEvents() {
+        isListeningForEvents = true
+
+        /**
+         * To preserve the original behavior we had (before the Modifier.Node migration) we need to
+         * scope the DragStopped and DragCancel methods to the node's coroutine scope instead of using
+         * the one provided by the pointer input modifier, this is to ensure that even when the pointer
+         * input scope is reset we will continue any coroutine scope scope that we started from these
+         * methods while the pointer input scope was active.
+         */
         coroutineScope.launch {
             while (isActive) {
                 var event = channel.receive()
@@ -349,6 +352,13 @@
                             velocityTracker,
                             orientation
                         )?.let {
+                            /**
+                             * The gesture crossed the touch slop, events are now relevant
+                             * and should be propagated
+                             */
+                            if (!isListeningForEvents) {
+                                startListeningForEvents()
+                            }
                             var isDragSuccessful = false
                             try {
                                 isDragSuccessful = awaitDrag(
@@ -391,6 +401,7 @@
     private var dragInteraction: DragInteraction.Start? = null
 
     override fun onDetach() {
+        isListeningForEvents = false
         disposeInteractionSource()
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 7a75b6b..47afcb9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -42,6 +42,13 @@
 import androidx.compose.ui.focus.FocusPropertiesModifierNode
 import androidx.compose.ui.focus.FocusTargetModifierNode
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.KeyInputModifierNode
+import androidx.compose.ui.input.key.isCtrlPressed
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.type
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -103,6 +110,7 @@
  * @param interactionSource [MutableInteractionSource] that will be used to emit
  * drag events when this scrollable is being dragged.
  */
+@Stable
 @OptIn(ExperimentalFoundationApi::class)
 fun Modifier.scrollable(
     state: ScrollableState,
@@ -156,6 +164,7 @@
  * Note: This API is experimental as it brings support for some experimental features:
  * [overscrollEffect] and [bringIntoViewScroller].
  */
+@Stable
 @ExperimentalFoundationApi
 fun Modifier.scrollable(
     state: ScrollableState,
@@ -267,7 +276,8 @@
     private var interactionSource: MutableInteractionSource?,
     bringIntoViewSpec: BringIntoViewSpec
 ) : DelegatingNode(), ObserverModifierNode, CompositionLocalConsumerModifierNode,
-    FocusPropertiesModifierNode {
+    FocusPropertiesModifierNode, KeyInputModifierNode {
+
     val nestedScrollDispatcher = NestedScrollDispatcher()
 
     // Place holder fling behavior, we'll initialize it when the density is available.
@@ -391,6 +401,56 @@
     override fun applyFocusProperties(focusProperties: FocusProperties) {
         focusProperties.canFocus = false
     }
+
+    // Key handler for Page up/down scrolling behavior.
+    override fun onKeyEvent(event: KeyEvent): Boolean {
+        return if (enabled &&
+            (event.key == Key.PageDown || event.key == Key.PageUp) &&
+            (event.type == KeyEventType.KeyDown) &&
+            (!event.isCtrlPressed)
+            ) {
+            with(scrollingLogic) {
+                val scrollAmount: Offset = if (orientation == Orientation.Vertical) {
+                    val viewportHeight = contentInViewNode.viewportSize.height
+
+                    val yAmount = if (event.key == Key.PageUp) {
+                        viewportHeight.toFloat()
+                    } else {
+                        -viewportHeight.toFloat()
+                    }
+
+                    Offset(0f, yAmount)
+                } else {
+                    val viewportWidth = contentInViewNode.viewportSize.width
+
+                    val xAmount = if (event.key == Key.PageUp) {
+                        viewportWidth.toFloat()
+                    } else {
+                        -viewportWidth.toFloat()
+                    }
+
+                    Offset(xAmount, 0f)
+                }
+
+                // A coroutine is launched for every individual scroll event in the
+                // larger scroll gesture. If we see degradation in the future (that is,
+                // a fast scroll gesture on a slow device causes UI jank [not seen up to
+                // this point), we can switch to a more efficient solution where we
+                // lazily launch one coroutine (with the first event) and use a Channel
+                // to communicate the scroll amount to the UI thread.
+                coroutineScope.launch {
+                    scrollableState.scroll(MutatePriority.UserInput) {
+                        dispatchScroll(scrollAmount, Wheel)
+                    }
+                }
+            }
+            true
+        } else {
+            false
+        }
+    }
+
+    override fun onPreKeyEvent(event: KeyEvent) = false
 }
 
 /**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyGridSnapLayoutInfoProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyGridSnapLayoutInfoProvider.kt
index 236c03f..79f8886 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyGridSnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyGridSnapLayoutInfoProvider.kt
@@ -24,11 +24,11 @@
 import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
 import androidx.compose.foundation.lazy.grid.LazyGridLayoutInfo
 import androidx.compose.foundation.lazy.grid.LazyGridState
-import androidx.compose.ui.unit.Density
 import androidx.compose.ui.util.fastFilter
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastSumBy
 import kotlin.math.absoluteValue
+import kotlin.math.floor
 import kotlin.math.sign
 
 /**
@@ -49,11 +49,18 @@
     private val layoutInfo: LazyGridLayoutInfo
         get() = lazyGridState.layoutInfo
 
-    override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
-        val decayAnimationSpec: DecayAnimationSpec<Float> = splineBasedDecay(this)
+    override fun calculateApproachOffset(initialVelocity: Float): Float {
+        val decayAnimationSpec: DecayAnimationSpec<Float> = splineBasedDecay(lazyGridState.density)
         val offset =
             decayAnimationSpec.calculateTargetValue(NoDistance, initialVelocity).absoluteValue
-        val finalDecayOffset = (offset - calculateSnapStepSize()).coerceAtLeast(0f)
+
+        val estimatedNumberOfItemsInDecay = floor(offset.absoluteValue / averageItemSize())
+
+        // Decay to exactly half an item before the item where this decay would let us finish.
+        // The rest of the animation will be a snapping animation.
+        val approachOffset = estimatedNumberOfItemsInDecay * averageItemSize() - averageItemSize()
+        val finalDecayOffset = approachOffset.coerceAtLeast(0f)
+
         return if (finalDecayOffset == 0f) {
             finalDecayOffset
         } else {
@@ -71,7 +78,7 @@
         }
     }
 
-    override fun Density.calculateSnappingOffset(
+    override fun calculateSnappingOffset(
         currentVelocity: Float
     ): Float {
         var distanceFromItemBeforeTarget = Float.NEGATIVE_INFINITY
@@ -107,7 +114,7 @@
         )
     }
 
-    override fun Density.calculateSnapStepSize(): Float {
+    fun averageItemSize(): Float {
         val items = singleAxisItems()
         return if (items.isNotEmpty()) {
             val size = if (layoutInfo.orientation == Orientation.Vertical) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
index 20ad4cf..512b509 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
@@ -26,10 +26,10 @@
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
-import androidx.compose.ui.unit.Density
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastSumBy
 import kotlin.math.absoluteValue
+import kotlin.math.floor
 import kotlin.math.sign
 
 /**
@@ -52,11 +52,17 @@
         get() = lazyListState.layoutInfo
 
     // Decayed page snapping is the default
-    override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
-        val decayAnimationSpec: DecayAnimationSpec<Float> = splineBasedDecay(this)
+    override fun calculateApproachOffset(initialVelocity: Float): Float {
+        val decayAnimationSpec: DecayAnimationSpec<Float> = splineBasedDecay(lazyListState.density)
         val offset =
             decayAnimationSpec.calculateTargetValue(NoDistance, initialVelocity).absoluteValue
-        val finalDecayOffset = (offset - calculateSnapStepSize()).coerceAtLeast(0f)
+
+        val estimatedNumberOfItemsInDecay = floor(offset.absoluteValue / averageItemSize())
+
+        // Decay to exactly half an item before the item where this decay would let us finish.
+        // The rest of the animation will be a snapping animation.
+        val approachOffset = estimatedNumberOfItemsInDecay * averageItemSize() - averageItemSize()
+        val finalDecayOffset = approachOffset.coerceAtLeast(0f)
         return if (finalDecayOffset == 0f) {
             finalDecayOffset
         } else {
@@ -64,7 +70,7 @@
         }
     }
 
-    override fun Density.calculateSnappingOffset(currentVelocity: Float): Float {
+    override fun calculateSnappingOffset(currentVelocity: Float): Float {
         var lowerBoundOffset = Float.NEGATIVE_INFINITY
         var upperBoundOffset = Float.POSITIVE_INFINITY
 
@@ -94,7 +100,7 @@
         return calculateFinalOffset(currentVelocity, lowerBoundOffset, upperBoundOffset)
     }
 
-    override fun Density.calculateSnapStepSize(): Float = with(layoutInfo) {
+    fun averageItemSize(): Float = with(layoutInfo) {
         if (visibleItemsInfo.isNotEmpty()) {
             visibleItemsInfo.fastSumBy { it.size } / visibleItemsInfo.size.toFloat()
         } else {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
index 4df8844..2aa4948 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
@@ -38,8 +38,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import kotlin.math.abs
 import kotlin.math.absoluteValue
@@ -74,9 +72,8 @@
  * @param highVelocityAnimationSpec The animation spec used to approach the target offset. When
  * the fling velocity is large enough. Large enough means large enough to naturally decay.
  * @param snapAnimationSpec The animation spec used to finally snap to the correct bound.
- * @param density The screen [Density]
- * @param shortSnapVelocityThreshold Use the given velocity to determine if it's a
- * short or long snap.
+ * @param shortSnapVelocityThreshold Use the given velocity to determine if it's a short or long
+ * snap. The velocity should be provided in pixels/s.
  *
  */
 @ExperimentalFoundationApi
@@ -85,11 +82,9 @@
     private val lowVelocityAnimationSpec: AnimationSpec<Float>,
     private val highVelocityAnimationSpec: DecayAnimationSpec<Float>,
     private val snapAnimationSpec: AnimationSpec<Float>,
-    private val density: Density,
-    private val shortSnapVelocityThreshold: Dp = MinFlingVelocityDp
+    private val shortSnapVelocityThreshold: Float
 ) : FlingBehavior {
 
-    private val velocityThreshold = with(density) { shortSnapVelocityThreshold.toPx() }
     internal var motionScaleDuration = DefaultScrollMotionDurationScale
 
     override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
@@ -129,7 +124,7 @@
     ): AnimationResult<Float, AnimationVector1D> {
         // If snapping from scroll (short snap) or fling (long snap)
         val result = withContext(motionScaleDuration) {
-            if (abs(initialVelocity) <= abs(velocityThreshold)) {
+            if (abs(initialVelocity) <= abs(shortSnapVelocityThreshold)) {
                 shortSnap(initialVelocity, onRemainingScrollOffsetUpdate)
             } else {
                 longSnap(initialVelocity, onRemainingScrollOffsetUpdate)
@@ -145,9 +140,7 @@
         onRemainingScrollOffsetUpdate: (Float) -> Unit
     ): AnimationResult<Float, AnimationVector1D> {
         debugLog { "Short Snapping" }
-        val closestOffset = with(snapLayoutInfoProvider) {
-            density.calculateSnappingOffset(0f)
-        }
+        val closestOffset = snapLayoutInfoProvider.calculateSnappingOffset(0f)
 
         var remainingScrollOffset = closestOffset
 
@@ -169,7 +162,7 @@
     ): AnimationResult<Float, AnimationVector1D> {
         debugLog { "Long Snapping" }
         val initialOffset =
-            with(snapLayoutInfoProvider) { density.calculateApproachOffset(initialVelocity) }.let {
+            snapLayoutInfoProvider.calculateApproachOffset(initialVelocity).let {
                 abs(it) * sign(initialVelocity) // ensure offset sign is correct
             }
         var remainingScrollOffset = initialOffset
@@ -211,11 +204,7 @@
                 HighVelocityApproachAnimation(highVelocityAnimationSpec)
             } else {
                 debugLog { "Low Velocity Approach" }
-                LowVelocityApproachAnimation(
-                    lowVelocityAnimationSpec,
-                    snapLayoutInfoProvider,
-                    density
-                )
+                LowVelocityApproachAnimation(lowVelocityAnimationSpec)
             }
 
         return approach(
@@ -223,7 +212,6 @@
             initialVelocity,
             animation,
             snapLayoutInfoProvider,
-            density,
             onAnimationStep
         )
     }
@@ -236,12 +224,11 @@
         velocity: Float
     ): Boolean {
         val decayOffset = highVelocityAnimationSpec.calculateTargetValue(NoDistance, velocity)
-        val snapStepSize = with(snapLayoutInfoProvider) { density.calculateSnapStepSize() }
         debugLog {
             "Evaluating decay possibility with " +
                 "decayOffset=$decayOffset and proposed approach=$offset"
         }
-        return decayOffset.absoluteValue >= (offset.absoluteValue + snapStepSize)
+        return decayOffset.absoluteValue >= offset.absoluteValue
     }
 
     override fun equals(other: Any?): Boolean {
@@ -250,7 +237,6 @@
                 other.highVelocityAnimationSpec == this.highVelocityAnimationSpec &&
                 other.lowVelocityAnimationSpec == this.lowVelocityAnimationSpec &&
                 other.snapLayoutInfoProvider == this.snapLayoutInfoProvider &&
-                other.density == this.density &&
                 other.shortSnapVelocityThreshold == this.shortSnapVelocityThreshold
         } else {
             false
@@ -262,7 +248,6 @@
         .let { 31 * it + highVelocityAnimationSpec.hashCode() }
         .let { 31 * it + lowVelocityAnimationSpec.hashCode() }
         .let { 31 * it + snapLayoutInfoProvider.hashCode() }
-        .let { 31 * it + density.hashCode() }
         .let { 31 * it + shortSnapVelocityThreshold.hashCode() }
 }
 
@@ -287,7 +272,7 @@
             lowVelocityAnimationSpec = tween(easing = LinearEasing),
             highVelocityAnimationSpec = highVelocityApproachSpec,
             snapAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow),
-            density = density
+            shortSnapVelocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
         )
     }
 }
@@ -309,7 +294,6 @@
     initialVelocity: Float,
     animation: ApproachAnimation<Float, AnimationVector1D>,
     snapLayoutInfoProvider: SnapLayoutInfoProvider,
-    density: Density,
     onAnimationStep: (delta: Float) -> Unit
 ): AnimationResult<Float, AnimationVector1D> {
 
@@ -320,9 +304,8 @@
         onAnimationStep
     )
 
-    val remainingOffset = with(snapLayoutInfoProvider) {
-        density.calculateSnappingOffset(currentAnimationState.velocity)
-    }
+    val remainingOffset =
+        snapLayoutInfoProvider.calculateSnappingOffset(currentAnimationState.velocity)
 
     // will snap the remainder
     return AnimationResult(remainingOffset, currentAnimationState)
@@ -455,9 +438,7 @@
 
 @OptIn(ExperimentalFoundationApi::class)
 private class LowVelocityApproachAnimation(
-    private val lowVelocityAnimationSpec: AnimationSpec<Float>,
-    private val layoutInfoProvider: SnapLayoutInfoProvider,
-    private val density: Density
+    private val lowVelocityAnimationSpec: AnimationSpec<Float>
 ) : ApproachAnimation<Float, AnimationVector1D> {
     override suspend fun approachAnimation(
         scope: ScrollScope,
@@ -466,10 +447,7 @@
         onAnimationStep: (delta: Float) -> Unit
     ): AnimationResult<Float, AnimationVector1D> {
         val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
-        val targetOffset =
-            (abs(offset) + with(layoutInfoProvider) { density.calculateSnapStepSize() }) * sign(
-                velocity
-            )
+        val targetOffset = offset.absoluteValue * sign(velocity)
         return with(scope) {
             animateSnap(
                 targetOffset = targetOffset,
@@ -532,6 +510,7 @@
 }
 
 private const val DEBUG = false
+
 private inline fun debugLog(generateMsg: () -> String) {
     if (DEBUG) {
         println("SnapFlingBehavior: ${generateMsg()}")
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapLayoutInfoProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapLayoutInfoProvider.kt
index 5f05d42..2de3743 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapLayoutInfoProvider.kt
@@ -17,21 +17,22 @@
 package androidx.compose.foundation.gestures.snapping
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.ui.unit.Density
 
 /**
  * Provides information about the layout that is using a SnapFlingBehavior.
  * The provider should give the following information:
- * 1) Snapping bounds, the previous and the next snap position offset.
- * 2) Snap Step Size, the minimum size that the SnapFlingBehavior can animate.
- * 3) Approach offset calculation, an offset to be consumed before snapping to a defined bound.
+ * 1) Snapping offset: The next snap position offset.
+ * 2) Approach offset: An offset to be consumed before snapping to a defined bound.
+ *
+ * In snapping, the approach offset and the snapping offset can be used to control how a snapping
+ * animation will look in a given SnappingLayout. The complete snapping animation can be split
+ * into 2 phases: Approach and Snapping. In the Approach phase, we'll use an animation to consume
+ * all of the offset provided by [calculateApproachOffset]. In the snapping phase,
+ * [SnapFlingBehavior] will use an animation to consume all of the offset
+ * provided by [calculateSnappingOffset].
  */
 @ExperimentalFoundationApi
 interface SnapLayoutInfoProvider {
-    /**
-     * The minimum offset that snapping will use to animate.(e.g. an item size)
-     */
-    fun Density.calculateSnapStepSize(): Float
 
     /**
      * Calculate the distance to navigate before settling into the next snapping bound.
@@ -39,7 +40,7 @@
      * @param initialVelocity The current fling movement velocity. You can use this tho calculate a
      * velocity based offset.
      */
-    fun Density.calculateApproachOffset(initialVelocity: Float): Float
+    fun calculateApproachOffset(initialVelocity: Float): Float
 
     /**
      * Given a target placement in a layout, the snapping offset is the next snapping position
@@ -50,5 +51,5 @@
      * @param currentVelocity The current fling movement velocity. This may change throughout the
      * fling animation.
      */
-    fun Density.calculateSnappingOffset(currentVelocity: Float): Float
+    fun calculateSnappingOffset(currentVelocity: Float): Float
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapPositionInLayout.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapPositionInLayout.kt
index f881aba..b0923d6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapPositionInLayout.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapPositionInLayout.kt
@@ -17,7 +17,6 @@
 package androidx.compose.foundation.gestures.snapping
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.ui.unit.Density
 
 /**
  * Describes the general positioning of a given snap item in its containing layout.
@@ -30,7 +29,7 @@
      * align the item with a position within the container. As a base line, if we wanted to align
      * the start of the container and the start of the item, we would return 0 in this function.
      */
-    fun Density.position(layoutSize: Int, itemSize: Int, itemIndex: Int): Int
+    fun position(layoutSize: Int, itemSize: Int, itemIndex: Int): Int
 
     companion object {
         /**
@@ -42,7 +41,7 @@
 }
 
 @OptIn(ExperimentalFoundationApi::class)
-internal fun Density.calculateDistanceToDesiredSnapPosition(
+internal fun calculateDistanceToDesiredSnapPosition(
     mainAxisViewPortSize: Int,
     beforeContentPadding: Int,
     afterContentPadding: Int,
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 7df5322..7122368 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
@@ -16,12 +16,14 @@
 
 package androidx.compose.foundation.lazy
 
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastSumBy
 import kotlin.math.abs
 
+@OptIn(ExperimentalFoundationApi::class)
 internal class LazyListAnimateScrollScope(
     private val state: LazyListState
 ) : LazyLayoutAnimateScrollScope {
@@ -36,20 +38,20 @@
     override val itemCount: Int
         get() = state.layoutInfo.totalItemsCount
 
-    override fun getOffsetForItem(index: Int): Int? =
+    override fun getVisibleItemScrollOffset(index: Int): Int =
         state.layoutInfo.visibleItemsInfo.fastFirstOrNull {
             it.index == index
-        }?.offset
+        }?.offset ?: 0
 
     override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
         state.snapToItemIndexInternal(index, scrollOffset)
     }
 
-    override fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
-        val averageSize = averageItemSize
-        val indexesDiff = index - firstVisibleItemIndex
-        var coercedOffset = minOf(abs(targetScrollOffset), averageSize)
-        if (targetScrollOffset < 0) coercedOffset *= -1
+    override fun calculateDistanceTo(targetIndex: Int, targetItemOffset: Int): Float {
+        val averageSize = visibleItemsAverageSize
+        val indexesDiff = targetIndex - firstVisibleItemIndex
+        var coercedOffset = minOf(abs(targetItemOffset), averageSize)
+        if (targetItemOffset < 0) coercedOffset *= -1
         return (averageSize * indexesDiff).toFloat() +
             coercedOffset - firstVisibleItemScrollOffset
     }
@@ -58,7 +60,7 @@
         state.scroll(block = block)
     }
 
-    override val averageItemSize: Int
+    override val visibleItemsAverageSize: Int
         get() {
             val layoutInfo = state.layoutInfo
             val visibleItems = layoutInfo.visibleItemsInfo
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index 0f08e81..36572c7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -30,6 +30,7 @@
 import androidx.compose.ui.util.fastFilter
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachReversed
 import kotlin.math.abs
 import kotlin.math.roundToInt
 import kotlin.math.sign
@@ -473,7 +474,7 @@
         list.add(measuredItemProvider.getAndMeasure(i))
     }
 
-    pinnedItems.fastForEach { index ->
+    pinnedItems.fastForEachReversed { index ->
         if (index < start) {
             if (list == null) list = mutableListOf()
             list?.add(measuredItemProvider.getAndMeasure(index))
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 e398593..bf0a56e 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
@@ -16,12 +16,14 @@
 
 package androidx.compose.foundation.lazy.grid
 
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope
 import androidx.compose.ui.util.fastFirstOrNull
 import kotlin.math.abs
 import kotlin.math.max
 
+@OptIn(ExperimentalFoundationApi::class)
 internal class LazyGridAnimateScrollScope(
     private val state: LazyGridState
 ) : LazyLayoutAnimateScrollScope {
@@ -35,7 +37,10 @@
 
     override val itemCount: Int get() = state.layoutInfo.totalItemsCount
 
-    override fun getOffsetForItem(index: Int): Int? =
+    override val visibleItemsAverageSize: Int
+        get() = calculateLineAverageMainAxisSize(state.layoutInfo, state.isVertical)
+
+    override fun getVisibleItemScrollOffset(index: Int): Int =
         state.layoutInfo.visibleItemsInfo
             .fastFirstOrNull {
                 it.index == index
@@ -45,22 +50,22 @@
                 } else {
                     item.offset.x
                 }
-            }
+            } ?: 0
 
     override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
         state.snapToItemIndexInternal(index, scrollOffset)
     }
 
-    override fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
+    override fun calculateDistanceTo(targetIndex: Int, targetItemOffset: Int): Float {
         val slotsPerLine = state.slotsPerLine
-        val averageLineMainAxisSize = averageItemSize
-        val before = index < firstVisibleItemIndex
+        val averageLineMainAxisSize = visibleItemsAverageSize
+        val before = targetIndex < firstVisibleItemIndex
         val linesDiff =
-            (index - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
+            (targetIndex - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
                 slotsPerLine
 
-        var coercedOffset = minOf(abs(targetScrollOffset), averageLineMainAxisSize)
-        if (targetScrollOffset < 0) coercedOffset *= -1
+        var coercedOffset = minOf(abs(targetItemOffset), averageLineMainAxisSize)
+        if (targetItemOffset < 0) coercedOffset *= -1
         return (averageLineMainAxisSize * linesDiff).toFloat() +
             coercedOffset - firstVisibleItemScrollOffset
     }
@@ -112,7 +117,4 @@
     override suspend fun scroll(block: suspend ScrollScope.() -> Unit) {
         state.scroll(block = block)
     }
-
-    override val averageItemSize: Int
-        get() = calculateLineAverageMainAxisSize(state.layoutInfo, state.isVertical)
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 997dcd6..f69ead7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -28,6 +28,7 @@
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.util.fastFilter
 import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachReversed
 import androidx.compose.ui.util.fastSumBy
 import kotlin.math.abs
 import kotlin.math.min
@@ -396,7 +397,7 @@
     } else {
         var currentMainAxis = firstLineScrollOffset
 
-        itemsBefore.fastForEach {
+        itemsBefore.fastForEachReversed {
             currentMainAxis -= it.mainAxisSizeWithSpacings
             it.position(currentMainAxis, 0, layoutWidth, layoutHeight)
             positionedItems.add(it)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyAnimateScroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyAnimateScroll.kt
index eac8969..538aa2a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyAnimateScroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyAnimateScroll.kt
@@ -20,6 +20,7 @@
 import androidx.compose.animation.core.AnimationVector1D
 import androidx.compose.animation.core.animateTo
 import androidx.compose.animation.core.copy
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.dp
@@ -43,30 +44,73 @@
 }
 
 /**
- * Abstraction over animated scroll for using [animateScrollToItem] in different layouts.
- * todo(b/243786897): revisit this API and make it public
+ * A scope to allow customization of animated scroll in LazyLayouts. This scope contains all needed
+ * information to perform an animatedScroll in a scrollable LazyLayout.
  */
+@ExperimentalFoundationApi
 internal interface LazyLayoutAnimateScrollScope {
 
+    /**
+     * The index of the first visible item in the lazy layout.
+     */
     val firstVisibleItemIndex: Int
 
+    /**
+     * The offset of the first visible item.
+     */
     val firstVisibleItemScrollOffset: Int
 
+    /**
+     * The last visible item in the LazyLayout, lastVisibleItemIndex - firstVisibleItemOffset + 1
+     * is the number of visible items.
+     */
     val lastVisibleItemIndex: Int
 
+    /**
+     * The total item count.
+     */
     val itemCount: Int
 
-    val averageItemSize: Int
+    /**
+     * The average size of visible items.
+     */
+    val visibleItemsAverageSize: Int
 
-    fun getOffsetForItem(index: Int): Int?
+    /**
+     * Retrieves the scroll offset for an item that is currently visible.
+     */
+    fun getVisibleItemScrollOffset(index: Int): Int
 
+    /**
+     * Immediately scroll to [index] and settle in [scrollOffset].
+     */
     fun ScrollScope.snapToItem(index: Int, scrollOffset: Int)
 
-    fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float
+    /**
+     * The "expected" distance to [targetIndex]. This means, how far one needs to scroll to have
+     * [targetIndex] be the [firstVisibleItemIndex] and [firstVisibleItemScrollOffset] be
+     * [targetItemOffset]. In other words, how far one needs to scroll to reach [targetIndex].
+     */
+    fun calculateDistanceTo(targetIndex: Int, targetItemOffset: Int): Float
 
+    /**
+     * Call this function to take control of scrolling and gain the ability to send scroll events
+     * via [ScrollScope.scrollBy] and [ScrollScope.snapToItem]. All actions that change the logical
+     * scroll position must be performed within a [scroll] block (even if they don't call any other
+     * methods on this object) in order to guarantee that mutual exclusion is enforced.
+     *
+     * If [scroll] is called from elsewhere, this will be canceled.
+     */
     suspend fun scroll(block: suspend ScrollScope.() -> Unit)
 }
 
+@Suppress("PrimitiveInLambda")
+@OptIn(ExperimentalFoundationApi::class)
+internal fun LazyLayoutAnimateScrollScope.isItemVisible(index: Int): Boolean {
+    return index in firstVisibleItemIndex..lastVisibleItemIndex
+}
+
+@OptIn(ExperimentalFoundationApi::class)
 internal suspend fun LazyLayoutAnimateScrollScope.animateScrollToItem(
     index: Int,
     scrollOffset: Int,
@@ -82,8 +126,9 @@
             val minDistancePx = with(density) { MinimumDistance.toPx() }
             var loop = true
             var anim = AnimationState(0f)
-            val targetItemInitialOffset = getOffsetForItem(index)
-            if (targetItemInitialOffset != null) {
+
+            if (isItemVisible(index)) {
+                val targetItemInitialOffset = getVisibleItemScrollOffset(index)
                 // It's already visible, just animate directly
                 throw ItemFoundInScroll(targetItemInitialOffset, anim)
             }
@@ -119,7 +164,7 @@
 
             var loops = 1
             while (loop && itemCount > 0) {
-                val expectedDistance = expectedDistanceTo(index, scrollOffset)
+                val expectedDistance = calculateDistanceTo(index, scrollOffset)
                 val target = if (abs(expectedDistance) < targetDistancePx) {
                     val absTargetPx = maxOf(abs(expectedDistance), minDistancePx)
                     if (forward) absTargetPx else -absTargetPx
@@ -140,9 +185,7 @@
                     sequentialAnimation = (anim.velocity != 0f)
                 ) {
                     // If we haven't found the item yet, check if it's visible.
-                    var targetItemOffset = getOffsetForItem(index)
-
-                    if (targetItemOffset == null) {
+                    if (!isItemVisible(index)) {
                         // Springs can overshoot their target, clamp to the desired range
                         val coercedValue = if (target > 0) {
                             value.coerceAtMost(target)
@@ -155,8 +198,7 @@
                         }
 
                         val consumed = scrollBy(delta)
-                        targetItemOffset = getOffsetForItem(index)
-                        if (targetItemOffset != null) {
+                        if (isItemVisible(index)) {
                             debugLog { "Found the item after performing scrollBy()" }
                         } else if (!isOvershot()) {
                             if (delta != consumed) {
@@ -218,7 +260,8 @@
                         loop = false
                         cancelAnimation()
                         return@animateTo
-                    } else if (targetItemOffset != null) {
+                    } else if (isItemVisible(index)) {
+                        val targetItemOffset = getVisibleItemScrollOffset(index)
                         debugLog { "Found item" }
                         throw ItemFoundInScroll(targetItemOffset, anim)
                     }
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 e16e767..7585ff4 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
@@ -36,10 +36,13 @@
 
     override val itemCount: Int get() = state.layoutInfo.totalItemsCount
 
-    override fun getOffsetForItem(index: Int): Int? =
-        state.layoutInfo.findVisibleItem(index)?.offset?.let {
+    override fun getVisibleItemScrollOffset(index: Int): Int {
+        val searchedIndex = state.layoutInfo.visibleItemsInfo.binarySearch { it.index - index }
+        val item = state.layoutInfo.visibleItemsInfo[searchedIndex]
+        return item.offset.let {
             if (state.isVertical) it.y else it.x
         }
+    }
 
     override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
         with(state) {
@@ -47,12 +50,12 @@
         }
     }
 
-    override fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
-        val averageMainAxisItemSize = averageItemSize
+    override fun calculateDistanceTo(targetIndex: Int, targetItemOffset: Int): Float {
+        val averageMainAxisItemSize = visibleItemsAverageSize
 
-        val lineDiff = index / state.laneCount - firstVisibleItemIndex / state.laneCount
-        var coercedOffset = minOf(abs(targetScrollOffset), averageMainAxisItemSize)
-        if (targetScrollOffset < 0) coercedOffset *= -1
+        val lineDiff = targetIndex / state.laneCount - firstVisibleItemIndex / state.laneCount
+        var coercedOffset = minOf(abs(targetItemOffset), averageMainAxisItemSize)
+        if (targetItemOffset < 0) coercedOffset *= -1
         return averageMainAxisItemSize * lineDiff.toFloat() +
             coercedOffset - firstVisibleItemScrollOffset
     }
@@ -61,7 +64,7 @@
         state.scroll(block = block)
     }
 
-    override val averageItemSize: Int
+    override val visibleItemsAverageSize: Int
         get() {
             val layoutInfo = state.layoutInfo
             val visibleItems = layoutInfo.visibleItemsInfo
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index cacee1b..131225f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -30,6 +30,7 @@
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachIndexed
+import androidx.compose.ui.util.fastForEachReversed
 import androidx.compose.ui.util.fastMaxOfOrNull
 import androidx.compose.ui.util.packInts
 import androidx.compose.ui.util.unpackInt1
@@ -755,7 +756,8 @@
                         firstItemIndices[lane] > itemIndex
                     }
                 }
-            }
+            },
+            beforeVisibleBounds = true
         )
 
         val visibleItems = calculateVisibleItems(
@@ -787,7 +789,8 @@
                         currentItemIndices[lane] < itemIndex
                     }
                 }
-            }
+            },
+            beforeVisibleBounds = false
         )
 
         val positionedItems = mutableListOf<LazyStaggeredGridMeasuredItem>()
@@ -885,11 +888,12 @@
 @ExperimentalFoundationApi
 private inline fun LazyStaggeredGridMeasureContext.calculateExtraItems(
     position: (LazyStaggeredGridMeasuredItem) -> Unit,
-    filter: (itemIndex: Int) -> Boolean
+    filter: (itemIndex: Int) -> Boolean,
+    beforeVisibleBounds: Boolean
 ): List<LazyStaggeredGridMeasuredItem> {
     var result: MutableList<LazyStaggeredGridMeasuredItem>? = null
 
-    pinnedItems.fastForEach { index ->
+    pinnedItems.fastForEach(beforeVisibleBounds) { index ->
         if (filter(index)) {
             val spanRange = itemProvider.getSpanRange(index, 0)
             if (result == null) {
@@ -904,6 +908,10 @@
     return result ?: emptyList()
 }
 
+private inline fun <T> List<T>.fastForEach(reverse: Boolean = false, action: (T) -> Unit) {
+    if (reverse) fastForEachReversed(action) else fastForEach(action)
+}
+
 @JvmInline
 internal value class SpanRange private constructor(val packedValue: Long) {
     constructor(lane: Int, span: Int) : this(packInts(lane, lane + span))
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
index 6582c25..764eef5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
@@ -45,6 +45,7 @@
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.pointer.PointerEventPass
@@ -268,6 +269,7 @@
                 val downEvent =
                     awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
                 var upEventOrCancellation: PointerInputChange? = null
+                state.upDownDifference = Offset.Zero // Reset
                 while (upEventOrCancellation == null) {
                     val event = awaitPointerEvent(pass = PointerEventPass.Initial)
                     if (event.changes.fastAll { it.changedToUp() }) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
index 30d025e..881469e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
@@ -373,8 +373,7 @@
                 lowVelocityAnimationSpec = lowVelocityAnimationSpec,
                 highVelocityAnimationSpec = highVelocityAnimationSpec,
                 snapAnimationSpec = snapAnimationSpec,
-                density = density,
-                shortSnapVelocityThreshold = snapVelocityThreshold
+                shortSnapVelocityThreshold = with(density) { snapVelocityThreshold.toPx() }
             )
         }
     }
@@ -496,7 +495,7 @@
             return this != Float.POSITIVE_INFINITY && this != Float.NEGATIVE_INFINITY
         }
 
-        override fun Density.calculateSnappingOffset(currentVelocity: Float): Float {
+        override fun calculateSnappingOffset(currentVelocity: Float): Float {
             var lowerBoundOffset = Float.NEGATIVE_INFINITY
             var upperBoundOffset = Float.POSITIVE_INFINITY
 
@@ -537,9 +536,23 @@
             val finalDistance = when (sign(currentVelocity)) {
                 0f -> {
                     if (offsetFromSnappedPositionOverflow.absoluteValue > snapPositionalThreshold) {
+                        // If we crossed the threshold, go to the next bound
                         if (isForward) upperBoundOffset else lowerBoundOffset
                     } else {
-                        if (isForward) lowerBoundOffset else upperBoundOffset
+                        // if we haven't crossed the threshold. but scrolled minimally, we should
+                        // bound to the previous bound
+                        if (abs(pagerState.currentPageOffsetFraction) >=
+                            abs(pagerState.positionThresholdFraction)
+                        ) {
+                            if (isForward) lowerBoundOffset else upperBoundOffset
+                        } else {
+                            // if we haven't scrolled minimally, settle for the closest bound
+                            if (lowerBoundOffset.absoluteValue < upperBoundOffset.absoluteValue) {
+                                lowerBoundOffset
+                            } else {
+                                upperBoundOffset
+                            }
+                        }
                     }
                 }
 
@@ -555,9 +568,7 @@
             }
         }
 
-        override fun Density.calculateSnapStepSize(): Float = layoutInfo.pageSize.toFloat()
-
-        override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
+        override fun calculateApproachOffset(initialVelocity: Float): Float {
             debugLog { "Approach Velocity=$initialVelocity" }
             val effectivePageSizePx = pagerState.pageSize + pagerState.pageSpacing
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLazyAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLazyAnimateScrollScope.kt
new file mode 100644
index 0000000..e69ce84
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLazyAnimateScrollScope.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope
+import androidx.compose.ui.util.fastFirstOrNull
+
+/**
+ * A [LazyLayoutAnimateScrollScope] that allows customization of animated scroll in [Pager].
+ * The scope contains information about the layout where animated scroll can be performed as well as
+ * the necessary tools to do that respecting the scroll mutation priority.
+ *
+ */
+@ExperimentalFoundationApi
+internal fun PagerLazyAnimateScrollScope(state: PagerState): LazyLayoutAnimateScrollScope {
+    return object : LazyLayoutAnimateScrollScope {
+
+        override val firstVisibleItemIndex: Int get() = state.firstVisiblePage
+
+        override val firstVisibleItemScrollOffset: Int get() = state.firstVisiblePageOffset
+
+        override val lastVisibleItemIndex: Int
+            get() =
+                state.layoutInfo.visiblePagesInfo.last().index
+
+        override val itemCount: Int get() = state.pageCount
+
+        override fun getVisibleItemScrollOffset(index: Int): Int {
+            return state.layoutInfo.visiblePagesInfo.fastFirstOrNull { it.index == index }?.offset
+                ?: 0
+        }
+
+        override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
+            state.snapToItem(index, scrollOffset)
+        }
+
+        override fun calculateDistanceTo(targetIndex: Int, targetItemOffset: Int): Float {
+            return (targetIndex - state.currentPage) * visibleItemsAverageSize.toFloat() +
+                targetItemOffset
+        }
+
+        override suspend fun scroll(block: suspend ScrollScope.() -> Unit) {
+            state.scroll(block = block)
+        }
+
+        override val visibleItemsAverageSize: Int
+            get() = state.pageSize + state.pageSpacing
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt
index 1c8676f..5c6aaf5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt
@@ -52,15 +52,14 @@
     verticalAlignment: Alignment.Vertical?,
     pageCount: () -> Int,
 ) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
-    contentPadding,
-    pageSpacing,
-    pageSize,
     state,
     contentPadding,
     reverseLayout,
     orientation,
     horizontalAlignment,
     verticalAlignment,
+    pageSpacing,
+    pageSize,
     pageCount,
 ) {
     { containerConstraints ->
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 8efdab9..de4ecdb 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
@@ -28,7 +28,6 @@
 import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope
 import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
 import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
@@ -151,7 +150,8 @@
      */
     internal var upDownDifference: Offset by mutableStateOf(Offset.Zero)
     internal var snapRemainingScrollOffset by mutableFloatStateOf(0f)
-    private var animateScrollScope = PagerLazyAnimateScrollScope(this)
+
+    private val animatedScrollScope = PagerLazyAnimateScrollScope(this)
 
     private var isScrollingForward: Boolean by mutableStateOf(false)
 
@@ -233,7 +233,7 @@
      * How far the current page needs to scroll so the target page is considered to be the next
      * page.
      */
-    private val positionThresholdFraction: Float
+    internal val positionThresholdFraction: Float
         get() = with(density) {
             val minThreshold = minOf(DefaultPositionThreshold.toPx(), pageSize / 2f)
             minThreshold / pageSize.toFloat()
@@ -259,7 +259,7 @@
      */
     val currentPage: Int get() = scrollPosition.currentPage
 
-    private var animationTargetPage by mutableIntStateOf(-1)
+    private var programmaticScrollTargetPage by mutableIntStateOf(-1)
 
     private var settledPageState by mutableIntStateOf(initialPage)
 
@@ -291,8 +291,8 @@
     val targetPage: Int by derivedStateOf(structuralEqualityPolicy()) {
         val finalPage = if (!isScrollInProgress) {
             currentPage
-        } else if (animationTargetPage != -1) {
-            animationTargetPage
+        } else if (programmaticScrollTargetPage != -1) {
+            programmaticScrollTargetPage
         } else if (snapRemainingScrollOffset == 0.0f) {
             // act on scroll only
             if (abs(currentPageOffsetFraction) >= abs(positionThresholdFraction)) {
@@ -399,6 +399,41 @@
         snapToItem(targetPage, offset)
     }
 
+    /**
+     * Jump immediately to a given [page] with a given [pageOffsetFraction] inside
+     * a [ScrollScope]. Use this method to create custom animated scrolling experiences. This will
+     * update the value of [currentPage] and [currentPageOffsetFraction] immediately, but can only
+     * be used inside a [ScrollScope], use [scroll] to gain access to a [ScrollScope].
+     *
+     * Please refer to the sample to learn how to use this API.
+     * @sample androidx.compose.foundation.samples.PagerCustomAnimateScrollToPage
+     *
+     * @param page The destination page to scroll to
+     * @param pageOffsetFraction A fraction of the page size that indicates the offset the
+     * destination page will be offset from its snapped position.
+     */
+    fun ScrollScope.updateCurrentPage(page: Int, pageOffsetFraction: Float = 0.0f) {
+        val targetPageOffsetToSnappedPosition = (pageOffsetFraction * pageAvailableSpace).toInt()
+        with(animatedScrollScope) {
+            snapToItem(page, targetPageOffsetToSnappedPosition)
+        }
+    }
+
+    /**
+     * Used to update [targetPage] during a programmatic scroll operation. This can only be called
+     * inside a [ScrollScope] and should be called anytime a custom scroll (through [scroll]) is
+     * executed in order to correctly update [targetPage]. This will not move the pages and it's
+     * still the responsibility of the caller to call [ScrollScope.scrollBy] in order to actually
+     * get to [targetPage]. By the end of the [scroll] block, when the [Pager] is no longer
+     * scrolling [targetPage] will assume the value of [currentPage].
+     *
+     * Please refer to the sample to learn how to use this API.
+     * @sample androidx.compose.foundation.samples.PagerCustomAnimateScrollToPage
+     */
+    fun ScrollScope.updateTargetPage(targetPage: Int) {
+        programmaticScrollTargetPage = targetPage.coerceInPageRange()
+    }
+
     internal fun snapToItem(page: Int, offset: Int) {
         scrollPosition.requestPosition(page, offset)
         remeasurement?.forceRemeasure()
@@ -431,14 +466,48 @@
             "pageOffsetFraction $pageOffsetFraction is not within the range -0.5 to 0.5"
         }
         val targetPage = page.coerceInPageRange()
-        animationTargetPage = targetPage
         val targetPageOffsetToSnappedPosition = (pageOffsetFraction * pageAvailableSpace).toInt()
-        animateScrollScope.animateScrollToItem(
-            targetPage,
-            targetPageOffsetToSnappedPosition,
-            animationSpec
-        )
-        animationTargetPage = -1
+
+        with(animatedScrollScope) {
+            scroll {
+                updateTargetPage(targetPage)
+                val forward = targetPage > firstVisibleItemIndex
+                val visiblePages = lastVisibleItemIndex - firstVisibleItemIndex + 1
+                if (((forward && targetPage > lastVisibleItemIndex) ||
+                        (!forward && targetPage < firstVisibleItemIndex)) &&
+                    abs(targetPage - firstVisibleItemIndex) >= MaxPagesForAnimateScroll
+                ) {
+                    val preJumpPosition = if (forward) {
+                        (targetPage - visiblePages).coerceAtLeast(firstVisibleItemIndex)
+                    } else {
+                        (targetPage + visiblePages).coerceAtMost(firstVisibleItemIndex)
+                    }
+
+                    debugLog {
+                        "animateScrollToPage with pre-jump to position=$preJumpPosition"
+                    }
+
+                    // Pre-jump to 1 viewport away from destination page, if possible
+                    snapToItem(preJumpPosition, 0)
+                }
+                val pageAvailableSpace = visibleItemsAverageSize
+                val currentPosition = firstVisibleItemIndex
+                val targetOffset = targetPage * pageAvailableSpace
+                val currentOffset = currentPosition * pageAvailableSpace
+
+                // The final delta displacement will be the difference between the pages offsets
+                // discounting whatever offset the original page had scrolled plus the offset
+                // fraction requested by the user.
+                val displacement = (targetOffset - currentOffset -
+                    firstVisibleItemScrollOffset + targetPageOffsetToSnappedPosition).toFloat()
+
+                debugLog { "animateScrollToPage $displacement pixels" }
+                var previousValue = 0f
+                animate(0f, displacement, animationSpec = animationSpec) { currentValue, _ ->
+                    previousValue += scrollBy(currentValue - previousValue)
+                }
+            }
+        }
     }
 
     private suspend fun awaitScrollDependencies() {
@@ -450,7 +519,12 @@
         block: suspend ScrollScope.() -> Unit
     ) {
         awaitScrollDependencies()
+        // will scroll and it's not scrolling already update settled page
+        if (!isScrollInProgress) {
+            settledPageState = currentPage
+        }
         scrollableState.scroll(scrollPriority, block)
+        programmaticScrollTargetPage = -1 // reset animated scroll target page indicator
     }
 
     override fun dispatchRawDelta(delta: Float): Float {
@@ -480,10 +554,6 @@
             result.firstVisiblePageOffset != 0
         numMeasurePasses++
         cancelPrefetchIfVisibleItemsChanged(result)
-        if (!isScrollInProgress) {
-            settledPageState = currentPage
-            upDownDifference = Offset.Zero
-        }
     }
 
     private fun Int.coerceInPageRange() = if (pageCount > 0) {
@@ -660,79 +730,3 @@
         println("PagerState: ${generateMsg()}")
     }
 }
-
-@OptIn(ExperimentalFoundationApi::class)
-private class PagerLazyAnimateScrollScope(val state: PagerState) : LazyLayoutAnimateScrollScope {
-
-    override val firstVisibleItemIndex: Int get() = state.firstVisiblePage
-
-    override val firstVisibleItemScrollOffset: Int get() = state.firstVisiblePageOffset
-
-    override val lastVisibleItemIndex: Int get() = state.layoutInfo.visiblePagesInfo.last().index
-
-    override val itemCount: Int get() = state.pageCount
-
-    override fun getOffsetForItem(index: Int): Int? {
-        return state.layoutInfo.visiblePagesInfo.fastFirstOrNull { it.index == index }?.offset
-    }
-
-    override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
-        state.snapToItem(index, scrollOffset)
-    }
-
-    override fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
-        return (index - state.currentPage) * averageItemSize.toFloat() + targetScrollOffset
-    }
-
-    override suspend fun scroll(block: suspend ScrollScope.() -> Unit) {
-        state.scroll(block = block)
-    }
-
-    override val averageItemSize: Int
-        get() = state.pageSize + state.pageSpacing
-}
-
-private suspend fun LazyLayoutAnimateScrollScope.animateScrollToItem(
-    index: Int,
-    offset: Int,
-    animationSpec: AnimationSpec<Float>
-) {
-    scroll {
-        val forward = index > firstVisibleItemIndex
-        val visiblePages = lastVisibleItemIndex - firstVisibleItemIndex + 1
-        if (((forward && index > lastVisibleItemIndex) ||
-                (!forward && index < firstVisibleItemIndex)) &&
-            abs(index - firstVisibleItemIndex) >= MaxPagesForAnimateScroll
-        ) {
-            val preJumpPosition = if (forward) {
-                (index - visiblePages).coerceAtLeast(firstVisibleItemIndex)
-            } else {
-                (index + visiblePages).coerceAtMost(firstVisibleItemIndex)
-            }
-
-            debugLog {
-                "animateScrollToPage with pre-jump to position=$preJumpPosition"
-            }
-
-            // Pre-jump to 1 viewport away from destination page, if possible
-            snapToItem(preJumpPosition, 0)
-        }
-        val targetPage = index
-        val pageAvailableSpace = averageItemSize
-        val currentPosition = firstVisibleItemIndex
-        val targetOffset = targetPage * pageAvailableSpace
-        val currentOffset = currentPosition * pageAvailableSpace
-
-        // The final delta displacement will be the difference between the pages offsets
-        // discounting whatever offset the original page had scrolled plus the offset
-        // fraction requested by the user.
-        val displacement =
-            (targetOffset - currentOffset - firstVisibleItemScrollOffset + offset).toFloat()
-
-        debugLog { "animateScrollToPage $displacement pixels" }
-        var previousValue = 0f
-        animate(0f, displacement, animationSpec = animationSpec) { currentValue, _ ->
-            previousValue += scrollBy(currentValue - previousValue)
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
index 8041415..8001053 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
@@ -38,6 +38,9 @@
  *
  * Here is a sample where part of a composable is brought into view:
  * @sample androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
+ *
+ * Note: this API is experimental while we optimise the performance and find the right API shape
+ * for it
  */
 @ExperimentalFoundationApi
 sealed interface BringIntoViewRequester {
@@ -73,6 +76,9 @@
  *
  * Here is a sample where a part of a composable is brought into view:
  * @sample androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
+ *
+ * Note: this API is experimental while we optimise the performance and find the right API shape
+ * for it
  */
 @ExperimentalFoundationApi
 fun BringIntoViewRequester(): BringIntoViewRequester {
@@ -94,6 +100,9 @@
  *     hoisted object can be used to send
  *     [bringIntoView][BringIntoViewRequester.bringIntoView] requests to parents
  *     of the current composable.
+ *
+ * Note: this API is experimental while we optimise the performance and find the right API shape
+ * for it
  */
 @Suppress("ModifierInspectorInfo")
 @ExperimentalFoundationApi
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
index 71820cc..770c8a06 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
@@ -48,6 +48,9 @@
  * @sample androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
  *
  * @see BringIntoViewRequester
+ *
+ * Note: this API is experimental while we optimise the performance and find the right API shape
+ * for it
  */
 @ExperimentalFoundationApi
 interface BringIntoViewResponder {
@@ -94,6 +97,9 @@
  * @sample androidx.compose.foundation.samples.BringIntoViewSample
  *
  * @see BringIntoViewRequester
+ *
+ * Note: this API is experimental while we optimise the performance and find the right API shape
+ * for it
  */
 @Suppress("ModifierInspectorInfo")
 @ExperimentalFoundationApi
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/SelectableGroup.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/SelectableGroup.kt
index 913d91f..b6ab655 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/SelectableGroup.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/SelectableGroup.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.selection
 
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.semantics.selectableGroup
 import androidx.compose.ui.semantics.semantics
@@ -26,6 +27,7 @@
  *
  * @see selectableGroup
  */
+@Stable
 fun Modifier.selectableGroup() = this.semantics {
     selectableGroup()
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
index 95bc432..e5219f2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
@@ -29,6 +29,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.ColorProducer
@@ -95,8 +96,13 @@
     val selectionRegistrar = LocalSelectionRegistrar.current
     val selectionController = if (selectionRegistrar != null) {
         val backgroundSelectionColor = LocalTextSelectionColors.current.backgroundColor
-        remember(selectionRegistrar, backgroundSelectionColor) {
+        val selectableId =
+            rememberSaveable(selectionRegistrar, saver = selectionIdSaver(selectionRegistrar)) {
+                selectionRegistrar.nextSelectableId()
+            }
+        remember(selectableId, selectionRegistrar, backgroundSelectionColor) {
             SelectionController(
+                selectableId,
                 selectionRegistrar,
                 backgroundSelectionColor
             )
@@ -184,8 +190,13 @@
     val selectionRegistrar = LocalSelectionRegistrar.current
     val selectionController = if (selectionRegistrar != null) {
         val backgroundSelectionColor = LocalTextSelectionColors.current.backgroundColor
-        remember(selectionRegistrar, backgroundSelectionColor) {
+        val selectableId =
+            rememberSaveable(selectionRegistrar, saver = selectionIdSaver(selectionRegistrar)) {
+                selectionRegistrar.nextSelectableId()
+            }
+        remember(selectableId, selectionRegistrar, backgroundSelectionColor) {
             SelectionController(
+                selectableId,
                 selectionRegistrar,
                 backgroundSelectionColor
             )
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
index e54ae82..57adcad 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
@@ -55,7 +55,7 @@
     minLines: Int = DefaultMinLines,
     placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
     onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
-    private val selectionController: SelectionController? = null,
+    private var selectionController: SelectionController? = null,
     overrideColor: ColorProducer? = null
 ) : DelegatingNode(), LayoutModifierNode, DrawModifierNode, GlobalPositionAwareModifierNode {
 
@@ -147,6 +147,7 @@
                 selectionController = selectionController
             ),
         )
+        this.selectionController = selectionController
         // we always relayout when we're selectable
         invalidateMeasurement()
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectionController.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectionController.kt
index 34a9f2c9..248d6d7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectionController.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectionController.kt
@@ -49,7 +49,9 @@
     }
 
     open val shouldClip: Boolean
-        get() = textLayoutResult?.layoutInput?.overflow == TextOverflow.Visible
+        get() = textLayoutResult?.let {
+            it.layoutInput.overflow != TextOverflow.Visible && it.hasVisualOverflow
+        } ?: false
 
     // if this copy shows up in traces, this class may become mutable
     fun copy(
@@ -68,13 +70,13 @@
  */
 // This is _basically_ a Modifier.Node but moved into remember because we need to do pointerInput
 internal class SelectionController(
+    private val selectableId: Long,
     private val selectionRegistrar: SelectionRegistrar,
     private val backgroundSelectionColor: Color,
     // TODO: Move these into Modifer.element eventually
     private var params: StaticTextSelectionParams = StaticTextSelectionParams.Empty
 ) : RememberObserver {
     private var selectable: Selectable? = null
-    private val selectableId = selectionRegistrar.nextSelectableId()
 
     val modifier: Modifier = selectionRegistrar
         .makeSelectionModifier(
@@ -115,6 +117,7 @@
 
     fun updateGlobalPosition(coordinates: LayoutCoordinates) {
         params = params.copy(layoutCoordinates = coordinates)
+        selectionRegistrar.notifyPositionChange(selectableId)
     }
 
     fun draw(drawScope: DrawScope) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
index 8fbd187..44104e34 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
@@ -240,15 +240,20 @@
         getOffsetForPosition(previousHandlePosition, textLayoutResult)
     }
 
-    val startHandleDirection = getDirection(startPosition, bounds)
-    val endHandleDirection = getDirection(endPosition, bounds)
+    val startXHandleDirection = getXDirection(startPosition, bounds)
+    val endXHandleDirection = getXDirection(endPosition, bounds)
+
+    val startYHandleDirection = getYDirection(startPosition, bounds)
+    val endYHandleDirection = getYDirection(endPosition, bounds)
 
     appendInfo(
         selectableId = selectableId,
         rawStartHandleOffset = rawStartHandleOffset,
-        startHandleDirection = startHandleDirection,
+        startXHandleDirection = startXHandleDirection,
+        startYHandleDirection = startYHandleDirection,
         rawEndHandleOffset = rawEndHandleOffset,
-        endHandleDirection = endHandleDirection,
+        endXHandleDirection = endXHandleDirection,
+        endYHandleDirection = endYHandleDirection,
         rawPreviousHandleOffset = rawPreviousHandleOffset,
         textLayoutResult = textLayoutResult,
     )
@@ -271,7 +276,13 @@
     }
 }
 
-private fun getDirection(position: Offset, bounds: Rect): Direction = when {
+private fun getXDirection(position: Offset, bounds: Rect): Direction = when {
+    position.x < bounds.left -> Direction.BEFORE
+    position.x > bounds.right -> Direction.AFTER
+    else -> Direction.ON
+}
+
+private fun getYDirection(position: Offset, bounds: Rect): Direction = when {
     position.y < bounds.top -> Direction.BEFORE
     position.y > bounds.bottom -> Direction.AFTER
     else -> Direction.ON
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
index 2e6d9cc..5a570fc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
@@ -24,6 +24,7 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.pointer.pointerInput
@@ -81,7 +82,10 @@
     onSelectionChange: (Selection?) -> Unit,
     children: @Composable () -> Unit
 ) {
-    val registrarImpl = remember { SelectionRegistrarImpl() }
+    val registrarImpl = rememberSaveable(saver = SelectionRegistrarImpl.Saver) {
+        SelectionRegistrarImpl()
+    }
+
     val manager = remember { SelectionManager(registrarImpl) }
 
     manager.hapticFeedBack = LocalHapticFeedback.current
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionLayout.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionLayout.kt
index f7c4e2c..424134d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionLayout.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionLayout.kt
@@ -352,7 +352,7 @@
  * @param rawStartHandleOffset the index of the start handle
  * @param rawEndHandleOffset the index of the end handle
  * @param rawPreviousHandleOffset the previous handle offset based on [isStartHandle],
- * or -1 if none
+ * or [UNASSIGNED_SLOT] if none
  * @param previousSelectionRange the previous selection
  * @param isStartOfSelection whether this is the start of a selection gesture (no previous context)
  * @param isStartHandle whether this is the start or end anchor
@@ -404,6 +404,9 @@
     COLLAPSED
 }
 
+/** Slot has not been assigned yet */
+internal const val UNASSIGNED_SLOT = -1
+
 /**
  * A builder for [SelectionLayout] that ensures the data structures and slots
  * are properly constructed.
@@ -426,9 +429,9 @@
 ) {
     private val selectableIdToInfoListIndex: MutableMap<Long, Int> = mutableMapOf()
     private val infoList: MutableList<SelectableInfo> = mutableListOf()
-    private var startSlot: Int = -1
-    private var endSlot: Int = -1
-    private var currentSlot: Int = -1
+    private var startSlot: Int = UNASSIGNED_SLOT
+    private var endSlot: Int = UNASSIGNED_SLOT
+    private var currentSlot: Int = UNASSIGNED_SLOT
 
     /**
      * Finishes building the [SelectionLayout] and returns it.
@@ -452,8 +455,8 @@
                 MultiSelectionLayout(
                     selectableIdToInfoListIndex = selectableIdToInfoListIndex,
                     infoList = infoList,
-                    startSlot = if (startSlot == -1) lastSlot else startSlot,
-                    endSlot = if (endSlot == -1) lastSlot else endSlot,
+                    startSlot = if (startSlot == UNASSIGNED_SLOT) lastSlot else startSlot,
+                    endSlot = if (endSlot == UNASSIGNED_SLOT) lastSlot else endSlot,
                     isStartHandle = isStartHandle,
                     previousSelection = previousSelection,
                 )
@@ -467,9 +470,11 @@
     fun appendInfo(
         selectableId: Long,
         rawStartHandleOffset: Int,
-        startHandleDirection: Direction,
+        startXHandleDirection: Direction,
+        startYHandleDirection: Direction,
         rawEndHandleOffset: Int,
-        endHandleDirection: Direction,
+        endXHandleDirection: Direction,
+        endYHandleDirection: Direction,
         rawPreviousHandleOffset: Int,
         textLayoutResult: TextLayoutResult,
     ): SelectableInfo {
@@ -486,41 +491,78 @@
             textLayoutResult = textLayoutResult,
         )
 
-        if (startSlot == -1) {
-            startSlot = updateBasedOnDirection(startHandleDirection)
-        }
-
-        if (endSlot == -1) {
-            endSlot = updateBasedOnDirection(endHandleDirection)
-        }
-
+        startSlot = updateSlot(startSlot, startXHandleDirection, startYHandleDirection)
+        endSlot = updateSlot(endSlot, endXHandleDirection, endYHandleDirection)
         selectableIdToInfoListIndex[selectableId] = infoList.size
         infoList += selectableInfo
         return selectableInfo
     }
 
-    private fun updateBasedOnDirection(direction: Direction): Int = when (direction) {
-        Direction.BEFORE -> currentSlot - 1
-        Direction.ON -> currentSlot
-        else -> -1
+    /**
+     * Find the slot for a selectable given the current position's directions from the selectable.
+     *
+     * The selectables must be ordered in the order in which they would be selected, and then
+     * this function should be called for each of those selectables.
+     *
+     * It is expected that the input [slot] is also assigned the result of this function.
+     *
+     * This function is stateful.
+     *
+     * @param slot the current value of this slot.
+     * @param xPositionDirection Where the x-position is relative to the selectable
+     * @param yPositionDirection Where the y-position is relative to the selectable
+     */
+    private fun updateSlot(
+        slot: Int,
+        xPositionDirection: Direction,
+        yPositionDirection: Direction,
+    ): Int {
+        if (slot != UNASSIGNED_SLOT) {
+            // don't overwrite if the slot has already been determined
+            return slot
+        }
+
+        // slot has not been determined yet,
+        // see if we are on or past the selectable we are looking for
+        return when (yPositionDirection) {
+            // If we get here, that means we never found a selectable that intersects our gesture
+            // position on the y-axis. This is the first selectable that is after the position,
+            // so our slot must be between the previous and current selectables.
+            Direction.BEFORE -> currentSlot - 1
+
+            // The gesture position intersects the bounds of the selectable in the y-axis.
+            // Now, search along the x-axis.
+            Direction.ON -> {
+                when (xPositionDirection) {
+                    // Same logic as BEFORE above, but along the x-axis.
+                    Direction.BEFORE -> currentSlot - 1
+
+                    // The gesture position is directly on this selectable, so use this one.
+                    Direction.ON -> currentSlot
+
+                    // keep looking
+                    Direction.AFTER -> slot
+                }
+            }
+
+            // keep looking
+            Direction.AFTER -> slot
+        }
     }
 }
 
 /**
  * Where the position of a cursor/press is compared to a selectable.
  */
-@JvmInline
-internal value class Direction(val direction: Int) {
-    companion object {
-        /** The cursor/press is before the selectable */
-        val BEFORE = Direction(-1)
+internal enum class Direction {
+    /** The cursor/press is before the selectable */
+    BEFORE,
 
-        /** The cursor/press is on the selectable */
-        val ON = Direction(0)
+    /** The cursor/press is on the selectable */
+    ON,
 
-        /** The cursor/press is after the selectable */
-        val AFTER = Direction(1)
-    }
+    /** The cursor/press is after the selectable */
+    AFTER
 }
 
 /**
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 a02f411..5bf0b16 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
@@ -23,6 +23,7 @@
 import androidx.compose.foundation.text.Handle
 import androidx.compose.foundation.text.TextDragObserver
 import androidx.compose.foundation.text.selection.Selection.AnchorInfo
+import androidx.compose.foundation.text2.input.internal.coerceIn
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
@@ -50,14 +51,14 @@
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.TextToolbarStatus
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastFilter
 import androidx.compose.ui.util.fastFold
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachIndexed
 import kotlin.math.absoluteValue
-import kotlin.math.max
-import kotlin.math.min
 
 /**
  * A bridge class between user interaction to the text composables for text selection.
@@ -87,7 +88,7 @@
         set(value) {
             if (_isInTouchMode.value != value) {
                 _isInTouchMode.value = value
-                if (value && showToolbar) showSelectionToolbar() else hideSelectionToolbar()
+                updateSelectionToolbar()
             }
         }
 
@@ -162,7 +163,7 @@
                 if (previousPosition != positionInWindow) {
                     previousPosition = positionInWindow
                     updateHandleOffsets()
-                    updateSelectionToolbarPosition()
+                    updateSelectionToolbar()
                 }
             }
         }
@@ -221,12 +222,9 @@
 
     init {
         selectionRegistrar.onPositionChangeCallback = { selectableId ->
-            if (
-                selectableId == selection?.start?.selectableId ||
-                selectableId == selection?.end?.selectableId
-            ) {
+            if (selectableId in selectionRegistrar.subselections) {
                 updateHandleOffsets()
-                updateSelectionToolbarPosition()
+                updateSelectionToolbar()
             }
         }
 
@@ -297,22 +295,26 @@
 
         selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
             if (selectableKey in selectionRegistrar.subselections) {
-                // clear the selection range of each Selectable.
+                // Clear the selection range of each Selectable.
                 onRelease()
                 selection = null
             }
         }
 
-        selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
-            if (
-                selectableKey == selection?.start?.selectableId ||
-                selectableKey == selection?.end?.selectableId
-            ) {
+        selectionRegistrar.afterSelectableUnsubscribe = { selectableId ->
+            if (selectableId == selection?.start?.selectableId) {
                 // The selectable that contains a selection handle just unsubscribed.
-                // Hide selection handles for now
+                // Hide the associated selection handle
                 startHandlePosition = null
+            }
+            if (selectableId == selection?.end?.selectableId) {
                 endHandlePosition = null
             }
+
+            if (selectableId in selectionRegistrar.subselections) {
+                // Unsubscribing the selectable may make the selection empty, which would hide it.
+                updateSelectionToolbar()
+            }
         }
     }
 
@@ -331,46 +333,38 @@
         val endSelectable = selection?.end?.let(::getAnchorSelectable)
         val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
         val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
+
         if (
             selection == null ||
             containerCoordinates == null ||
             !containerCoordinates.isAttached ||
-            startLayoutCoordinates == null ||
-            endLayoutCoordinates == null
+            (startLayoutCoordinates == null && endLayoutCoordinates == null)
         ) {
             this.startHandlePosition = null
             this.endHandlePosition = null
             return
         }
 
-        val startHandlePosition = containerCoordinates.localPositionOf(
-            startLayoutCoordinates,
-            startSelectable.getHandlePosition(
-                selection = selection,
-                isStartHandle = true
-            )
-        )
-        val endHandlePosition = containerCoordinates.localPositionOf(
-            endLayoutCoordinates,
-            endSelectable.getHandlePosition(
-                selection = selection,
-                isStartHandle = false
-            )
-        )
-
         val visibleBounds = containerCoordinates.visibleBounds()
-
-        // set the new handle position only if the handle is in visible bounds or
-        // the handle is still dragging. If handle goes out of visible bounds during drag, handle
-        // popup is also removed from composition, halting the drag gesture. This affects multiple
-        // text selection when selected text is configured with maxLines=1 and overflow=clip.
-        this.startHandlePosition = startHandlePosition.takeIf {
-            visibleBounds.containsInclusive(startHandlePosition) ||
-                draggingHandle == Handle.SelectionStart
+        this.startHandlePosition = startLayoutCoordinates?.let { handleCoordinates ->
+            // Set the new handle position only if the handle is in visible bounds or
+            // the handle is still dragging. If handle goes out of visible bounds during drag,
+            // handle popup is also removed from composition, halting the drag gesture. This
+            // affects multiple text selection when selected text is configured with maxLines=1
+            // and overflow=clip.
+            val handlePosition = startSelectable.getHandlePosition(selection, isStartHandle = true)
+            val position = containerCoordinates.localPositionOf(handleCoordinates, handlePosition)
+            position.takeIf {
+                draggingHandle == Handle.SelectionStart || visibleBounds.containsInclusive(it)
+            }
         }
-        this.endHandlePosition = endHandlePosition.takeIf {
-            visibleBounds.containsInclusive(endHandlePosition) ||
-                draggingHandle == Handle.SelectionEnd
+
+        this.endHandlePosition = endLayoutCoordinates?.let { handleCoordinates ->
+            val handlePosition = endSelectable.getHandlePosition(selection, isStartHandle = false)
+            val position = containerCoordinates.localPositionOf(handleCoordinates, handlePosition)
+            position.takeIf {
+                draggingHandle == Handle.SelectionEnd || visibleBounds.containsInclusive(it)
+            }
         }
     }
 
@@ -428,61 +422,49 @@
             return false
         }
 
-        var betweenSelectables = false
-        selectionRegistrar.sort(requireContainerCoordinates()).fastForEach {
-            if (
-                it.selectableId != selection.start.selectableId &&
-                it.selectableId != selection.end.selectableId &&
-                !betweenSelectables
-            ) {
-                // haven't found our selection yet, continue
-                return@fastForEach
-            }
-
-            betweenSelectables = true
-            if (!isCurrentSelectionEmpty(selection = selection, selectable = it)) {
-                return true
-            }
-
-            // short-circuit if this is the last selectable
-            if (it.selectableId == selection.end.selectableId && !selection.handlesCrossed ||
-                it.selectableId == selection.start.selectableId && selection.handlesCrossed
-            ) return false
+        if (selection.start.selectableId == selection.end.selectableId) {
+            // Selection is in the same selectable, but not the same anchors,
+            // so there must be some selected text.
+            return true
         }
-        return false
+
+        // All subselections associated with a selectable must be an empty selection.
+        return selectionRegistrar.sort(requireContainerCoordinates()).fastAny { selectable ->
+            selectionRegistrar.subselections[selectable.selectableId]
+                ?.run { start.offset != end.offset }
+                ?: false
+        }
     }
 
     internal fun getSelectedText(): AnnotatedString? {
-        val selectables = selectionRegistrar.sort(requireContainerCoordinates())
-        var selectedText: AnnotatedString? = null
+        if (selection == null || selectionRegistrar.subselections.isEmpty()) {
+            return null
+        }
 
-        selection?.let {
-            for (i in selectables.indices) {
-                val selectable = selectables[i]
-                // Continue if the current selectable is before the selection starts.
-                if (selectable.selectableId != it.start.selectableId &&
-                    selectable.selectableId != it.end.selectableId &&
-                    selectedText == null
-                ) continue
+        return buildAnnotatedString {
+            selectionRegistrar.sort(requireContainerCoordinates()).fastForEach { selectable ->
+                selectionRegistrar.subselections[selectable.selectableId]?.let { subSelection ->
+                    val currentText = selectable.getText()
+                    val currentSelectedText = if (subSelection.handlesCrossed) {
+                        currentText.subSequence(
+                            subSelection.end.offset,
+                            subSelection.start.offset
+                        )
+                    } else {
+                        currentText.subSequence(
+                            subSelection.start.offset,
+                            subSelection.end.offset
+                        )
+                    }
 
-                val currentSelectedText = getCurrentSelectedText(
-                    selectable = selectable,
-                    selection = it
-                )
-                selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
-
-                // Break if the current selectable is the last selected selectable.
-                if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
-                    selectable.selectableId == it.start.selectableId && it.handlesCrossed
-                ) break
+                    append(currentSelectedText)
+                }
             }
         }
-        return selectedText
     }
 
     internal fun copy() {
-        val selectedText = getSelectedText()
-        selectedText?.takeIf { it.isNotEmpty() }?.let { clipboardManager?.setText(it) }
+        getSelectedText()?.takeIf { it.isNotEmpty() }?.let { clipboardManager?.setText(it) }
     }
 
     /**
@@ -494,103 +476,114 @@
     internal var showToolbar = false
         internal set(value) {
             field = value
-            if (value && isInTouchMode) showSelectionToolbar() else hideSelectionToolbar()
+            updateSelectionToolbar()
         }
 
+    private fun toolbarCopy() {
+        copy()
+        onRelease()
+    }
+
+    private fun updateSelectionToolbar() {
+        if (!hasFocus) {
+            return
+        }
+
+        val textToolbar = textToolbar ?: return
+        if (showToolbar && isInTouchMode && isNonEmptySelection()) {
+            val rect = getContentRect() ?: return
+            textToolbar.showMenu(rect = rect, onCopyRequested = ::toolbarCopy)
+        } else if (textToolbar.status == TextToolbarStatus.Shown) {
+            textToolbar.hide()
+        }
+    }
+
     /**
-     * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
-     * to make the FloatingToolbar show up in the proper place. In addition, this function passes
-     * the copy method as a callback when "copy" is clicked.
+     * Calculate selected region as [Rect].
+     * The result is the smallest [Rect] that encapsulates the entire selection,
+     * coerced into visible bounds.
      */
-    private fun showSelectionToolbar() {
-        if (hasFocus && isNonEmptySelection()) {
-            textToolbar?.showMenu(
-                getContentRect(),
-                onCopyRequested = {
-                    copy()
-                    onRelease()
+    private fun getContentRect(): Rect? {
+        selection ?: return null
+        val containerCoordinates = containerLayoutCoordinates ?: return null
+        if (!containerCoordinates.isAttached) return null
+        val visibleBounds = containerCoordinates.visibleBounds()
+
+        var anyExists = false
+        var rootLeft = Float.POSITIVE_INFINITY
+        var rootTop = Float.POSITIVE_INFINITY
+        var rootRight = Float.NEGATIVE_INFINITY
+        var rootBottom = Float.NEGATIVE_INFINITY
+
+        val sortedSelectables = selectionRegistrar.sort(requireContainerCoordinates())
+            .fastFilter {
+                it.selectableId in selectionRegistrar.subselections
+            }
+
+        if (sortedSelectables.isEmpty()) {
+            return null
+        }
+
+        val selectedSelectables = if (sortedSelectables.size == 1) {
+            sortedSelectables
+        } else {
+            listOf(sortedSelectables.first(), sortedSelectables.last())
+        }
+
+        selectedSelectables.fastForEach { selectable ->
+            val subSelection = selectionRegistrar.subselections[selectable.selectableId]
+                ?: return@fastForEach
+
+            val coordinates = selectable.getLayoutCoordinates()
+                ?: return@fastForEach
+
+            with(subSelection) {
+                if (start.offset == end.offset) {
+                    return@fastForEach
                 }
-            )
+
+                val minOffset = minOf(start.offset, end.offset)
+                val maxOffset = maxOf(start.offset, end.offset)
+
+                var left = Float.POSITIVE_INFINITY
+                var top = Float.POSITIVE_INFINITY
+                var right = Float.NEGATIVE_INFINITY
+                var bottom = Float.NEGATIVE_INFINITY
+                for (i in intArrayOf(minOffset, maxOffset)) {
+                    val rect = selectable.getBoundingBox(i)
+                    left = minOf(left, rect.left)
+                    top = minOf(top, rect.top)
+                    right = maxOf(right, rect.right)
+                    bottom = maxOf(bottom, rect.bottom)
+                }
+
+                val localTopLeft = Offset(left, top)
+                val localBottomRight = Offset(right, bottom)
+
+                val containerTopLeft =
+                    containerCoordinates.localPositionOf(coordinates, localTopLeft)
+                val containerBottomRight =
+                    containerCoordinates.localPositionOf(coordinates, localBottomRight)
+
+                val rootVisibleTopLeft =
+                    containerCoordinates.localToRoot(containerTopLeft.coerceIn(visibleBounds))
+                val rootVisibleBottomRight =
+                    containerCoordinates.localToRoot(containerBottomRight.coerceIn(visibleBounds))
+
+                rootLeft = minOf(rootLeft, rootVisibleTopLeft.x)
+                rootTop = minOf(rootTop, rootVisibleTopLeft.y)
+                rootRight = maxOf(rootRight, rootVisibleBottomRight.x)
+                rootBottom = maxOf(rootBottom, rootVisibleBottomRight.y)
+                anyExists = true
+            }
         }
-    }
 
-    private fun hideSelectionToolbar() {
-        if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
-            textToolbar?.hide()
+        if (!anyExists) {
+            return null
         }
-    }
 
-    private fun updateSelectionToolbarPosition() {
-        if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
-            showSelectionToolbar()
-        }
-    }
-
-    /**
-     * Calculate selected region as [Rect]. The top is the top of the first selected
-     * line, and the bottom is the bottom of the last selected line. The left is the leftmost
-     * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
-     */
-    private fun getContentRect(): Rect {
-        val selection = selection ?: return Rect.Zero
-        val startSelectable = getAnchorSelectable(selection.start)
-        val endSelectable = getAnchorSelectable(selection.end)
-        val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
-        val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
-
-        val localLayoutCoordinates = containerLayoutCoordinates
-        if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
-            var startOffset = localLayoutCoordinates.localPositionOf(
-                startLayoutCoordinates,
-                startSelectable.getHandlePosition(
-                    selection = selection,
-                    isStartHandle = true
-                )
-            )
-            var endOffset = localLayoutCoordinates.localPositionOf(
-                endLayoutCoordinates,
-                endSelectable.getHandlePosition(
-                    selection = selection,
-                    isStartHandle = false
-                )
-            )
-
-            startOffset = localLayoutCoordinates.localToRoot(startOffset)
-            endOffset = localLayoutCoordinates.localToRoot(endOffset)
-
-            val left = min(startOffset.x, endOffset.x)
-            val right = max(startOffset.x, endOffset.x)
-
-            var startTop = localLayoutCoordinates.localPositionOf(
-                startLayoutCoordinates,
-                Offset(
-                    0f,
-                    startSelectable.getBoundingBox(selection.start.offset).top
-                )
-            )
-
-            var endTop = localLayoutCoordinates.localPositionOf(
-                endLayoutCoordinates,
-                Offset(
-                    0.0f,
-                    endSelectable.getBoundingBox(selection.end.offset).top
-                )
-            )
-
-            startTop = localLayoutCoordinates.localToRoot(startTop)
-            endTop = localLayoutCoordinates.localToRoot(endTop)
-
-            val top = min(startTop.y, endTop.y)
-            val bottom = max(startOffset.y, endOffset.y) + (HandleHeight.value * 4.0).toFloat()
-
-            return Rect(
-                left,
-                top,
-                right,
-                bottom
-            )
-        }
-        return Rect.Zero
+        rootBottom += HandleHeight.value * 4
+        return Rect(rootLeft, rootTop, rootRight, rootBottom)
     }
 
     // This is for PressGestureDetector to cancel the selection.
@@ -985,70 +978,6 @@
     )
 }
 
-private fun isCurrentSelectionEmpty(
-    selectable: Selectable,
-    selection: Selection
-): Boolean {
-    val selectableId = selectable.selectableId
-    val startSelectableId = selection.start.selectableId
-    val endSelectableId = selection.end.selectableId
-
-    if (selectableId == startSelectableId && selectableId == endSelectableId) {
-        return selection.start.offset == selection.end.offset
-    }
-
-    val text = selectable.getText()
-    val handlesCrossed = selection.handlesCrossed
-    return when (selectableId) {
-        startSelectableId -> selection.start.offset == if (handlesCrossed) 0 else text.length
-        endSelectableId -> selection.end.offset == if (handlesCrossed) text.length else 0
-        else -> text.isEmpty()
-    }
-}
-
-internal fun getCurrentSelectedText(
-    selectable: Selectable,
-    selection: Selection
-): AnnotatedString {
-    val currentText = selectable.getText()
-
-    return if (
-        selectable.selectableId != selection.start.selectableId &&
-        selectable.selectableId != selection.end.selectableId
-    ) {
-        // Select the full text content if the current selectable is between the
-        // start and the end selectables.
-        currentText
-    } else if (
-        selectable.selectableId == selection.start.selectableId &&
-        selectable.selectableId == selection.end.selectableId
-    ) {
-        // Select partial text content if the current selectable is the start and
-        // the end selectable.
-        if (selection.handlesCrossed) {
-            currentText.subSequence(selection.end.offset, selection.start.offset)
-        } else {
-            currentText.subSequence(selection.start.offset, selection.end.offset)
-        }
-    } else if (selectable.selectableId == selection.start.selectableId) {
-        // Select partial text content if the current selectable is the start
-        // selectable.
-        if (selection.handlesCrossed) {
-            currentText.subSequence(0, selection.start.offset)
-        } else {
-            currentText.subSequence(selection.start.offset, currentText.length)
-        }
-    } else {
-        // Selectable partial text content if the current selectable is the end
-        // selectable.
-        if (selection.handlesCrossed) {
-            currentText.subSequence(selection.end.offset, currentText.length)
-        } else {
-            currentText.subSequence(0, selection.end.offset)
-        }
-    }
-}
-
 /** Returns the boundary of the visible area in this [LayoutCoordinates]. */
 internal fun LayoutCoordinates.visibleBounds(): Rect {
     // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt
index 907c15f..dce40ef 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt
@@ -19,11 +19,23 @@
 import androidx.compose.foundation.AtomicLong
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.LayoutCoordinates
 
-internal class SelectionRegistrarImpl : SelectionRegistrar {
+internal class SelectionRegistrarImpl private constructor(
+    initialIncrementId: Long
+) : SelectionRegistrar {
+    companion object {
+        val Saver = Saver<SelectionRegistrarImpl, Long>(
+            save = { it.incrementId.get() },
+            restore = { SelectionRegistrarImpl(it) }
+        )
+    }
+
+    constructor() : this(initialIncrementId = 1L)
+
     /**
      * A flag to check if the [Selectable]s have already been sorted.
      */
@@ -54,7 +66,7 @@
      * denote an invalid id.
      * @see SelectionRegistrar.InvalidSelectableId
      */
-    private var incrementId = AtomicLong(1)
+    private var incrementId = AtomicLong(initialIncrementId)
 
     /**
      * The callback to be invoked when the position change was triggered.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
index 4ec59e2..5b9e34f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
@@ -38,6 +38,7 @@
 import androidx.compose.foundation.text.textFieldMinSize
 import androidx.compose.foundation.text2.input.CodepointTransformation
 import androidx.compose.foundation.text2.input.InputTransformation
+import androidx.compose.foundation.text2.input.SingleLineCodepointTransformation
 import androidx.compose.foundation.text2.input.TextFieldLineLimits
 import androidx.compose.foundation.text2.input.TextFieldLineLimits.MultiLine
 import androidx.compose.foundation.text2.input.TextFieldLineLimits.SingleLine
@@ -46,6 +47,7 @@
 import androidx.compose.foundation.text2.input.internal.TextFieldDecoratorModifier
 import androidx.compose.foundation.text2.input.internal.TextFieldTextLayoutModifier
 import androidx.compose.foundation.text2.input.internal.TextLayoutState
+import androidx.compose.foundation.text2.input.internal.TransformedTextFieldState
 import androidx.compose.foundation.text2.input.internal.selection.TextFieldSelectionHandle2
 import androidx.compose.foundation.text2.input.internal.selection.TextFieldSelectionState
 import androidx.compose.foundation.text2.input.internal.syncTextFieldState
@@ -448,11 +450,19 @@
     val isFocused = interactionSource.collectIsFocusedAsState().value
     val textLayoutState = remember { TextLayoutState() }
 
-    val textFieldSelectionState = remember(state, textLayoutState) {
+    val transformedState = remember(state, inputTransformation, codepointTransformation) {
+        // First prefer provided codepointTransformation if not null, e.g. BasicSecureTextField
+        // would send PasswordTransformation. Second, apply a SingleLineCodepointTransformation if
+        // text field is configured to be single line. Else, don't apply any visual transformation.
+        val appliedCodepointTransformation = codepointTransformation
+            ?: SingleLineCodepointTransformation.takeIf { singleLine }
+        TransformedTextFieldState(state, inputTransformation, appliedCodepointTransformation)
+    }
+
+    val textFieldSelectionState = remember(transformedState) {
         TextFieldSelectionState(
-            textFieldState = state,
+            textFieldState = transformedState,
             textLayoutState = textLayoutState,
-            inputTransformation = inputTransformation,
             density = density,
             editable = enabled && !readOnly,
             isFocused = isFocused
@@ -468,7 +478,6 @@
             hapticFeedBack = currentHapticFeedback,
             clipboardManager = currentClipboardManager,
             textToolbar = currentTextToolbar,
-            inputTransformation = inputTransformation,
             density = density,
             editable = enabled && !readOnly,
         )
@@ -484,7 +493,7 @@
         .then(
             // semantics + some focus + input session + touch to focus
             TextFieldDecoratorModifier(
-                textFieldState = state,
+                textFieldState = transformedState,
                 textLayoutState = textLayoutState,
                 textFieldSelectionState = textFieldSelectionState,
                 filter = inputTransformation,
@@ -534,7 +543,7 @@
                         TextFieldCoreModifier(
                             isFocused = isFocused,
                             textLayoutState = textLayoutState,
-                            textFieldState = state,
+                            textFieldState = transformedState,
                             textFieldSelectionState = textFieldSelectionState,
                             cursorBrush = cursorBrush,
                             writeable = enabled && !readOnly,
@@ -546,8 +555,7 @@
                 Box(
                     modifier = TextFieldTextLayoutModifier(
                         textLayoutState = textLayoutState,
-                        textFieldState = state,
-                        codepointTransformation = codepointTransformation,
+                        textFieldState = transformedState,
                         textStyle = textStyle,
                         singleLine = singleLine,
                         onTextLayout = onTextLayout
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/AllCapsTransformation.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/AllCapsTransformation.kt
index 8bc8ce2..ffd4b82 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/AllCapsTransformation.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/AllCapsTransformation.kt
@@ -21,23 +21,24 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.text.input.KeyboardCapitalization
 import androidx.compose.ui.text.intl.Locale
+import androidx.compose.ui.text.substring
 import androidx.compose.ui.text.toUpperCase
 
 /**
  * Returns a [InputTransformation] that forces all text to be uppercase.
  *
- * This filter automatically configures the keyboard to capitalize all characters.
+ * This transformation automatically configures the keyboard to capitalize all characters.
  *
  * @param locale The [Locale] in which to perform the case conversion.
  */
 @ExperimentalFoundationApi
 @Stable
 fun InputTransformation.Companion.allCaps(locale: Locale): InputTransformation =
-    AllCapsFilter(locale)
+    AllCapsTransformation(locale)
 
 // This is a very naive implementation for now, not intended to be production-ready.
 @OptIn(ExperimentalFoundationApi::class)
-private data class AllCapsFilter(private val locale: Locale) : InputTransformation {
+private data class AllCapsTransformation(private val locale: Locale) : InputTransformation {
     override val keyboardOptions = KeyboardOptions(
         capitalization = KeyboardCapitalization.Characters
     )
@@ -46,13 +47,16 @@
         originalValue: TextFieldCharSequence,
         valueWithChanges: TextFieldBuffer
     ) {
-        val selection = valueWithChanges.selectionInCodepoints
-        valueWithChanges.replace(
-            0,
-            valueWithChanges.length,
-            valueWithChanges.toString().toUpperCase(locale)
-        )
-        valueWithChanges.selectCodepointsIn(selection)
+        // only update inserted content
+        valueWithChanges.changes.forEachChange { range, _ ->
+            if (!range.collapsed) {
+                valueWithChanges.replace(
+                    range.min,
+                    range.max,
+                    valueWithChanges.asCharSequence().substring(range).toUpperCase(locale)
+                )
+            }
+        }
     }
 
     override fun toString(): String = "InputTransformation.allCaps(locale=$locale)"
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/CodepointTransformation.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/CodepointTransformation.kt
index 7dd443e..81efc4c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/CodepointTransformation.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/CodepointTransformation.kt
@@ -18,6 +18,9 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.text.appendCodePointX
+import androidx.compose.foundation.text2.input.internal.OffsetMappingCalculator
+import androidx.compose.foundation.text2.input.internal.charCount
+import androidx.compose.foundation.text2.input.internal.codePointAt
 import androidx.compose.runtime.Stable
 
 /**
@@ -84,17 +87,36 @@
 }
 
 @OptIn(ExperimentalFoundationApi::class)
-internal fun CharSequence.toVisualText(
-    codepointTransformation: CodepointTransformation?
+internal fun TextFieldCharSequence.toVisualText(
+    codepointTransformation: CodepointTransformation,
+    offsetMappingCalculator: OffsetMappingCalculator
 ): CharSequence {
-    codepointTransformation ?: return this
     val text = this
-    return buildString {
-        (0 until Character.codePointCount(text, 0, text.length)).forEach { codepointIndex ->
-            val codepoint = codepointTransformation.transform(
-                codepointIndex, Character.codePointAt(text, codepointIndex)
-            )
-            appendCodePointX(codepoint)
+    var changed = false
+    val newText = buildString {
+        var charOffset = 0
+        var codePointOffset = 0
+        while (charOffset < text.length) {
+            val codePoint = text.codePointAt(charOffset)
+            val newCodePoint = codepointTransformation.transform(codePointOffset, codePoint)
+            val charCount = charCount(codePoint)
+            if (newCodePoint != codePoint) {
+                changed = true
+                val newCharCount = charCount(newCodePoint)
+                offsetMappingCalculator.recordEditOperation(
+                    sourceStart = length,
+                    sourceEnd = length + charCount,
+                    newLength = newCharCount
+                )
+            }
+            appendCodePointX(newCodePoint)
+
+            charOffset += charCount
+            codePointOffset += 1
         }
     }
+
+    // Return the same instance if nothing changed, which signals to the caller that nothing changed
+    // and allows the new string to be GC'd earlier.
+    return if (changed) newText else this
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/TextFieldBuffer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/TextFieldBuffer.kt
index 10ac37e..a332068 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/TextFieldBuffer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/TextFieldBuffer.kt
@@ -150,7 +150,7 @@
     ) {
         require(start <= end) { "Expected start=$start <= end=$end" }
         require(textStart <= textEnd) { "Expected textStart=$textStart <= textEnd=$textEnd" }
-        onTextWillChange(TextRange(start, end), textEnd - textStart)
+        onTextWillChange(start, end, textEnd - textStart)
         buffer.replace(start, end, text, textStart, textEnd)
     }
 
@@ -168,7 +168,7 @@
     // This append overload should be first so it ends up being the target of links to this method.
     override fun append(text: CharSequence?): Appendable = apply {
         if (text != null) {
-            onTextWillChange(TextRange(length), text.length)
+            onTextWillChange(length, length, text.length)
             buffer.replace(buffer.length, buffer.length, text)
         }
     }
@@ -176,30 +176,31 @@
     // Doc inherited from Appendable.
     override fun append(text: CharSequence?, start: Int, end: Int): Appendable = apply {
         if (text != null) {
-            onTextWillChange(TextRange(length), end - start)
+            onTextWillChange(length, length, end - start)
             buffer.replace(buffer.length, buffer.length, text.subSequence(start, end))
         }
     }
 
     // Doc inherited from Appendable.
     override fun append(char: Char): Appendable = apply {
-        onTextWillChange(TextRange(length), 1)
+        onTextWillChange(length, length, 1)
         buffer.replace(buffer.length, buffer.length, char.toString())
     }
 
     /**
      * Called just before the text contents are about to change.
      *
-     * @param rangeToBeReplaced The range in the current text that's about to be replaced.
+     * @param replaceStart The first offset to be replaced (inclusive).
+     * @param replaceEnd The last offset to be replaced (exclusive).
      * @param newLength The length of the replacement.
      */
-    private fun onTextWillChange(rangeToBeReplaced: TextRange, newLength: Int) {
+    private fun onTextWillChange(replaceStart: Int, replaceEnd: Int, newLength: Int) {
         (changeTracker ?: ChangeTracker().also { changeTracker = it })
-            .trackChange(rangeToBeReplaced, newLength)
+            .trackChange(replaceStart, replaceEnd, newLength)
 
         // Adjust selection.
-        val start = rangeToBeReplaced.min
-        val end = rangeToBeReplaced.max
+        val start = minOf(replaceStart, replaceEnd)
+        val end = maxOf(replaceStart, replaceEnd)
         var selStart = selectionInChars.min
         var selEnd = selectionInChars.max
 
@@ -448,7 +449,7 @@
     /**
      * The ordered list of non-overlapping and discontinuous changes performed on a
      * [TextFieldBuffer] during the current [edit][TextFieldState.edit] or
-     * [filter][TextEditFilter.filter] operation. Changes are listed in the order they appear in the
+     * [filter][InputTransformation.transformInput] operation. Changes are listed in the order they appear in the
      * text, not the order in which they were made. Overlapping changes are represented as a single
      * change.
      */
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/TextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/TextFieldState.kt
index f234dc8..80511e30 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/TextFieldState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/TextFieldState.kt
@@ -21,6 +21,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.text2.input.internal.EditingBuffer
+import androidx.compose.foundation.text2.input.internal.undo.TextFieldEditUndoBehavior
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.collection.mutableVectorOf
@@ -59,11 +60,22 @@
  */
 @ExperimentalFoundationApi
 @Stable
-class TextFieldState(
-    initialText: String = "",
-    initialSelectionInChars: TextRange = TextRange.Zero
+class TextFieldState internal constructor(
+    initialText: String,
+    initialSelectionInChars: TextRange,
+    initialTextUndoManager: TextUndoManager
 ) {
 
+    constructor(
+        initialText: String = "",
+        initialSelectionInChars: TextRange = TextRange.Zero
+    ) : this(initialText, initialSelectionInChars, TextUndoManager())
+
+    /**
+     * Manages the history of edit operations that happen in this [TextFieldState].
+     */
+    internal val textUndoManager: TextUndoManager = initialTextUndoManager
+
     /**
      * The editing buffer used for applying editor commands from IME. All edits coming from gestures
      * or IME commands, must be reflected on this buffer eventually.
@@ -116,6 +128,13 @@
     override fun toString(): String =
         "TextFieldState(selectionInChars=${text.selectionInChars}, text=\"$text\")"
 
+    /**
+     * Undo history controller for this TextFieldState.
+     */
+    // TextField does not implement UndoState because Undo related APIs should be able to remain
+    // separately experimental than TextFieldState
+    internal val undoState: UndoState = UndoState(this)
+
     @Suppress("ShowingMemberInHiddenClass")
     @PublishedApi
     internal fun startEdit(value: TextFieldCharSequence): TextFieldBuffer =
@@ -137,6 +156,7 @@
             val finalValue = newValue.toTextFieldCharSequence()
             resetStateAndNotifyIme(finalValue)
         }
+        textUndoManager.clearHistory()
     }
 
     /**
@@ -148,6 +168,11 @@
      * thread, or global snapshot. Also, this function is defined as inline for performance gains,
      * and it's not actually safe to early return from [block].
      *
+     * Also all user edits should be recorded by [textUndoManager] since reverting to a previous
+     * state requires all edit operations to be executed in reverse. However, some commands like
+     * cut, and paste should be atomic operations that do not merge with previous or next operations
+     * in the Undo stack. This can be controlled by [undoBehavior].
+     *
      * @param inputTransformation [InputTransformation] to run after [block] is applied
      * @param notifyImeOfChanges Whether IME should be notified of these changes. Only pass false to
      * this argument if the source of the changes is IME itself.
@@ -156,6 +181,7 @@
     internal inline fun editAsUser(
         inputTransformation: InputTransformation?,
         notifyImeOfChanges: Boolean = true,
+        undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible,
         block: EditingBuffer.() -> Unit
     ) {
         val previousValue = text
@@ -170,12 +196,40 @@
             return
         }
 
-        commitEditAsUser(inputTransformation, notifyImeOfChanges)
+        commitEditAsUser(previousValue, inputTransformation, notifyImeOfChanges, undoBehavior)
+    }
+
+    /**
+     * Edits the contents of this [TextFieldState] without going through an [InputTransformation],
+     * or recording the changes to the [textUndoManager]. IME would still be notified of any changes
+     * committed by [block].
+     *
+     * This method of editing is not recommended for majority of use cases. It is originally added
+     * to support applying of undo/redo actions without clearing the history. Also, it doesn't
+     * allocate an additional buffer like [edit] method because changes are ignored and it's not
+     * a public API.
+     */
+    internal inline fun editWithNoSideEffects(block: EditingBuffer.() -> Unit) {
+        val previousValue = text
+
+        mainBuffer.changeTracker.clearChanges()
+        mainBuffer.block()
+
+        val afterEditValue = TextFieldCharSequence(
+            text = mainBuffer.toString(),
+            selection = mainBuffer.selection,
+            composition = mainBuffer.composition
+        )
+
+        text = afterEditValue
+        notifyIme(previousValue, afterEditValue)
     }
 
     private fun commitEditAsUser(
+        previousValue: TextFieldCharSequence,
         inputTransformation: InputTransformation?,
-        notifyImeOfChanges: Boolean
+        notifyImeOfChanges: Boolean,
+        undoBehavior: TextFieldEditUndoBehavior
     ) {
         val afterEditValue = TextFieldCharSequence(
             text = mainBuffer.toString(),
@@ -189,12 +243,13 @@
             if (notifyImeOfChanges) {
                 notifyIme(oldValue, afterEditValue)
             }
+            recordEditForUndo(previousValue, text, mainBuffer.changeTracker, undoBehavior)
             return
         }
 
         val oldValue = text
 
-        // if only difference is composition, don't run filter
+        // if only difference is composition, don't run filter, don't send it to undo manager
         if (afterEditValue.contentEquals(oldValue) &&
             afterEditValue.selectionInChars == oldValue.selectionInChars
         ) {
@@ -227,6 +282,41 @@
         } else {
             resetStateAndNotifyIme(afterFilterValue)
         }
+        // mutableValue contains all the changes from user and the filter.
+        recordEditForUndo(previousValue, text, mutableValue.changes, undoBehavior)
+    }
+
+    /**
+     * Records the difference between [previousValue] and [postValue], defined by [changes],
+     * into [textUndoManager] according to the strategy defined by [undoBehavior].
+     */
+    private fun recordEditForUndo(
+        previousValue: TextFieldCharSequence,
+        postValue: TextFieldCharSequence,
+        changes: TextFieldBuffer.ChangeList,
+        undoBehavior: TextFieldEditUndoBehavior
+    ) {
+        when (undoBehavior) {
+            TextFieldEditUndoBehavior.ClearHistory -> {
+                textUndoManager.clearHistory()
+            }
+            TextFieldEditUndoBehavior.MergeIfPossible -> {
+                textUndoManager.recordChanges(
+                    pre = previousValue,
+                    post = postValue,
+                    changes = changes,
+                    allowMerge = true
+                )
+            }
+            TextFieldEditUndoBehavior.NeverMerge -> {
+                textUndoManager.recordChanges(
+                    pre = previousValue,
+                    post = postValue,
+                    changes = changes,
+                    allowMerge = false
+                )
+            }
+        }
     }
 
     internal fun addNotifyImeListener(notifyImeListener: NotifyImeListener) {
@@ -325,20 +415,29 @@
     // Preserve nullability since this is public API.
     @Suppress("RedundantNullableReturnType")
     object Saver : androidx.compose.runtime.saveable.Saver<TextFieldState, Any> {
-        override fun SaverScope.save(value: TextFieldState): Any? = listOf(
-            value.text.toString(),
-            value.text.selectionInChars.start,
-            value.text.selectionInChars.end
-        )
+
+        override fun SaverScope.save(value: TextFieldState): Any? {
+            return listOf(
+                value.text.toString(),
+                value.text.selectionInChars.start,
+                value.text.selectionInChars.end,
+                with(TextUndoManager.Companion.Saver) {
+                    save(value.textUndoManager)
+                }
+            )
+        }
 
         override fun restore(value: Any): TextFieldState? {
-            val (text, selectionStart, selectionEnd) = value as List<*>
+            val (text, selectionStart, selectionEnd, savedTextUndoManager) = value as List<*>
             return TextFieldState(
                 initialText = text as String,
                 initialSelectionInChars = TextRange(
                     start = selectionStart as Int,
                     end = selectionEnd as Int
-                )
+                ),
+                initialTextUndoManager = with(TextUndoManager.Companion.Saver) {
+                    restore(savedTextUndoManager!!)
+                }!!
             )
         }
     }
@@ -460,12 +559,3 @@
     textAsFlow().collectLatest(block)
     error("textAsFlow expected not to complete without exception")
 }
-
-@OptIn(ExperimentalFoundationApi::class)
-internal fun TextFieldState.deselect() {
-    if (!text.selectionInChars.collapsed) {
-        edit {
-            selectCharsIn(TextRange(text.selectionInChars.max))
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/TextUndoManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/TextUndoManager.kt
new file mode 100644
index 0000000..a080737
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/TextUndoManager.kt
@@ -0,0 +1,266 @@
+/*
+ * 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.foundation.text2.input
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text.SNAPSHOTS_INTERVAL_MILLIS
+import androidx.compose.foundation.text2.input.internal.undo.TextDeleteType
+import androidx.compose.foundation.text2.input.internal.undo.TextEditType
+import androidx.compose.foundation.text2.input.internal.undo.TextUndoOperation
+import androidx.compose.foundation.text2.input.internal.undo.UndoManager
+import androidx.compose.foundation.text2.input.internal.undo.redo
+import androidx.compose.foundation.text2.input.internal.undo.undo
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.SaverScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.text.substring
+
+/**
+ * An undo manager designed specifically for text editing. This class is mostly responsible for
+ * delegating the incoming undo/redo/record/clear requests to its own [undoManager]. Its most
+ * important task is to keep a separate staging area for incoming text edit operations to possibly
+ * merge them before committing as a single undoable action.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class TextUndoManager(
+    initialStagingUndo: TextUndoOperation? = null,
+    private val undoManager: UndoManager<TextUndoOperation> = UndoManager(
+        capacity = TEXT_UNDO_CAPACITY
+    )
+) {
+    private var stagingUndo by mutableStateOf<TextUndoOperation?>(initialStagingUndo)
+
+    val canUndo: Boolean
+        get() = undoManager.canUndo || stagingUndo != null
+
+    val canRedo: Boolean
+        get() = undoManager.canRedo && stagingUndo == null
+
+    fun undo(state: TextFieldState) {
+        if (!canUndo) {
+            return
+        }
+
+        flush()
+        state.undo(undoManager.undo())
+    }
+
+    fun redo(state: TextFieldState) {
+        if (!canRedo) {
+            return
+        }
+
+        state.redo(undoManager.redo())
+    }
+
+    fun record(op: TextUndoOperation) {
+        val unobservedStagingUndo = Snapshot.withoutReadObservation { stagingUndo }
+        if (unobservedStagingUndo == null) {
+            stagingUndo = op
+            return
+        }
+
+        val mergedOp = unobservedStagingUndo.merge(op)
+
+        if (mergedOp != null) {
+            // merged operation should replace the top op
+            stagingUndo = mergedOp
+            return
+        }
+
+        // no merge, flush the staging area and put the new operation in
+        flush()
+        stagingUndo = op
+    }
+
+    fun clearHistory() {
+        stagingUndo = null
+        undoManager.clearHistory()
+    }
+
+    private fun flush() {
+        val unobservedStagingUndo = Snapshot.withoutReadObservation { stagingUndo }
+        unobservedStagingUndo?.let { undoManager.record(it) }
+        stagingUndo = null
+    }
+
+    companion object {
+        object Saver : androidx.compose.runtime.saveable.Saver<TextUndoManager, Any> {
+            private val undoManagerSaver = UndoManager.createSaver(TextUndoOperation.Saver)
+
+            override fun SaverScope.save(value: TextUndoManager): Any {
+                return listOf(
+                    value.stagingUndo?.let {
+                        with(TextUndoOperation.Saver) {
+                            save(it)
+                        }
+                    },
+                    with(undoManagerSaver) {
+                        save(value.undoManager)
+                    }
+                )
+            }
+
+            override fun restore(value: Any): TextUndoManager? {
+                val (savedStagingUndo, savedUndoManager) = value as List<*>
+                return TextUndoManager(
+                    savedStagingUndo?.let {
+                        with(TextUndoOperation.Saver) {
+                            restore(it)
+                        }
+                    },
+                    with(undoManagerSaver) {
+                        restore(savedUndoManager!!)
+                    }!!
+                )
+            }
+        }
+    }
+}
+
+/**
+ * Try to merge this [TextUndoOperation] with the [next]. Chronologically the [next] op must
+ * come after this one. If merge is not possible, this function returns null.
+ *
+ * There are many rules that govern the grouping logic of successive undo operations. Here we try
+ * to cover the most basic requirements but this is certainly not an exhaustive list.
+ *
+ * 1. Each action defines whether they can be merged at all. For example, text edits that are
+ * caused by cut or paste define themselves as unmergeable no matter what comes before or next.
+ * 2. If certain amount of time has passed since the latest grouping has begun.
+ * 3. Enter key (hard line break) is unmergeable.
+ * 4. Only same type of text edits can be merged. An insertion must be grouped by other insertions,
+ * a deletion by other deletions. Replace type of edit is never mergeable.
+ *   4.a. Two insertions can only be merged if the chronologically next one is a suffix of the
+ *   previous insertion. In other words, cursor should always be moving forwards.
+ *   4.b. Deletions have directionality. Cursor can only insert in place and move forwards but
+ *   deletion can be requested either forwards (delete) or backwards (backspace). Only deletions
+ *   that have the same direction can be merged. They also have to share a boundary.
+ */
+internal fun TextUndoOperation.merge(next: TextUndoOperation): TextUndoOperation? {
+    if (!canMerge || !next.canMerge) return null
+    // Do not merge if [other] came before this op, or if certain amount of time has passed
+    // between these ops
+    if (
+        next.timeInMillis < timeInMillis ||
+        next.timeInMillis - timeInMillis >= SNAPSHOTS_INTERVAL_MILLIS
+    ) return null
+    // Do not merge undo history when one of the ops is a new line insertion
+    if (isNewLineInsert || next.isNewLineInsert) return null
+    // Only same type of ops can be merged together
+    if (textEditType != next.textEditType) return null
+
+    // only merge insertions if the chronologically next one continuous from the end of
+    // this previous insertion
+    if (textEditType == TextEditType.Insert && index + postText.length == next.index) {
+        return TextUndoOperation(
+            index = index,
+            preText = "",
+            postText = postText + next.postText,
+            preSelection = this.preSelection,
+            postSelection = next.postSelection,
+            timeInMillis = timeInMillis
+        )
+    } else if (textEditType == TextEditType.Delete) {
+        // only merge consecutive deletions if both have the same directionality
+        if (
+            deletionType == next.deletionType &&
+            (deletionType == TextDeleteType.Start || deletionType == TextDeleteType.End)
+        ) {
+            // This op deletes
+            if (index == next.index + next.preText.length) {
+                return TextUndoOperation(
+                    index = next.index,
+                    preText = next.preText + preText,
+                    postText = "",
+                    preSelection = preSelection,
+                    postSelection = next.postSelection,
+                    timeInMillis = timeInMillis
+                )
+            } else if (index == next.index) {
+                return TextUndoOperation(
+                    index = index,
+                    preText = preText + next.preText,
+                    postText = "",
+                    preSelection = preSelection,
+                    postSelection = next.postSelection,
+                    timeInMillis = timeInMillis
+                )
+            }
+        }
+    }
+    return null
+}
+
+/**
+ * Adds the [changes] to this [UndoManager] by converting from [TextFieldBuffer.ChangeList] space
+ * to [TextUndoOperation] space.
+ *
+ * @param pre State of the [TextFieldBuffer] before any changes are applied
+ * @param post State of the [TextFieldBuffer] after all the changes are applied
+ * @param changes List of changes that are applied on [pre] that transforms it to [post].
+ * @param allowMerge Whether to allow merging the calculated operation with the last operation
+ * in the stack.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal fun TextUndoManager.recordChanges(
+    pre: TextFieldCharSequence,
+    post: TextFieldCharSequence,
+    changes: TextFieldBuffer.ChangeList,
+    allowMerge: Boolean = true
+) {
+    // if there are unmerged changes coming from a single edit, force merge all of them to
+    // create a single replace undo operation
+    if (changes.changeCount > 1) {
+        record(
+            TextUndoOperation(
+                index = 0,
+                preText = pre.toString(),
+                postText = post.toString(),
+                preSelection = pre.selectionInChars,
+                postSelection = post.selectionInChars,
+                canMerge = false
+            )
+        )
+    } else if (changes.changeCount == 1) {
+        val preRange = changes.getOriginalRange(0)
+        val postRange = changes.getRange(0)
+        if (!preRange.collapsed || !postRange.collapsed) {
+            record(
+                TextUndoOperation(
+                    index = preRange.min,
+                    preText = pre.substring(preRange),
+                    postText = post.substring(postRange),
+                    preSelection = pre.selectionInChars,
+                    postSelection = post.selectionInChars,
+                    canMerge = allowMerge
+                )
+            )
+        }
+    }
+}
+
+/**
+ * Determines whether this operation is adding a new hard line break. This type of change produces
+ * unmergable [TextUndoOperation].
+ */
+private val TextUndoOperation.isNewLineInsert
+    get() = this.postText == "\n" || this.postText == "\r\n"
+
+private const val TEXT_UNDO_CAPACITY = 100
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/UndoState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/UndoState.kt
new file mode 100644
index 0000000..716d708
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/UndoState.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.foundation.text2.input
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+
+/**
+ * Defines an interactable undo history.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class UndoState internal constructor(private val state: TextFieldState) {
+
+    /**
+     * Whether it is possible to execute a meaningful undo action right now. If this value is false,
+     * calling `undo` would be a no-op.
+     */
+    @Suppress("GetterSetterNames")
+    @get:Suppress("GetterSetterNames")
+    val canUndo: Boolean
+        get() = state.textUndoManager.canUndo
+
+    /**
+     * Whether it is possible to execute a meaningful redo action right now. If this value is false,
+     * calling `redo` would be a no-op.
+     */
+    @Suppress("GetterSetterNames")
+    @get:Suppress("GetterSetterNames")
+    val canRedo: Boolean
+        get() = state.textUndoManager.canRedo
+
+    /**
+     * Reverts the latest edit action or a group of actions that are merged together. Calling it
+     * repeatedly can continue undoing the previous actions.
+     */
+    fun undo() {
+        state.textUndoManager.undo(state)
+    }
+
+    /**
+     * Re-applies a change that was previously reverted via [undo].
+     */
+    fun redo() {
+        state.textUndoManager.redo(state)
+    }
+
+    /**
+     * Clears all undo and redo history up to this point.
+     */
+    fun clearHistory() {
+        state.textUndoManager.clearHistory()
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/ChangeTracker.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/ChangeTracker.kt
index a3f31a5..8273ced 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/ChangeTracker.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/ChangeTracker.kt
@@ -64,15 +64,17 @@
      *     for the new text.
      *  3. Offset all remaining changes are to account for the new text.
      */
-    fun trackChange(preRange: TextRange, postLength: Int) {
-        if (preRange.collapsed && postLength == 0) {
+    fun trackChange(preStart: Int, preEnd: Int, postLength: Int) {
+        if (preStart == preEnd && postLength == 0) {
             // Ignore noop changes.
             return
         }
+        val preMin = minOf(preStart, preEnd)
+        val preMax = maxOf(preStart, preEnd)
 
         var i = 0
         var recordedNewChange = false
-        val postDelta = postLength - preRange.length
+        val postDelta = postLength - (preMax - preMin)
 
         var mergedOverlappingChange: Change? = null
         while (i < _changes.size) {
@@ -80,8 +82,8 @@
 
             // Merge adjacent and overlapping changes as we go.
             if (
-                change.preStart in preRange.min..preRange.max ||
-                change.preEnd in preRange.min..preRange.max
+                change.preStart in preMin..preMax ||
+                change.preEnd in preMin..preMax
             ) {
                 if (mergedOverlappingChange == null) {
                     mergedOverlappingChange = change
@@ -94,10 +96,10 @@
                 continue
             }
 
-            if (change.preStart > preRange.max && !recordedNewChange) {
+            if (change.preStart > preMax && !recordedNewChange) {
                 // First non-overlapping change after the new one – record the change before
                 // proceeding.
-                appendNewChange(mergedOverlappingChange, preRange, postDelta)
+                appendNewChange(mergedOverlappingChange, preMin, preMax, postDelta)
                 recordedNewChange = true
             }
 
@@ -112,7 +114,7 @@
         if (!recordedNewChange) {
             // The new change is after or overlapping all previous changes so it hasn't been
             // appended yet.
-            appendNewChange(mergedOverlappingChange, preRange, postDelta)
+            appendNewChange(mergedOverlappingChange, preMin, preMax, postDelta)
         }
 
         // Swap the lists.
@@ -146,7 +148,8 @@
 
     private fun appendNewChange(
         mergedOverlappingChange: Change?,
-        preRange: TextRange,
+        preMin: Int,
+        preMax: Int,
         postDelta: Int
     ) {
         var originalDelta = if (_changesTemp.isEmpty()) 0 else {
@@ -155,11 +158,11 @@
         val newChange: Change
         if (mergedOverlappingChange == null) {
             // There were no overlapping changes, so allocate a new one.
-            val originalStart = preRange.min - originalDelta
-            val originalEnd = originalStart + preRange.length
+            val originalStart = preMin - originalDelta
+            val originalEnd = originalStart + (preMax - preMin)
             newChange = Change(
-                preStart = preRange.min,
-                preEnd = preRange.max + postDelta,
+                preStart = preMin,
+                preEnd = preMax + postDelta,
                 originalStart = originalStart,
                 originalEnd = originalEnd
             )
@@ -167,16 +170,16 @@
             newChange = mergedOverlappingChange
             // Convert the merged overlapping changes to the `post` space.
             // Merge the new changed with the merged overlapping changes.
-            if (newChange.preStart > preRange.min) {
+            if (newChange.preStart > preMin) {
                 // The new change starts before the merged overlapping set.
-                newChange.preStart = preRange.min
-                newChange.originalStart = preRange.min
+                newChange.preStart = preMin
+                newChange.originalStart = preMin
             }
-            if (preRange.max > newChange.preEnd) {
+            if (preMax > newChange.preEnd) {
                 // The new change ends after the merged overlapping set.
                 originalDelta = newChange.preEnd - newChange.originalEnd
-                newChange.preEnd = preRange.max
-                newChange.originalEnd = preRange.max - originalDelta
+                newChange.preEnd = preMax
+                newChange.originalEnd = preMax - originalDelta
             }
             newChange.preEnd += postDelta
         }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/CodepointHelpers.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/CodepointHelpers.kt
new file mode 100644
index 0000000..7fd39aa
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/CodepointHelpers.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.foundation.text2.input.internal
+
+internal expect fun CharSequence.codePointAt(index: Int): Int
+internal expect fun CharSequence.codePointCount(): Int
+internal expect fun charCount(codePoint: Int): Int
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/EditingBuffer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/EditingBuffer.kt
index 49b22ae..c24a869 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/EditingBuffer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/EditingBuffer.kt
@@ -138,24 +138,19 @@
         checkRange(selection.start, selection.end)
     }
 
-    fun replace(start: Int, end: Int, text: AnnotatedString) {
-        replace(start, end, text.text)
-    }
-
     /**
      * Replace the text and move the cursor to the end of inserted text.
      *
      * This function cancels selection if there is any.
      *
      * @throws IndexOutOfBoundsException if start or end offset is outside of current buffer
-     * @throws IllegalArgumentException if start is larger than end. (reversed range)
      */
-    fun replace(start: Int, end: Int, text: String) {
+    fun replace(start: Int, end: Int, text: CharSequence) {
         checkRange(start, end)
         val min = minOf(start, end)
         val max = maxOf(start, end)
 
-        changeTracker.trackChange(TextRange(start, end), text.length)
+        changeTracker.trackChange(start, end, text.length)
 
         gapBuffer.replace(min, max, text)
 
@@ -164,8 +159,8 @@
         // the end offset of the editing area for desktop like application. In case of Android,
         // implementation will call setSelection immediately after replace function to update this
         // tentative cursor location.
-        selectionStart = start + text.length
-        selectionEnd = start + text.length
+        selectionStart = min + text.length
+        selectionEnd = min + text.length
 
         // Similarly, if text modification happens, cancel ongoing composition. If caller wants to
         // change the composition text, it is caller's responsibility to call setComposition again
@@ -184,7 +179,7 @@
         checkRange(start, end)
         val deleteRange = TextRange(start, end)
 
-        changeTracker.trackChange(deleteRange, 0)
+        changeTracker.trackChange(start, end, 0)
 
         gapBuffer.replace(deleteRange.min, deleteRange.max, "")
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/OffsetMappingCalculator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/OffsetMappingCalculator.kt
new file mode 100644
index 0000000..6c9ebb7
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/OffsetMappingCalculator.kt
@@ -0,0 +1,415 @@
+/*
+ * 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.foundation.text2.input.internal
+
+import androidx.compose.ui.text.TextRange
+
+/**
+ * Builds up bidirectional mapping functions to map offsets from an original string to corresponding
+ * in a string that has some edit operations applied.
+ *
+ * Edit operations must be reported via [recordEditOperation]. Offsets can be mapped
+ * [from the source string][mapFromSource] and [back to the source][mapFromDest]. Mapping between
+ * source and transformed offsets is a symmetric operation – given a sequence of edit operations,
+ * the result of mapping each offset from the source to transformed strings will be the same as
+ * mapping backwards on the inverse sequence of edit operations.
+ *
+ * By default, offsets map one-to-one. However, around an edit operation, some alternative rules
+ * apply. In general, offsets are mapped one-to-one where they can be unambiguously. When there
+ * isn't enough information, the mapping is ambiguous, and the mapping result will be a [TextRange]
+ * instead of a single value, where the range represents all the possible offsets that it could map
+ * to.
+ *
+ * _Note: In the following discussion, `I` refers to the start offset in the source text, `N` refers
+ * to the length of the range in the source text, and `N'` refers to the length of the replacement
+ * text. So given `"abc"` and replacing `"bc"` with `"xyz"`, `I=1`, `N=2`, and `N'=3`._
+ *
+ * ### Insertions
+ * When text is inserted (i.e. zero-length range is replaced with non-zero-length range), the
+ * mapping is ambiguous because all of the offsets in the inserted text map back to the same offset
+ * in the source text - the offset where the text was inserted. That means the insertion point can
+ * map to any of the offsets in the inserted text. I.e. I -> I..N'
+ *
+ * - This is slightly different than the replacement case, because the offset of the start of
+ *   the operation and the offset of the end of the operation (which are the same) map to a
+ *   range instead of a scalar. This is because there is not enough information to map the start
+ *   and end offsets 1-to-1 to offsets in the transformed text.
+ * - This is symmetric with deletion: Mapping backward from an insertion is the same as mapping
+ *   forward from a deletion.
+ *
+ * ### Deletions
+ * In the inverse case, when text is deleted, mapping is unambiguous. All the offsets in the
+ * deleted range map to the start of the deleted range. I.e. I..N -> I
+ *
+ * - This is symmetric with insertion: Mapping backward from a deletion is the same as mapping
+ *   forward from an insertion.
+ *
+ * ### Replacements
+ * When text is replaced, there are both ambiguous and unambiguous mappings:
+ * - The offsets at the _ends_ of the replaced range map unambiguously to the offsets at the
+ *   corresponding edges of the replaced text. I -> I and I+1 -> I+N
+ * - The offsets _inside_ the replaced range (exclusive) map ambiguously to the entire replaced
+ *   range. I+1..I+N-1 -> I+1..I+N'-1
+ * - Note that this means that when a string with length >1 is replaced by a single character,
+ *   all the offsets inside that string will map not to the index of the replacement character
+ *   but to a single-char _range_ containing that character.
+ *
+ * ### Examples
+ *
+ * #### Inserting text
+ * ```
+ *     012
+ * A: "ab"
+ *     | \
+ *     |  \
+ * B: "azzzb"
+ *     012345
+ * ```
+ *
+ * Forward mapping:
+ *
+ * | from A: | 0 | 1 | 2 |
+ * |--------:|:-:|:-:|:-:|
+ * |   to B: | 0 |1-4| 5 |
+ *
+ * Reverse mapping:
+ *
+ * | from B: | 0 | 1 | 2 | 3 | 4 | 5 |
+ * |--------:|:-:|:-:|:-:|:-:|:-:|:-:|
+ * |   to A: | 0 | 1 | 1 | 1 | 1 | 2 |
+ *
+ * #### Deleting text
+ * ```
+ *     012345
+ * A: "azzzb"
+ *     |  /
+ *     | /
+ * B: "ab"
+ *     012
+ * ```
+ *
+ * Forward mapping:
+ *
+ * | from A: | 0 | 1 | 2 | 3 | 4 | 5 |
+ * |--------:|:-:|:-:|:-:|:-:|:-:|:-:|
+ * |   to B: | 0 | 1 | 1 | 1 | 1 | 2 |
+ *
+ * Reverse mapping:
+ *
+ * | from B: | 0 | 1 | 2 |
+ * |--------:|:-:|:-:|:-:|
+ * |   to A: | 0 |1-4| 5 |
+ *
+ * #### Replacing text: single char with char
+ * ```
+ *     0123
+ * A: "abc"
+ *      |
+ *      |
+ * B: "azc"
+ *     0123
+ * ```
+ *
+ * Forward/reverse mapping: identity
+ *
+ * | from: | 0 | 1 | 2 | 3 |
+ * |------:|:-:|:-:|:-:|:-:|
+ * |   to: | 0 | 1 | 2 | 3 |
+ *
+ * #### Replacing text: char with chars
+ * ```
+ *     0123
+ * A: "abc"
+ *      |
+ *      |\
+ * B: "azzc"
+ *     01234
+ * ```
+ *
+ * Forward mapping:
+ *
+ * | from A: | 0 | 1 | 2 | 3 |
+ * |--------:|:-:|:-:|:-:|:-:|
+ * |   to B: | 0 | 1 | 3 | 4 |
+ *
+ * Reverse mapping:
+ *
+ * | from B: | 0 | 1 | 2 | 3 | 4 |
+ * |--------:|:-:|:-:|:-:|:-:|:-:|
+ * |   to A: | 0 | 1 | 1 | 2 | 3 |
+ *
+ * #### Replacing text: chars with chars
+ * ```
+ *     012345
+ * A: "abcde"
+ *      | |
+ *      | /
+ * B: "azze"
+ *     01234
+ * ```
+ *
+ * Forward mapping:
+ *
+ * | from A: | 0 | 1 | 2 | 3 | 4 | 5 |
+ * |--------:|:-:|:-:|:-:|:-:|:-:|:-:|
+ * |   to B: | 0 | 1 |1-3|1-3| 3 | 4 |
+ *
+ * Reverse mapping:
+ *
+ * | from B: | 0 | 1 | 2 | 3 | 4 |
+ * |--------:|:-:|:-:|:-:|:-:|:-:|
+ * |   to A: | 0 | 1 |1-4| 4 | 5 |
+ *
+ * ### Multiple operations
+ *
+ * While the above apply to single edit operations, when multiple edit operations are recorded the
+ * same rules apply. The rules are applied to the first operation, then the result of that is
+ * effectively used as the input text for the next operation, etc. Because an offset can map to a
+ * range at each step, we track both a start and end offset (which start as the same value), and
+ * at each step combine the start and end _ranges_ by taking their union.
+ *
+ * #### Multiple char-to-char replacements (codepoint transformation):
+ * ```
+ *     0123
+ * A: "abc"
+ *     |
+ *    "•bc"
+ *      |
+ *    "••c"
+ *       |
+ * B: "•••"
+ *     0123
+ * ```
+ *
+ * Forward/reverse mapping: identity
+ *
+ * | from: | 0 | 1 | 2 | 3 |
+ * |------:|:-:|:-:|:-:|:-:|
+ * |   to: | 0 | 1 | 2 | 3 |
+ *
+ * #### Multiple inserts:
+ * ```
+ *     01234
+ * A: "abcd"
+ *      |
+ *    "a(bcd"
+ *         |
+ * B: "a(bc)d"
+ *     0123456
+ * ```
+ *
+ * Forward mapping:
+ *
+ * | from A: | 0 | 1 | 2 | 3 | 4 |
+ * |--------:|:-:|:-:|:-:|:-:|:-:|
+ * |   to B: | 0 |1-2| 3 |4-5| 6 |
+ *
+ * Reverse mapping:
+ *
+ * | from B: | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
+ * |--------:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
+ * |   to A: | 0 | 1 | 1 | 2 | 3 | 3 | 4 |
+ *
+ * #### Multiple replacements of the same range:
+ * ```
+ *     01234
+ * A: "abcd"
+ *      ||
+ *    "awxd"
+ *      ||
+ * B: "ayzd"
+ *     01234
+ * ```
+ *
+ * Forward mapping:
+ *
+ * | from A: | 0 | 1 | 2 | 3 | 4 |
+ * |--------:|:-:|:-:|:-:|:-:|:-:|
+ * |   to B: | 0 | 1 |1-3| 3 | 4 |
+ *
+ * Reverse mapping:
+ *
+ * | from B: | 0 | 1 | 2 | 3 | 4 |
+ * |--------:|:-:|:-:|:-:|:-:|:-:|
+ * |   to A: | 0 | 1 |1-3| 3 | 4 |
+ *
+ * For other edge cases, including overlapping replacements, see `OffsetMappingCalculatorTest`.
+ */
+internal class OffsetMappingCalculator {
+    /** Resizable array of edit operations, size is defined by [opsSize]. */
+    private var ops = OpArray(size = 10)
+    private var opsSize = 0
+
+    /**
+     * Records an edit operation that replaces the range from [sourceStart] (inclusive) to
+     * [sourceEnd] (exclusive) in the original text with some text with length [newLength].
+     */
+    fun recordEditOperation(sourceStart: Int, sourceEnd: Int, newLength: Int) {
+        require(newLength >= 0) { "Expected newLen to be ≥ 0, was $newLength" }
+        val sourceMin = minOf(sourceStart, sourceEnd)
+        val sourceMax = maxOf(sourceMin, sourceEnd)
+        val sourceLength = sourceMax - sourceMin
+
+        // 0,0 is a noop, and 1,1 is always a 1-to-1 mapping so we don't need to record it.
+        if (sourceLength < 2 && sourceLength == newLength) return
+
+        // Append the operation to the array, growing it if necessary.
+        val newSize = opsSize + 1
+        if (newSize > ops.size) {
+            val newCapacity = maxOf(newSize * 2, ops.size * 2)
+            ops = ops.copyOf(newCapacity)
+        }
+        ops.set(opsSize, sourceMin, sourceLength, newLength)
+        opsSize = newSize
+    }
+
+    /**
+     * Maps an [offset] in the original string to the corresponding offset, or range of offsets,
+     * in the transformed string.
+     */
+    fun mapFromSource(offset: Int): TextRange = map(offset, fromSource = true)
+
+    /**
+     * Maps an [offset] in the original string to the corresponding offset, or range of offsets,
+     * in the transformed string.
+     */
+    fun mapFromDest(offset: Int): TextRange = map(offset, fromSource = false)
+
+    private fun map(offset: Int, fromSource: Boolean): TextRange {
+        var start = offset
+        var end = offset
+
+        // This algorithm works for both forward and reverse mapping, we just need to iterate
+        // backwards to do reverse mapping.
+        ops.forEach(max = opsSize, reversed = !fromSource) { opOffset, opSrcLen, opDestLen ->
+            val newStart = mapStep(
+                offset = start,
+                opOffset = opOffset,
+                untransformedLen = opSrcLen,
+                transformedLen = opDestLen,
+                fromSource = fromSource
+            )
+            val newEnd = mapStep(
+                offset = end,
+                opOffset = opOffset,
+                untransformedLen = opSrcLen,
+                transformedLen = opDestLen,
+                fromSource = fromSource
+            )
+            // range = newStart ∪ newEnd
+            // Note we don't read TextRange.min/max here because the above code always returns
+            // normalized ranges. It's no less correct, but there's no need to do the additional
+            // min/max calls inside the min/max properties.
+            start = minOf(newStart.start, newEnd.start)
+            end = maxOf(newStart.end, newEnd.end)
+        }
+
+        return TextRange(start, end)
+    }
+
+    private fun mapStep(
+        offset: Int,
+        opOffset: Int,
+        untransformedLen: Int,
+        transformedLen: Int,
+        fromSource: Boolean
+    ): TextRange {
+        val srcLen = if (fromSource) untransformedLen else transformedLen
+        val destLen = if (fromSource) transformedLen else untransformedLen
+        return when {
+            // Before the operation, no change.
+            offset < opOffset -> TextRange(offset)
+
+            offset == opOffset -> {
+                if (srcLen == 0) {
+                    // On insertion point, map to inserted range.
+                    TextRange(opOffset, opOffset + destLen)
+                } else {
+                    // On start of replacement, map to start of replaced range.
+                    TextRange(opOffset)
+                }
+            }
+
+            offset < opOffset + srcLen -> {
+                if (destLen == 0) {
+                    // In deleted range, map to start of deleted range.
+                    TextRange(opOffset)
+                } else {
+                    // In replaced range, map to transformed range.
+                    TextRange(opOffset, opOffset + destLen)
+                }
+            }
+
+            // On end of or after replaced range, offset the offset.
+            else -> TextRange(offset - srcLen + destLen)
+        }
+    }
+}
+
+/**
+ * An array of 3-tuples of ints. Each element's values are stored as individual values in the
+ * underlying array.
+ */
+@kotlin.jvm.JvmInline
+private value class OpArray private constructor(private val values: IntArray) {
+    constructor(size: Int) : this(IntArray(size * ElementSize))
+
+    val size: Int get() = values.size / ElementSize
+
+    fun set(index: Int, offset: Int, srcLen: Int, destLen: Int) {
+        values[index * ElementSize] = offset
+        values[index * ElementSize + 1] = srcLen
+        values[index * ElementSize + 2] = destLen
+    }
+
+    fun copyOf(newSize: Int) = OpArray(values.copyOf(newSize * ElementSize))
+
+    /**
+     * Loops through the array between 0 and [max] (exclusive). If [reversed] is false (the
+     * default), iterates forward from 0. When it's true, iterates backwards from `max-1`.
+     */
+    inline fun forEach(
+        max: Int,
+        reversed: Boolean = false,
+        block: (offset: Int, srcLen: Int, destLen: Int) -> Unit
+    ) {
+        if (max < 0) return
+        // Note: This stamps out block twice at the callsite, which is normally bad for an inline
+        // function to do. However, this is a file-private function which is only called in one
+        // place that would need to have two copies of mostly-identical code anyway. Doing the
+        // duplication here keeps the more complicated logic at the callsite more readable.
+        if (reversed) {
+            for (i in max - 1 downTo 0) {
+                val offset = values[i * ElementSize]
+                val srcLen = values[i * ElementSize + 1]
+                val destLen = values[i * ElementSize + 2]
+                block(offset, srcLen, destLen)
+            }
+        } else {
+            for (i in 0 until max) {
+                val offset = values[i * ElementSize]
+                val srcLen = values[i * ElementSize + 1]
+                val destLen = values[i * ElementSize + 2]
+                block(offset, srcLen, destLen)
+            }
+        }
+    }
+
+    private companion object {
+        const val ElementSize = 3
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldCoreModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldCoreModifier.kt
index 40afbee9..dc92bfc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldCoreModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldCoreModifier.kt
@@ -26,7 +26,6 @@
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
 import androidx.compose.foundation.text2.BasicTextField2
-import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.foundation.text2.input.internal.selection.TextFieldSelectionState
 import androidx.compose.foundation.text2.input.internal.selection.textFieldMagnifierNode
 import androidx.compose.runtime.snapshotFlow
@@ -78,11 +77,10 @@
  *
  * This modifier mostly handles layout and draw.
  */
-@OptIn(ExperimentalFoundationApi::class)
 internal data class TextFieldCoreModifier(
     private val isFocused: Boolean,
     private val textLayoutState: TextLayoutState,
-    private val textFieldState: TextFieldState,
+    private val textFieldState: TransformedTextFieldState,
     private val textFieldSelectionState: TextFieldSelectionState,
     private val cursorBrush: Brush,
     private val writeable: Boolean,
@@ -124,7 +122,7 @@
 internal class TextFieldCoreModifierNode(
     private var isFocused: Boolean,
     private var textLayoutState: TextLayoutState,
-    private var textFieldState: TextFieldState,
+    private var textFieldState: TransformedTextFieldState,
     private var textFieldSelectionState: TextFieldSelectionState,
     private var cursorBrush: Brush,
     private var writeable: Boolean,
@@ -177,7 +175,7 @@
     fun updateNode(
         isFocused: Boolean,
         textLayoutState: TextLayoutState,
-        textFieldState: TextFieldState,
+        textFieldState: TransformedTextFieldState,
         textFieldSelectionState: TextFieldSelectionState,
         cursorBrush: Brush,
         writeable: Boolean,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldDecoratorModifier.kt
index e4ef563..3faa87b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldDecoratorModifier.kt
@@ -22,8 +22,6 @@
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.foundation.text2.BasicTextField2
 import androidx.compose.foundation.text2.input.InputTransformation
-import androidx.compose.foundation.text2.input.TextFieldState
-import androidx.compose.foundation.text2.input.deselect
 import androidx.compose.foundation.text2.input.internal.selection.TextFieldSelectionState
 import androidx.compose.ui.focus.FocusDirection
 import androidx.compose.ui.focus.FocusEventModifierNode
@@ -66,6 +64,7 @@
 import androidx.compose.ui.semantics.setText
 import androidx.compose.ui.semantics.textSelectionRange
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.ImeOptions
 import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -84,7 +83,7 @@
  */
 @OptIn(ExperimentalFoundationApi::class)
 internal data class TextFieldDecoratorModifier(
-    private val textFieldState: TextFieldState,
+    private val textFieldState: TransformedTextFieldState,
     private val textLayoutState: TextLayoutState,
     private val textFieldSelectionState: TextFieldSelectionState,
     private val filter: InputTransformation?,
@@ -128,7 +127,7 @@
 /** Modifier node for [TextFieldDecoratorModifier]. */
 @OptIn(ExperimentalFoundationApi::class)
 internal class TextFieldDecoratorModifierNode(
-    var textFieldState: TextFieldState,
+    var textFieldState: TransformedTextFieldState,
     var textLayoutState: TextLayoutState,
     var textFieldSelectionState: TextFieldSelectionState,
     var filter: InputTransformation?,
@@ -169,9 +168,7 @@
      * Manages key events. These events often are sourced by a hardware keyboard but it's also
      * possible that IME or some other platform system simulates a KeyEvent.
      */
-    private val textFieldKeyEventHandler = createTextFieldKeyEventHandler().also {
-        it.setFilter(filter)
-    }
+    private val textFieldKeyEventHandler = createTextFieldKeyEventHandler()
 
     private val keyboardActionScope = object : KeyboardActionScope {
         private val focusManager: FocusManager
@@ -219,7 +216,7 @@
      * Updates all the related properties and invalidates internal state based on the changes.
      */
     fun updateNode(
-        textFieldState: TextFieldState,
+        textFieldState: TransformedTextFieldState,
         textLayoutState: TextLayoutState,
         textFieldSelectionState: TextFieldSelectionState,
         filter: InputTransformation?,
@@ -270,8 +267,6 @@
             invalidateSemantics()
         }
 
-        textFieldKeyEventHandler.setFilter(filter)
-
         if (textFieldSelectionState != previousTextFieldSelectionState) {
             pointerInputNode.resetPointerInputHandler()
         }
@@ -282,54 +277,57 @@
 
     // This function is called inside a snapshot observer.
     override fun SemanticsPropertyReceiver.applySemantics() {
-        val text = textFieldState.text
+        val text = textFieldState.untransformedText
         val selection = text.selectionInChars
+        editableText = AnnotatedString(text.toString())
+        textSelectionRange = selection
 
         getTextLayoutResult {
             textLayoutState.layoutResult?.let { result -> it.add(result) } ?: false
         }
-        editableText = AnnotatedString(text.toString())
-        textSelectionRange = selection
         if (!enabled) disabled()
 
         setText { newText ->
             if (readOnly || !enabled) return@setText false
 
-            textFieldState.editAsUser(filter) {
-                deleteAll()
-                commitText(newText.toString(), 1)
-            }
+            textFieldState.replaceAll(newText)
             true
         }
-        setSelection { start, end, _ ->
-            // BasicTextField2 doesn't have VisualTransformation for the time being and
-            // probably won't have something that uses offsetMapping design. We can safely
-            // skip relativeToOriginalText flag. Assume it's always true.
-
-            if (!enabled) {
-                false
-            } else if (start == selection.start && end == selection.end) {
-                false
-            } else if (start.coerceAtMost(end) >= 0 &&
-                start.coerceAtLeast(end) <= text.length
-            ) {
-                textFieldState.editAsUser(filter) {
-                    setSelection(start, end)
-                }
-                true
+        @Suppress("NAME_SHADOWING")
+        setSelection { start, end, relativeToOriginal ->
+            val text = if (relativeToOriginal) {
+                textFieldState.untransformedText
             } else {
-                false
+                textFieldState.text
             }
+            val selection = text.selectionInChars
+
+            if (!enabled ||
+                minOf(start, end) < 0 ||
+                maxOf(start, end) > text.length
+            ) {
+                return@setSelection false
+            }
+
+            // Selection is already selected, don't need to do any work.
+            if (start == selection.start && end == selection.end) {
+                return@setSelection true
+            }
+
+            val selectionRange = TextRange(start, end)
+            if (relativeToOriginal) {
+                textFieldState.selectUntransformedCharsIn(selectionRange)
+            } else {
+                textFieldState.selectCharsIn(selectionRange)
+            }
+            return@setSelection true
         }
         insertTextAtCursor { newText ->
             if (readOnly || !enabled) return@insertTextAtCursor false
 
-            textFieldState.editAsUser(filter) {
-                // Finish composing text first because when the field is focused the IME
-                // might set composition.
-                commitComposition()
-                commitText(newText.toString(), 1)
-            }
+            // Finish composing text first because when the field is focused the IME
+            // might set composition.
+            textFieldState.replaceSelectedText(newText, clearComposition = true)
             true
         }
         onImeAction(keyboardOptions.imeAction) {
@@ -381,7 +379,7 @@
             }
         } else {
             disposeInputSession()
-            textFieldState.deselect()
+            textFieldState.collapseSelectionToMax()
         }
     }
 
@@ -419,7 +417,6 @@
         return textFieldKeyEventHandler.onKeyEvent(
             event = event,
             textFieldState = textFieldState,
-            inputTransformation = filter,
             textLayoutState = textLayoutState,
             textFieldSelectionState = textFieldSelectionState,
             editable = enabled && !readOnly,
@@ -441,7 +438,6 @@
                 platformSpecificTextInputSession(
                     textFieldState,
                     keyboardOptions.toImeOptions(singleLine),
-                    filter = filter,
                     onImeAction = onImeActionPerformed
                 )
             }
@@ -461,11 +457,9 @@
 /**
  * Runs platform-specific text input logic.
  */
-@OptIn(ExperimentalFoundationApi::class)
 internal expect suspend fun PlatformTextInputSession.platformSpecificTextInputSession(
-    state: TextFieldState,
+    state: TransformedTextFieldState,
     imeOptions: ImeOptions,
-    filter: InputTransformation?,
     onImeAction: ((ImeAction) -> Unit)?
 ): Nothing
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldKeyEventHandler.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldKeyEventHandler.kt
index 3c886d6..1df7c42 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldKeyEventHandler.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldKeyEventHandler.kt
@@ -24,8 +24,6 @@
 import androidx.compose.foundation.text.isTypedEvent
 import androidx.compose.foundation.text.platformDefaultKeyMapping
 import androidx.compose.foundation.text.showCharacterPalette
-import androidx.compose.foundation.text2.input.InputTransformation
-import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.foundation.text2.input.internal.TextFieldPreparedSelection.Companion.NoCharacterFound
 import androidx.compose.foundation.text2.input.internal.selection.TextFieldSelectionState
 import androidx.compose.ui.focus.FocusManager
@@ -51,15 +49,10 @@
     private val preparedSelectionState = TextFieldPreparedSelectionState()
     private val deadKeyCombiner = DeadKeyCombiner()
     private val keyMapping = platformDefaultKeyMapping
-    private var filter: InputTransformation? = null
-
-    fun setFilter(filter: InputTransformation?) {
-        this.filter = filter
-    }
 
     open fun onPreKeyEvent(
         event: KeyEvent,
-        textFieldState: TextFieldState,
+        textFieldState: TransformedTextFieldState,
         textFieldSelectionState: TextFieldSelectionState,
         focusManager: FocusManager,
         keyboardController: SoftwareKeyboardController
@@ -75,8 +68,7 @@
 
     open fun onKeyEvent(
         event: KeyEvent,
-        textFieldState: TextFieldState,
-        inputTransformation: InputTransformation?,
+        textFieldState: TransformedTextFieldState,
         textLayoutState: TextLayoutState,
         textFieldSelectionState: TextFieldSelectionState,
         editable: Boolean,
@@ -92,7 +84,7 @@
             if (codePoint != null) {
                 val text = StringBuilder(2).appendCodePointX(codePoint).toString()
                 return if (editable) {
-                    textFieldState.editAsUser(filter) {
+                    textFieldState.editUntransformedTextAsUser {
                         commitComposition()
                         commitText(text, 1)
                     }
@@ -131,7 +123,7 @@
                 KeyCommand.HOME -> moveCursorToHome()
                 KeyCommand.END -> moveCursorToEnd()
                 KeyCommand.DELETE_PREV_CHAR ->
-                    textFieldState.editAsUser(filter) {
+                    textFieldState.editUntransformedTextAsUser {
                         if (!deleteIfSelected()) {
                             deleteSurroundingText(
                                 selection.end - getPrecedingCharacterIndex(),
@@ -142,7 +134,7 @@
                 KeyCommand.DELETE_NEXT_CHAR -> {
                     // Note that some software keyboards, such as Samsung, go through this code
                     // path instead of making calls on the InputConnection directly.
-                    textFieldState.editAsUser(filter) {
+                    textFieldState.editUntransformedTextAsUser {
                         if (!deleteIfSelected()) {
                             val nextCharacterIndex = getNextCharacterIndex()
                             // If there's no next character, it means the cursor is at the end of the
@@ -155,7 +147,7 @@
                 }
 
                 KeyCommand.DELETE_PREV_WORD ->
-                    textFieldState.editAsUser(filter) {
+                    textFieldState.editUntransformedTextAsUser {
                         if (!deleteIfSelected()) {
                             getPreviousWordOffset()?.let {
                                 deleteSurroundingText(selection.end - it, 0)
@@ -163,7 +155,7 @@
                         }
                     }
                 KeyCommand.DELETE_NEXT_WORD ->
-                    textFieldState.editAsUser(filter) {
+                    textFieldState.editUntransformedTextAsUser {
                         if (!deleteIfSelected()) {
                             getNextWordOffset()?.let {
                                 deleteSurroundingText(0, it - selection.end)
@@ -171,7 +163,7 @@
                         }
                     }
                 KeyCommand.DELETE_FROM_LINE_START ->
-                    textFieldState.editAsUser(filter) {
+                    textFieldState.editUntransformedTextAsUser {
                         if (!deleteIfSelected()) {
                             getLineStartByOffset()?.let {
                                 deleteSurroundingText(selection.end - it, 0)
@@ -179,7 +171,7 @@
                         }
                     }
                 KeyCommand.DELETE_TO_LINE_END ->
-                    textFieldState.editAsUser(filter) {
+                    textFieldState.editUntransformedTextAsUser {
                         if (!deleteIfSelected()) {
                             getLineEndByOffset()?.let {
                                 deleteSurroundingText(0, it - selection.end)
@@ -188,7 +180,7 @@
                     }
                 KeyCommand.NEW_LINE ->
                     if (!singleLine) {
-                        textFieldState.editAsUser(filter) {
+                        textFieldState.editUntransformedTextAsUser {
                             commitComposition()
                             commitText("\n", 1)
                         }
@@ -198,7 +190,7 @@
 
                 KeyCommand.TAB ->
                     if (!singleLine) {
-                        textFieldState.editAsUser(filter) {
+                        textFieldState.editUntransformedTextAsUser {
                             commitComposition()
                             commitText("\t", 1)
                         }
@@ -225,25 +217,21 @@
                 KeyCommand.SELECT_END -> moveCursorToEnd().selectMovement()
                 KeyCommand.DESELECT -> deselect()
                 KeyCommand.UNDO -> {
-                    // undoManager?.makeSnapshot(value)
-                    // undoManager?.undo()?.let { this@TextFieldKeyInput.onValueChange(it) }
+                    textFieldState.undo()
                 }
-
                 KeyCommand.REDO -> {
-                    // undoManager?.redo()?.let { this@TextFieldKeyInput.onValueChange(it) }
+                    textFieldState.redo()
                 }
-
                 KeyCommand.CHARACTER_PALETTE -> {
                     showCharacterPalette()
                 }
             }
         }
-        // undoManager?.forceNextSnapshot()
         return consumed
     }
 
     private inline fun preparedSelectionContext(
-        state: TextFieldState,
+        state: TransformedTextFieldState,
         textLayoutState: TextLayoutState,
         block: TextFieldPreparedSelection.() -> Unit
     ) {
@@ -255,9 +243,7 @@
         preparedSelection.block()
         if (preparedSelection.selection != preparedSelection.initialValue.selectionInChars) {
             // selection changes are applied atomically at the end of context evaluation
-            state.editAsUser(filter) {
-                setSelection(preparedSelection.selection.start, preparedSelection.selection.end)
-            }
+            state.selectCharsIn(preparedSelection.selection)
         }
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldLayoutStateCache.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldLayoutStateCache.kt
index 36f9333..794c6ab 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldLayoutStateCache.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldLayoutStateCache.kt
@@ -19,12 +19,9 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.text.InternalFoundationTextApi
 import androidx.compose.foundation.text.TextDelegate
-import androidx.compose.foundation.text2.input.CodepointTransformation
-import androidx.compose.foundation.text2.input.SingleLineCodepointTransformation
 import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.foundation.text2.input.internal.TextFieldLayoutStateCache.MeasureInputs
 import androidx.compose.foundation.text2.input.internal.TextFieldLayoutStateCache.NonMeasureInputs
-import androidx.compose.foundation.text2.input.toVisualText
 import androidx.compose.runtime.SnapshotMutationPolicy
 import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
@@ -103,15 +100,13 @@
      * @see layoutWithNewMeasureInputs
      */
     fun updateNonMeasureInputs(
-        textFieldState: TextFieldState,
-        codepointTransformation: CodepointTransformation?,
+        textFieldState: TransformedTextFieldState,
         textStyle: TextStyle,
         singleLine: Boolean,
         softWrap: Boolean,
     ) {
         nonMeasureInputs = NonMeasureInputs(
             textFieldState = textFieldState,
-            codepointTransformation = codepointTransformation,
             textStyle = textStyle,
             singleLine = singleLine,
             softWrap = softWrap,
@@ -150,16 +145,7 @@
         nonMeasureInputs: NonMeasureInputs,
         measureInputs: MeasureInputs
     ): TextLayoutResult {
-        // If there's a transformation, we need to apply it every time, since it may contain state
-        // reads and conditional logic that we can't check avoid running.
-        // First prefer provided codepointTransformation if not null, e.g. BasicSecureTextField
-        // would send PasswordTransformation. Second, apply a SingleLineCodepointTransformation if
-        // text field is configured to be single line. Else, don't apply any visual transformation.
-        val appliedCodepointTransformation = nonMeasureInputs.codepointTransformation
-            ?: SingleLineCodepointTransformation.takeIf { nonMeasureInputs.singleLine }
-        // This will return untransformedText if the transformation is null.
         val visualText = nonMeasureInputs.textFieldState.text
-            .toVisualText(appliedCodepointTransformation)
 
         // Use withCurrent here so the cache itself is never reported as a read state object. It
         // doesn't need to be, because it's always guaranteed to return the same value for the same
@@ -324,8 +310,7 @@
 
     // region Input holders
     private class NonMeasureInputs(
-        val textFieldState: TextFieldState,
-        val codepointTransformation: CodepointTransformation?,
+        val textFieldState: TransformedTextFieldState,
         val textStyle: TextStyle,
         val singleLine: Boolean,
         val softWrap: Boolean,
@@ -333,7 +318,6 @@
 
         override fun toString(): String = "NonMeasureInputs(" +
             "textFieldState=$textFieldState, " +
-            "codepointTransformation=$codepointTransformation, " +
             "textStyle=$textStyle, " +
             "singleLine=$singleLine, " +
             "softWrap=$softWrap" +
@@ -355,7 +339,6 @@
                         // invalidating if the TextFieldState is a different instance but with the same
                         // text, but that is unlikely to happen.
                         a.textFieldState === b.textFieldState &&
-                            a.codepointTransformation == b.codepointTransformation &&
                             a.textStyle.hasSameLayoutAffectingAttributes(b.textStyle) &&
                             a.singleLine == b.singleLine &&
                             a.softWrap == b.softWrap
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldTextLayoutModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldTextLayoutModifier.kt
index ec07b7e..303888a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldTextLayoutModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldTextLayoutModifier.kt
@@ -16,9 +16,6 @@
 
 package androidx.compose.foundation.text2.input.internal
 
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text2.input.CodepointTransformation
-import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.FirstBaseline
 import androidx.compose.ui.layout.LastBaseline
@@ -46,11 +43,9 @@
  * coordinates of [TextLayoutResult] to make it relatively easier to calculate the offset between
  * exact touch coordinates and where they map on the [TextLayoutResult].
  */
-@OptIn(ExperimentalFoundationApi::class)
 internal data class TextFieldTextLayoutModifier(
     private val textLayoutState: TextLayoutState,
-    private val textFieldState: TextFieldState,
-    private val codepointTransformation: CodepointTransformation?,
+    private val textFieldState: TransformedTextFieldState,
     private val textStyle: TextStyle,
     private val singleLine: Boolean,
     private val onTextLayout: Density.(getResult: () -> TextLayoutResult?) -> Unit
@@ -58,7 +53,6 @@
     override fun create(): TextFieldTextLayoutModifierNode = TextFieldTextLayoutModifierNode(
         textLayoutState = textLayoutState,
         textFieldState = textFieldState,
-        codepointTransformation = codepointTransformation,
         textStyle = textStyle,
         singleLine = singleLine,
         onTextLayout = onTextLayout
@@ -68,7 +62,6 @@
         node.updateNode(
             textLayoutState = textLayoutState,
             textFieldState = textFieldState,
-            codepointTransformation = codepointTransformation,
             textStyle = textStyle,
             singleLine = singleLine,
             onTextLayout = onTextLayout
@@ -80,11 +73,9 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 internal class TextFieldTextLayoutModifierNode(
     private var textLayoutState: TextLayoutState,
-    textFieldState: TextFieldState,
-    codepointTransformation: CodepointTransformation?,
+    textFieldState: TransformedTextFieldState,
     textStyle: TextStyle,
     singleLine: Boolean,
     onTextLayout: Density.(getResult: () -> TextLayoutResult?) -> Unit
@@ -97,7 +88,6 @@
         textLayoutState.onTextLayout = onTextLayout
         textLayoutState.updateNonMeasureInputs(
             textFieldState = textFieldState,
-            codepointTransformation = codepointTransformation,
             textStyle = textStyle,
             singleLine = singleLine,
             softWrap = !singleLine
@@ -109,8 +99,7 @@
      */
     fun updateNode(
         textLayoutState: TextLayoutState,
-        textFieldState: TextFieldState,
-        codepointTransformation: CodepointTransformation?,
+        textFieldState: TransformedTextFieldState,
         textStyle: TextStyle,
         singleLine: Boolean,
         onTextLayout: Density.(getResult: () -> TextLayoutResult?) -> Unit
@@ -119,7 +108,6 @@
         this.textLayoutState.onTextLayout = onTextLayout
         this.textLayoutState.updateNonMeasureInputs(
             textFieldState = textFieldState,
-            codepointTransformation = codepointTransformation,
             textStyle = textStyle,
             singleLine = singleLine,
             softWrap = !singleLine
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextLayoutState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextLayoutState.kt
index 991dae0..883e4cf 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextLayoutState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextLayoutState.kt
@@ -17,8 +17,6 @@
 package androidx.compose.foundation.text2.input.internal
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text2.input.CodepointTransformation
-import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.neverEqualPolicy
@@ -65,15 +63,13 @@
      * @see layoutWithNewMeasureInputs
      */
     fun updateNonMeasureInputs(
-        textFieldState: TextFieldState,
-        codepointTransformation: CodepointTransformation?,
+        textFieldState: TransformedTextFieldState,
         textStyle: TextStyle,
         singleLine: Boolean,
         softWrap: Boolean,
     ) {
         layoutCache.updateNonMeasureInputs(
             textFieldState = textFieldState,
-            codepointTransformation = codepointTransformation,
             textStyle = textStyle,
             singleLine = singleLine,
             softWrap = softWrap,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextPreparedSelection.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextPreparedSelection.kt
index 63681d0..b4ce440 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextPreparedSelection.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextPreparedSelection.kt
@@ -21,7 +21,6 @@
 import androidx.compose.foundation.text.findParagraphEnd
 import androidx.compose.foundation.text.findParagraphStart
 import androidx.compose.foundation.text.findPrecedingBreak
-import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.text.TextLayoutResult
@@ -66,7 +65,7 @@
  */
 @OptIn(ExperimentalFoundationApi::class)
 internal class TextFieldPreparedSelection(
-    private val state: TextFieldState,
+    private val state: TransformedTextFieldState,
     private val textLayoutState: TextLayoutState,
     private val textPreparedSelectionState: TextFieldPreparedSelectionState
 ) {
@@ -89,8 +88,9 @@
     private val text: String = initialValue.toString()
 
     /**
-     * If there is a non-collapsed selection, delete its contents. If the selection is collapsed,
-     * execute the given [or] block.
+     * If there is a non-collapsed selection, delete its contents.
+     *
+     * @return Whether a selected region is deleted.
      */
     fun EditingBuffer.deleteIfSelected(): Boolean {
         if (selection.collapsed) return false
@@ -147,7 +147,7 @@
      *
      * @param resetCachedX Whether to reset the cachedX parameter in [TextFieldPreparedSelectionState].
      */
-    inline fun applyIfNotEmpty(
+    private inline fun applyIfNotEmpty(
         resetCachedX: Boolean = true,
         block: TextFieldPreparedSelection.() -> Unit
     ): TextFieldPreparedSelection {
@@ -330,8 +330,8 @@
         }
     }
 
-    // it selects a text from the original selection start to a current selection end
-    fun selectMovement() = applyIfNotEmpty(false) {
+    /** Selects a text from the original selection start to a current selection end. */
+    fun selectMovement() = applyIfNotEmpty(resetCachedX = false) {
         selection = TextRange(initialValue.selectionInChars.start, selection.end)
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TransformedTextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TransformedTextFieldState.kt
new file mode 100644
index 0000000..152ea3c
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TransformedTextFieldState.kt
@@ -0,0 +1,371 @@
+/*
+ * 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.foundation.text2.input.internal
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text2.input.CodepointTransformation
+import androidx.compose.foundation.text2.input.InputTransformation
+import androidx.compose.foundation.text2.input.TextFieldCharSequence
+import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.foundation.text2.input.internal.undo.TextFieldEditUndoBehavior
+import androidx.compose.foundation.text2.input.toVisualText
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.ui.text.TextRange
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * A mutable view of a [TextFieldState] where the text and selection values are transformed by a
+ * [CodepointTransformation].
+ *
+ * [text] returns the transformed text, with selection and composition mapped to the corresponding
+ * offsets from the untransformed text. The transformed text is cached in a
+ * [derived state][derivedStateOf] and only recalculated when the [TextFieldState] changes or some
+ * state read by the [CodepointTransformation] changes.
+ *
+ * This class defines methods for various operations that can be performed on the underlying
+ * [TextFieldState]. When possible, these methods should be used instead of editing the state
+ * directly, since this class ensures the correct offset mappings are used. If an operation is too
+ * complex to warrant a method here, use [editUntransformedTextAsUser] but be careful to make sure
+ * any offsets are mapped correctly.
+ *
+ * To map offsets from transformed to untransformed text or back, use the [mapFromTransformed] and
+ * [mapToTransformed] methods.
+ *
+ * All operations call [TextFieldState.editAsUser] internally and pass [inputTransformation].
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Stable
+internal class TransformedTextFieldState(
+    private val textFieldState: TextFieldState,
+    private val inputTransformation: InputTransformation?,
+    private val codepointTransformation: CodepointTransformation?,
+) {
+    private val transformedText: State<TransformedText?>? =
+        // Don't allocate a derived state object if we don't need it, they're expensive.
+        codepointTransformation?.let { transformation ->
+            derivedStateOf {
+                // text is a state read. transformation may also perform state reads when ran.
+                calculateTransformedText(textFieldState.text, transformation)
+            }
+        }
+
+    /**
+     * The text that should be presented to the user in most cases. Ifa  [CodepointTransformation]
+     * is specified, this text has the transformation applied. If there's no transformation,
+     * this will be the same as [untransformedText].
+     */
+    val text: TextFieldCharSequence
+        get() = transformedText?.value?.text ?: textFieldState.text
+
+    /**
+     * The raw text in the underlying [TextFieldState]. This text does not have any
+     * [CodepointTransformation] applied.
+     */
+    val untransformedText: TextFieldCharSequence
+        get() = textFieldState.text
+
+    fun placeCursorBeforeCharAt(transformedOffset: Int) {
+        selectCharsIn(TextRange(transformedOffset))
+    }
+
+    fun selectCharsIn(transformedRange: TextRange) {
+        val untransformedRange = mapFromTransformed(transformedRange)
+        selectUntransformedCharsIn(untransformedRange)
+    }
+
+    fun selectUntransformedCharsIn(untransformedRange: TextRange) {
+        textFieldState.editAsUser(inputTransformation) {
+            setSelection(untransformedRange.start, untransformedRange.end)
+        }
+    }
+
+    fun replaceAll(newText: CharSequence) {
+        textFieldState.editAsUser(inputTransformation) {
+            deleteAll()
+            commitText(newText.toString(), 1)
+        }
+    }
+
+    fun selectAll() {
+        textFieldState.editAsUser(inputTransformation) {
+            setSelection(0, length)
+        }
+    }
+
+    fun deleteSelectedText() {
+        textFieldState.editAsUser(
+            inputTransformation,
+            undoBehavior = TextFieldEditUndoBehavior.NeverMerge
+        ) {
+            // `selection` is read from the buffer, so we don't need to transform it.
+            delete(selection.min, selection.max)
+            setSelection(selection.min, selection.min)
+        }
+    }
+
+    fun replaceSelectedText(
+        newText: CharSequence,
+        clearComposition: Boolean = false,
+        undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible
+    ) {
+        textFieldState.editAsUser(inputTransformation, undoBehavior = undoBehavior) {
+            if (clearComposition) {
+                commitComposition()
+            }
+
+            // `selection` is read from the buffer, so we don't need to transform it.
+            val selection = selection
+            replace(
+                selection.min,
+                selection.max,
+                newText
+            )
+            val cursor = selection.min + newText.length
+            setSelection(cursor, cursor)
+        }
+    }
+
+    fun collapseSelectionToMax() {
+        textFieldState.editAsUser(inputTransformation) {
+            // `selection` is read from the buffer, so we don't need to transform it.
+            setSelection(selection.max, selection.max)
+        }
+    }
+
+    fun collapseSelectionToEnd() {
+        textFieldState.editAsUser(inputTransformation) {
+            // `selection` is read from the buffer, so we don't need to transform it.
+            setSelection(selection.end, selection.end)
+        }
+    }
+
+    fun undo() {
+        textFieldState.undoState.undo()
+    }
+
+    fun redo() {
+        textFieldState.undoState.redo()
+    }
+
+    /**
+     * Runs [block] with a buffer that contains the source untransformed text. This is the text that
+     * will be fed into the [codepointTransformation]. Any operations performed on this buffer MUST
+     * take care to explicitly convert between transformed and untransformed offsets and ranges.
+     * When possible, use the other methods on this class to manipulate selection to avoid having
+     * to do these conversions manually.
+     *
+     * @see mapToTransformed
+     * @see mapFromTransformed
+     */
+    inline fun editUntransformedTextAsUser(
+        notifyImeOfChanges: Boolean = true,
+        block: EditingBuffer.() -> Unit
+    ) {
+        textFieldState.editAsUser(
+            inputTransformation = inputTransformation,
+            notifyImeOfChanges = notifyImeOfChanges,
+            block = block
+        )
+    }
+
+    /**
+     * Maps an [offset] in the untransformed text to the corresponding offset or range in [text].
+     *
+     * An untransformed offset will map to non-collapsed range if the offset is in the middle of
+     * a surrogate pair in the untransformed text, in which case it will return the range of the
+     * codepoint that the surrogate maps to. Offsets on either side of a surrogate pair will return
+     * collapsed ranges.
+     *
+     * If there is no transformation, or the transformation does not change the text, a collapsed
+     * range of [offset] will be returned.
+     *
+     * @see mapFromTransformed
+     */
+    fun mapToTransformed(offset: Int): TextRange {
+        val mapping = transformedText?.value?.offsetMapping ?: return TextRange(offset)
+        return mapping.mapFromSource(offset)
+    }
+
+    /**
+     * Maps a [range] in the untransformed text to the corresponding range in [text].
+     *
+     * If there is no transformation, or the transformation does not change the text, [range]
+     * will be returned.
+     *
+     * @see mapFromTransformed
+     */
+    fun mapToTransformed(range: TextRange): TextRange {
+        val mapping = transformedText?.value?.offsetMapping ?: return range
+        return mapToTransformed(range, mapping)
+    }
+
+    /**
+     * Maps an [offset] in [text] to the corresponding offset in the untransformed text.
+     *
+     * Multiple transformed offsets may map to the same untransformed offset. In particular, any
+     * offset in the middle of a surrogate pair will map to offset of the corresponding codepoint
+     * in the untransformed text.
+     *
+     * If there is no transformation, or the transformation does not change the text, [offset]
+     * will be returned.
+     *
+     * @see mapToTransformed
+     */
+    fun mapFromTransformed(offset: Int): Int {
+        val mapping = transformedText?.value?.offsetMapping ?: return offset
+        return mapping.mapFromDest(offset).min
+    }
+
+    /**
+     * Maps a [range] in [text] to the corresponding range in the untransformed text.
+     *
+     * If there is no transformation, or the transformation does not change the text, [range]
+     * will be returned.
+     *
+     * @see mapToTransformed
+     */
+    fun mapFromTransformed(range: TextRange): TextRange {
+        val mapping = transformedText?.value?.offsetMapping ?: return range
+        return mapFromTransformed(range, mapping)
+    }
+
+    // TODO(b/296583846) Get rid of this.
+    /**
+     * Adds [notifyImeListener] to the underlying [TextFieldState] and then suspends until
+     * cancelled, removing the listener before continuing.
+     */
+    suspend fun collectImeNotifications(
+        notifyImeListener: TextFieldState.NotifyImeListener
+    ): Nothing {
+        suspendCancellableCoroutine<Nothing> { continuation ->
+            textFieldState.addNotifyImeListener(notifyImeListener)
+            continuation.invokeOnCancellation {
+                textFieldState.removeNotifyImeListener(notifyImeListener)
+            }
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TransformedTextFieldState) return false
+        if (textFieldState != other.textFieldState) return false
+        return codepointTransformation == other.codepointTransformation
+    }
+
+    override fun hashCode(): Int {
+        var result = textFieldState.hashCode()
+        result = 31 * result + (codepointTransformation?.hashCode() ?: 0)
+        return result
+    }
+
+    override fun toString(): String = "TransformedTextFieldState(" +
+        "textFieldState=$textFieldState, " +
+        "codepointTransformation=$codepointTransformation, " +
+        "transformedText=$transformedText, " +
+        "text=\"$text\"" +
+        ")"
+
+    private data class TransformedText(
+        val text: TextFieldCharSequence,
+        val offsetMapping: OffsetMappingCalculator,
+    )
+
+    private companion object {
+
+        /**
+         * Applies a [CodepointTransformation] to a [TextFieldCharSequence], returning the
+         * transformed text content, the selection/cursor from the [untransformedText] mapped to the
+         * offsets in the transformed text, and an [OffsetMappingCalculator] that can be used to map
+         * offsets in both directions between the transformed and untransformed text.
+         *
+         * This function is relatively expensive, since it creates a copy of [untransformedText], so
+         * its result should be cached.
+         */
+        @kotlin.jvm.JvmStatic
+        private fun calculateTransformedText(
+            untransformedText: TextFieldCharSequence,
+            codepointTransformation: CodepointTransformation
+        ): TransformedText? {
+            val offsetMappingCalculator = OffsetMappingCalculator()
+
+            // This is the call to external code. Returns same instance if no codepoints change.
+            val transformedText =
+                untransformedText.toVisualText(codepointTransformation, offsetMappingCalculator)
+
+            // Avoid allocations + mapping if there weren't actually any transformations.
+            if (transformedText === untransformedText) {
+                return null
+            }
+
+            val transformedTextWithSelection = TextFieldCharSequence(
+                text = transformedText,
+                // Pass the calculator explicitly since the one on transformedText won't be updated
+                // yet.
+                selection = mapToTransformed(
+                    untransformedText.selectionInChars,
+                    offsetMappingCalculator
+                ),
+                composition = untransformedText.compositionInChars?.let {
+                    mapToTransformed(it, offsetMappingCalculator)
+                }
+            )
+            return TransformedText(transformedTextWithSelection, offsetMappingCalculator)
+        }
+
+        @kotlin.jvm.JvmStatic
+        private fun mapToTransformed(
+            range: TextRange,
+            mapping: OffsetMappingCalculator
+        ): TextRange {
+            val transformedStart = mapping.mapFromSource(range.start)
+            // Avoid calculating mapping again if it's going to be the same value.
+            val transformedEnd = if (range.collapsed) transformedStart else {
+                mapping.mapFromSource(range.end)
+            }
+
+            val transformedMin = minOf(transformedStart.min, transformedEnd.min)
+            val transformedMax = maxOf(transformedStart.max, transformedEnd.max)
+            return if (range.reversed) {
+                TextRange(transformedMax, transformedMin)
+            } else {
+                TextRange(transformedMin, transformedMax)
+            }
+        }
+
+        @kotlin.jvm.JvmStatic
+        private fun mapFromTransformed(
+            range: TextRange,
+            mapping: OffsetMappingCalculator
+        ): TextRange {
+            val untransformedStart = mapping.mapFromDest(range.start)
+            // Avoid calculating mapping again if it's going to be the same value.
+            val untransformedEnd = if (range.collapsed) untransformedStart else {
+                mapping.mapFromDest(range.end)
+            }
+
+            val untransformedMin = minOf(untransformedStart.min, untransformedEnd.min)
+            val untransformedMax = maxOf(untransformedStart.max, untransformedEnd.max)
+            return if (range.reversed) {
+                TextRange(untransformedMax, untransformedMin)
+            } else {
+                TextRange(untransformedMin, untransformedMax)
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifier.kt
index 74309cf..180fe23 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifier.kt
@@ -19,8 +19,8 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.text.Handle
 import androidx.compose.foundation.text.selection.visibleBounds
-import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.foundation.text2.input.internal.TextLayoutState
+import androidx.compose.foundation.text2.input.internal.TransformedTextFieldState
 import androidx.compose.foundation.text2.input.internal.coerceIn
 import androidx.compose.foundation.text2.input.internal.fromInnerToDecoration
 import androidx.compose.ui.geometry.Offset
@@ -35,14 +35,13 @@
 import androidx.compose.ui.unit.IntSize
 import kotlin.math.absoluteValue
 
-@OptIn(ExperimentalFoundationApi::class)
 internal abstract class TextFieldMagnifierNode : DelegatingNode(),
     OnGloballyPositionedModifier,
     DrawModifierNode,
     SemanticsModifierNode {
 
     abstract fun update(
-        textFieldState: TextFieldState,
+        textFieldState: TransformedTextFieldState,
         textFieldSelectionState: TextFieldSelectionState,
         textLayoutState: TextLayoutState,
         isFocused: Boolean
@@ -55,10 +54,9 @@
     override fun SemanticsPropertyReceiver.applySemantics() {}
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
 internal expect fun textFieldMagnifierNode(
-    textFieldState: TextFieldState,
+    textFieldState: TransformedTextFieldState,
     textFieldSelectionState: TextFieldSelectionState,
     textLayoutState: TextLayoutState,
     isFocused: Boolean
@@ -66,7 +64,7 @@
 
 @OptIn(ExperimentalFoundationApi::class)
 internal fun calculateSelectionMagnifierCenterAndroid(
-    textFieldState: TextFieldState,
+    textFieldState: TransformedTextFieldState,
     selectionState: TextFieldSelectionState,
     textLayoutState: TextLayoutState,
     magnifierSize: IntSize
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionState.kt
index 0db02b3..bf4ed35 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionState.kt
@@ -30,13 +30,12 @@
 import androidx.compose.foundation.text.selection.getTextFieldSelectionLayout
 import androidx.compose.foundation.text.selection.isPrecisePointer
 import androidx.compose.foundation.text.selection.visibleBounds
-import androidx.compose.foundation.text2.input.InputTransformation
 import androidx.compose.foundation.text2.input.TextFieldCharSequence
-import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.foundation.text2.input.getSelectedText
-import androidx.compose.foundation.text2.input.internal.EditingBuffer
 import androidx.compose.foundation.text2.input.internal.TextLayoutState
+import androidx.compose.foundation.text2.input.internal.TransformedTextFieldState
 import androidx.compose.foundation.text2.input.internal.coerceIn
+import androidx.compose.foundation.text2.input.internal.undo.TextFieldEditUndoBehavior
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -70,12 +69,11 @@
 
 @OptIn(ExperimentalFoundationApi::class)
 internal class TextFieldSelectionState(
-    private val textFieldState: TextFieldState,
+    private val textFieldState: TransformedTextFieldState,
     private val textLayoutState: TextLayoutState,
-    private var inputTransformation: InputTransformation?,
     private var density: Density,
     private var editable: Boolean,
-    var isFocused: Boolean
+    var isFocused: Boolean,
 ) {
     /**
      * [HapticFeedback] handle to perform haptic feedback.
@@ -271,14 +269,12 @@
         hapticFeedBack: HapticFeedback,
         clipboardManager: ClipboardManager,
         textToolbar: TextToolbar,
-        inputTransformation: InputTransformation?,
         density: Density,
         editable: Boolean,
     ) {
         this.hapticFeedBack = hapticFeedBack
         this.clipboardManager = clipboardManager
         this.textToolbar = textToolbar
-        this.inputTransformation = inputTransformation
         this.density = density
         this.editable = editable
     }
@@ -415,9 +411,7 @@
                     val cursorIndex = textLayoutState.getOffsetForPosition(offset)
                     // update the state
                     if (cursorIndex >= 0) {
-                        editAsUser {
-                            selectCharsIn(TextRange(cursorIndex))
-                        }
+                        textFieldState.placeCursorBeforeCharAt(cursorIndex)
                     }
                 }
             },
@@ -441,9 +435,7 @@
                     isStartHandle = false,
                     adjustment = SelectionAdjustment.Word,
                 )
-                editAsUser {
-                    selectCharsIn(newSelection)
-                }
+                textFieldState.selectCharsIn(newSelection)
             }
         )
     }
@@ -487,9 +479,7 @@
                     change.consume()
                     // TODO: only perform haptic feedback if filter does not override the change
                     hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
-                    editAsUser {
-                        selectCharsIn(newSelection)
-                    }
+                    textFieldState.selectCharsIn(newSelection)
                 }
             )
         } finally {
@@ -529,9 +519,7 @@
                     val offset = textLayoutState.getOffsetForPosition(dragStartOffset)
 
                     hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
-                    editAsUser {
-                        selectCharsIn(TextRange(offset))
-                    }
+                    textFieldState.placeCursorBeforeCharAt(offset)
                     showCursorHandle = true
                     showCursorHandleToolbar = true
                 } else {
@@ -549,9 +537,7 @@
                         isStartHandle = false,
                         adjustment = SelectionAdjustment.CharacterWithWordAccelerate,
                     )
-                    editAsUser {
-                        selectCharsIn(newSelection)
-                    }
+                    textFieldState.selectCharsIn(newSelection)
                     showCursorHandle = false
                     // For touch, set the begin offset to the adjusted selection.
                     // When char based selection is used, we want to ensure we snap the
@@ -645,9 +631,7 @@
                 // Do not allow selection to collapse on itself while dragging. Selection can
                 // reverse but does not collapse.
                 if (prevSelection.collapsed || !newSelection.collapsed) {
-                    editAsUser {
-                        selectCharsIn(newSelection)
-                    }
+                    textFieldState.selectCharsIn(newSelection)
                 }
                 updateHandleDragging(
                     handle = actingHandle,
@@ -719,9 +703,7 @@
                     // Do not allow selection to collapse on itself while dragging selection
                     // handles. Selection can reverse but does not collapse.
                     if (prevSelection.collapsed || !newSelection.collapsed) {
-                        editAsUser {
-                            selectCharsIn(newSelection)
-                        }
+                        textFieldState.selectCharsIn(newSelection)
                     }
                 }
             )
@@ -951,11 +933,7 @@
 
         clipboardManager?.setText(AnnotatedString(text.getSelectedText().toString()))
 
-        editAsUser {
-            replace(selection.min, selection.max, "")
-            selectCharsIn(TextRange(selection.min))
-        }
-        // TODO(halilibo): undoManager force snapshot
+        textFieldState.deleteSelectedText()
     }
 
     /**
@@ -976,9 +954,7 @@
 
         if (!cancelSelection) return
 
-        editAsUser {
-            selectCharsIn(TextRange(selection.max))
-        }
+        textFieldState.collapseSelectionToMax()
     }
 
     /**
@@ -993,16 +969,10 @@
     fun paste() {
         val clipboardText = clipboardManager?.getText()?.text ?: return
 
-        editAsUser {
-            val selection = textFieldState.text.selectionInChars
-            replace(
-                selection.min,
-                selection.max,
-                clipboardText
-            )
-            selectCharsIn(TextRange(selection.min + clipboardText.length))
-        }
-        // TODO(halilibo): undoManager force snapshot
+        textFieldState.replaceSelectedText(
+            clipboardText,
+            undoBehavior = TextFieldEditUndoBehavior.NeverMerge
+        )
     }
 
     /**
@@ -1038,7 +1008,7 @@
 
         val selectAll: (() -> Unit)? = if (selection.length != textFieldState.text.length) {
             {
-                editAsUser { selectCharsIn(TextRange(0, length)) }
+                textFieldState.selectAll()
                 showCursorHandleToolbar = false
             }
         } else null
@@ -1053,24 +1023,14 @@
     }
 
     fun deselect() {
-        val selection = textFieldState.text.selectionInChars
-        if (!selection.collapsed) {
-            editAsUser {
-                selectCharsIn(TextRange(selection.end))
-            }
+        if (!textFieldState.text.selectionInChars.collapsed) {
+            textFieldState.collapseSelectionToEnd()
         }
 
         showCursorHandle = false
         showCursorHandleToolbar = false
     }
 
-    /**
-     * Edits the TextFieldState content with a filter applied if available.
-     */
-    private fun editAsUser(block: EditingBuffer.() -> Unit) {
-        textFieldState.editAsUser(inputTransformation, block = block)
-    }
-
     private fun hideTextToolbar() {
         if (textToolbar?.status == TextToolbarStatus.Shown) {
             textToolbar?.hide()
@@ -1162,10 +1122,6 @@
 
 private fun TextRange.reverse() = TextRange(end, start)
 
-private fun EditingBuffer.selectCharsIn(range: TextRange) {
-    setSelection(range.start, range.end)
-}
-
 private const val DEBUG = false
 private const val DEBUG_TAG = "TextFieldSelectionState"
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/undo/TextUndoOperation.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/undo/TextUndoOperation.kt
new file mode 100644
index 0000000..ce572cb4
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/undo/TextUndoOperation.kt
@@ -0,0 +1,178 @@
+/*
+ * 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.foundation.text2.input.internal.undo
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text.timeNowMillis
+import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.SaverScope
+import androidx.compose.ui.text.TextRange
+
+/**
+ * An undo identifier designed for text editors. Defines a single atomic change that can be applied
+ * directly or in reverse to modify the contents of a text editor.
+ *
+ * @param index Start point of [preText] and [postText].
+ * @param preText Previously written text that's deleted starting from [index].
+ * @param postText New text that's inserted at [index]
+ * @param preSelection Previous selection before changes are applied
+ * @param postSelection New selection after changes are applied
+ * @param timeInMillis When did this change was first committed
+ * @param canMerge Whether this change can be merged with the next or previous change in an undo
+ * stack. There are many other rules that affect the merging strategy between two
+ * [TextUndoOperation]s but this flag is a sure way to force a non-mergeable property.
+ */
+internal class TextUndoOperation(
+    val index: Int,
+    val preText: String,
+    val postText: String,
+    val preSelection: TextRange,
+    val postSelection: TextRange,
+    val timeInMillis: Long = timeNowMillis(),
+    val canMerge: Boolean = true
+) {
+
+    /**
+     * What kind of edit operation is defined by this change. Edit type is decided by forward the
+     * behavior of this change in forward direction (pre -> post).
+     */
+    val textEditType: TextEditType = when {
+        preText.isEmpty() && postText.isEmpty() ->
+            throw IllegalArgumentException("Either pre or post text must not be empty")
+
+        preText.isEmpty() && postText.isNotEmpty() -> TextEditType.Insert
+        preText.isNotEmpty() && postText.isEmpty() -> TextEditType.Delete
+        else -> TextEditType.Replace
+    }
+
+    /**
+     * Only required while deciding whether to merge two deletion type undo operations.
+     */
+    val deletionType: TextDeleteType
+        get() {
+            if (textEditType != TextEditType.Delete) return TextDeleteType.NotByUser
+            if (!postSelection.collapsed) return TextDeleteType.NotByUser
+            if (preSelection.collapsed) {
+                return if (preSelection.start > postSelection.start) {
+                    TextDeleteType.Start
+                } else {
+                    TextDeleteType.End
+                }
+            } else if (preSelection.start == postSelection.start && preSelection.start == index) {
+                return TextDeleteType.Inner
+            }
+            return TextDeleteType.NotByUser
+        }
+
+    companion object {
+
+        val Saver = object : Saver<TextUndoOperation, Any> {
+            override fun SaverScope.save(value: TextUndoOperation): Any = listOf(
+                value.index,
+                value.preText,
+                value.postText,
+                value.preSelection.start,
+                value.preSelection.end,
+                value.postSelection.start,
+                value.postSelection.end,
+                value.timeInMillis
+            )
+
+            override fun restore(value: Any): TextUndoOperation {
+                return with((value as List<*>)) {
+                    TextUndoOperation(
+                        index = get(0) as Int,
+                        preText = get(1) as String,
+                        postText = get(2) as String,
+                        preSelection = TextRange(get(3) as Int, get(4) as Int),
+                        postSelection = TextRange(get(5) as Int, get(6) as Int),
+                        timeInMillis = get(7) as Long,
+                    )
+                }
+            }
+        }
+    }
+}
+
+/**
+ * Apply a given [TextUndoOperation] in reverse to undo this [TextFieldState].
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal fun TextFieldState.undo(op: TextUndoOperation) {
+    editWithNoSideEffects {
+        replace(op.index, op.index + op.postText.length, op.preText)
+        setSelection(op.preSelection.start, op.preSelection.end)
+    }
+}
+
+/**
+ * Apply a given [TextUndoOperation] in forward direction to redo this [TextFieldState].
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal fun TextFieldState.redo(op: TextUndoOperation) {
+    editWithNoSideEffects {
+        replace(op.index, op.index + op.preText.length, op.postText)
+        setSelection(op.postSelection.start, op.postSelection.end)
+    }
+}
+
+/**
+ * Possible types of a text operation.
+ *
+ * 1. Insert; if the edited range has 0 length, and the new text is longer than 0 length
+ * 2. Delete: if the edited range is longer than 0, and the new text has 0 length
+ * 3. Replace: All other changes.
+ */
+internal enum class TextEditType {
+    Insert, Delete, Replace
+}
+
+/**
+ * When a delete occurs during text editing, it can happen in various shapes.
+ *
+ * 1. Start; When a single character is removed to the start (towards 0) of the cursor, backspace
+ * key behavior.
+ *   "abcd|efg" -> "abc|efg"
+ * 2. End; When a single character is removed to the end (towards length) of the cursor, delete
+ * key behavior.
+ *   "abcd|efg" -> "abcd|fg"
+ * 3. Inner; When a selection of characters are removed, directionless. Both backspace and delete
+ * express the same behavior in this case.
+ *   "ab|cde|fg" -> "ab|fg"
+ * 4. NotByUser; A text editing operation that cannot be executed via a hardware or software
+ * keyboard. For example when a portion of text is removed but it's not next to a cursor or
+ * selection, or selection remains after removal.
+ *   "abcd|efg"  -> "bcd|efg"
+ *   "abc|def|g" -> "a|bc|g"
+ */
+internal enum class TextDeleteType {
+    Start, End, Inner, NotByUser
+}
+
+/**
+ * There are multiple strategies while deciding how to add certain edit operations to undo stack.
+ *   - Normally, merge is decided by UndoOperation's own merge logic, comparing itself to the
+ *   latest operation in the Undo stack.
+ *   - Programmatic updates should clear the history completely.
+ *   - Some atomic actions like cut, and paste shouldn't merge to previous or next actions.
+ */
+internal enum class TextFieldEditUndoBehavior {
+    MergeIfPossible,
+    ClearHistory,
+    NeverMerge
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/undo/UndoManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/undo/UndoManager.kt
new file mode 100644
index 0000000..f127be51
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/undo/UndoManager.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.foundation.text2.input.internal.undo
+
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.SaverScope
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.ui.util.fastForEach
+
+/**
+ * A generic purpose undo/redo stack manager.
+ *
+ * @param initialUndoStack Previous undo stack if this manager is being restored from a saved state.
+ * @param initialRedoStack Previous redo stack if this manager is being restored from a saved state.
+ * @param capacity Maximum number of elements that can be hosted by this UndoManager. Total element
+ * count is the sum of undo and redo stack sizes.
+ */
+internal class UndoManager<T>(
+    initialUndoStack: List<T> = emptyList(),
+    initialRedoStack: List<T> = emptyList(),
+    private val capacity: Int = 100
+) {
+
+    private var undoStack = SnapshotStateList<T>().apply {
+        addAll(initialUndoStack)
+    }
+    private var redoStack = SnapshotStateList<T>().apply {
+        addAll(initialRedoStack)
+    }
+
+    internal val canUndo: Boolean
+        get() = undoStack.isNotEmpty()
+
+    internal val canRedo: Boolean
+        get() = redoStack.isNotEmpty()
+
+    val size: Int
+        get() = undoStack.size + redoStack.size
+
+    init {
+        require(capacity >= 0) {
+            "Capacity must be a positive integer"
+        }
+        require(size <= capacity) {
+            "Initial list of undo and redo operations have a size=($size) greater " +
+                "than the given capacity=($capacity)."
+        }
+    }
+
+    fun record(undoableAction: T) {
+        // First clear the redo stack.
+        redoStack.clear()
+
+        while (size > capacity - 1) { // leave room for the immediate `add`
+            undoStack.removeFirst()
+        }
+        undoStack.add(undoableAction)
+    }
+
+    /**
+     * Request undo.
+     *
+     * This method returns the item that was on top of the undo stack. By the time this function
+     * returns, the given item has already been carried to the redo stack.
+     */
+    fun undo(): T {
+        check(canUndo) {
+            "It's an error to call undo while there is nothing to undo. " +
+                "Please first check `canUndo` value before calling the `undo` function."
+        }
+
+        val topOperation = undoStack.removeLast()
+
+        redoStack.add(topOperation)
+        return topOperation
+    }
+
+    /**
+     * Request redo.
+     *
+     * This method returns the item that was on top of the redo stack. By the time this function
+     * returns, the given item has already been carried back to the undo stack.
+     */
+    fun redo(): T {
+        check(canRedo) {
+            "It's an error to call redo while there is nothing to redo. " +
+                "Please first check `canRedo` value before calling the `redo` function."
+        }
+
+        val topOperation = redoStack.removeLast()
+
+        undoStack.add(topOperation)
+        return topOperation
+    }
+
+    fun clearHistory() {
+        undoStack.clear()
+        redoStack.clear()
+    }
+
+    companion object {
+
+        /**
+         * Saver factory for a generic [UndoManager].
+         *
+         * @param itemSaver Since [UndoManager] is defined as a generic class, a specific item saver
+         * is required to _serialize_ each individual item in undo and redo stacks.
+         */
+        inline fun <reified T> createSaver(
+            itemSaver: Saver<T, Any>
+        ) = object : Saver<UndoManager<T>, Any> {
+            /**
+             * Saves the contents of given [value] to a list.
+             *
+             * List's structure is
+             *   - Capacity
+             *   - n; Number of items in undo stack
+             *   - m; Number of items in redo stack
+             *   - n items in order from undo stack
+             *   - m items in order from redo stack
+             */
+            override fun SaverScope.save(value: UndoManager<T>): Any = buildList {
+                add(value.capacity)
+                add(value.undoStack.size)
+                add(value.redoStack.size)
+                value.undoStack.fastForEach {
+                    with(itemSaver) {
+                        add(save(it))
+                    }
+                }
+                value.redoStack.fastForEach {
+                    with(itemSaver) {
+                        add(save(it))
+                    }
+                }
+            }
+
+            @Suppress("UNCHECKED_CAST")
+            override fun restore(value: Any): UndoManager<T> {
+                val list = value as List<Any>
+                val (capacity, undoSize, redoSize) = (list as List<Int>)
+                var i = 3
+                val undoStackItems = buildList {
+                    while (i < undoSize + 3) {
+                        add(itemSaver.restore(list[i])!!)
+                        i++
+                    }
+                }
+                val redoStackItems = buildList {
+                    while (i < undoSize + redoSize + 3) {
+                        add(itemSaver.restore(list[i])!!)
+                        i++
+                    }
+                }
+                return UndoManager(undoStackItems, redoStackItems, capacity)
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text2/input/internal/DesktopTextInputSession.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text2/input/internal/DesktopTextInputSession.desktop.kt
index 9b902c0..e71d352 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text2/input/internal/DesktopTextInputSession.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text2/input/internal/DesktopTextInputSession.desktop.kt
@@ -19,8 +19,6 @@
 package androidx.compose.foundation.text2.input.internal
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text2.input.InputTransformation
-import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.ui.platform.PlatformTextInputSession
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.ImeOptions
@@ -30,9 +28,8 @@
  * Runs desktop-specific text input session logic.
  */
 internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSession(
-    state: TextFieldState,
+    state: TransformedTextFieldState,
     imeOptions: ImeOptions,
-    filter: InputTransformation?,
     onImeAction: ((ImeAction) -> Unit)?
 ): Nothing {
     // TODO(b/267235947) Wire up desktop.
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/DesktopTextFieldMagnifier.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/DesktopTextFieldMagnifier.kt
index 52b60da..948806a 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/DesktopTextFieldMagnifier.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/DesktopTextFieldMagnifier.kt
@@ -16,22 +16,20 @@
 
 package androidx.compose.foundation.text2.input.internal.selection
 
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.foundation.text2.input.internal.TextLayoutState
+import androidx.compose.foundation.text2.input.internal.TransformedTextFieldState
 
 /**
  * There is no magnifier on Desktop. Return a noop [TextFieldMagnifierNode] implementation.
  */
-@OptIn(ExperimentalFoundationApi::class)
 internal actual fun textFieldMagnifierNode(
-    textFieldState: TextFieldState,
+    textFieldState: TransformedTextFieldState,
     textFieldSelectionState: TextFieldSelectionState,
     textLayoutState: TextLayoutState,
     isFocused: Boolean
 ) = object : TextFieldMagnifierNode() {
     override fun update(
-        textFieldState: TextFieldState,
+        textFieldState: TransformedTextFieldState,
         textFieldSelectionState: TextFieldSelectionState,
         textLayoutState: TextLayoutState,
         isFocused: Boolean
diff --git a/compose/foundation/foundation/src/jvmMain/kotlin/androidx/compose/foundation/text2/input/internal/CodepointHelpers.jvm.kt b/compose/foundation/foundation/src/jvmMain/kotlin/androidx/compose/foundation/text2/input/internal/CodepointHelpers.jvm.kt
new file mode 100644
index 0000000..95a0458
--- /dev/null
+++ b/compose/foundation/foundation/src/jvmMain/kotlin/androidx/compose/foundation/text2/input/internal/CodepointHelpers.jvm.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.foundation.text2.input.internal
+
+internal actual fun CharSequence.codePointAt(index: Int): Int =
+    java.lang.Character.codePointAt(this, index)
+
+internal actual fun CharSequence.codePointCount(): Int =
+    java.lang.Character.codePointCount(this, 0, length)
+
+internal actual fun charCount(codePoint: Int): Int =
+    java.lang.Character.charCount(codePoint)
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt
deleted file mode 100644
index 37377dc..0000000
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt
+++ /dev/null
@@ -1,347 +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.compose.foundation.text.selection
-
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.MultiParagraph
-import androidx.compose.ui.text.TextLayoutInput
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.style.ResolvedTextDirection
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.packInts
-import org.mockito.kotlin.any
-import org.mockito.kotlin.mock
-
-internal fun getSingleSelectionLayoutFake(
-    text: String = "hello",
-    rawStartHandleOffset: Int = 0,
-    rawEndHandleOffset: Int = 5,
-    rawPreviousHandleOffset: Int = -1,
-    rtlRanges: List<IntRange> = emptyList(),
-    wordBoundaries: List<TextRange> = listOf(),
-    lineBreaks: List<Int> = emptyList(),
-    crossStatus: CrossStatus = when {
-        rawStartHandleOffset < rawEndHandleOffset -> CrossStatus.NOT_CROSSED
-        rawStartHandleOffset > rawEndHandleOffset -> CrossStatus.CROSSED
-        else -> CrossStatus.COLLAPSED
-    },
-    isStartHandle: Boolean = false,
-    previousSelection: Selection? = null,
-    shouldRecomputeSelection: Boolean = true,
-    subSelections: Map<Long, Selection> = emptyMap(),
-): SelectionLayout {
-    return getSelectionLayoutFake(
-        infos = listOf(
-            getSelectableInfoFake(
-                text = text,
-                selectableId = 1,
-                slot = 1,
-                rawStartHandleOffset = rawStartHandleOffset,
-                rawEndHandleOffset = rawEndHandleOffset,
-                rawPreviousHandleOffset = rawPreviousHandleOffset,
-                rtlRanges = rtlRanges,
-                wordBoundaries = wordBoundaries,
-                lineBreaks = lineBreaks,
-            )
-        ),
-        currentInfoIndex = 0,
-        startSlot = 1,
-        endSlot = 1,
-        crossStatus = crossStatus,
-        isStartHandle = isStartHandle,
-        previousSelection = previousSelection,
-        shouldRecomputeSelection = shouldRecomputeSelection,
-        subSelections = subSelections,
-    )
-}
-
-internal fun getTextLayoutResultMock(
-    text: String = "hello",
-    rtlCharRanges: List<IntRange> = emptyList(),
-    rtlLines: Set<Int> = emptySet(),
-    wordBoundaries: List<TextRange> = listOf(),
-    lineBreaks: List<Int> = emptyList(),
-): TextLayoutResult {
-    val annotatedString = AnnotatedString(text)
-
-    val textLayoutInput = TextLayoutInput(
-        text = annotatedString,
-        style = TextStyle.Default,
-        placeholders = emptyList(),
-        maxLines = Int.MAX_VALUE,
-        softWrap = false,
-        overflow = TextOverflow.Visible,
-        density = Density(1f),
-        layoutDirection = LayoutDirection.Ltr,
-        fontFamilyResolver = mock(),
-        constraints = Constraints(0L)
-    )
-
-    fun lineForOffset(offset: Int): Int {
-        var line = 0
-        lineBreaks.fastForEach {
-            if (it > offset) {
-                return line
-            }
-            line++
-        }
-        return line
-    }
-
-    val multiParagraph = mock<MultiParagraph> {
-        on { getBidiRunDirection(any()) }.thenAnswer { invocation ->
-            val offset = invocation.arguments[0] as Int
-            if (rtlCharRanges.any { offset in it })
-                ResolvedTextDirection.Rtl else ResolvedTextDirection.Ltr
-        }
-
-        on { getParagraphDirection(any()) }.thenAnswer { invocation ->
-            val offset = invocation.arguments[0] as Int
-            val line = lineForOffset(offset)
-            if (line in rtlLines) ResolvedTextDirection.Rtl else ResolvedTextDirection.Ltr
-        }
-
-        on { getWordBoundary(any()) }.thenAnswer { invocation ->
-            val offset = invocation.arguments[0] as Int
-            val wordBoundary = wordBoundaries.find { offset in it.start..it.end }
-            // Workaround: Mockito doesn't work with inline class now. The packed Long is
-            // equal to TextRange(start, end).
-            packInts(wordBoundary!!.start, wordBoundary.end)
-        }
-
-        on { getLineForOffset(any()) }.thenAnswer { invocation ->
-            val offset = invocation.arguments[0] as Int
-            lineForOffset(offset)
-        }
-
-        on { getLineStart(any()) }.thenAnswer { invocation ->
-            val lineIndex = invocation.arguments[0] as Int
-            if (lineIndex == 0) 0 else lineBreaks[lineIndex - 1]
-        }
-
-        on { getLineEnd(any(), any()) }.thenAnswer { invocation ->
-            val lineIndex = invocation.arguments[0] as Int
-            if (lineIndex == lineBreaks.size) text.length else lineBreaks[lineIndex] - 1
-        }
-    }
-
-    return TextLayoutResult(textLayoutInput, multiParagraph, IntSize.Zero)
-}
-
-internal fun getSelectableInfoFake(
-    text: String = "hello",
-    selectableId: Long = 1L,
-    slot: Int = 1,
-    rawStartHandleOffset: Int = 0,
-    rawEndHandleOffset: Int = text.length,
-    rawPreviousHandleOffset: Int = -1,
-    rtlRanges: List<IntRange> = emptyList(),
-    wordBoundaries: List<TextRange> = listOf(),
-    lineBreaks: List<Int> = emptyList(),
-): SelectableInfo = SelectableInfo(
-    selectableId = selectableId,
-    slot = slot,
-    rawStartHandleOffset = rawStartHandleOffset,
-    rawEndHandleOffset = rawEndHandleOffset,
-    rawPreviousHandleOffset = rawPreviousHandleOffset,
-    textLayoutResult = getTextLayoutResultMock(
-        text = text,
-        rtlCharRanges = rtlRanges,
-        wordBoundaries = wordBoundaries,
-        lineBreaks = lineBreaks,
-    ),
-)
-
-internal fun getSelectionLayoutFake(
-    infos: List<SelectableInfo>,
-    startSlot: Int,
-    endSlot: Int,
-    currentInfoIndex: Int = 0,
-    crossStatus: CrossStatus = when {
-        startSlot < endSlot -> CrossStatus.NOT_CROSSED
-        startSlot > endSlot -> CrossStatus.CROSSED
-        else -> infos.single().rawCrossStatus
-    },
-    startInfo: SelectableInfo =
-        with(infos) { if (crossStatus == CrossStatus.CROSSED) last() else first() },
-    endInfo: SelectableInfo =
-        with(infos) { if (crossStatus == CrossStatus.CROSSED) first() else last() },
-    firstInfo: SelectableInfo = if (crossStatus == CrossStatus.CROSSED) endInfo else startInfo,
-    lastInfo: SelectableInfo = if (crossStatus == CrossStatus.CROSSED) startInfo else endInfo,
-    middleInfos: List<SelectableInfo> =
-        if (infos.size < 2) emptyList() else infos.subList(1, infos.size - 1),
-    isStartHandle: Boolean = false,
-    previousSelection: Selection? = null,
-    shouldRecomputeSelection: Boolean = true,
-    subSelections: Map<Long, Selection> = emptyMap(),
-): SelectionLayout = FakeSelectionLayout(
-    size = infos.size,
-    crossStatus = crossStatus,
-    startSlot = startSlot,
-    endSlot = endSlot,
-    startInfo = startInfo,
-    endInfo = endInfo,
-    currentInfo = infos[currentInfoIndex],
-    firstInfo = firstInfo,
-    lastInfo = lastInfo,
-    middleInfos = middleInfos,
-    isStartHandle = isStartHandle,
-    previousSelection = previousSelection,
-    shouldRecomputeSelection = shouldRecomputeSelection,
-    subSelections = subSelections,
-)
-
-internal class FakeSelectionLayout(
-    override val size: Int,
-    override val crossStatus: CrossStatus,
-    override val startSlot: Int,
-    override val endSlot: Int,
-    override val startInfo: SelectableInfo,
-    override val endInfo: SelectableInfo,
-    override val currentInfo: SelectableInfo,
-    override val firstInfo: SelectableInfo,
-    override val lastInfo: SelectableInfo,
-    override val isStartHandle: Boolean,
-    override val previousSelection: Selection?,
-    private val middleInfos: List<SelectableInfo>,
-    private val shouldRecomputeSelection: Boolean,
-    private val subSelections: Map<Long, Selection>,
-) : SelectionLayout {
-    override fun createSubSelections(selection: Selection): Map<Long, Selection> = subSelections
-    override fun forEachMiddleInfo(block: (SelectableInfo) -> Unit) {
-        middleInfos.forEach(block)
-    }
-
-    override fun shouldRecomputeSelection(other: SelectionLayout?): Boolean =
-        shouldRecomputeSelection
-}
-
-internal fun getSelection(
-    startOffset: Int = 0,
-    endOffset: Int = 5,
-    startSelectableId: Long = 1L,
-    endSelectableId: Long = 1L,
-    handlesCrossed: Boolean = startSelectableId == endSelectableId && startOffset > endOffset,
-    startLayoutDirection: ResolvedTextDirection = ResolvedTextDirection.Ltr,
-    endLayoutDirection: ResolvedTextDirection = ResolvedTextDirection.Ltr,
-): Selection = Selection(
-    start = Selection.AnchorInfo(
-        direction = startLayoutDirection,
-        offset = startOffset,
-        selectableId = startSelectableId,
-    ),
-    end = Selection.AnchorInfo(
-        direction = endLayoutDirection,
-        offset = endOffset,
-        selectableId = endSelectableId,
-    ),
-    handlesCrossed = handlesCrossed,
-)
-
-internal class FakeSelectable : Selectable {
-    override var selectableId = 0L
-    var getTextCalledTimes = 0
-    var textToReturn: AnnotatedString? = null
-
-    var rawStartHandleOffset = 0
-    var startHandleDirection = Direction.ON
-    var rawEndHandleOffset = 0
-    var endHandleDirection = Direction.ON
-    var rawPreviousHandleOffset = -1 // -1 = no previous offset
-
-    private val selectableKey = 1L
-    private val fakeSelectAllSelection: Selection = Selection(
-        start = Selection.AnchorInfo(
-            direction = ResolvedTextDirection.Ltr,
-            offset = 0,
-            selectableId = selectableKey
-        ),
-        end = Selection.AnchorInfo(
-            direction = ResolvedTextDirection.Ltr,
-            offset = 10,
-            selectableId = selectableKey
-        )
-    )
-
-    override fun appendSelectableInfoToBuilder(builder: SelectionLayoutBuilder) {
-        builder.appendInfo(
-            selectableKey,
-            rawStartHandleOffset,
-            startHandleDirection,
-            rawEndHandleOffset,
-            endHandleDirection,
-            rawPreviousHandleOffset,
-            getTextLayoutResultMock(),
-        )
-    }
-
-    override fun getSelectAllSelection(): Selection {
-        return fakeSelectAllSelection
-    }
-
-    override fun getText(): AnnotatedString {
-        getTextCalledTimes++
-        return textToReturn!!
-    }
-
-    override fun getLayoutCoordinates(): LayoutCoordinates? {
-        return null
-    }
-
-    override fun getHandlePosition(selection: Selection, isStartHandle: Boolean): Offset {
-        return Offset.Zero
-    }
-
-    override fun getBoundingBox(offset: Int): Rect {
-        return Rect.Zero
-    }
-
-    override fun getLineLeft(offset: Int): Float {
-        return 0f
-    }
-
-    override fun getLineRight(offset: Int): Float {
-        return 0f
-    }
-
-    override fun getCenterYForOffset(offset: Int): Float {
-        return 0f
-    }
-
-    override fun getRangeOfLineContaining(offset: Int): TextRange {
-        return TextRange.Zero
-    }
-
-    override fun getLastVisibleOffset(): Int {
-        return 0
-    }
-
-    fun clear() {
-        getTextCalledTimes = 0
-        textToReturn = null
-    }
-}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionLayoutTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionLayoutTest.kt
deleted file mode 100644
index 16c6157..0000000
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionLayoutTest.kt
+++ /dev/null
@@ -1,1601 +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.compose.foundation.text.selection
-
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.text.TextRange
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import kotlin.contracts.ExperimentalContracts
-import kotlin.contracts.InvocationKind
-import kotlin.contracts.contract
-import kotlin.test.assertFailsWith
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@SmallTest
-@RunWith(JUnit4::class)
-class SelectionLayoutTest {
-    @Test
-    fun layoutBuilderSizeZero_throws() {
-        assertFailsWith(IllegalStateException::class) {
-            buildSelectionLayoutForTest { }
-        }
-    }
-
-    @Test
-    fun singleLayout_verifySimpleParameters() {
-        val selection = getSelection()
-        val layout = getSingleSelectionLayoutForTest(
-            isStartHandle = true,
-            previousSelection = selection,
-        )
-        assertThat(layout.isStartHandle).isTrue()
-        assertThat(layout.previousSelection).isEqualTo(selection)
-    }
-
-    @Test
-    fun layoutBuilder_verifySimpleParameters() {
-        val selection = getSelection()
-        val layout = buildSelectionLayoutForTest(
-            isStartHandle = true,
-            previousSelection = selection,
-        ) {
-            appendInfoForTest()
-        }
-        assertThat(layout.isStartHandle).isTrue()
-        assertThat(layout.previousSelection).isEqualTo(selection)
-    }
-
-    @Test
-    fun singleLayout_sameInfoForAllSelectableInfoFunctions() {
-        val layout = getSingleSelectionLayoutForTest()
-        // since there is only one info, each info function should return the same
-        val info = layout.currentInfo
-        assertThat(layout.startInfo).isSameInstanceAs(info)
-        assertThat(layout.endInfo).isSameInstanceAs(info)
-        assertThat(layout.firstInfo).isSameInstanceAs(info)
-        assertThat(layout.lastInfo).isSameInstanceAs(info)
-    }
-
-    @Test
-    fun size_singleLayout_returnsOne() {
-        val selection = getSingleSelectionLayoutForTest()
-        assertThat(selection.size).isEqualTo(1)
-    }
-
-    @Test
-    fun size_layoutBuilderSizeOne_returnsOne() {
-        val selection = buildSelectionLayoutForTest {
-            appendInfoForTest()
-        }
-        assertThat(selection.size).isEqualTo(1)
-    }
-
-    @Test
-    fun size_layoutBuilderSizeMoreThanOne_returnsSize() {
-        val selection = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 3L,
-                startHandleDirection = Direction.BEFORE,
-            )
-        }
-        assertThat(selection.size).isEqualTo(3)
-    }
-
-    @Test
-    fun startSlot_singleLayout_equalsOnlyInfo() {
-        val layout = getSingleSelectionLayoutForTest()
-        // when there is only one info, slot doesn't matter
-        // so, ensure that the slot is equal to the only info's slot
-        assertThat(layout.startSlot).isEqualTo(layout.currentInfo.slot)
-    }
-
-    @Test
-    fun startSlot_layoutBuilder_onBefore_equalsZero() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.startSlot).isEqualTo(0)
-    }
-
-    @Test
-    fun startSlot_layoutBuilder_onFirst_equalsOne() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.startSlot).isEqualTo(1)
-    }
-
-    @Test
-    fun startSlot_layoutBuilder_onMiddle_equalsTwo() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.startSlot).isEqualTo(2)
-    }
-
-    @Test
-    fun startSlot_layoutBuilder_onLast_equalsThree() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.startSlot).isEqualTo(3)
-    }
-
-    @Test
-    fun startSlot_layoutBuilder_onAfter_equalsFour() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.startSlot).isEqualTo(4)
-    }
-
-    @Test
-    fun endSlot_singleLayout_equalsOnlyInfo() {
-        val layout = getSingleSelectionLayoutForTest()
-        // when there is only one info, slot doesn't matter
-        // so, ensure that the slot is equal to the only info's slot
-        assertThat(layout.endSlot).isEqualTo(layout.currentInfo.slot)
-    }
-
-    @Test
-    fun endSlot_layoutBuilder_onBefore_equalsZero() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.BEFORE,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.BEFORE,
-            )
-        }
-        assertThat(layout.endSlot).isEqualTo(0)
-    }
-
-    @Test
-    fun endSlot_layoutBuilder_onFirst_equalsOne() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.ON,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.BEFORE,
-            )
-        }
-        assertThat(layout.endSlot).isEqualTo(1)
-    }
-
-    @Test
-    fun endSlot_layoutBuilder_onMiddle_equalsTwo() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.BEFORE,
-            )
-        }
-        assertThat(layout.endSlot).isEqualTo(2)
-    }
-
-    @Test
-    fun endSlot_layoutBuilder_onLast_equalsThree() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.endSlot).isEqualTo(3)
-    }
-
-    @Test
-    fun endSlot_layoutBuilder_onAfter_equalsFour() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-        }
-        assertThat(layout.endSlot).isEqualTo(4)
-    }
-
-    @Test
-    fun crossStatus_singleLayout_collapsed() {
-        val layout = getSingleSelectionLayoutForTest(
-            rawStartHandleOffset = 0,
-            rawEndHandleOffset = 0,
-        )
-        assertThat(layout.crossStatus).isEqualTo(CrossStatus.COLLAPSED)
-    }
-
-    @Test
-    fun crossStatus_singleLayout_crossed() {
-        val layout = getSingleSelectionLayoutForTest(
-            rawStartHandleOffset = 1,
-            rawEndHandleOffset = 0,
-        )
-        assertThat(layout.crossStatus).isEqualTo(CrossStatus.CROSSED)
-    }
-
-    @Test
-    fun crossStatus_singleLayout_notCrossed() {
-        val layout = getSingleSelectionLayoutForTest(
-            rawStartHandleOffset = 0,
-            rawEndHandleOffset = 1,
-        )
-        assertThat(layout.crossStatus).isEqualTo(CrossStatus.NOT_CROSSED)
-    }
-
-    @Test
-    fun crossStatus_layoutBuilder_collapsed() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.ON,
-                rawStartHandleOffset = 0,
-                rawEndHandleOffset = 0,
-            )
-        }
-        assertThat(layout.crossStatus).isEqualTo(CrossStatus.COLLAPSED)
-    }
-
-    @Test
-    fun crossStatus_layoutBuilder_crossed() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.ON,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.BEFORE,
-            )
-        }
-        assertThat(layout.crossStatus).isEqualTo(CrossStatus.CROSSED)
-    }
-
-    @Test
-    fun crossStatus_layoutBuilder_notCrossed() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.crossStatus).isEqualTo(CrossStatus.NOT_CROSSED)
-    }
-
-    // No startInfo test for singleLayout because it is covered in
-    // singleLayout_sameInfoForAllSelectableInfoFunctions
-
-    @Test
-    fun startInfo_layoutBuilder_onSlotZero_equalsFirstInfo() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.startInfo.selectableId).isEqualTo(1L)
-    }
-
-    @Test
-    fun startInfo_layoutBuilder_onSlotOne_equalsFirstInfo() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.startInfo.selectableId).isEqualTo(1L)
-    }
-
-    @Test
-    fun startInfo_layoutBuilder_onSlotTwo_equalsSecondInfo() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.startInfo.selectableId).isEqualTo(2L)
-    }
-
-    @Test
-    fun startInfo_layoutBuilder_onSlotThree_equalsSecondInfo() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.startInfo.selectableId).isEqualTo(2L)
-    }
-
-    @Test
-    fun startInfo_layoutBuilder_onSlotFour_equalsSecondInfo() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.startInfo.selectableId).isEqualTo(2L)
-    }
-
-    // No endInfo test for singleLayout because it is covered in
-    // singleLayout_sameInfoForAllSelectableInfoFunctions
-
-    @Test
-    fun endInfo_layoutBuilder_onSlotZero_equalsFirstInfo() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.BEFORE,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.BEFORE,
-            )
-        }
-        assertThat(layout.endInfo.selectableId).isEqualTo(1L)
-    }
-
-    @Test
-    fun endInfo_layoutBuilder_onSlotOne_equalsFirstInfo() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.ON,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.BEFORE,
-            )
-        }
-        assertThat(layout.endInfo.selectableId).isEqualTo(1L)
-    }
-
-    @Test
-    fun endInfo_layoutBuilder_onSlotTwo_equalsFirstInfo() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.BEFORE,
-            )
-        }
-        assertThat(layout.endInfo.selectableId).isEqualTo(1L)
-    }
-
-    @Test
-    fun endInfo_layoutBuilder_onSlotThree_equalsSecondInfo() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.endInfo.selectableId).isEqualTo(2L)
-    }
-
-    @Test
-    fun endInfo_layoutBuilder_onSlotFour_equalsSecondInfo() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-        }
-        assertThat(layout.endInfo.selectableId).isEqualTo(2L)
-    }
-
-    @Test
-    fun currentInfo_layoutBuilder_currentInfo_startHandle_equalsFirst() {
-        val layout = buildSelectionLayoutForTest(isStartHandle = true) {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.currentInfo.selectableId).isEqualTo(1L)
-    }
-
-    @Test
-    fun currentInfo_layoutBuilder_endHandle_equalsSecond() {
-        val layout = buildSelectionLayoutForTest(isStartHandle = false) {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.currentInfo.selectableId).isEqualTo(2L)
-    }
-
-    @Test
-    fun firstInfo_layoutBuilder_notCrossed_equalsFirst() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.firstInfo.selectableId).isEqualTo(1L)
-    }
-
-    @Test
-    fun firstInfo_layoutBuilder_crossed_equalsFirst() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.ON,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.BEFORE,
-            )
-        }
-        assertThat(layout.firstInfo.selectableId).isEqualTo(1L)
-    }
-
-    @Test
-    fun lastInfo_layoutBuilder_notCrossed_equalsSecond() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.lastInfo.selectableId).isEqualTo(2L)
-    }
-
-    @Test
-    fun lastInfo_layoutBuilder_crossed_equalsSecond() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.ON,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.BEFORE,
-            )
-        }
-        assertThat(layout.lastInfo.selectableId).isEqualTo(2L)
-    }
-
-    @Test
-    fun middleInfos_singleLayout_isEmpty() {
-        val layout = getSingleSelectionLayoutForTest()
-        val infoList = mutableListOf<SelectableInfo>()
-        layout.forEachMiddleInfo { infoList += it }
-        assertThat(infoList).isEmpty()
-    }
-
-    @Test
-    fun middleInfos_layoutBuilder_twoInfos_isEmpty() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-
-        val infoList = mutableListOf<SelectableInfo>()
-        layout.forEachMiddleInfo { infoList += it }
-        assertThat(infoList).isEmpty()
-    }
-
-    @Test
-    fun middleInfos_layoutBuilder_threeInfos_containsOneElement() {
-        val info: SelectableInfo
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            info = appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 3L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        val infoList = mutableListOf<SelectableInfo>()
-        layout.forEachMiddleInfo { infoList += it }
-        assertThat(infoList).containsExactly(info)
-    }
-
-    @Test
-    fun middleInfos_layoutBuilder_fourInfos_containsTwoElements() {
-        val infoOne: SelectableInfo
-        val infoTwo: SelectableInfo
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            infoOne = appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-            infoTwo = appendInfoForTest(
-                selectableId = 3L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 4L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        val infoList = mutableListOf<SelectableInfo>()
-        layout.forEachMiddleInfo { infoList += it }
-        assertThat(infoList).containsExactly(infoOne, infoTwo).inOrder()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_singleLayout_otherNull_returnsTrue() {
-        val layout = getSingleSelectionLayoutForTest(
-            rawPreviousHandleOffset = 5,
-            previousSelection = getSelection()
-        )
-        assertThat(layout.shouldRecomputeSelection(null)).isTrue()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_singleLayout_otherMulti_returnsTrue() {
-        val layout = getSingleSelectionLayoutForTest(
-            rawPreviousHandleOffset = 5,
-            previousSelection = getSelection()
-        )
-        val otherLayout = buildSelectionLayoutForTest {
-            appendInfoForTest()
-            appendInfoForTest()
-        }
-        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_singleLayout_differentHandle_returnsTrue() {
-        val layout = getSingleSelectionLayoutForTest(
-            isStartHandle = true,
-            rawPreviousHandleOffset = 5,
-            previousSelection = getSelection()
-        )
-        val otherLayout = getSingleSelectionLayoutForTest(
-            isStartHandle = false,
-            rawPreviousHandleOffset = 5,
-            previousSelection = getSelection()
-        )
-        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_singleLayout_differentInfo_returnsTrue() {
-        val layout = getSingleSelectionLayoutForTest(
-            rawStartHandleOffset = 0,
-            previousSelection = getSelection()
-        )
-        val otherLayout = getSingleSelectionLayoutForTest(
-            rawStartHandleOffset = 1,
-            previousSelection = getSelection()
-        )
-        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_singleLayout_noPreviousSelection_returnsTrue() {
-        val layout = getSingleSelectionLayoutForTest(
-            rawPreviousHandleOffset = 5,
-        )
-        val otherLayout = getSingleSelectionLayoutForTest(
-            rawPreviousHandleOffset = 5,
-            previousSelection = getSelection()
-        )
-        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_singleLayout_sameLayout_returnsFalse() {
-        val layout = getSingleSelectionLayoutForTest(
-            rawPreviousHandleOffset = 5,
-            previousSelection = getSelection()
-        )
-        assertThat(layout.shouldRecomputeSelection(layout)).isFalse()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_singleLayout_equalLayout_returnsFalse() {
-        val layout = getSingleSelectionLayoutForTest(
-            rawPreviousHandleOffset = 5,
-            previousSelection = getSelection()
-        )
-        val otherLayout = getSingleSelectionLayoutForTest(
-            rawPreviousHandleOffset = 5,
-            previousSelection = getSelection()
-        )
-        assertThat(layout.shouldRecomputeSelection(otherLayout)).isFalse()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_layoutBuilder_otherNull_returnsTrue() {
-        val layout = buildSelectionLayoutForTest(
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-        }
-        assertThat(layout.shouldRecomputeSelection(null)).isTrue()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_layoutBuilder_otherSingle_returnsTrue() {
-        val layout = buildSelectionLayoutForTest(
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-        }
-        val otherLayout = getSingleSelectionLayoutForTest(
-            previousSelection = getSelection(),
-            rawPreviousHandleOffset = 5
-        )
-        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_layoutBuilder_differentStartSlot_returnsTrue() {
-        val layout = buildSelectionLayoutForTest(
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(
-                rawEndHandleOffset = 5,
-                rawPreviousHandleOffset = 5,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                rawEndHandleOffset = 5,
-                rawPreviousHandleOffset = 5,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        val otherLayout = buildSelectionLayoutForTest(
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(
-                rawEndHandleOffset = 5,
-                rawPreviousHandleOffset = 5,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                rawEndHandleOffset = 5,
-                rawPreviousHandleOffset = 5,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_layoutBuilder_differentEndSlot_returnsTrue() {
-        val layout = buildSelectionLayoutForTest(
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(
-                rawEndHandleOffset = 5,
-                rawPreviousHandleOffset = 5,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                rawEndHandleOffset = 5,
-                rawPreviousHandleOffset = 5,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-        }
-        val otherLayout = buildSelectionLayoutForTest(
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(
-                rawEndHandleOffset = 5,
-                rawPreviousHandleOffset = 5,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                rawEndHandleOffset = 5,
-                rawPreviousHandleOffset = 5,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_layoutBuilder_differentHandle_returnsTrue() {
-        val layout = buildSelectionLayoutForTest(
-            isStartHandle = true,
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-        }
-        val otherLayout = buildSelectionLayoutForTest(
-            previousSelection = getSelection(),
-            isStartHandle = false
-        ) {
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-        }
-        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_layoutBuilder_differentSize_returnsTrue() {
-        val layout = buildSelectionLayoutForTest(
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-        }
-        val otherLayout = buildSelectionLayoutForTest(
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-        }
-        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_layoutBuilder_differentInfo_returnsTrue() {
-        val layout = buildSelectionLayoutForTest(
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-        }
-        val otherLayout = buildSelectionLayoutForTest(
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-            appendInfoForTest(rawEndHandleOffset = 4, rawPreviousHandleOffset = 5)
-        }
-        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_layoutBuilder_sameLayout_returnsFalse() {
-        val layout = buildSelectionLayoutForTest(
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-        }
-        assertThat(layout.shouldRecomputeSelection(layout)).isFalse()
-    }
-
-    @Test
-    fun shouldRecomputeSelection_layoutBuilder_equalLayout_returnsFalse() {
-        val layout = buildSelectionLayoutForTest(
-            previousSelection = getSelection()
-        ) {
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-        }
-        val otherLayout = buildSelectionLayoutForTest {
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
-        }
-        assertThat(layout.shouldRecomputeSelection(otherLayout)).isFalse()
-    }
-
-    @Test
-    fun createSubSelections_singleLayout_missNonCrossedSelection_throws() {
-        val layout = getSingleSelectionLayoutForTest()
-        val selection = getSelection(startOffset = 1, endOffset = 0, handlesCrossed = false)
-        assertFailsWith(IllegalStateException::class) {
-            layout.createSubSelections(selection)
-        }
-    }
-
-    @Test
-    fun createSubSelections_singleLayout_missCrossedSelection_throws() {
-        val layout = getSingleSelectionLayoutForTest()
-        val selection = getSelection(startOffset = 0, endOffset = 1, handlesCrossed = true)
-        assertFailsWith(IllegalStateException::class) {
-            layout.createSubSelections(selection)
-        }
-    }
-
-    @Test
-    fun createSubSelections_singleLayout_validSelection_returnsInputSelection() {
-        val layout = getSingleSelectionLayoutForTest()
-        val selection = getSelection()
-        val actual = layout.createSubSelections(selection)
-        assertThat(actual).hasSize(1)
-        // We don't care about the selectableId since it isn't used anyways
-        assertThat(actual.toList().single().second).isEqualTo(selection)
-    }
-
-    @Test
-    fun createSubSelections_builtSingleLayout_validSelection_returnsInputSelection() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(selectableId = 1L)
-        }
-        val selection = getSelection()
-        assertThat(layout.createSubSelections(selection)).containsExactly(1L, selection)
-    }
-
-    @Test
-    fun createSubSelections_layoutBuilder_missNonCrossedSingleSelection_throws() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(selectableId = 1L)
-        }
-        val selection = getSelection(startOffset = 1, endOffset = 0, handlesCrossed = false)
-        assertFailsWith(IllegalStateException::class) {
-            layout.createSubSelections(selection)
-        }
-    }
-
-    @Test
-    fun createSubSelections_layoutBuilder_missCrossedSingleSelection_throws() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(selectableId = 1L)
-        }
-        val selection = getSelection(startOffset = 0, endOffset = 1, handlesCrossed = true)
-        assertFailsWith(IllegalStateException::class) {
-            layout.createSubSelections(selection)
-        }
-    }
-
-    @Test
-    fun createSubSelections_layoutBuilder_selectionInOneSelectable_returnsInputSelection() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(selectableId = 1L)
-            appendInfoForTest(selectableId = 2L)
-        }
-        val selection = getSelection(startSelectableId = 2L, endSelectableId = 2L)
-        assertThat(layout.createSubSelections(selection)).containsExactly(2L, selection)
-    }
-
-    @Test
-    fun createSubSelections_layoutBuilder_selectionInTwoSelectables() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        val selection = getSelection(startSelectableId = 1L, endSelectableId = 2L)
-        assertThat(layout.createSubSelections(selection)).containsExactly(
-            1L, getSelection(startSelectableId = 1L, endSelectableId = 1L),
-            2L, getSelection(startSelectableId = 2L, endSelectableId = 2L),
-        )
-    }
-
-    @Test
-    fun createSubSelections_layoutBuilder_selectionInThreeSelectables() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 3L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        val selection = getSelection(startSelectableId = 1L, endSelectableId = 3L)
-        assertThat(layout.createSubSelections(selection)).containsExactly(
-            1L, getSelection(startSelectableId = 1L, endSelectableId = 1L),
-            2L, getSelection(startSelectableId = 2L, endSelectableId = 2L),
-            3L, getSelection(startSelectableId = 3L, endSelectableId = 3L),
-        )
-    }
-
-    @Test
-    fun createSubSelections_layoutBuilder_selectionInFourSelectables() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 3L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.AFTER,
-            )
-            appendInfoForTest(
-                selectableId = 4L,
-                startHandleDirection = Direction.BEFORE,
-                endHandleDirection = Direction.ON,
-            )
-        }
-        val selection = getSelection(startSelectableId = 1L, endSelectableId = 4L)
-        assertThat(layout.createSubSelections(selection)).containsExactly(
-            1L, getSelection(startSelectableId = 1L, endSelectableId = 1L),
-            2L, getSelection(startSelectableId = 2L, endSelectableId = 2L),
-            3L, getSelection(startSelectableId = 3L, endSelectableId = 3L),
-            4L, getSelection(startSelectableId = 4L, endSelectableId = 4L),
-        )
-    }
-
-    @Test
-    fun createSubSelections_layoutBuilder_crossedSelectionInOneSelectable() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.ON,
-                rawStartHandleOffset = 5,
-                rawEndHandleOffset = 0,
-            )
-        }
-        val selection = getSelection(
-            startSelectableId = 1L,
-            startOffset = 5,
-            endSelectableId = 1L,
-            endOffset = 0,
-            handlesCrossed = true
-        )
-        assertThat(layout.createSubSelections(selection)).containsExactly(1L, selection)
-    }
-
-    @Test
-    fun createSubSelections_layoutBuilder_crossedSelectionInTwoSelectables() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.ON,
-                rawStartHandleOffset = 5,
-                rawEndHandleOffset = 0,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.BEFORE,
-                rawStartHandleOffset = 5,
-                rawEndHandleOffset = 0,
-            )
-        }
-        val selection = getSelection(
-            startSelectableId = 2L,
-            startOffset = 5,
-            endSelectableId = 1L,
-            endOffset = 0,
-            handlesCrossed = true
-        )
-        assertThat(layout.createSubSelections(selection)).containsExactly(
-            1L,
-            getSelection(
-                startSelectableId = 1L,
-                endSelectableId = 1L,
-                startOffset = 5,
-                endOffset = 0,
-                handlesCrossed = true
-            ),
-            2L,
-            getSelection(
-                startSelectableId = 2L,
-                endSelectableId = 2L,
-                startOffset = 5,
-                endOffset = 0,
-                handlesCrossed = true
-            ),
-        )
-    }
-
-    @Test
-    fun createSubSelections_layoutBuilder_crossedSelectionInThreeSelectables() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.ON,
-                rawStartHandleOffset = 5,
-                rawEndHandleOffset = 0,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.BEFORE,
-                rawStartHandleOffset = 5,
-                rawEndHandleOffset = 0,
-            )
-            appendInfoForTest(
-                selectableId = 3L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.BEFORE,
-                rawStartHandleOffset = 5,
-                rawEndHandleOffset = 0,
-            )
-        }
-        val selection = getSelection(
-            startSelectableId = 3L,
-            startOffset = 5,
-            endSelectableId = 1L,
-            endOffset = 0,
-            handlesCrossed = true
-        )
-        assertThat(layout.createSubSelections(selection)).containsExactly(
-            1L,
-            getSelection(
-                startSelectableId = 1L,
-                endSelectableId = 1L,
-                startOffset = 5,
-                endOffset = 0,
-                handlesCrossed = true
-            ),
-            2L,
-            getSelection(
-                startSelectableId = 2L,
-                endSelectableId = 2L,
-                startOffset = 5,
-                endOffset = 0,
-                handlesCrossed = true
-            ),
-            3L,
-            getSelection(
-                startSelectableId = 3L,
-                endSelectableId = 3L,
-                startOffset = 5,
-                endOffset = 0,
-                handlesCrossed = true
-            ),
-        )
-    }
-
-    @Test
-    fun createSubSelections_layoutBuilder_crossedSelectionInFourSelectables() {
-        val layout = buildSelectionLayoutForTest {
-            appendInfoForTest(
-                selectableId = 1L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.ON,
-                rawStartHandleOffset = 5,
-                rawEndHandleOffset = 0,
-            )
-            appendInfoForTest(
-                selectableId = 2L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.BEFORE,
-                rawStartHandleOffset = 5,
-                rawEndHandleOffset = 0,
-            )
-            appendInfoForTest(
-                selectableId = 3L,
-                startHandleDirection = Direction.AFTER,
-                endHandleDirection = Direction.BEFORE,
-                rawStartHandleOffset = 5,
-                rawEndHandleOffset = 0,
-            )
-            appendInfoForTest(
-                selectableId = 4L,
-                startHandleDirection = Direction.ON,
-                endHandleDirection = Direction.BEFORE,
-                rawStartHandleOffset = 5,
-                rawEndHandleOffset = 0,
-            )
-        }
-        val selection = getSelection(
-            startSelectableId = 4L,
-            startOffset = 5,
-            endSelectableId = 1L,
-            endOffset = 0,
-            handlesCrossed = true
-        )
-        assertThat(layout.createSubSelections(selection)).containsExactly(
-            1L,
-            getSelection(
-                startSelectableId = 1L,
-                endSelectableId = 1L,
-                startOffset = 5,
-                endOffset = 0,
-                handlesCrossed = true
-            ),
-            2L,
-            getSelection(
-                startSelectableId = 2L,
-                endSelectableId = 2L,
-                startOffset = 5,
-                endOffset = 0,
-                handlesCrossed = true
-            ),
-            3L,
-            getSelection(
-                startSelectableId = 3L,
-                endSelectableId = 3L,
-                startOffset = 5,
-                endOffset = 0,
-                handlesCrossed = true
-            ),
-            4L,
-            getSelection(
-                startSelectableId = 4L,
-                endSelectableId = 4L,
-                startOffset = 5,
-                endOffset = 0,
-                handlesCrossed = true
-            ),
-        )
-    }
-
-    @Test
-    fun selection_isCollapsed_nullSelection_returnsTrue() {
-        assertThat(null.isCollapsed(getSingleSelectionLayoutFake())).isTrue()
-    }
-
-    @Test
-    fun selection_isCollapsed_nullLayout_returnsTrue() {
-        assertThat(getSelection().isCollapsed(null)).isTrue()
-    }
-
-    @Test
-    fun selection_isCollapsed_singleLayout_empty_returnsTrue() {
-        val selection = getSelection(startOffset = 0, endOffset = 0)
-        val layout = getSingleSelectionLayoutFake(text = "")
-        assertThat(selection.isCollapsed(layout)).isTrue()
-    }
-
-    @Test
-    fun selection_isCollapsed_singleLayout_collapsed_returnsTrue() {
-        val selection = getSelection(startOffset = 0, endOffset = 0)
-        val layout = getSingleSelectionLayoutFake(text = "hello")
-        assertThat(selection.isCollapsed(layout)).isTrue()
-    }
-
-    @Test
-    fun selection_isCollapsed_singleLayout_notCollapsed_returnsFalse() {
-        val selection = getSelection(startOffset = 0, endOffset = 5)
-        val layout = getSingleSelectionLayoutFake(text = "hello")
-        assertThat(selection.isCollapsed(layout)).isFalse()
-    }
-
-    @Test
-    fun selection_isCollapsed_layoutBuilder_twoLayouts_empty_returnsTrue() {
-        val selection = getSelection(
-            startSelectableId = 1L,
-            startOffset = 0,
-            endSelectableId = 2L,
-            endOffset = 0
-        )
-        val layout = getSelectionLayoutFake(
-            infos = listOf(
-                getSelectableInfoFake(selectableId = 1L, text = ""),
-                getSelectableInfoFake(selectableId = 2L, text = ""),
-            ),
-            startSlot = 1,
-            endSlot = 3,
-        )
-        assertThat(selection.isCollapsed(layout)).isTrue()
-    }
-
-    @Test
-    fun selection_isCollapsed_layoutBuilder_twoLayouts_collapsed_returnsTrue() {
-        val selection = getSelection(
-            startSelectableId = 1L,
-            startOffset = 5,
-            endSelectableId = 2L,
-            endOffset = 0
-        )
-        val layout = getSelectionLayoutFake(
-            infos = listOf(
-                getSelectableInfoFake(selectableId = 1L, text = "hello"),
-                getSelectableInfoFake(selectableId = 2L, text = "hello"),
-            ),
-            startSlot = 1,
-            endSlot = 3,
-        )
-        assertThat(selection.isCollapsed(layout)).isTrue()
-    }
-
-    @Test
-    fun selection_isCollapsed_layoutBuilder_twoLayouts_notCollapsedInFirst_returnsFalse() {
-        val selection = getSelection(
-            startSelectableId = 1L,
-            startOffset = 4,
-            endSelectableId = 2L,
-            endOffset = 0
-        )
-        val layout = getSelectionLayoutFake(
-            infos = listOf(
-                getSelectableInfoFake(selectableId = 1L, text = "hello"),
-                getSelectableInfoFake(selectableId = 2L, text = "hello"),
-            ),
-            startSlot = 1,
-            endSlot = 3,
-        )
-        assertThat(selection.isCollapsed(layout)).isFalse()
-    }
-
-    @Test
-    fun selection_isCollapsed_layoutBuilder_twoLayouts_notCollapsedInSecond_returnsFalse() {
-        val selection = getSelection(
-            startSelectableId = 1L,
-            startOffset = 5,
-            endSelectableId = 2L,
-            endOffset = 1
-        )
-        val layout = getSelectionLayoutFake(
-            infos = listOf(
-                getSelectableInfoFake(selectableId = 1L, text = "hello"),
-                getSelectableInfoFake(selectableId = 2L, text = "hello"),
-            ),
-            startSlot = 1,
-            endSlot = 3,
-        )
-        assertThat(selection.isCollapsed(layout)).isFalse()
-    }
-
-    @Test
-    fun selection_isCollapsed_layoutBuilder_threeLayouts_empty_returnsTrue() {
-        val selection = getSelection(
-            startSelectableId = 1L,
-            startOffset = 0,
-            endSelectableId = 3L,
-            endOffset = 0
-        )
-        val layout = getSelectionLayoutFake(
-            infos = listOf(
-                getSelectableInfoFake(selectableId = 1L, text = ""),
-                getSelectableInfoFake(selectableId = 2L, text = ""),
-                getSelectableInfoFake(selectableId = 3L, text = ""),
-            ),
-            startSlot = 1,
-            endSlot = 5,
-        )
-        assertThat(selection.isCollapsed(layout)).isTrue()
-    }
-
-    @Test
-    fun selection_isCollapsed_layoutBuilder_threeLayouts_collapsed_returnsTrue() {
-        val selection = getSelection(
-            startSelectableId = 1L,
-            startOffset = 5,
-            endSelectableId = 3L,
-            endOffset = 0
-        )
-        val layout = getSelectionLayoutFake(
-            infos = listOf(
-                getSelectableInfoFake(selectableId = 1L, text = "hello"),
-                getSelectableInfoFake(selectableId = 2L, text = ""),
-                getSelectableInfoFake(selectableId = 3L, text = "hello"),
-            ),
-            startSlot = 1,
-            endSlot = 5,
-        )
-        assertThat(selection.isCollapsed(layout)).isTrue()
-    }
-
-    @Test
-    fun selection_isCollapsed_layoutBuilder_threeLayouts_notCollapsed_returnsFalse() {
-        val selection = getSelection(
-            startSelectableId = 1L,
-            startOffset = 5,
-            endSelectableId = 3L,
-            endOffset = 0
-        )
-        val layout = getSelectionLayoutFake(
-            infos = listOf(
-                getSelectableInfoFake(selectableId = 1L, text = "hello"),
-                getSelectableInfoFake(selectableId = 2L, text = "."),
-                getSelectableInfoFake(selectableId = 3L, text = "hello"),
-            ),
-            startSlot = 1,
-            endSlot = 5,
-        )
-        assertThat(selection.isCollapsed(layout)).isFalse()
-    }
-
-    /** Calls [getTextFieldSelectionLayout] to get a [SelectionLayout]. */
-    @OptIn(ExperimentalContracts::class)
-    private fun buildSelectionLayoutForTest(
-        startHandlePosition: Offset = Offset(5f, 5f),
-        endHandlePosition: Offset = Offset(25f, 5f),
-        previousHandlePosition: Offset = Offset.Unspecified,
-        containerCoordinates: LayoutCoordinates = MockCoordinates(),
-        isStartHandle: Boolean = false,
-        previousSelection: Selection? = null,
-        selectableIdOrderingComparator: Comparator<Long> = naturalOrder(),
-        block: SelectionLayoutBuilder.() -> Unit,
-    ): SelectionLayout {
-        contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
-        return SelectionLayoutBuilder(
-            startHandlePosition = startHandlePosition,
-            endHandlePosition = endHandlePosition,
-            previousHandlePosition = previousHandlePosition,
-            containerCoordinates = containerCoordinates,
-            isStartHandle = isStartHandle,
-            previousSelection = previousSelection,
-            selectableIdOrderingComparator = selectableIdOrderingComparator,
-        ).run {
-            block()
-            build()
-        }
-    }
-
-    private fun SelectionLayoutBuilder.appendInfoForTest(
-        selectableId: Long = 1L,
-        text: String = "hello",
-        rawStartHandleOffset: Int = 0,
-        startHandleDirection: Direction = Direction.ON,
-        rawEndHandleOffset: Int = 5,
-        endHandleDirection: Direction = Direction.ON,
-        rawPreviousHandleOffset: Int = -1,
-        rtlRanges: List<IntRange> = emptyList(),
-        wordBoundaries: List<TextRange> = listOf(),
-        lineBreaks: List<Int> = emptyList(),
-    ): SelectableInfo {
-        val layoutResult = getTextLayoutResultMock(
-            text = text,
-            rtlCharRanges = rtlRanges,
-            wordBoundaries = wordBoundaries,
-            lineBreaks = lineBreaks,
-        )
-        return appendInfo(
-            selectableId = selectableId,
-            rawStartHandleOffset = rawStartHandleOffset,
-            startHandleDirection = startHandleDirection,
-            rawEndHandleOffset = rawEndHandleOffset,
-            endHandleDirection = endHandleDirection,
-            rawPreviousHandleOffset = rawPreviousHandleOffset,
-            textLayoutResult = layoutResult
-        )
-    }
-
-    /** Calls [getTextFieldSelectionLayout] to get a [SelectionLayout]. */
-    private fun getSingleSelectionLayoutForTest(
-        text: String = "hello",
-        rawStartHandleOffset: Int = 0,
-        rawEndHandleOffset: Int = 5,
-        rawPreviousHandleOffset: Int = -1,
-        rtlRanges: List<IntRange> = emptyList(),
-        wordBoundaries: List<TextRange> = listOf(),
-        lineBreaks: List<Int> = emptyList(),
-        isStartHandle: Boolean = false,
-        previousSelection: Selection? = null,
-        isStartOfSelection: Boolean = previousSelection == null,
-    ): SelectionLayout {
-        val layoutResult = getTextLayoutResultMock(
-            text = text,
-            rtlCharRanges = rtlRanges,
-            wordBoundaries = wordBoundaries,
-            lineBreaks = lineBreaks,
-        )
-        return getTextFieldSelectionLayout(
-            layoutResult = layoutResult,
-            rawStartHandleOffset = rawStartHandleOffset,
-            rawEndHandleOffset = rawEndHandleOffset,
-            rawPreviousHandleOffset = rawPreviousHandleOffset,
-            previousSelectionRange = previousSelection?.toTextRange() ?: TextRange.Zero,
-            isStartOfSelection = isStartOfSelection,
-            isStartHandle = isStartHandle
-        )
-    }
-}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
deleted file mode 100644
index b72ff38..0000000
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
+++ /dev/null
@@ -1,699 +0,0 @@
-/*
- * Copyright 2021 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.text.selection
-
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.platform.ClipboardManager
-import androidx.compose.ui.platform.TextToolbar
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.style.ResolvedTextDirection
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.kotlin.any
-import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.eq
-import org.mockito.kotlin.isNull
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
-import org.mockito.kotlin.spy
-import org.mockito.kotlin.times
-import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
-
-@RunWith(JUnit4::class)
-class SelectionManagerTest {
-    private val selectionRegistrar = spy(SelectionRegistrarImpl())
-    private val selectable = FakeSelectable()
-    private val selectableId = 1L
-    private val selectionManager = SelectionManager(selectionRegistrar)
-    private var onSelectionChangeCalledTimes = 0
-
-    private val containerLayoutCoordinates = mock<LayoutCoordinates> {
-        on { isAttached } doReturn true
-    }
-
-    private val startSelectableId = 2L
-    private val startSelectable = mock<Selectable> {
-        whenever(it.selectableId).thenReturn(startSelectableId)
-    }
-
-    private val endSelectableId = 3L
-    private val endSelectable = mock<Selectable> {
-        whenever(it.selectableId).thenReturn(endSelectableId)
-    }
-
-    private val middleSelectableId = 4L
-    private val middleSelectable = mock<Selectable> {
-        whenever(it.selectableId).thenReturn(middleSelectableId)
-    }
-
-    private val lastSelectableId = 5L
-    private val lastSelectable = mock<Selectable> {
-        whenever(it.selectableId).thenReturn(lastSelectableId)
-    }
-
-    private val fakeSelection =
-        Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = 0,
-                selectableId = startSelectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = 5,
-                selectableId = endSelectableId
-            )
-        )
-
-    private val hapticFeedback = mock<HapticFeedback>()
-    private val clipboardManager = mock<ClipboardManager>()
-    private val textToolbar = mock<TextToolbar>()
-
-    @Before
-    fun setup() {
-        selectable.clear()
-        selectable.selectableId = selectableId
-        selectionRegistrar.subscribe(selectable)
-        selectionRegistrar.subselections = mapOf(
-            selectableId to fakeSelection,
-            startSelectableId to fakeSelection,
-            endSelectableId to fakeSelection
-        )
-        selectionManager.containerLayoutCoordinates = containerLayoutCoordinates
-        selectionManager.hapticFeedBack = hapticFeedback
-        selectionManager.clipboardManager = clipboardManager
-        selectionManager.textToolbar = textToolbar
-        selectionManager.selection = fakeSelection
-        selectionManager.onSelectionChange = { onSelectionChangeCalledTimes++ }
-    }
-
-    @Test
-    fun updateSelection_onInitial_returnsTrue() {
-        val startHandlePosition = Offset(x = 5f, y = 5f)
-        val endHandlePosition = Offset(x = 25f, y = 5f)
-        selectable.apply {
-            textToReturn = AnnotatedString("hello")
-            rawStartHandleOffset = 0
-            rawEndHandleOffset = 5
-        }
-
-        val actual = selectionManager.updateSelection(
-            startHandlePosition = startHandlePosition,
-            endHandlePosition = endHandlePosition,
-            previousHandlePosition = endHandlePosition - Offset(x = 5f, y = 0f),
-            isStartHandle = false,
-            adjustment = SelectionAdjustment.None,
-        )
-
-        assertThat(actual).isTrue()
-        assertThat(onSelectionChangeCalledTimes).isEqualTo(1)
-    }
-
-    @Test
-    fun updateSelection_onNoChange_returnsFalse() {
-        val startHandlePosition = Offset(x = 5f, y = 5f)
-        val endHandlePosition = Offset(x = 25f, y = 5f)
-        selectable.apply {
-            textToReturn = AnnotatedString("hello")
-            rawStartHandleOffset = 0
-            rawEndHandleOffset = 5
-            rawPreviousHandleOffset = 5
-        }
-
-        // run once to set context for the "previous" selection update
-        selectionManager.updateSelection(
-            startHandlePosition = startHandlePosition,
-            endHandlePosition = endHandlePosition,
-            previousHandlePosition = endHandlePosition,
-            isStartHandle = false,
-            adjustment = SelectionAdjustment.None,
-        )
-
-        // run again since we are testing the "no changes" case
-        val actual = selectionManager.updateSelection(
-            startHandlePosition = startHandlePosition,
-            endHandlePosition = endHandlePosition,
-            previousHandlePosition = endHandlePosition,
-            isStartHandle = false,
-            adjustment = SelectionAdjustment.None,
-        )
-
-        assertThat(actual).isFalse()
-        assertThat(onSelectionChangeCalledTimes).isEqualTo(1)
-    }
-
-    @Test
-    fun updateSelection_onChange_returnsTrue() {
-        val startHandlePosition = Offset(x = 5f, y = 5f)
-        val endHandlePosition = Offset(x = 25f, y = 5f)
-        selectable.apply {
-            textToReturn = AnnotatedString("hello")
-            rawStartHandleOffset = 0
-            rawEndHandleOffset = 5
-            rawPreviousHandleOffset = 5
-        }
-
-        // run once to set context for the "previous" selection update
-        selectionManager.updateSelection(
-            startHandlePosition = startHandlePosition,
-            endHandlePosition = endHandlePosition,
-            previousHandlePosition = endHandlePosition,
-            isStartHandle = false,
-            adjustment = SelectionAdjustment.None,
-        )
-
-        // run again with a change in end handle
-        selectable.rawEndHandleOffset = 4
-        val actual = selectionManager.updateSelection(
-            startHandlePosition = startHandlePosition,
-            endHandlePosition = endHandlePosition,
-            previousHandlePosition = endHandlePosition - Offset(x = 5f, y = 0f),
-            isStartHandle = false,
-            adjustment = SelectionAdjustment.None,
-        )
-
-        assertThat(actual).isTrue()
-        assertThat(onSelectionChangeCalledTimes).isEqualTo(2)
-    }
-
-    @Test
-    fun shouldPerformHaptics_notInTouchMode_returnsFalse() {
-        selectionManager.isInTouchMode = false
-        selectable.textToReturn = AnnotatedString("hello")
-        val actual = selectionManager.shouldPerformHaptics()
-        assertThat(actual).isFalse()
-    }
-
-    @Test
-    fun shouldPerformHaptics_allEmptyTextSelectables_returnsFalse() {
-        selectionManager.isInTouchMode = true
-        selectable.textToReturn = AnnotatedString("")
-        val actual = selectionManager.shouldPerformHaptics()
-        assertThat(actual).isFalse()
-    }
-
-    @Test
-    fun shouldPerformHaptics_inTouchModeAndNonEmpty_returnsTrue() {
-        selectionManager.isInTouchMode = true
-        selectable.textToReturn = AnnotatedString("hello")
-        val actual = selectionManager.shouldPerformHaptics()
-        assertThat(actual).isTrue()
-    }
-
-    @Test
-    fun mergeSelections_selectAll() {
-        val anotherSelectableId = 100L
-        val selectableAnother = mock<Selectable>()
-        whenever(selectableAnother.selectableId).thenReturn(anotherSelectableId)
-
-        selectionRegistrar.subscribe(selectableAnother)
-
-        selectionManager.selectAll(
-            selectableId = selectableId,
-            previousSelection = fakeSelection
-        )
-
-        verify(selectableAnother, times(0)).getSelectAllSelection()
-        verify(
-            hapticFeedback,
-            times(1)
-        ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
-    }
-
-    @Test
-    fun isNonEmptySelection_whenNonEmptySelection_sameLine_returnsTrue() {
-        val text = "Text Demo"
-        val annotatedString = AnnotatedString(text)
-        val startOffset = text.indexOf('e')
-        val endOffset = text.indexOf('m')
-        selectable.textToReturn = annotatedString
-        selectionManager.selection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = endOffset,
-                selectableId = selectableId
-            ),
-            handlesCrossed = false
-        )
-
-        assertThat(selectionManager.isNonEmptySelection()).isTrue()
-    }
-
-    @Test
-    fun isNonEmptySelection_whenEmptySelection_sameLine_returnsFalse() {
-        val text = "Text Demo"
-        val annotatedString = AnnotatedString(text)
-        val startOffset = text.indexOf('e')
-        selectable.textToReturn = annotatedString
-        selectionManager.selection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = selectableId
-            ),
-            handlesCrossed = false
-        )
-
-        assertThat(selectionManager.isNonEmptySelection()).isFalse()
-    }
-
-    @Test
-    fun isNonEmptySelection_whenNonEmptySelection_multiLine_returnsTrue() {
-        val text = "Text Demo"
-        val annotatedString = AnnotatedString(text = text)
-        val startOffset = text.indexOf('m')
-        val endOffset = text.indexOf('x')
-
-        selectionRegistrar.subscribe(endSelectable)
-        selectionRegistrar.subscribe(middleSelectable)
-        selectionRegistrar.subscribe(startSelectable)
-        selectionRegistrar.subscribe(lastSelectable)
-        selectionRegistrar.sorted = true
-        whenever(startSelectable.getText()).thenReturn(annotatedString)
-        whenever(middleSelectable.getText()).thenReturn(annotatedString)
-        whenever(endSelectable.getText()).thenReturn(annotatedString)
-        selectionManager.selection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = startSelectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = endOffset,
-                selectableId = endSelectableId
-            ),
-            handlesCrossed = true
-        )
-
-        assertThat(selectionManager.isNonEmptySelection()).isTrue()
-    }
-
-    @Test
-    fun isNonEmptySelection_whenEmptySelection_multiLine_returnsFalse() {
-        val text = "Text Demo"
-        val annotatedString = AnnotatedString(text)
-        val startOffset = text.length
-        val endOffset = 0
-
-        selectionRegistrar.subscribe(startSelectable)
-        selectionRegistrar.subscribe(middleSelectable)
-        selectionRegistrar.subscribe(endSelectable)
-        selectionRegistrar.subscribe(lastSelectable)
-        selectionRegistrar.sorted = true
-        whenever(startSelectable.getText()).thenReturn(annotatedString)
-        whenever(middleSelectable.getText()).thenReturn(AnnotatedString(""))
-        whenever(endSelectable.getText()).thenReturn(annotatedString)
-        selectionManager.selection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = startSelectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = endOffset,
-                selectableId = endSelectableId
-            ),
-            handlesCrossed = false
-        )
-
-        assertThat(selectionManager.isNonEmptySelection()).isFalse()
-    }
-
-    @Test
-    fun isNonEmptySelection_whenEmptySelection_multiLineCrossed_returnsFalse() {
-        val text = "Text Demo"
-        val annotatedString = AnnotatedString(text)
-        val startOffset = 0
-        val endOffset = text.length
-
-        selectionRegistrar.subscribe(endSelectable)
-        selectionRegistrar.subscribe(middleSelectable)
-        selectionRegistrar.subscribe(startSelectable)
-        selectionRegistrar.subscribe(lastSelectable)
-        selectionRegistrar.sorted = true
-        whenever(startSelectable.getText()).thenReturn(annotatedString)
-        whenever(middleSelectable.getText()).thenReturn(AnnotatedString(""))
-        whenever(endSelectable.getText()).thenReturn(annotatedString)
-        selectionManager.selection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = startSelectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = endOffset,
-                selectableId = endSelectableId
-            ),
-            handlesCrossed = true
-        )
-
-        assertThat(selectionManager.isNonEmptySelection()).isFalse()
-    }
-
-    @Test
-    fun getSelectedText_selection_null_return_null() {
-        selectionManager.selection = null
-
-        assertThat(selectionManager.getSelectedText()).isNull()
-        assertThat(selectable.getTextCalledTimes).isEqualTo(0)
-    }
-
-    @Test
-    fun getSelectedText_not_crossed_single_widget() {
-        val text = "Text Demo"
-        val annotatedString = AnnotatedString(text = text)
-        val startOffset = text.indexOf('e')
-        val endOffset = text.indexOf('m')
-        selectable.textToReturn = annotatedString
-        selectionManager.selection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = endOffset,
-                selectableId = selectableId
-            ),
-            handlesCrossed = false
-        )
-
-        assertThat(selectionManager.getSelectedText())
-            .isEqualTo(annotatedString.subSequence(startOffset, endOffset))
-        assertThat(selectable.getTextCalledTimes).isEqualTo(1)
-    }
-
-    @Test
-    fun getSelectedText_crossed_single_widget() {
-        val text = "Text Demo"
-        val annotatedString = AnnotatedString(text = text)
-        val startOffset = text.indexOf('m')
-        val endOffset = text.indexOf('x')
-        selectable.textToReturn = annotatedString
-        selectionManager.selection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = endOffset,
-                selectableId = selectableId
-            ),
-            handlesCrossed = true
-        )
-
-        assertThat(selectionManager.getSelectedText())
-            .isEqualTo(annotatedString.subSequence(endOffset, startOffset))
-        assertThat(selectable.getTextCalledTimes).isEqualTo(1)
-    }
-
-    @Test
-    fun getSelectedText_not_crossed_multi_widgets() {
-        val text = "Text Demo"
-        val annotatedString = AnnotatedString(text = text)
-        val startOffset = text.indexOf('m')
-        val endOffset = text.indexOf('x')
-
-        selectionRegistrar.subscribe(startSelectable)
-        selectionRegistrar.subscribe(middleSelectable)
-        selectionRegistrar.subscribe(endSelectable)
-        selectionRegistrar.subscribe(lastSelectable)
-        selectionRegistrar.sorted = true
-        whenever(startSelectable.getText()).thenReturn(annotatedString)
-        whenever(middleSelectable.getText()).thenReturn(annotatedString)
-        whenever(endSelectable.getText()).thenReturn(annotatedString)
-        selectionManager.selection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = startSelectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = endOffset,
-                selectableId = endSelectableId
-            ),
-            handlesCrossed = false
-        )
-
-        val result = annotatedString.subSequence(startOffset, annotatedString.length) +
-            annotatedString + annotatedString.subSequence(0, endOffset)
-        assertThat(selectionManager.getSelectedText()).isEqualTo(result)
-        assertThat(selectable.getTextCalledTimes).isEqualTo(0)
-        verify(startSelectable, times(1)).getText()
-        verify(middleSelectable, times(1)).getText()
-        verify(endSelectable, times(1)).getText()
-        verify(lastSelectable, times(0)).getText()
-    }
-
-    @Test
-    fun getSelectedText_crossed_multi_widgets() {
-        val text = "Text Demo"
-        val annotatedString = AnnotatedString(text = text)
-        val startOffset = text.indexOf('m')
-        val endOffset = text.indexOf('x')
-
-        selectionRegistrar.subscribe(endSelectable)
-        selectionRegistrar.subscribe(middleSelectable)
-        selectionRegistrar.subscribe(startSelectable)
-        selectionRegistrar.subscribe(lastSelectable)
-        selectionRegistrar.sorted = true
-        whenever(startSelectable.getText()).thenReturn(annotatedString)
-        whenever(middleSelectable.getText()).thenReturn(annotatedString)
-        whenever(endSelectable.getText()).thenReturn(annotatedString)
-        selectionManager.selection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = startSelectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = endOffset,
-                selectableId = endSelectableId
-            ),
-            handlesCrossed = true
-        )
-
-        val result = annotatedString.subSequence(endOffset, annotatedString.length) +
-            annotatedString + annotatedString.subSequence(0, startOffset)
-        assertThat(selectionManager.getSelectedText()).isEqualTo(result)
-        assertThat(selectable.getTextCalledTimes).isEqualTo(0)
-        verify(startSelectable, times(1)).getText()
-        verify(middleSelectable, times(1)).getText()
-        verify(endSelectable, times(1)).getText()
-        verify(lastSelectable, times(0)).getText()
-    }
-
-    @Test
-    fun copy_selection_null_not_trigger_clipboardManager() {
-        selectionManager.selection = null
-
-        selectionManager.copy()
-
-        verify(clipboardManager, times(0)).setText(any())
-    }
-
-    @Test
-    fun copy_selection_not_null_trigger_clipboardManager_setText() {
-        val text = "Text Demo"
-        val annotatedString = AnnotatedString(text = text)
-        val startOffset = text.indexOf('m')
-        val endOffset = text.indexOf('x')
-        selectable.textToReturn = annotatedString
-        selectionManager.selection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = endOffset,
-                selectableId = selectableId
-            ),
-            handlesCrossed = true
-        )
-
-        selectionManager.copy()
-
-        verify(clipboardManager, times(1)).setText(
-            annotatedString.subSequence(
-                endOffset,
-                startOffset
-            )
-        )
-    }
-
-    @Test
-    fun showSelectionToolbar_trigger_textToolbar_showMenu() {
-        val text = "Text Demo"
-        val annotatedString = AnnotatedString(text = text)
-        val startOffset = text.indexOf('m')
-        val endOffset = text.indexOf('x')
-        selectable.textToReturn = annotatedString
-        selectionManager.selection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = endOffset,
-                selectableId = selectableId
-            ),
-            handlesCrossed = true
-        )
-        selectionManager.hasFocus = true
-
-        selectionManager.showToolbar = true
-
-        verify(textToolbar, times(1)).showMenu(
-            eq(Rect.Zero),
-            any(),
-            isNull(),
-            isNull(),
-            isNull()
-        )
-    }
-
-    @Test
-    fun showSelectionToolbar_withoutFocus_notTrigger_textToolbar_showMenu() {
-        val text = "Text Demo"
-        val annotatedString = AnnotatedString(text = text)
-        val startOffset = text.indexOf('m')
-        val endOffset = text.indexOf('x')
-        selectable.textToReturn = annotatedString
-        selectionManager.selection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = startOffset,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = endOffset,
-                selectableId = selectableId
-            ),
-            handlesCrossed = true
-        )
-        selectionManager.hasFocus = false
-
-        selectionManager.showToolbar = true
-
-        verify(textToolbar, never()).showMenu(
-            eq(Rect.Zero),
-            any(),
-            isNull(),
-            isNull(),
-            isNull()
-        )
-    }
-
-    @Test
-    fun onRelease_selectionMap_is_setToEmpty() {
-        val fakeSelection =
-            Selection(
-                start = Selection.AnchorInfo(
-                    direction = ResolvedTextDirection.Ltr,
-                    offset = 0,
-                    selectableId = startSelectableId
-                ),
-                end = Selection.AnchorInfo(
-                    direction = ResolvedTextDirection.Ltr,
-                    offset = 5,
-                    selectableId = endSelectableId
-                )
-            )
-        var selection: Selection? = fakeSelection
-        val lambda: (Selection?) -> Unit = { selection = it }
-        val spyLambda = spy(lambda)
-        selectionManager.onSelectionChange = spyLambda
-        selectionManager.selection = fakeSelection
-
-        selectionManager.onRelease()
-
-        verify(selectionRegistrar).subselections = emptyMap()
-
-        assertThat(selection).isNull()
-        verify(spyLambda, times(1)).invoke(null)
-        verify(
-            hapticFeedback,
-            times(1)
-        ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
-    }
-
-    @Test
-    fun notifySelectableChange_clears_selection() {
-        val fakeSelection =
-            Selection(
-                start = Selection.AnchorInfo(
-                    direction = ResolvedTextDirection.Ltr,
-                    offset = 0,
-                    selectableId = startSelectableId
-                ),
-                end = Selection.AnchorInfo(
-                    direction = ResolvedTextDirection.Ltr,
-                    offset = 5,
-                    selectableId = startSelectableId
-                )
-            )
-        var selection: Selection? = fakeSelection
-        val lambda: (Selection?) -> Unit = { selection = it }
-        val spyLambda = spy(lambda)
-        selectionManager.onSelectionChange = spyLambda
-        selectionManager.selection = fakeSelection
-
-        selectionRegistrar.subselections = mapOf(
-            startSelectableId to fakeSelection
-        )
-        selectionRegistrar.notifySelectableChange(startSelectableId)
-
-        verify(selectionRegistrar).subselections = emptyMap()
-        assertThat(selection).isNull()
-        verify(spyLambda, times(1)).invoke(null)
-        verify(
-            hapticFeedback,
-            times(1)
-        ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
-    }
-}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/TextFieldStateSaverTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/TextFieldStateSaverTest.kt
deleted file mode 100644
index 308918e..0000000
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/TextFieldStateSaverTest.kt
+++ /dev/null
@@ -1,45 +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.compose.foundation.text2.input
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.saveable.SaverScope
-import androidx.compose.ui.text.TextRange
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.assertNotNull
-import org.junit.Test
-
-@OptIn(ExperimentalFoundationApi::class)
-class TextFieldStateSaverTest {
-
-    @Test
-    fun savesAndRestoresTextAndSelection() {
-        val state = TextFieldState("hello, world", initialSelectionInChars = TextRange(0, 5))
-
-        val saved = with(TextFieldState.Saver) { TestSaverScope.save(state) }
-        assertNotNull(saved)
-        val restoredState = TextFieldState.Saver.restore(saved)
-
-        assertNotNull(restoredState)
-        assertThat(restoredState.text.toString()).isEqualTo("hello, world")
-        assertThat(restoredState.text.selectionInChars).isEqualTo(TextRange(0, 5))
-    }
-
-    private object TestSaverScope : SaverScope {
-        override fun canBeSaved(value: Any): Boolean = true
-    }
-}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/ChangeTrackerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/ChangeTrackerTest.kt
deleted file mode 100644
index 5c08053..0000000
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/ChangeTrackerTest.kt
+++ /dev/null
@@ -1,208 +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.compose.foundation.text2.input.internal
-
-import androidx.compose.ui.text.TextRange
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class ChangeTrackerTest {
-
-    @Test
-    fun initialInsert() {
-        val buffer = SimpleBuffer()
-
-        buffer.append("hello")
-
-        assertThat(buffer.toString()).isEqualTo("hello")
-        assertThat(buffer.changes.changeCount).isEqualTo(1)
-        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 5))
-        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0))
-    }
-
-    @Test
-    fun deleteAll() {
-        val buffer = SimpleBuffer("hello")
-
-        buffer.replace("hello", "")
-
-        assertThat(buffer.toString()).isEqualTo("")
-        assertThat(buffer.changes.changeCount).isEqualTo(1)
-        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 0))
-        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 5))
-    }
-
-    @Test
-    fun multipleDiscontinuousChanges() {
-        val buffer = SimpleBuffer("hello world")
-
-        buffer.replace("world", "Compose")
-        buffer.replace("hello", "goodbye")
-
-        assertThat(buffer.toString()).isEqualTo("goodbye Compose")
-        assertThat(buffer.changes.changeCount).isEqualTo(2)
-        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 7))
-        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 5))
-        assertThat(buffer.changes.getRange(1)).isEqualTo(TextRange(8, 15))
-        assertThat(buffer.changes.getOriginalRange(1)).isEqualTo(TextRange(6, 11))
-    }
-
-    @Test
-    fun twoAppends() {
-        val buffer = SimpleBuffer()
-
-        buffer.append("foo")
-        buffer.append("bar")
-
-        assertThat(buffer.toString()).isEqualTo("foobar")
-        assertThat(buffer.changes.changeCount).isEqualTo(1)
-        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 6))
-        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0))
-    }
-
-    @Test
-    fun threeAppends() {
-        val buffer = SimpleBuffer()
-
-        buffer.append("foo")
-        buffer.append("bar")
-        buffer.append("baz")
-
-        assertThat(buffer.toString()).isEqualTo("foobarbaz")
-        assertThat(buffer.changes.changeCount).isEqualTo(1)
-        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 9))
-        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0))
-    }
-
-    @Test
-    fun multipleAdjacentReplaces_whenPerformedInOrder_replacementsShorter() {
-        val buffer = SimpleBuffer("abcd")
-
-        buffer.replace("ab", "e") // ecd
-        buffer.replace("cd", "f")
-
-        assertThat(buffer.toString()).isEqualTo("ef")
-        assertThat(buffer.changes.changeCount).isEqualTo(1)
-        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 2))
-        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 4))
-    }
-
-    @Test
-    fun multipleAdjacentReplaces_whenPerformedInOrder_replacementsLonger() {
-        val buffer = SimpleBuffer("abcd")
-
-        buffer.replace("ab", "efg") // efgcd
-        buffer.replace("cd", "hij")
-
-        assertThat(buffer.toString()).isEqualTo("efghij")
-        assertThat(buffer.changes.changeCount).isEqualTo(1)
-        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 6))
-        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 4))
-    }
-
-    @Test
-    fun multipleAdjacentReplaces_whenPerformedInReverseOrder_replacementsShorter() {
-        val buffer = SimpleBuffer("abcd")
-
-        buffer.replace("cd", "f") // abf
-        buffer.replace("ab", "e")
-
-        assertThat(buffer.toString()).isEqualTo("ef")
-        assertThat(buffer.changes.changeCount).isEqualTo(1)
-        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 2))
-        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 4))
-    }
-
-    @Test
-    fun multipleAdjacentReplaces_whenPerformedInReverseOrder_replacementsLonger() {
-        val buffer = SimpleBuffer("abcd")
-
-        buffer.replace("cd", "efg") // abhij
-        buffer.replace("ab", "hij")
-
-        assertThat(buffer.toString()).isEqualTo("hijefg")
-        assertThat(buffer.changes.changeCount).isEqualTo(1)
-        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 6))
-        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 4))
-    }
-
-    @Test
-    fun multiplePartiallyOverlappingChanges_atStart() {
-        val buffer = SimpleBuffer("abcd")
-
-        buffer.replace("bc", "ef") // aefd
-        buffer.replace("ae", "gh")
-
-        assertThat(buffer.toString()).isEqualTo("ghfd")
-        // Overlapping changes are merged.
-        assertThat(buffer.changes.changeCount).isEqualTo(1)
-        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(0, 3))
-        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(0, 3))
-    }
-
-    @Test
-    fun multiplePartiallyOverlappingChanges_atEnd() {
-        val buffer = SimpleBuffer("abcd")
-
-        buffer.replace("bc", "ef") // aefd
-        buffer.replace("fd", "gh")
-
-        assertThat(buffer.toString()).isEqualTo("aegh")
-        // Overlapping changes are merged.
-        assertThat(buffer.changes.changeCount).isEqualTo(1)
-        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(1, 4))
-        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(1, 4))
-    }
-
-    @Test
-    fun multipleFullyOverlappingChanges() {
-        val buffer = SimpleBuffer("abcd")
-
-        buffer.replace("bc", "ef") // aefd
-        buffer.replace("ef", "gh")
-
-        assertThat(buffer.toString()).isEqualTo("aghd")
-        // Overlapping changes are merged.
-        assertThat(buffer.changes.changeCount).isEqualTo(1)
-        assertThat(buffer.changes.getRange(0)).isEqualTo(TextRange(1, 3))
-        assertThat(buffer.changes.getOriginalRange(0)).isEqualTo(TextRange(1, 3))
-    }
-
-    private class SimpleBuffer(initialText: String = "") {
-        private val builder = StringBuilder(initialText)
-        val changes = ChangeTracker()
-
-        fun append(text: String) {
-            changes.trackChange(TextRange(builder.length), text.length)
-            builder.append(text)
-        }
-
-        fun replace(substring: String, text: String) {
-            val start = builder.indexOf(substring)
-            if (start != -1) {
-                val end = start + substring.length
-                changes.trackChange(TextRange(start, end), text.length)
-                builder.replace(start, end, text)
-            }
-        }
-
-        override fun toString(): String = builder.toString()
-    }
-}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferTest.kt
deleted file mode 100644
index 99de440..0000000
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferTest.kt
+++ /dev/null
@@ -1,472 +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.compose.foundation.text2.input.internal
-
-import androidx.compose.foundation.text2.input.internal.matchers.assertThat
-import androidx.compose.ui.text.TextRange
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class EditingBufferTest {
-
-    @Test
-    fun insert() {
-        val eb = EditingBuffer("", TextRange.Zero)
-
-        eb.replace(0, 0, "A")
-
-        assertThat(eb).hasChars("A")
-        assertThat(eb.cursor).isEqualTo(1)
-        assertThat(eb.selectionStart).isEqualTo(1)
-        assertThat(eb.selectionEnd).isEqualTo(1)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-
-        // Keep inserting text to the end of string. Cursor should follow.
-        eb.replace(1, 1, "BC")
-        assertThat(eb).hasChars("ABC")
-        assertThat(eb.cursor).isEqualTo(3)
-        assertThat(eb.selectionStart).isEqualTo(3)
-        assertThat(eb.selectionEnd).isEqualTo(3)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-
-        // Insert into middle position. Cursor should be end of inserted text.
-        eb.replace(1, 1, "D")
-        assertThat(eb).hasChars("ADBC")
-        assertThat(eb.cursor).isEqualTo(2)
-        assertThat(eb.selectionStart).isEqualTo(2)
-        assertThat(eb.selectionEnd).isEqualTo(2)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-    }
-
-    @Test
-    fun delete() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.replace(0, 1, "")
-
-        // Delete the left character at the cursor.
-        assertThat(eb).hasChars("BCDE")
-        assertThat(eb.cursor).isEqualTo(0)
-        assertThat(eb.selectionStart).isEqualTo(0)
-        assertThat(eb.selectionEnd).isEqualTo(0)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-
-        // Delete the text before the cursor
-        eb.replace(0, 2, "")
-        assertThat(eb).hasChars("DE")
-        assertThat(eb.cursor).isEqualTo(0)
-        assertThat(eb.selectionStart).isEqualTo(0)
-        assertThat(eb.selectionEnd).isEqualTo(0)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-
-        // Delete end of the text.
-        eb.replace(1, 2, "")
-        assertThat(eb).hasChars("D")
-        assertThat(eb.cursor).isEqualTo(1)
-        assertThat(eb.selectionStart).isEqualTo(1)
-        assertThat(eb.selectionEnd).isEqualTo(1)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-    }
-
-    @Test
-    fun setSelection() {
-        val eb = EditingBuffer("ABCDE", TextRange(0, 3))
-        assertThat(eb).hasChars("ABCDE")
-        assertThat(eb.cursor).isEqualTo(-1)
-        assertThat(eb.selectionStart).isEqualTo(0)
-        assertThat(eb.selectionEnd).isEqualTo(3)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-
-        eb.setSelection(0, 5) // Change the selection
-        assertThat(eb).hasChars("ABCDE")
-        assertThat(eb.cursor).isEqualTo(-1)
-        assertThat(eb.selectionStart).isEqualTo(0)
-        assertThat(eb.selectionEnd).isEqualTo(5)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-
-        eb.replace(0, 3, "X") // replace function cancel the selection and place cursor.
-        assertThat(eb).hasChars("XDE")
-        assertThat(eb.cursor).isEqualTo(1)
-        assertThat(eb.selectionStart).isEqualTo(1)
-        assertThat(eb.selectionEnd).isEqualTo(1)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-
-        eb.setSelection(0, 2) // Set the selection again
-        assertThat(eb).hasChars("XDE")
-        assertThat(eb.cursor).isEqualTo(-1)
-        assertThat(eb.selectionStart).isEqualTo(0)
-        assertThat(eb.selectionEnd).isEqualTo(2)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-    }
-
-    @Test fun setSelection_coerces_whenNegativeStart() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.setSelection(-1, 1)
-
-        assertThat(eb.selectionStart).isEqualTo(0)
-        assertThat(eb.selectionEnd).isEqualTo(1)
-    }
-
-    @Test fun setSelection_coerces_whenNegativeEnd() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.setSelection(1, -1)
-
-        assertThat(eb.selectionStart).isEqualTo(1)
-        assertThat(eb.selectionEnd).isEqualTo(0)
-    }
-
-    @Test
-    fun setSelection_allowReversedSelection() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-        eb.setSelection(4, 2)
-
-        assertThat(eb.selection).isEqualTo(TextRange(4, 2))
-    }
-
-    @Test
-    fun setComposition_and_cancelComposition() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.setComposition(0, 5) // Make all text as composition
-        assertThat(eb).hasChars("ABCDE")
-        assertThat(eb.cursor).isEqualTo(0)
-        assertThat(eb.selectionStart).isEqualTo(0)
-        assertThat(eb.selectionEnd).isEqualTo(0)
-        assertThat(eb.hasComposition()).isTrue()
-        assertThat(eb.compositionStart).isEqualTo(0)
-        assertThat(eb.compositionEnd).isEqualTo(5)
-
-        eb.replace(2, 3, "X") // replace function cancel the composition text.
-        assertThat(eb).hasChars("ABXDE")
-        assertThat(eb.cursor).isEqualTo(3)
-        assertThat(eb.selectionStart).isEqualTo(3)
-        assertThat(eb.selectionEnd).isEqualTo(3)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-
-        eb.setComposition(2, 4) // set composition again
-        assertThat(eb).hasChars("ABXDE")
-        assertThat(eb.cursor).isEqualTo(3)
-        assertThat(eb.selectionStart).isEqualTo(3)
-        assertThat(eb.selectionEnd).isEqualTo(3)
-        assertThat(eb.hasComposition()).isTrue()
-        assertThat(eb.compositionStart).isEqualTo(2)
-        assertThat(eb.compositionEnd).isEqualTo(4)
-    }
-
-    @Test
-    fun setComposition_and_commitComposition() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.setComposition(0, 5) // Make all text as composition
-        assertThat(eb).hasChars("ABCDE")
-        assertThat(eb.cursor).isEqualTo(0)
-        assertThat(eb.selectionStart).isEqualTo(0)
-        assertThat(eb.selectionEnd).isEqualTo(0)
-        assertThat(eb.hasComposition()).isTrue()
-        assertThat(eb.compositionStart).isEqualTo(0)
-        assertThat(eb.compositionEnd).isEqualTo(5)
-
-        eb.replace(2, 3, "X") // replace function cancel the composition text.
-        assertThat(eb).hasChars("ABXDE")
-        assertThat(eb.cursor).isEqualTo(3)
-        assertThat(eb.selectionStart).isEqualTo(3)
-        assertThat(eb.selectionEnd).isEqualTo(3)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-
-        eb.setComposition(2, 4) // set composition again
-        assertThat(eb).hasChars("ABXDE")
-        assertThat(eb.cursor).isEqualTo(3)
-        assertThat(eb.selectionStart).isEqualTo(3)
-        assertThat(eb.selectionEnd).isEqualTo(3)
-        assertThat(eb.hasComposition()).isTrue()
-        assertThat(eb.compositionStart).isEqualTo(2)
-        assertThat(eb.compositionEnd).isEqualTo(4)
-
-        eb.commitComposition() // commit the composition
-        assertThat(eb).hasChars("ABXDE")
-        assertThat(eb.cursor).isEqualTo(3)
-        assertThat(eb.selectionStart).isEqualTo(3)
-        assertThat(eb.selectionEnd).isEqualTo(3)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-    }
-
-    @Test
-    fun setCursor_and_get_cursor() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.cursor = 1
-        assertThat(eb).hasChars("ABCDE")
-        assertThat(eb.cursor).isEqualTo(1)
-        assertThat(eb.selectionStart).isEqualTo(1)
-        assertThat(eb.selectionEnd).isEqualTo(1)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-
-        eb.cursor = 2
-        assertThat(eb).hasChars("ABCDE")
-        assertThat(eb.cursor).isEqualTo(2)
-        assertThat(eb.selectionStart).isEqualTo(2)
-        assertThat(eb.selectionEnd).isEqualTo(2)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-
-        eb.cursor = 5
-        assertThat(eb).hasChars("ABCDE")
-        assertThat(eb.cursor).isEqualTo(5)
-        assertThat(eb.selectionStart).isEqualTo(5)
-        assertThat(eb.selectionEnd).isEqualTo(5)
-        assertThat(eb.hasComposition()).isFalse()
-        assertThat(eb.compositionStart).isEqualTo(-1)
-        assertThat(eb.compositionEnd).isEqualTo(-1)
-    }
-
-    @Test
-    fun delete_preceding_cursor_no_composition() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.delete(1, 2)
-        assertThat(eb).hasChars("ACDE")
-        assertThat(eb.cursor).isEqualTo(0)
-        assertThat(eb.hasComposition()).isFalse()
-    }
-
-    @Test
-    fun delete_trailing_cursor_no_composition() {
-        val eb = EditingBuffer("ABCDE", TextRange(3))
-
-        eb.delete(1, 2)
-        assertThat(eb).hasChars("ACDE")
-        assertThat(eb.cursor).isEqualTo(2)
-        assertThat(eb.hasComposition()).isFalse()
-    }
-
-    @Test
-    fun delete_preceding_selection_no_composition() {
-        val eb = EditingBuffer("ABCDE", TextRange(0, 1))
-
-        eb.delete(1, 2)
-        assertThat(eb).hasChars("ACDE")
-        assertThat(eb.selectionStart).isEqualTo(0)
-        assertThat(eb.selectionEnd).isEqualTo(1)
-        assertThat(eb.hasComposition()).isFalse()
-    }
-
-    @Test
-    fun delete_trailing_selection_no_composition() {
-        val eb = EditingBuffer("ABCDE", TextRange(4, 5))
-
-        eb.delete(1, 2)
-        assertThat(eb).hasChars("ACDE")
-        assertThat(eb.selectionStart).isEqualTo(3)
-        assertThat(eb.selectionEnd).isEqualTo(4)
-        assertThat(eb.hasComposition()).isFalse()
-    }
-
-    @Test
-    fun delete_covered_cursor() {
-        // AB[]CDE
-        val eb = EditingBuffer("ABCDE", TextRange(2, 2))
-
-        eb.delete(1, 3)
-        // A[]DE
-        assertThat(eb).hasChars("ADE")
-        assertThat(eb.selectionStart).isEqualTo(1)
-        assertThat(eb.selectionEnd).isEqualTo(1)
-    }
-
-    @Test
-    fun delete_covered_selection() {
-        // A[BC]DE
-        val eb = EditingBuffer("ABCDE", TextRange(1, 3))
-
-        eb.delete(0, 4)
-        // []E
-        assertThat(eb).hasChars("E")
-        assertThat(eb.selectionStart).isEqualTo(0)
-        assertThat(eb.selectionEnd).isEqualTo(0)
-    }
-
-    @Test
-    fun delete_covered_reversedSelection() {
-        // A[BC]DE
-        val eb = EditingBuffer("ABCDE", TextRange(3, 1))
-
-        eb.delete(0, 4)
-        // []E
-        assertThat(eb).hasChars("E")
-        assertThat(eb.selectionStart).isEqualTo(0)
-        assertThat(eb.selectionEnd).isEqualTo(0)
-    }
-
-    @Test
-    fun delete_intersects_first_half_of_selection() {
-        // AB[CD]E
-        val eb = EditingBuffer("ABCDE", TextRange(2, 4))
-
-        eb.delete(1, 3)
-        // A[D]E
-        assertThat(eb).hasChars("ADE")
-        assertThat(eb.selectionStart).isEqualTo(1)
-        assertThat(eb.selectionEnd).isEqualTo(2)
-    }
-
-    @Test
-    fun delete_intersects_first_half_of_reversedSelection() {
-        // AB[CD]E
-        val eb = EditingBuffer("ABCDE", TextRange(4, 2))
-
-        eb.delete(3, 1)
-        // A[D]E
-        assertThat(eb).hasChars("ADE")
-        assertThat(eb.selectionStart).isEqualTo(1)
-        assertThat(eb.selectionEnd).isEqualTo(2)
-    }
-
-    @Test
-    fun delete_intersects_second_half_of_selection() {
-        // A[BCD]EFG
-        val eb = EditingBuffer("ABCDEFG", TextRange(1, 4))
-
-        eb.delete(3, 5)
-        // A[BC]FG
-        assertThat(eb).hasChars("ABCFG")
-        assertThat(eb.selectionStart).isEqualTo(1)
-        assertThat(eb.selectionEnd).isEqualTo(3)
-    }
-
-    @Test
-    fun delete_intersects_second_half_of_reversedSelection() {
-        // A[BCD]EFG
-        val eb = EditingBuffer("ABCDEFG", TextRange(4, 1))
-
-        eb.delete(5, 3)
-        // A[BC]FG
-        assertThat(eb).hasChars("ABCFG")
-        assertThat(eb.selectionStart).isEqualTo(1)
-        assertThat(eb.selectionEnd).isEqualTo(3)
-    }
-
-    @Test
-    fun delete_preceding_composition_no_intersection() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.setComposition(1, 2)
-        eb.delete(2, 3)
-
-        assertThat(eb).hasChars("ABDE")
-        assertThat(eb.cursor).isEqualTo(0)
-        assertThat(eb.compositionStart).isEqualTo(1)
-        assertThat(eb.compositionEnd).isEqualTo(2)
-    }
-
-    @Test
-    fun delete_trailing_composition_no_intersection() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.setComposition(3, 4)
-        eb.delete(2, 3)
-
-        assertThat(eb).hasChars("ABDE")
-        assertThat(eb.cursor).isEqualTo(0)
-        assertThat(eb.compositionStart).isEqualTo(2)
-        assertThat(eb.compositionEnd).isEqualTo(3)
-    }
-
-    @Test
-    fun delete_preceding_composition_intersection() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.setComposition(1, 3)
-        eb.delete(2, 4)
-
-        assertThat(eb).hasChars("ABE")
-        assertThat(eb.cursor).isEqualTo(0)
-        assertThat(eb.compositionStart).isEqualTo(1)
-        assertThat(eb.compositionEnd).isEqualTo(2)
-    }
-
-    @Test
-    fun delete_trailing_composition_intersection() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.setComposition(3, 5)
-        eb.delete(2, 4)
-
-        assertThat(eb).hasChars("ABE")
-        assertThat(eb.cursor).isEqualTo(0)
-        assertThat(eb.compositionStart).isEqualTo(2)
-        assertThat(eb.compositionEnd).isEqualTo(3)
-    }
-
-    @Test
-    fun delete_composition_contains_delrange() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.setComposition(2, 5)
-        eb.delete(3, 4)
-
-        assertThat(eb).hasChars("ABCE")
-        assertThat(eb.cursor).isEqualTo(0)
-        assertThat(eb.compositionStart).isEqualTo(2)
-        assertThat(eb.compositionEnd).isEqualTo(4)
-    }
-
-    @Test
-    fun delete_delrange_contains_composition() {
-        val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
-        eb.setComposition(3, 4)
-        eb.delete(2, 5)
-
-        assertThat(eb).hasChars("AB")
-        assertThat(eb.cursor).isEqualTo(0)
-        assertThat(eb.hasComposition()).isFalse()
-    }
-}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldStateInternalBufferTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldStateInternalBufferTest.kt
deleted file mode 100644
index 63242e6..0000000
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldStateInternalBufferTest.kt
+++ /dev/null
@@ -1,518 +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.compose.foundation.text2.input.internal
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text2.input.InputTransformation
-import androidx.compose.foundation.text2.input.TextFieldCharSequence
-import androidx.compose.foundation.text2.input.TextFieldState
-import androidx.compose.ui.text.TextRange
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.fail
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalFoundationApi::class)
-@RunWith(JUnit4::class)
-class TextFieldStateInternalBufferTest {
-
-    @Test
-    fun initializeValue() {
-        val firstValue = TextFieldCharSequence("ABCDE", TextRange.Zero)
-        val state = TextFieldState(firstValue)
-
-        assertThat(state.text).isEqualTo(firstValue)
-    }
-
-    @Test
-    fun apply_commitTextCommand_changesValue() {
-        val firstValue = TextFieldCharSequence("ABCDE", TextRange.Zero)
-        val state = TextFieldState(firstValue)
-
-        var resetCalled = 0
-        state.addNotifyImeListener { _, _ -> resetCalled++ }
-
-        state.editAsUser { commitText("X", 1) }
-        val newState = state.text
-
-        assertThat(newState.toString()).isEqualTo("XABCDE")
-        assertThat(newState.selectionInChars.min).isEqualTo(1)
-        assertThat(newState.selectionInChars.max).isEqualTo(1)
-        // edit command updates should not trigger reset listeners.
-        assertThat(resetCalled).isEqualTo(0)
-    }
-
-    @Test
-    fun apply_setSelectionCommand_changesValue() {
-        val firstValue = TextFieldCharSequence("ABCDE", TextRange.Zero)
-        val state = TextFieldState(firstValue)
-
-        var resetCalled = 0
-        state.addNotifyImeListener { _, _ -> resetCalled++ }
-
-        state.editAsUser { setSelection(0, 2) }
-        val newState = state.text
-
-        assertThat(newState.toString()).isEqualTo("ABCDE")
-        assertThat(newState.selectionInChars.min).isEqualTo(0)
-        assertThat(newState.selectionInChars.max).isEqualTo(2)
-        // edit command updates should not trigger reset listeners.
-        assertThat(resetCalled).isEqualTo(0)
-    }
-
-    @Test
-    fun testNewState_bufferNotUpdated_ifSameModelStructurally() {
-        val state = TextFieldState()
-        var resetCalled = 0
-        state.addNotifyImeListener { _, _ -> resetCalled++ }
-
-        val initialBuffer = state.mainBuffer
-        state.resetStateAndNotifyIme(
-            TextFieldCharSequence("qwerty", TextRange.Zero, TextRange.Zero)
-        )
-        assertThat(state.mainBuffer).isNotSameInstanceAs(initialBuffer)
-
-        val updatedBuffer = state.mainBuffer
-        state.resetStateAndNotifyIme(
-            TextFieldCharSequence("qwerty", TextRange.Zero, TextRange.Zero)
-        )
-        assertThat(state.mainBuffer).isSameInstanceAs(updatedBuffer)
-
-        assertThat(resetCalled).isEqualTo(2)
-    }
-
-    @Test
-    fun testNewState_new_buffer_created_if_text_is_different() {
-        val state = TextFieldState()
-        var resetCalled = 0
-        state.addNotifyImeListener { _, _ -> resetCalled++ }
-
-        val textFieldValue = TextFieldCharSequence("qwerty", TextRange.Zero, TextRange.Zero)
-        state.resetStateAndNotifyIme(textFieldValue)
-        val initialBuffer = state.mainBuffer
-
-        val newTextFieldValue = TextFieldCharSequence("abc")
-        state.resetStateAndNotifyIme(newTextFieldValue)
-
-        assertThat(state.mainBuffer).isNotSameInstanceAs(initialBuffer)
-        assertThat(resetCalled).isEqualTo(2)
-    }
-
-    @Test
-    fun testNewState_buffer_not_recreated_if_selection_is_different() {
-        val state = TextFieldState()
-        var resetCalled = 0
-        state.addNotifyImeListener { _, _ -> resetCalled++ }
-
-        val textFieldValue = TextFieldCharSequence("qwerty", TextRange.Zero, TextRange.Zero)
-        state.resetStateAndNotifyIme(textFieldValue)
-        val initialBuffer = state.mainBuffer
-
-        val newTextFieldValue = TextFieldCharSequence(textFieldValue, selection = TextRange(1))
-        state.resetStateAndNotifyIme(newTextFieldValue)
-
-        assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
-        assertThat(newTextFieldValue.selectionInChars.start)
-            .isEqualTo(state.mainBuffer.selectionStart)
-        assertThat(newTextFieldValue.selectionInChars.end).isEqualTo(
-            state.mainBuffer.selectionEnd
-        )
-        assertThat(resetCalled).isEqualTo(2)
-    }
-
-    @Test
-    fun testNewState_buffer_not_recreated_if_composition_is_different() {
-        val state = TextFieldState()
-        var resetCalled = 0
-        state.addNotifyImeListener { _, _ -> resetCalled++ }
-
-        val textFieldValue = TextFieldCharSequence("qwerty", TextRange.Zero, TextRange(1))
-        state.resetStateAndNotifyIme(textFieldValue)
-        val initialBuffer = state.mainBuffer
-
-        // composition can not be set from app, IME owns it.
-        assertThat(EditingBuffer.NOWHERE).isEqualTo(initialBuffer.compositionStart)
-        assertThat(EditingBuffer.NOWHERE).isEqualTo(initialBuffer.compositionEnd)
-
-        val newTextFieldValue = TextFieldCharSequence(
-            textFieldValue,
-            textFieldValue.selectionInChars,
-            composition = null
-        )
-        state.resetStateAndNotifyIme(newTextFieldValue)
-
-        assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
-        assertThat(EditingBuffer.NOWHERE).isEqualTo(state.mainBuffer.compositionStart)
-        assertThat(EditingBuffer.NOWHERE).isEqualTo(state.mainBuffer.compositionEnd)
-        assertThat(resetCalled).isEqualTo(2)
-    }
-
-    @Test
-    fun testNewState_reversedSelection_setsTheSelection() {
-        val initialSelection = TextRange(2, 1)
-        val textFieldValue = TextFieldCharSequence("qwerty", initialSelection, TextRange(1))
-        val state = TextFieldState(textFieldValue)
-        var resetCalled = 0
-        state.addNotifyImeListener { _, _ -> resetCalled++ }
-
-        val initialBuffer = state.mainBuffer
-
-        assertThat(initialSelection.start).isEqualTo(initialBuffer.selectionStart)
-        assertThat(initialSelection.end).isEqualTo(initialBuffer.selectionEnd)
-
-        val updatedSelection = TextRange(3, 0)
-        val newTextFieldValue = TextFieldCharSequence(textFieldValue, selection = updatedSelection)
-        // set the new selection
-        state.resetStateAndNotifyIme(newTextFieldValue)
-
-        assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
-        assertThat(updatedSelection.start).isEqualTo(initialBuffer.selectionStart)
-        assertThat(updatedSelection.end).isEqualTo(initialBuffer.selectionEnd)
-        assertThat(resetCalled).isEqualTo(1)
-    }
-
-    @Test
-    fun compositionIsCleared_when_textChanged() {
-        val state = TextFieldState()
-        var resetCalled = 0
-        state.addNotifyImeListener { _, _ -> resetCalled++ }
-
-        // set the initial value
-        state.editAsUser {
-            commitText("ab", 0)
-            setComposingRegion(0, 2)
-        }
-
-        // change the text
-        val newValue =
-            TextFieldCharSequence(
-                "cd",
-                state.text.selectionInChars,
-                state.text.compositionInChars
-            )
-        state.resetStateAndNotifyIme(newValue)
-
-        assertThat(state.text.toString()).isEqualTo(newValue.toString())
-        assertThat(state.text.compositionInChars).isNull()
-    }
-
-    @Test
-    fun compositionIsNotCleared_when_textIsSame() {
-        val state = TextFieldState()
-        val composition = TextRange(0, 2)
-
-        // set the initial value
-        state.editAsUser {
-            commitText("ab", 0)
-            setComposingRegion(composition.start, composition.end)
-        }
-
-        // use the same TextFieldValue
-        val newValue =
-            TextFieldCharSequence(
-                state.text,
-                state.text.selectionInChars,
-                state.text.compositionInChars
-            )
-        state.resetStateAndNotifyIme(newValue)
-
-        assertThat(state.text.toString()).isEqualTo(newValue.toString())
-        assertThat(state.text.compositionInChars).isEqualTo(composition)
-    }
-
-    @Test
-    fun compositionIsCleared_when_compositionReset() {
-        val state = TextFieldState()
-
-        // set the initial value
-        state.editAsUser {
-            commitText("ab", 0)
-            setComposingRegion(-1, -1)
-        }
-
-        // change the composition
-        val newValue =
-            TextFieldCharSequence(
-                state.text,
-                state.text.selectionInChars,
-                composition = TextRange(0, 2)
-            )
-        state.resetStateAndNotifyIme(newValue)
-
-        assertThat(state.text.toString()).isEqualTo(newValue.toString())
-        assertThat(state.text.compositionInChars).isNull()
-    }
-
-    @Test
-    fun compositionIsCleared_when_compositionChanged() {
-        val state = TextFieldState()
-
-        // set the initial value
-        state.editAsUser {
-            commitText("ab", 0)
-            setComposingRegion(0, 2)
-        }
-
-        // change the composition
-        val newValue = TextFieldCharSequence(
-            state.text,
-            state.text.selectionInChars,
-            composition = TextRange(0, 1)
-        )
-        state.resetStateAndNotifyIme(newValue)
-
-        assertThat(state.text.toString()).isEqualTo(newValue.toString())
-        assertThat(state.text.compositionInChars).isNull()
-    }
-
-    @Test
-    fun compositionIsNotCleared_when_onlySelectionChanged() {
-        val state = TextFieldState()
-
-        val composition = TextRange(0, 2)
-        val selection = TextRange(0, 2)
-
-        // set the initial value
-        state.editAsUser {
-            commitText("ab", 0)
-            setComposingRegion(composition.start, composition.end)
-            setSelection(selection.start, selection.end)
-        }
-
-        // change selection
-        val newSelection = TextRange(1)
-        val newValue = TextFieldCharSequence(
-            state.text,
-            selection = newSelection,
-            composition = state.text.compositionInChars
-        )
-        state.resetStateAndNotifyIme(newValue)
-
-        assertThat(state.text.toString()).isEqualTo(newValue.toString())
-        assertThat(state.text.compositionInChars).isEqualTo(composition)
-        assertThat(state.text.selectionInChars).isEqualTo(newSelection)
-    }
-
-    @Test
-    fun filterThatDoesNothing_doesNotResetBuffer() {
-        val state = TextFieldState(
-            TextFieldCharSequence(
-                "abc",
-                selection = TextRange(3),
-                composition = TextRange(0, 3)
-            )
-        )
-
-        val initialBuffer = state.mainBuffer
-
-        state.editAsUser { commitText("d", 4) }
-
-        val value = state.text
-
-        assertThat(value.toString()).isEqualTo("abcd")
-        assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
-    }
-
-    @Test
-    fun returningTheEquivalentValueFromFilter_doesNotResetBuffer() {
-        val state = TextFieldState(
-            TextFieldCharSequence(
-                "abc",
-                selection = TextRange(3),
-                composition = TextRange(0, 3)
-            )
-        )
-
-        val initialBuffer = state.mainBuffer
-
-        state.editAsUser { commitText("d", 4) }
-
-        val value = state.text
-
-        assertThat(value.toString()).isEqualTo("abcd")
-        assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
-    }
-
-    @Test
-    fun returningOldValueFromFilter_resetsTheBuffer() {
-        val state = TextFieldState(
-            TextFieldCharSequence(
-                "abc",
-                selection = TextRange(3),
-                composition = TextRange(0, 3)
-            )
-        )
-
-        var resetCalledOld: TextFieldCharSequence? = null
-        var resetCalledNew: TextFieldCharSequence? = null
-        state.addNotifyImeListener { old, new ->
-            resetCalledOld = old
-            resetCalledNew = new
-        }
-
-        val initialBuffer = state.mainBuffer
-
-        state.editAsUser(
-            inputTransformation = { _, new -> new.revertAllChanges() },
-            notifyImeOfChanges = false
-        ) {
-            commitText("d", 4)
-        }
-
-        val value = state.text
-
-        assertThat(value.toString()).isEqualTo("abc")
-        assertThat(state.mainBuffer).isNotSameInstanceAs(initialBuffer)
-        assertThat(resetCalledOld?.toString()).isEqualTo("abcd") // what IME applied
-        assertThat(resetCalledNew?.toString()).isEqualTo("abc") // what is decided by filter
-    }
-
-    @Test
-    fun filterNotRan_whenNoCommands() {
-        val initialValue =
-            TextFieldCharSequence("hello", selection = TextRange(2), composition = null)
-        val state = TextFieldState(initialValue)
-        val inputTransformation = InputTransformation { old, new ->
-            fail("filter ran, old=\"$old\", new=\"$new\"")
-        }
-
-        state.editAsUser(inputTransformation, notifyImeOfChanges = false) {}
-    }
-
-    @Test
-    fun filterNotRan_whenOnlyFinishComposingTextCommand_noComposition() {
-        val initialValue =
-            TextFieldCharSequence("hello", selection = TextRange(2), composition = null)
-        val state = TextFieldState(initialValue)
-        val inputTransformation = InputTransformation { old, new ->
-            fail("filter ran, old=\"$old\", new=\"$new\"")
-        }
-
-        state.editAsUser(
-            inputTransformation = inputTransformation,
-            notifyImeOfChanges = false
-        ) { finishComposingText() }
-    }
-
-    @Test
-    fun filterNotRan_whenOnlyFinishComposingTextCommand_withComposition() {
-        val initialValue =
-            TextFieldCharSequence("hello", selection = TextRange(2), composition = TextRange(0, 5))
-        val state = TextFieldState(initialValue)
-        val inputTransformation = InputTransformation { old, new ->
-            fail("filter ran, old=\"$old\", new=\"$new\"")
-        }
-
-        state.editAsUser(
-            inputTransformation = inputTransformation,
-            notifyImeOfChanges = false
-        ) { finishComposingText() }
-    }
-
-    @Test
-    fun filterNotRan_whenCommandsResultInInitialValue() {
-        val initialValue =
-            TextFieldCharSequence("hello", selection = TextRange(2), composition = TextRange(0, 5))
-        val state = TextFieldState(initialValue)
-        val inputTransformation = InputTransformation { old, new ->
-            fail(
-                "filter ran, old=\"$old\" (${old.selectionInChars}), " +
-                    "new=\"$new\" (${new.selectionInChars})"
-            )
-        }
-
-        state.editAsUser(inputTransformation = inputTransformation, notifyImeOfChanges = false) {
-            setComposingRegion(0, 5)
-            commitText("hello", 1)
-            setSelection(2, 2)
-        }
-    }
-
-    @Test
-    fun filterRan_whenOnlySelectionChanges() {
-        val initialValue =
-            TextFieldCharSequence("hello", selection = TextRange(2), composition = null)
-        var filterRan = false
-        val state = TextFieldState(initialValue)
-        val inputTransformation = InputTransformation { old, new ->
-            // Filter should only run once.
-            assertThat(filterRan).isFalse()
-            filterRan = true
-            assertThat(new.toString()).isEqualTo(old.toString())
-            assertThat(old.selectionInChars).isEqualTo(TextRange(2))
-            assertThat(new.selectionInChars).isEqualTo(TextRange(0, 5))
-        }
-
-        state.editAsUser(
-            inputTransformation = inputTransformation,
-            notifyImeOfChanges = false
-        ) { setSelection(0, 5) }
-    }
-
-    @Test
-    fun filterRan_whenOnlyTextChanges() {
-        val initialValue =
-            TextFieldCharSequence("hello", selection = TextRange(2), composition = null)
-        var filterRan = false
-        val state = TextFieldState(initialValue)
-        val inputTransformation = InputTransformation { old, new ->
-            // Filter should only run once.
-            assertThat(filterRan).isFalse()
-            filterRan = true
-            assertThat(new.selectionInChars).isEqualTo(old.selectionInChars)
-            assertThat(old.toString()).isEqualTo("hello")
-            assertThat(new.toString()).isEqualTo("world")
-        }
-
-        state.editAsUser(inputTransformation = inputTransformation, notifyImeOfChanges = false) {
-            deleteAll()
-            commitText("world", 1)
-            setSelection(2, 2)
-        }
-    }
-
-    @Test
-    fun stateUpdated_whenOnlyCompositionChanges_noFilter() {
-        val initialValue =
-            TextFieldCharSequence("hello", selection = TextRange(5), composition = TextRange(0, 5))
-        val state = TextFieldState(initialValue)
-
-        state.editAsUser { setComposingRegion(2, 3) }
-
-        assertThat(state.text.compositionInChars).isEqualTo(TextRange(2, 3))
-    }
-
-    @Test
-    fun stateUpdated_whenOnlyCompositionChanges_withFilter() {
-        val initialValue =
-            TextFieldCharSequence("hello", selection = TextRange(5), composition = TextRange(0, 5))
-        val state = TextFieldState(initialValue)
-
-        state.editAsUser { setComposingRegion(2, 3) }
-
-        assertThat(state.text.compositionInChars).isEqualTo(TextRange(2, 3))
-    }
-
-    private fun TextFieldState(
-        value: TextFieldCharSequence
-    ) = TextFieldState(value.toString(), value.selectionInChars)
-
-    private fun TextFieldState.editAsUser(block: EditingBuffer.() -> Unit) {
-        editAsUser(null, false, block)
-    }
-}
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_view_carousel.xml b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_view_carousel.xml
index 61e862c..9b22250 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_view_carousel.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_view_carousel.xml
@@ -22,7 +22,7 @@
     android:layout_width="match_parent"
     android:layout_height="400dp">
 
-    <androidx.viewpager2.widget.ViewPager2
+    <androidx.recyclerview.widget.RecyclerView
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@+id/carousel"
         android:layout_gravity="center"
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_view_as_carousel_item.xml b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_view_as_carousel_item.xml
index 03eb6bd..5b5aa07 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_view_as_carousel_item.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_view_as_carousel_item.xml
@@ -17,7 +17,7 @@
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="200dp"
     android:background="#000000"
-    android:layout_height="200dp"
+    android:layout_height="match_parent"
     android:layout_gravity="center">
 
     <TextView
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/PagerAsCarouselBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/PagerAsCarouselBenchmark.kt
index becf364..f6b8b9c 100644
--- a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/PagerAsCarouselBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/PagerAsCarouselBenchmark.kt
@@ -29,6 +29,7 @@
 import androidx.test.uiautomator.Until
 import androidx.testutils.createCompilationParams
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -51,6 +52,7 @@
     }
 
     @Test
+    @Ignore("b/299155975")
     fun scroll() {
         val carousel = device.findObject(By.desc(ContentDescription))
         benchmarkRule.performRepeatedScroll(PackageName, compilationMode, Action, carousel) {
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/RecyclerViewAsCarouselBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/RecyclerViewAsCarouselBenchmark.kt
index 2152280..0316577 100644
--- a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/RecyclerViewAsCarouselBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/RecyclerViewAsCarouselBenchmark.kt
@@ -24,7 +24,6 @@
 import androidx.test.uiautomator.UiDevice
 import androidx.testutils.createCompilationParams
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -46,7 +45,6 @@
     }
 
     @Test
-    @Ignore("b/297398943")
     fun scroll() {
         val carousel = device.findObject(
             By.res(
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialPerfettoSdkBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialPerfettoSdkBenchmark.kt
index 002886f..1cbbd38 100644
--- a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialPerfettoSdkBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialPerfettoSdkBenchmark.kt
@@ -27,6 +27,7 @@
 import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig.InitialProcessState
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
+import junit.framework.TestCase.assertTrue
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -58,7 +59,12 @@
             setupBlock = {
                 PerfettoCapture().enableAndroidxTracingPerfetto(
                     PerfettoSdkConfig(PACKAGE_NAME, InitialProcessState.Alive)
-                )
+                ).let { (resultCode, _) ->
+                    assertTrue(
+                        "Ensuring Perfetto SDK is enabled",
+                        resultCode in arrayOf(1, 2) // 1 = success, 2 = already enabled
+                    )
+                }
             }
         ) {
             startActivityAndWait(Intent(ACTION))
diff --git a/compose/lint/internal-lint-checks/build.gradle b/compose/lint/internal-lint-checks/build.gradle
index 3039214..c5f47fa 100644
--- a/compose/lint/internal-lint-checks/build.gradle
+++ b/compose/lint/internal-lint-checks/build.gradle
@@ -26,6 +26,7 @@
     compileOnly(libs.androidLintApi)
     compileOnly(libs.kotlinStdlib)
     implementation(project(":compose:lint:common"))
+    implementation(projectOrArtifact(":collection:collection"))
 
     testImplementation(project(":compose:lint:common-test"))
     testImplementation(libs.kotlinStdlib)
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/AsCollectionDetector.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/AsCollectionDetector.kt
new file mode 100644
index 0000000..2d470e0
--- /dev/null
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/AsCollectionDetector.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.lint
+
+import androidx.collection.MutableObjectList
+import androidx.collection.MutableScatterMap
+import androidx.collection.MutableScatterSet
+import androidx.collection.ObjectList
+import androidx.collection.ScatterMap
+import androidx.collection.ScatterSet
+import androidx.collection.scatterSetOf
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.impl.source.PsiClassReferenceType
+import java.util.EnumSet
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+
+/**
+ * Using [ScatterMap.asMap], [ScatterSet.asSet], [ObjectList.asList], or their mutable
+ * counterparts indicates that the developer may be using the collection incorrectly.
+ * Using the interfaces is slower access. It is best to use those only for when it touches
+ * public API.
+ */
+class AsCollectionDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableUastTypes() = listOf<Class<out UElement>>(
+        UCallExpression::class.java
+    )
+
+    override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
+        override fun visitCallExpression(node: UCallExpression) {
+            val methodName = node.methodName ?: return
+            if (methodName in MethodNames) {
+                val receiverType = node.receiverType as? PsiClassReferenceType ?: return
+                val qualifiedName = receiverType.reference.qualifiedName ?: return
+                val indexOfAngleBracket = qualifiedName.indexOf('<')
+                if (indexOfAngleBracket > 0 &&
+                    qualifiedName.substring(0, indexOfAngleBracket) in CollectionClasses
+                ) {
+                    context.report(
+                        ISSUE,
+                        node,
+                        context.getLocation(node),
+                        "Use method $methodName() only for public API usage"
+                    )
+                }
+            }
+        }
+    }
+
+    companion object {
+        private val MethodNames = scatterSetOf(
+            "asMap",
+            "asMutableMap",
+            "asSet",
+            "asMutableSet",
+            "asList",
+            "asMutableList"
+        )
+        private val CollectionClasses = scatterSetOf(
+            ScatterMap::class.qualifiedName,
+            MutableScatterMap::class.qualifiedName,
+            ScatterSet::class.qualifiedName,
+            MutableScatterSet::class.qualifiedName,
+            ObjectList::class.qualifiedName,
+            MutableObjectList::class.qualifiedName,
+        )
+
+        private val AsCollectionDetectorId = "AsCollectionCall"
+
+        val ISSUE = Issue.create(
+            id = AsCollectionDetectorId,
+            briefDescription = "High performance collections don't implement standard collection " +
+                "interfaces so that they can remain high performance. Converting to standard " +
+                "collections wraps the classes with another object. Use these interface " +
+                "wrappers only for exposing to public API.",
+            explanation = "ScatterMap, ScatterSet, and AnyList are written for high " +
+                "performance access. Using the standard collection interfaces for these classes " +
+                "forces slower performance access to these collections. The methods returning " +
+                "these interfaces should be limited to public API, where standard collection " +
+                "interfaces are expected.",
+            category = Category.PERFORMANCE, priority = 3, severity = Severity.ERROR,
+            implementation = Implementation(
+                AsCollectionDetector::class.java,
+                EnumSet.of(Scope.JAVA_FILE)
+            )
+        )
+    }
+}
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
index 7e43f93..f13628f 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
@@ -28,6 +28,7 @@
     override val api = 14
     override val issues get(): List<Issue> {
         return listOf(
+            AsCollectionDetector.ISSUE,
             ExceptionMessageDetector.ISSUE,
             ListIteratorDetector.ISSUE,
             SteppedForLoopDetector.ISSUE,
diff --git a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/AsCollectionDetectorTest.kt b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/AsCollectionDetectorTest.kt
new file mode 100644
index 0000000..39ce2a1
--- /dev/null
+++ b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/AsCollectionDetectorTest.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/* ktlint-disable max-line-length */
+@RunWith(Parameterized::class)
+class AsCollectionDetectorTest(
+    val types: CollectionType
+) : LintDetectorTest() {
+
+    override fun getDetector(): Detector = AsCollectionDetector()
+
+    override fun getIssues(): MutableList<Issue> =
+        mutableListOf(AsCollectionDetector.ISSUE)
+
+    private val collectionTilde = "~".repeat(types.collection.length)
+
+    @Test
+    fun immutableAsImmutable() {
+        lint().files(
+            ScatterMapClass,
+            ScatterSetClass,
+            ObjectListClass,
+            kotlin(
+                """
+                        package androidx.compose.lint
+
+                        import androidx.collection.${types.immutable}
+
+                        fun foo(collection: ${types.immutable}${types.params}): ${types.collection}${types.params} =
+                            collection.as${types.collection}()
+                        """
+            )
+        ).run().expect(
+            """
+src/androidx/compose/lint/test.kt:7: Error: Use method as${types.collection}() only for public API usage [AsCollectionCall]
+                            collection.as${types.collection}()
+                            ~~~~~~~~~~~~~$collectionTilde~~
+1 errors, 0 warnings
+            """
+        )
+    }
+
+    @Test
+    fun mutableAsImmutable() {
+        lint().files(
+            ScatterMapClass,
+            ScatterSetClass,
+            ObjectListClass,
+            kotlin(
+                """
+                        package androidx.compose.lint
+
+                        import androidx.collection.Mutable${types.immutable}
+
+                        fun foo(collection: Mutable${types.immutable}${types.params}): ${types.collection}${types.params} =
+                            collection.as${types.collection}()
+                        """
+            )
+        ).run().expect(
+            """
+src/androidx/compose/lint/test.kt:7: Error: Use method as${types.collection}() only for public API usage [AsCollectionCall]
+                            collection.as${types.collection}()
+                            ~~~~~~~~~~~~~$collectionTilde~~
+1 errors, 0 warnings
+            """
+        )
+    }
+
+    @Test
+    fun mutableAsMutable() {
+        lint().files(
+            ScatterMapClass,
+            ScatterSetClass,
+            ObjectListClass,
+            kotlin(
+                """
+                        package androidx.compose.lint
+
+                        import androidx.collection.Mutable${types.immutable}
+
+                        fun foo(collection: Mutable${types.immutable}${types.params}): Mutable${types.collection}${types.params} =
+                            collection.asMutable${types.collection}()
+                        """
+            )
+        ).run().expect(
+            """
+src/androidx/compose/lint/test.kt:7: Error: Use method asMutable${types.collection}() only for public API usage [AsCollectionCall]
+                            collection.asMutable${types.collection}()
+                            ~~~~~~~~~~~~~~~~~~~~$collectionTilde~~
+1 errors, 0 warnings
+            """
+        )
+    }
+
+    @Test
+    fun nonCollectionAs() {
+        lint().files(
+            kotlin(
+                """
+                        package androidx.compose.lint
+
+                        fun foo(): ${types.collection}${types.params} =
+                            WeirdCollection().as${types.collection}()
+
+                        class WeirdCollection {
+                            fun asList(): List<String>? = null
+                            fun asSet(): Set<String>? = null
+                            fun asMap(): Map<String, String>? = null
+                        }
+                        """
+            )
+        ).run().expectClean()
+    }
+
+    class CollectionType(
+        val immutable: String,
+        val collection: String,
+        val params: String
+    )
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters() = listOf(
+            CollectionType("ScatterMap", "Map", "<String, String>"),
+            CollectionType("ScatterSet", "Set", "<String>"),
+            CollectionType("ObjectList", "List", "<String>")
+        )
+
+        val ScatterMapClass = kotlin(
+            """
+            package androidx.collection
+            sealed class ScatterMap<K, V> {
+                fun asMap(): Map<K, V> = mapOf()
+            }
+
+            class MutableScatterMap<K, V> : ScatterMap<K, V>() {
+                fun asMutableMap(): MutableMap<K, V> = mutableMapOf()
+            }
+            """.trimIndent()
+        )
+
+        val ScatterSetClass = kotlin(
+            """
+            package androidx.collection
+            sealed class ScatterSet<E> {
+                fun asSet(): Set<E> = setOf()
+            }
+
+            class MutableScatterSet<E> : ScatterSet<E>() {
+                fun asMutableSet(): MutableSet<E> = mutableSetOf()
+            }
+            """.trimIndent()
+        )
+
+        val ObjectListClass = kotlin(
+            """
+            package androidx.collection
+            sealed class ObjectList<E> {
+                fun asList(): List<E> = listOf()
+            }
+
+            class MutableObjectList<E> : ObjectList<E>() {
+                fun asMutableList(): MutableList<E> = mutableListOf()
+            }
+            """.trimIndent()
+        )
+    }
+}
diff --git a/compose/material/material-icons-core/build.gradle b/compose/material/material-icons-core/build.gradle
index fe670b2..ed5ea61 100644
--- a/compose/material/material-icons-core/build.gradle
+++ b/compose/material/material-icons-core/build.gradle
@@ -85,13 +85,13 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
             }
         }
 
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
             }
diff --git a/compose/material/material-icons-extended/build.gradle b/compose/material/material-icons-extended/build.gradle
index e6002b6..bd59f9f 100644
--- a/compose/material/material-icons-extended/build.gradle
+++ b/compose/material/material-icons-extended/build.gradle
@@ -88,7 +88,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:foundation:foundation"))
@@ -108,7 +108,7 @@
             }
         }
 
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
             }
diff --git a/compose/material/material-icons-extended/src/androidAndroidTest/kotlin/androidx/compose/material/icons/BaseIconComparisonTest.kt b/compose/material/material-icons-extended/src/androidInstrumentedTest/kotlin/androidx/compose/material/icons/BaseIconComparisonTest.kt
similarity index 100%
rename from compose/material/material-icons-extended/src/androidAndroidTest/kotlin/androidx/compose/material/icons/BaseIconComparisonTest.kt
rename to compose/material/material-icons-extended/src/androidInstrumentedTest/kotlin/androidx/compose/material/icons/BaseIconComparisonTest.kt
diff --git a/compose/material/material-icons-extended/src/androidAndroidTest/kotlin/androidx/compose/material/icons/CoreAutoMirroredIconComparisonTest.kt b/compose/material/material-icons-extended/src/androidInstrumentedTest/kotlin/androidx/compose/material/icons/CoreAutoMirroredIconComparisonTest.kt
similarity index 100%
rename from compose/material/material-icons-extended/src/androidAndroidTest/kotlin/androidx/compose/material/icons/CoreAutoMirroredIconComparisonTest.kt
rename to compose/material/material-icons-extended/src/androidInstrumentedTest/kotlin/androidx/compose/material/icons/CoreAutoMirroredIconComparisonTest.kt
diff --git a/compose/material/material-icons-extended/src/androidAndroidTest/kotlin/androidx/compose/material/icons/CoreIconComparisonTest.kt b/compose/material/material-icons-extended/src/androidInstrumentedTest/kotlin/androidx/compose/material/icons/CoreIconComparisonTest.kt
similarity index 100%
rename from compose/material/material-icons-extended/src/androidAndroidTest/kotlin/androidx/compose/material/icons/CoreIconComparisonTest.kt
rename to compose/material/material-icons-extended/src/androidInstrumentedTest/kotlin/androidx/compose/material/icons/CoreIconComparisonTest.kt
diff --git a/compose/material/material-icons-extended/src/androidAndroidTest/kotlin/androidx/compose/material/icons/ExtendedAutoMirroredIconComparisonTest.kt b/compose/material/material-icons-extended/src/androidInstrumentedTest/kotlin/androidx/compose/material/icons/ExtendedAutoMirroredIconComparisonTest.kt
similarity index 100%
rename from compose/material/material-icons-extended/src/androidAndroidTest/kotlin/androidx/compose/material/icons/ExtendedAutoMirroredIconComparisonTest.kt
rename to compose/material/material-icons-extended/src/androidInstrumentedTest/kotlin/androidx/compose/material/icons/ExtendedAutoMirroredIconComparisonTest.kt
diff --git a/compose/material/material-icons-extended/src/androidAndroidTest/kotlin/androidx/compose/material/icons/ExtendedIconComparisonTest.kt b/compose/material/material-icons-extended/src/androidInstrumentedTest/kotlin/androidx/compose/material/icons/ExtendedIconComparisonTest.kt
similarity index 100%
rename from compose/material/material-icons-extended/src/androidAndroidTest/kotlin/androidx/compose/material/icons/ExtendedIconComparisonTest.kt
rename to compose/material/material-icons-extended/src/androidInstrumentedTest/kotlin/androidx/compose/material/icons/ExtendedIconComparisonTest.kt
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index c2a484d..f4f8356 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -88,7 +88,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:test-utils"))
@@ -104,7 +104,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
diff --git a/compose/material/material-ripple/src/androidAndroidTest/kotlin/androidx/compose/material/ripple/RippleContainerTest.kt b/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleContainerTest.kt
similarity index 100%
rename from compose/material/material-ripple/src/androidAndroidTest/kotlin/androidx/compose/material/ripple/RippleContainerTest.kt
rename to compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleContainerTest.kt
diff --git a/compose/material/material-ripple/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt b/compose/material/material-ripple/src/androidUnitTest/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt
similarity index 100%
rename from compose/material/material-ripple/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt
rename to compose/material/material-ripple/src/androidUnitTest/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index d319950..7f4c7216 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -141,20 +141,21 @@
   }
 
   public final class BottomSheetScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BottomSheetScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.BottomSheetScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit>? topBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional int floatingActionButtonPosition, optional boolean sheetGesturesEnabled, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional float sheetPeekHeight, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @Deprecated @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BottomSheetScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.BottomSheetScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit>? topBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional int floatingActionButtonPosition, optional boolean sheetGesturesEnabled, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional float sheetPeekHeight, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BottomSheetScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.BottomSheetScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit>? topBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional int floatingActionButtonPosition, optional boolean sheetGesturesEnabled, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional float sheetPeekHeight, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
     method @Deprecated @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.material.BottomSheetState BottomSheetScaffoldState(androidx.compose.material.BottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmStateChange);
+    method @Deprecated @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.material.BottomSheetScaffoldState BottomSheetScaffoldState(androidx.compose.material.DrawerState drawerState, androidx.compose.material.BottomSheetState bottomSheetState, androidx.compose.material.SnackbarHostState snackbarHostState);
     method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public static androidx.compose.material.BottomSheetState BottomSheetState(androidx.compose.material.BottomSheetValue initialValue, androidx.compose.ui.unit.Density density, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmValueChange);
-    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomSheetScaffoldState rememberBottomSheetScaffoldState(optional androidx.compose.material.DrawerState drawerState, optional androidx.compose.material.BottomSheetState bottomSheetState, optional androidx.compose.material.SnackbarHostState snackbarHostState);
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomSheetScaffoldState rememberBottomSheetScaffoldState(optional androidx.compose.material.BottomSheetState bottomSheetState, optional androidx.compose.material.SnackbarHostState snackbarHostState);
+    method @Deprecated @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomSheetScaffoldState rememberBottomSheetScaffoldState(optional androidx.compose.material.DrawerState drawerState, optional androidx.compose.material.BottomSheetState bottomSheetState, optional androidx.compose.material.SnackbarHostState snackbarHostState);
     method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomSheetState rememberBottomSheetState(androidx.compose.material.BottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmStateChange);
   }
 
   @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public final class BottomSheetScaffoldState {
-    ctor public BottomSheetScaffoldState(androidx.compose.material.DrawerState drawerState, androidx.compose.material.BottomSheetState bottomSheetState, androidx.compose.material.SnackbarHostState snackbarHostState);
+    ctor public BottomSheetScaffoldState(androidx.compose.material.BottomSheetState bottomSheetState, androidx.compose.material.SnackbarHostState snackbarHostState);
     method public androidx.compose.material.BottomSheetState getBottomSheetState();
-    method public androidx.compose.material.DrawerState getDrawerState();
     method public androidx.compose.material.SnackbarHostState getSnackbarHostState();
     property public final androidx.compose.material.BottomSheetState bottomSheetState;
-    property public final androidx.compose.material.DrawerState drawerState;
     property public final androidx.compose.material.SnackbarHostState snackbarHostState;
   }
 
@@ -660,7 +661,10 @@
   public final class ScaffoldKt {
     method @androidx.compose.runtime.Composable public static void Scaffold(androidx.compose.foundation.layout.WindowInsets contentWindowInsets, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional boolean isFloatingActionButtonDocked, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional boolean isFloatingActionButtonDocked, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static boolean getScaffoldSubcomposeInMeasureFix();
     method @androidx.compose.runtime.Composable public static androidx.compose.material.ScaffoldState rememberScaffoldState(optional androidx.compose.material.DrawerState drawerState, optional androidx.compose.material.SnackbarHostState snackbarHostState);
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static void setScaffoldSubcomposeInMeasureFix(boolean);
+    property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final boolean ScaffoldSubcomposeInMeasureFix;
   }
 
   @androidx.compose.runtime.Stable public final class ScaffoldState {
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index d319950..7f4c7216 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -141,20 +141,21 @@
   }
 
   public final class BottomSheetScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BottomSheetScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.BottomSheetScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit>? topBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional int floatingActionButtonPosition, optional boolean sheetGesturesEnabled, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional float sheetPeekHeight, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @Deprecated @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BottomSheetScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.BottomSheetScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit>? topBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional int floatingActionButtonPosition, optional boolean sheetGesturesEnabled, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional float sheetPeekHeight, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BottomSheetScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.BottomSheetScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit>? topBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional int floatingActionButtonPosition, optional boolean sheetGesturesEnabled, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional float sheetPeekHeight, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
     method @Deprecated @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.material.BottomSheetState BottomSheetScaffoldState(androidx.compose.material.BottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmStateChange);
+    method @Deprecated @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.material.BottomSheetScaffoldState BottomSheetScaffoldState(androidx.compose.material.DrawerState drawerState, androidx.compose.material.BottomSheetState bottomSheetState, androidx.compose.material.SnackbarHostState snackbarHostState);
     method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public static androidx.compose.material.BottomSheetState BottomSheetState(androidx.compose.material.BottomSheetValue initialValue, androidx.compose.ui.unit.Density density, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmValueChange);
-    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomSheetScaffoldState rememberBottomSheetScaffoldState(optional androidx.compose.material.DrawerState drawerState, optional androidx.compose.material.BottomSheetState bottomSheetState, optional androidx.compose.material.SnackbarHostState snackbarHostState);
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomSheetScaffoldState rememberBottomSheetScaffoldState(optional androidx.compose.material.BottomSheetState bottomSheetState, optional androidx.compose.material.SnackbarHostState snackbarHostState);
+    method @Deprecated @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomSheetScaffoldState rememberBottomSheetScaffoldState(optional androidx.compose.material.DrawerState drawerState, optional androidx.compose.material.BottomSheetState bottomSheetState, optional androidx.compose.material.SnackbarHostState snackbarHostState);
     method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomSheetState rememberBottomSheetState(androidx.compose.material.BottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmStateChange);
   }
 
   @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public final class BottomSheetScaffoldState {
-    ctor public BottomSheetScaffoldState(androidx.compose.material.DrawerState drawerState, androidx.compose.material.BottomSheetState bottomSheetState, androidx.compose.material.SnackbarHostState snackbarHostState);
+    ctor public BottomSheetScaffoldState(androidx.compose.material.BottomSheetState bottomSheetState, androidx.compose.material.SnackbarHostState snackbarHostState);
     method public androidx.compose.material.BottomSheetState getBottomSheetState();
-    method public androidx.compose.material.DrawerState getDrawerState();
     method public androidx.compose.material.SnackbarHostState getSnackbarHostState();
     property public final androidx.compose.material.BottomSheetState bottomSheetState;
-    property public final androidx.compose.material.DrawerState drawerState;
     property public final androidx.compose.material.SnackbarHostState snackbarHostState;
   }
 
@@ -660,7 +661,10 @@
   public final class ScaffoldKt {
     method @androidx.compose.runtime.Composable public static void Scaffold(androidx.compose.foundation.layout.WindowInsets contentWindowInsets, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional boolean isFloatingActionButtonDocked, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional boolean isFloatingActionButtonDocked, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static boolean getScaffoldSubcomposeInMeasureFix();
     method @androidx.compose.runtime.Composable public static androidx.compose.material.ScaffoldState rememberScaffoldState(optional androidx.compose.material.DrawerState drawerState, optional androidx.compose.material.SnackbarHostState snackbarHostState);
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static void setScaffoldSubcomposeInMeasureFix(boolean);
+    property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final boolean ScaffoldSubcomposeInMeasureFix;
   }
 
   @androidx.compose.runtime.Stable public final class ScaffoldState {
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 8da78ab39..577c4b8 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -105,7 +105,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:material:material:material-samples"))
@@ -127,7 +127,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
index 111e82d..a3c0f64 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
@@ -23,6 +23,7 @@
 import androidx.compose.material.samples.BackdropScaffoldSample
 import androidx.compose.material.samples.BottomDrawerSample
 import androidx.compose.material.samples.BottomSheetScaffoldSample
+import androidx.compose.material.samples.BottomSheetScaffoldWithDrawerSample
 import androidx.compose.material.samples.ContentAlphaSample
 import androidx.compose.material.samples.CustomAlertDialogSample
 import androidx.compose.material.samples.CustomPullRefreshSample
@@ -52,7 +53,10 @@
         DemoCategory(
             "Bottom Sheets",
             listOf(
-                ComposableDemo("Bottom Sheet") { BottomSheetScaffoldSample() },
+                ComposableDemo("Standard Bottom Sheet") { BottomSheetScaffoldSample() },
+                ComposableDemo("Standard Bottom Sheet with Drawer") {
+                    BottomSheetScaffoldWithDrawerSample()
+                },
                 ComposableDemo("Modal Bottom Sheet") { ModalBottomSheetSample() },
             )
         ),
diff --git a/compose/material/material/lint-baseline.xml b/compose/material/material/lint-baseline.xml
index c8e0bc4..9b47bc3 100644
--- a/compose/material/material/lint-baseline.xml
+++ b/compose/material/material/lint-baseline.xml
@@ -7,7 +7,7 @@
         errorLine1="            Thread.sleep(300)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -16,7 +16,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -25,7 +25,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -34,7 +34,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -43,7 +43,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt"/>
     </issue>
 
     <issue
@@ -52,7 +52,7 @@
         errorLine1="            Thread.sleep(300)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt"/>
     </issue>
 
     <issue
@@ -61,7 +61,7 @@
         errorLine1="            Thread.sleep(300)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/NavigationRailScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/NavigationRailScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -70,7 +70,7 @@
         errorLine1="    Thread.sleep(1)"
         errorLine2="           ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/ObservableThemeTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/ObservableThemeTest.kt"/>
     </issue>
 
     <issue
@@ -79,7 +79,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -88,7 +88,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -97,7 +97,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -106,7 +106,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -115,7 +115,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -124,7 +124,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -133,7 +133,7 @@
         errorLine1="            Thread.sleep(300)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material/TabScreenshotTest.kt"/>
     </issue>
 
     <issue
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BottomSheetScaffoldSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BottomSheetScaffoldSamples.kt
index 116ab3e..cb3994c 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BottomSheetScaffoldSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BottomSheetScaffoldSamples.kt
@@ -27,17 +27,20 @@
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.BottomSheetScaffold
 import androidx.compose.material.Button
+import androidx.compose.material.DrawerValue
 import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.FabPosition
 import androidx.compose.material.FloatingActionButton
 import androidx.compose.material.Icon
 import androidx.compose.material.IconButton
+import androidx.compose.material.ModalDrawer
 import androidx.compose.material.Text
 import androidx.compose.material.TopAppBar
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.material.icons.filled.Menu
 import androidx.compose.material.rememberBottomSheetScaffoldState
+import androidx.compose.material.rememberDrawerState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -67,13 +70,17 @@
     BottomSheetScaffold(
         sheetContent = {
             Box(
-                Modifier.fillMaxWidth().height(128.dp),
+                Modifier
+                    .fillMaxWidth()
+                    .height(128.dp),
                 contentAlignment = Alignment.Center
             ) {
                 Text("Swipe up to expand sheet")
             }
             Column(
-                Modifier.fillMaxWidth().padding(64.dp),
+                Modifier
+                    .fillMaxWidth()
+                    .padding(64.dp),
                 horizontalAlignment = Alignment.CenterHorizontally
             ) {
                 Text("Sheet content")
@@ -89,14 +96,9 @@
         },
         scaffoldState = scaffoldState,
         topBar = {
-            TopAppBar(
-                title = { Text("Bottom sheet scaffold") },
-                navigationIcon = {
-                    IconButton(onClick = { scope.launch { scaffoldState.drawerState.open() } }) {
-                        Icon(Icons.Default.Menu, contentDescription = "Localized description")
-                    }
-                }
-            )
+            TopAppBar {
+                Text("Bottom sheet scaffold")
+            }
         },
         floatingActionButton = {
             var clickCount by remember { mutableStateOf(0) }
@@ -112,19 +114,7 @@
             }
         },
         floatingActionButtonPosition = FabPosition.End,
-        sheetPeekHeight = 128.dp,
-        drawerContent = {
-            Column(
-                Modifier.fillMaxWidth().padding(16.dp),
-                horizontalAlignment = Alignment.CenterHorizontally
-            ) {
-                Text("Drawer content")
-                Spacer(Modifier.height(20.dp))
-                Button(onClick = { scope.launch { scaffoldState.drawerState.close() } }) {
-                    Text("Click to close drawer")
-                }
-            }
-        }
+        sheetPeekHeight = 128.dp
     ) { innerPadding ->
         LazyColumn(contentPadding = innerPadding) {
             items(100) {
@@ -138,3 +128,95 @@
         }
     }
 }
+
+@Composable
+@OptIn(ExperimentalMaterialApi::class)
+fun BottomSheetScaffoldWithDrawerSample() {
+    val scope = rememberCoroutineScope()
+    val scaffoldState = rememberBottomSheetScaffoldState()
+    val drawerState = rememberDrawerState(DrawerValue.Closed)
+    ModalDrawer(
+        drawerState = drawerState,
+        drawerContent = {
+            Column(
+                Modifier
+                    .fillMaxWidth()
+                    .padding(16.dp),
+                horizontalAlignment = Alignment.CenterHorizontally
+            ) {
+                Text("Drawer content")
+                Spacer(Modifier.height(20.dp))
+                Button(onClick = { scope.launch { drawerState.close() } }) {
+                    Text("Click to close drawer")
+                }
+            }
+        }
+    ) {
+        BottomSheetScaffold(
+            sheetContent = {
+                Box(
+                    Modifier
+                        .fillMaxWidth()
+                        .height(128.dp),
+                    contentAlignment = Alignment.Center
+                ) {
+                    Text("Swipe up to expand sheet")
+                }
+                Column(
+                    Modifier
+                        .fillMaxWidth()
+                        .padding(64.dp),
+                    horizontalAlignment = Alignment.CenterHorizontally
+                ) {
+                    Text("Sheet content")
+                    Spacer(Modifier.height(20.dp))
+                    Button(
+                        onClick = {
+                            scope.launch { scaffoldState.bottomSheetState.collapse() }
+                        }
+                    ) {
+                        Text("Click to collapse sheet")
+                    }
+                }
+            },
+            scaffoldState = scaffoldState,
+            topBar = {
+                TopAppBar(
+                    title = { Text("Bottom sheet scaffold") },
+                    navigationIcon = {
+                        IconButton(onClick = { scope.launch { drawerState.open() } }) {
+                            Icon(Icons.Default.Menu, contentDescription = "Localized description")
+                        }
+                    }
+                )
+            },
+            floatingActionButton = {
+                var clickCount by remember { mutableStateOf(0) }
+                FloatingActionButton(
+                    onClick = {
+                        // show snackbar as a suspend function
+                        scope.launch {
+                            scaffoldState.snackbarHostState
+                                .showSnackbar("Snackbar #${++clickCount}")
+                        }
+                    }
+                ) {
+                    Icon(Icons.Default.Favorite, contentDescription = "Localized description")
+                }
+            },
+            floatingActionButtonPosition = FabPosition.End,
+            sheetPeekHeight = 128.dp
+        ) { innerPadding ->
+            LazyColumn(contentPadding = innerPadding) {
+                items(100) {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(50.dp)
+                            .background(colors[it % colors.size])
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
deleted file mode 100644
index c77c4e8..0000000
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
+++ /dev/null
@@ -1,789 +0,0 @@
-/*
- * Copyright 2020 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.material
-
-import android.os.Build
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.asAndroidBitmap
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.SubcomposeLayout
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.layout.positionInParent
-import androidx.compose.ui.layout.positionInRoot
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.swipeLeft
-import androidx.compose.ui.test.swipeRight
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.toSize
-import androidx.compose.ui.zIndex
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth
-import com.google.common.truth.Truth.assertThat
-import kotlin.math.roundToInt
-import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class ScaffoldTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    private val fabSpacing = 16.dp
-    private val scaffoldTag = "Scaffold"
-
-    @Test
-    fun scaffold_onlyContent_takesWholeScreen() {
-        rule.setMaterialContentForSizeAssertions(
-            parentMaxWidth = 100.dp,
-            parentMaxHeight = 100.dp
-        ) {
-            Scaffold {
-                Text("Scaffold body")
-            }
-        }
-            .assertWidthIsEqualTo(100.dp)
-            .assertHeightIsEqualTo(100.dp)
-    }
-
-    @Test
-    fun scaffold_onlyContent_stackSlot() {
-        var child1: Offset = Offset.Zero
-        var child2: Offset = Offset.Zero
-        rule.setMaterialContent {
-            Scaffold {
-                Text(
-                    "One",
-                    Modifier.onGloballyPositioned { child1 = it.positionInParent() }
-                )
-                Text(
-                    "Two",
-                    Modifier.onGloballyPositioned { child2 = it.positionInParent() }
-                )
-            }
-        }
-        assertThat(child1.y).isEqualTo(child2.y)
-        assertThat(child1.x).isEqualTo(child2.x)
-    }
-
-    @Test
-    fun scaffold_AppbarAndContent_inColumn() {
-        var appbarPosition: Offset = Offset.Zero
-        var appbarSize: IntSize = IntSize.Zero
-        var contentPosition: Offset = Offset.Zero
-        rule.setMaterialContent {
-            Scaffold(
-                topBar = {
-                    Box(
-                        Modifier
-                            .fillMaxWidth()
-                            .height(50.dp)
-                            .background(color = Color.Red)
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                appbarPosition = positioned.localToWindow(Offset.Zero)
-                                appbarSize = positioned.size
-                            }
-                    )
-                }
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxWidth()
-                        .height(50.dp)
-                        .background(Color.Blue)
-                        .onGloballyPositioned { contentPosition = it.localToWindow(Offset.Zero) }
-                )
-            }
-        }
-        assertThat(appbarPosition.y + appbarSize.height.toFloat())
-            .isEqualTo(contentPosition.y)
-    }
-
-    @Test
-    fun scaffold_bottomBarAndContent_inStack() {
-        var appbarPosition: Offset = Offset.Zero
-        var appbarSize: IntSize = IntSize.Zero
-        var contentPosition: Offset = Offset.Zero
-        var contentSize: IntSize = IntSize.Zero
-        rule.setMaterialContent {
-            Scaffold(
-                bottomBar = {
-                    Box(
-                        Modifier
-                            .fillMaxWidth()
-                            .height(50.dp)
-                            .background(color = Color.Red)
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                appbarPosition = positioned.positionInParent()
-                                appbarSize = positioned.size
-                            }
-                    )
-                }
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .height(50.dp)
-                        .background(color = Color.Blue)
-                        .onGloballyPositioned { positioned: LayoutCoordinates ->
-                            contentPosition = positioned.positionInParent()
-                            contentSize = positioned.size
-                        }
-                )
-            }
-        }
-        val appBarBottom = appbarPosition.y + appbarSize.height
-        val contentBottom = contentPosition.y + contentSize.height
-        assertThat(appBarBottom).isEqualTo(contentBottom)
-    }
-
-    @Test
-    @Ignore("unignore once animation sync is ready (b/147291885)")
-    fun scaffold_drawer_gestures() {
-        var drawerChildPosition: Offset = Offset.Zero
-        val drawerGesturedEnabledState = mutableStateOf(false)
-        rule.setContent {
-            Box(Modifier.testTag(scaffoldTag)) {
-                Scaffold(
-                    drawerContent = {
-                        Box(
-                            Modifier
-                                .fillMaxWidth()
-                                .height(50.dp)
-                                .background(color = Color.Blue)
-                                .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                    drawerChildPosition = positioned.positionInParent()
-                                }
-                        )
-                    },
-                    drawerGesturesEnabled = drawerGesturedEnabledState.value
-                ) {
-                    Box(
-                        Modifier
-                            .fillMaxWidth()
-                            .height(50.dp)
-                            .background(color = Color.Blue)
-                    )
-                }
-            }
-        }
-        assertThat(drawerChildPosition.x).isLessThan(0f)
-        rule.onNodeWithTag(scaffoldTag).performTouchInput {
-            swipeRight()
-        }
-        assertThat(drawerChildPosition.x).isLessThan(0f)
-        rule.onNodeWithTag(scaffoldTag).performTouchInput {
-            swipeLeft()
-        }
-        assertThat(drawerChildPosition.x).isLessThan(0f)
-
-        rule.runOnUiThread {
-            drawerGesturedEnabledState.value = true
-        }
-
-        rule.onNodeWithTag(scaffoldTag).performTouchInput {
-            swipeRight()
-        }
-        assertThat(drawerChildPosition.x).isEqualTo(0f)
-        rule.onNodeWithTag(scaffoldTag).performTouchInput {
-            swipeLeft()
-        }
-        assertThat(drawerChildPosition.x).isLessThan(0f)
-    }
-
-    @Test
-    @Ignore("unignore once animation sync is ready (b/147291885)")
-    fun scaffold_drawer_manualControl(): Unit = runBlocking {
-        var drawerChildPosition: Offset = Offset.Zero
-        lateinit var scaffoldState: ScaffoldState
-        rule.setContent {
-            scaffoldState = rememberScaffoldState()
-            Box(Modifier.testTag(scaffoldTag)) {
-                Scaffold(
-                    scaffoldState = scaffoldState,
-                    drawerContent = {
-                        Box(
-                            Modifier
-                                .fillMaxWidth()
-                                .height(50.dp)
-                                .background(color = Color.Blue)
-                                .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                    drawerChildPosition = positioned.positionInParent()
-                                }
-                        )
-                    }
-                ) {
-                    Box(
-                        Modifier
-                            .fillMaxWidth()
-                            .height(50.dp)
-                            .background(color = Color.Blue)
-                    )
-                }
-            }
-        }
-        assertThat(drawerChildPosition.x).isLessThan(0f)
-        scaffoldState.drawerState.open()
-        assertThat(drawerChildPosition.x).isLessThan(0f)
-        scaffoldState.drawerState.close()
-        assertThat(drawerChildPosition.x).isLessThan(0f)
-    }
-
-    @Test
-    fun scaffold_startDockedFab_position() {
-        var fabPosition: Offset = Offset.Zero
-        var fabSize: IntSize = IntSize.Zero
-        var bottomBarPosition: Offset = Offset.Zero
-        rule.setContent {
-            Scaffold(
-                floatingActionButton = {
-                    FloatingActionButton(
-                        modifier = Modifier.onGloballyPositioned { positioned ->
-                            fabSize = positioned.size
-                            fabPosition = positioned.positionInRoot()
-                        },
-                        onClick = {}
-                    ) {
-                        Icon(Icons.Filled.Favorite, null)
-                    }
-                },
-                floatingActionButtonPosition = FabPosition.Start,
-                isFloatingActionButtonDocked = true,
-                bottomBar = {
-                    BottomAppBar(
-                        Modifier
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                bottomBarPosition = positioned.positionInRoot()
-                            }
-                    ) {}
-                }
-            ) {
-                Text("body")
-            }
-        }
-        with(rule.density) {
-            assertThat(fabPosition.x).isWithin(1f).of(fabSpacing.toPx())
-        }
-        val expectedFabY = bottomBarPosition.y - (fabSize.height / 2)
-        assertThat(fabPosition.y).isEqualTo(expectedFabY)
-    }
-
-    @Test
-    fun scaffold_centerDockedFab_position() {
-        var fabPosition: Offset = Offset.Zero
-        var fabSize: IntSize = IntSize.Zero
-        var bottomBarPosition: Offset = Offset.Zero
-        rule.setContent {
-            Scaffold(
-                floatingActionButton = {
-                    FloatingActionButton(
-                        modifier = Modifier.onGloballyPositioned { positioned ->
-                            fabSize = positioned.size
-                            fabPosition = positioned.positionInRoot()
-                        },
-                        onClick = {}
-                    ) {
-                        Icon(Icons.Filled.Favorite, null)
-                    }
-                },
-                floatingActionButtonPosition = FabPosition.Center,
-                isFloatingActionButtonDocked = true,
-                bottomBar = {
-                    BottomAppBar(
-                        Modifier
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                bottomBarPosition = positioned.positionInRoot()
-                            }
-                    ) {}
-                }
-            ) {
-                Text("body")
-            }
-        }
-        with(rule.density) {
-            assertThat(fabPosition.x).isWithin(1f).of(
-                (rule.rootWidth().toPx() - fabSize.width) / 2f)
-        }
-        val expectedFabY = bottomBarPosition.y - (fabSize.height / 2)
-        assertThat(fabPosition.y).isEqualTo(expectedFabY)
-    }
-
-    @Test
-    fun scaffold_endDockedFab_position() {
-        var fabPosition: Offset = Offset.Zero
-        var fabSize: IntSize = IntSize.Zero
-        var bottomBarPosition: Offset = Offset.Zero
-        rule.setContent {
-            Scaffold(
-                floatingActionButton = {
-                    FloatingActionButton(
-                        modifier = Modifier.onGloballyPositioned { positioned ->
-                            fabSize = positioned.size
-                            fabPosition = positioned.positionInRoot()
-                        },
-                        onClick = {}
-                    ) {
-                        Icon(Icons.Filled.Favorite, null)
-                    }
-                },
-                floatingActionButtonPosition = FabPosition.End,
-                isFloatingActionButtonDocked = true,
-                bottomBar = {
-                    BottomAppBar(
-                        Modifier
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                bottomBarPosition = positioned.positionInRoot()
-                            }
-                    ) {}
-                }
-            ) {
-                Text("body")
-            }
-        }
-        with(rule.density) {
-            assertThat(fabPosition.x).isWithin(1f).of(
-                rule.rootWidth().toPx() - fabSize.width - fabSpacing.toPx())
-        }
-        val expectedFabY = bottomBarPosition.y - (fabSize.height / 2)
-        assertThat(fabPosition.y).isEqualTo(expectedFabY)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_topAppBarIsDrawnOnTopOfContent() {
-        rule.setContent {
-            Box(
-                Modifier
-                    .requiredSize(10.dp, 20.dp)
-                    .semantics(mergeDescendants = true) {}
-                    .testTag("Scaffold")
-            ) {
-                Scaffold(
-                    topBar = {
-                        Box(
-                            Modifier.requiredSize(10.dp)
-                                .shadow(4.dp)
-                                .zIndex(4f)
-                                .background(color = Color.White)
-                        )
-                    }
-                ) {
-                    Box(
-                        Modifier.requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-
-        rule.onNodeWithTag("Scaffold")
-            .captureToImage().asAndroidBitmap().apply {
-                // asserts the appbar(top half part) has the shadow
-                val yPos = height / 2 + 2
-                assertThat(Color(getPixel(0, yPos))).isNotEqualTo(Color.White)
-                assertThat(Color(getPixel(width / 2, yPos))).isNotEqualTo(Color.White)
-                assertThat(Color(getPixel(width - 1, yPos))).isNotEqualTo(Color.White)
-            }
-    }
-
-    @Test
-    fun scaffold_geometry_fabSize() {
-        var fabSize: IntSize = IntSize.Zero
-        val showFab = mutableStateOf(true)
-        var fabPlacement: FabPlacement? = null
-        rule.setContent {
-            val fab = @Composable {
-                if (showFab.value) {
-                    FloatingActionButton(
-                        modifier = Modifier.onGloballyPositioned { positioned ->
-                            fabSize = positioned.size
-                        },
-                        onClick = {}
-                    ) {
-                        Icon(Icons.Filled.Favorite, null)
-                    }
-                }
-            }
-            Scaffold(
-                floatingActionButton = fab,
-                floatingActionButtonPosition = FabPosition.End,
-                bottomBar = {
-                    fabPlacement = LocalFabPlacement.current
-                }
-            ) {
-                Text("body")
-            }
-        }
-        rule.runOnIdle {
-            assertThat(fabPlacement?.width).isEqualTo(fabSize.width)
-            assertThat(fabPlacement?.height).isEqualTo(fabSize.height)
-            showFab.value = false
-        }
-
-        rule.runOnIdle {
-            assertThat(fabPlacement).isEqualTo(null)
-            assertThat(fabPlacement).isEqualTo(null)
-        }
-    }
-
-    @Test
-    fun scaffold_geometry_animated_fabSize() {
-        val fabTestTag = "FAB TAG"
-        lateinit var showFab: MutableState<Boolean>
-        var actualFabSize: IntSize = IntSize.Zero
-        var actualFabPlacement: FabPlacement? = null
-        rule.setContent {
-            showFab = remember { mutableStateOf(true) }
-            val animatedFab = @Composable {
-                AnimatedVisibility(visible = showFab.value) {
-                    FloatingActionButton(
-                        modifier = Modifier.onGloballyPositioned { positioned ->
-                            actualFabSize = positioned.size
-                        }.testTag(fabTestTag),
-                        onClick = {}
-                    ) {
-                        Icon(Icons.Filled.Favorite, null)
-                    }
-                }
-            }
-            Scaffold(
-                floatingActionButton = animatedFab,
-                floatingActionButtonPosition = FabPosition.End,
-                bottomBar = {
-                    actualFabPlacement = LocalFabPlacement.current
-                }
-            ) {
-                Text("body")
-            }
-        }
-
-        val fabNode = rule.onNodeWithTag(fabTestTag)
-
-        fabNode.assertIsDisplayed()
-
-        rule.runOnIdle {
-            assertThat(actualFabPlacement?.width).isEqualTo(actualFabSize.width)
-            assertThat(actualFabPlacement?.height).isEqualTo(actualFabSize.height)
-            actualFabSize = IntSize.Zero
-            actualFabPlacement = null
-            showFab.value = false
-        }
-
-        fabNode.assertDoesNotExist()
-
-        rule.runOnIdle {
-            assertThat(actualFabPlacement).isNull()
-            actualFabSize = IntSize.Zero
-            actualFabPlacement = null
-            showFab.value = true
-        }
-
-        fabNode.assertIsDisplayed()
-
-        rule.runOnIdle {
-            assertThat(actualFabPlacement?.width).isEqualTo(actualFabSize.width)
-            assertThat(actualFabPlacement?.height).isEqualTo(actualFabSize.height)
-        }
-    }
-
-    @Test
-    fun scaffold_innerPadding_lambdaParam() {
-        var bottomBarSize: IntSize = IntSize.Zero
-        lateinit var innerPadding: PaddingValues
-
-        lateinit var scaffoldState: ScaffoldState
-        rule.setContent {
-            scaffoldState = rememberScaffoldState()
-            Scaffold(
-                scaffoldState = scaffoldState,
-                bottomBar = {
-                    Box(
-                        Modifier
-                            .fillMaxWidth()
-                            .height(100.dp)
-                            .background(color = Color.Red)
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                bottomBarSize = positioned.size
-                            }
-                    )
-                }
-            ) {
-                innerPadding = it
-                Text("body")
-            }
-        }
-        rule.runOnIdle {
-            with(rule.density) {
-                assertThat(innerPadding.calculateBottomPadding())
-                    .isEqualTo(bottomBarSize.toSize().height.toDp())
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_respectsConsumedWindowInsets() {
-        rule.setContent {
-            Box(
-                Modifier
-                    .requiredSize(10.dp, 40.dp)
-                    .windowInsetsPadding(WindowInsets(top = 10.dp, bottom = 10.dp))
-            ) {
-                Scaffold(
-                    contentWindowInsets = WindowInsets(top = 15.dp, bottom = 15.dp)
-                ) { paddingValues ->
-                    // Consumed windowInsetsPadding is omitted. This replicates behavior from
-                    // Modifier.windowInsetsPadding. (15.dp contentPadding - 10.dp consumedPadding)
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateTopPadding(),
-                        expected = 5.dp,
-                        threshold = roundingError
-                    )
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateBottomPadding(),
-                        expected = 5.dp,
-                        threshold = roundingError
-                    )
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_providesInsets_respectsTopAppBar() {
-        rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 40.dp)) {
-                Scaffold(
-                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
-                    topBar = {
-                        Box(Modifier.requiredSize(0.dp))
-                    }
-                ) { paddingValues ->
-                    // top is like the collapsed top app bar (i.e. 0dp) + rounding error
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateTopPadding(),
-                        expected = 0.dp,
-                        threshold = roundingError
-                    )
-                    // bottom is like the insets
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateBottomPadding(),
-                        expected = 3.dp,
-                        threshold = roundingError
-                    )
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_providesInsets_respectsBottomAppBar() {
-        rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 40.dp)) {
-                Scaffold(
-                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
-                    bottomBar = {
-                        Box(Modifier.requiredSize(10.dp))
-                    }
-                ) { paddingValues ->
-                    // bottom is like bottom app bar + rounding error
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateBottomPadding(),
-                        expected = 10.dp,
-                        threshold = roundingError
-                    )
-                    // top is like the insets
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateTopPadding(),
-                        expected = 5.dp,
-                        threshold = roundingError
-                    )
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_insetsTests_snackbarRespectsInsets() {
-        val hostState = SnackbarHostState()
-        var snackbarSize: IntSize? = null
-        var snackbarPosition: Offset? = null
-        var density: Density? = null
-        rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 40.dp)) {
-                density = LocalDensity.current
-                Scaffold(
-                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
-                    snackbarHost = {
-                        SnackbarHost(hostState = hostState,
-                            modifier = Modifier
-                                .onGloballyPositioned {
-                                    snackbarSize = it.size
-                                    snackbarPosition = it.positionInRoot()
-                                })
-                    }
-                ) {
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-        val snackbarBottomOffsetDp =
-            with(density!!) { (snackbarPosition!!.y.roundToInt() + snackbarSize!!.height).toDp() }
-        assertThat(rule.rootHeight() - snackbarBottomOffsetDp - 3.dp).isLessThan(1.dp)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_insetsTests_FabRespectsInsets() {
-        var fabSize: IntSize? = null
-        var fabPosition: Offset? = null
-        var density: Density? = null
-        rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 20.dp)) {
-                density = LocalDensity.current
-                Scaffold(
-                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
-                    floatingActionButton = {
-                        FloatingActionButton(onClick = {},
-                            modifier = Modifier
-                                .onGloballyPositioned {
-                                    fabSize = it.size
-                                    fabPosition = it.positionInRoot()
-                                }) {
-                            Text("Fab")
-                        }
-                    },
-                ) {
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-        val fabBottomOffsetDp =
-            with(density!!) { (fabPosition!!.y.roundToInt() + fabSize!!.height).toDp() }
-        assertThat(rule.rootHeight() - fabBottomOffsetDp - 3.dp).isLessThan(1.dp)
-    }
-
-    // Regression test for b/295536718
-    @Test
-    fun scaffold_onSizeChanged_calledBeforeLookaheadPlace() {
-        var size: IntSize? = null
-        var onSizeChangedCount = 0
-        var onPlaceCount = 0
-
-        rule.setContent {
-            LookaheadScope {
-                Scaffold {
-                    SubcomposeLayout { constraints ->
-                        val measurables = subcompose("second") {
-                            Box(
-                                Modifier
-                                    .size(45.dp)
-                                    .onSizeChanged {
-                                        onSizeChangedCount++
-                                        size = it
-                                    }
-                            )
-                        }
-                        val placeables = measurables.map { it.measure(constraints) }
-
-                        layout(constraints.maxWidth, constraints.maxHeight) {
-                            onPlaceCount++
-                            Truth.assertWithMessage("Expected onSizeChangedCount to be >= 1")
-                                .that(onSizeChangedCount).isAtLeast(1)
-                            assertThat(size).isNotNull()
-                            placeables.forEach { it.place(0, 0) }
-                        }
-                    }
-                }
-            }
-        }
-
-        Truth.assertWithMessage("Expected placeCount to be >= 1").that(onPlaceCount).isAtLeast(1)
-    }
-
-    private fun assertDpIsWithinThreshold(actual: Dp, expected: Dp, threshold: Dp) {
-        assertThat(actual.value).isWithin(threshold.value).of(expected.value)
-    }
-
-    private val roundingError = 0.5.dp
-}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AlertDialogScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/AlertDialogScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AlertDialogScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/AlertDialogScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AlertDialogTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/AlertDialogTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AlertDialogTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/AlertDialogTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/AppBarTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/AppBarTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AutoTestFrameClock.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/AutoTestFrameClock.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AutoTestFrameClock.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/AutoTestFrameClock.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BackdropScaffoldTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BackdropScaffoldTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BackdropScaffoldTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BackdropScaffoldTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BadgeScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BadgeScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BadgeTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BadgeTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ButtonTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ButtonTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CardTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/CardTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CardTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/CardTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/CheckboxTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/CheckboxTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ChipScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ChipScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ChipScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ChipScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ChipTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ChipTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ChipTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ChipTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ColorsTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ColorsTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ColorsTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ColorsTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ContentAlphaTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ContentAlphaTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ContentAlphaTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ContentAlphaTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/DividerUiTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/DividerUiTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/DrawerScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/DrawerScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/DrawerTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/DrawerTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ElevationOverlayTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ElevationOverlayTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ElevationOverlayTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ElevationOverlayTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/GoldenCommon.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/GoldenCommon.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/GoldenCommon.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/GoldenCommon.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/IconButtonTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/IconButtonTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/IconButtonTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/IconButtonTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/IconTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/IconTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/IconTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/IconTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ListItemTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ListItemTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/MaterialTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/MaterialTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/MenuTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/MenuTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetStateTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ModalBottomSheetStateTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetStateTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ModalBottomSheetStateTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/NavigationRailScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/NavigationRailScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/NavigationRailScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/NavigationRailScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/NavigationRailTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/NavigationRailTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/NavigationRailTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/NavigationRailTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ObservableThemeTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ObservableThemeTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ObservableThemeTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ObservableThemeTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/RadioButtonTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/RadioButtonTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ScaffoldScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ScaffoldScreenshotTest.kt
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ScaffoldTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ScaffoldTest.kt
new file mode 100644
index 0000000..85e9194
--- /dev/null
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ScaffoldTest.kt
@@ -0,0 +1,862 @@
+/*
+ * Copyright 2020 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.material
+
+import android.os.Build
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.zIndex
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.math.roundToInt
+import kotlinx.coroutines.runBlocking
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ScaffoldTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val fabSpacing = 16.dp
+    private val scaffoldTag = "Scaffold"
+
+    @Test
+    fun scaffold_onlyContent_takesWholeScreen() {
+        rule.setMaterialContentForSizeAssertions(
+            parentMaxWidth = 100.dp,
+            parentMaxHeight = 100.dp
+        ) {
+            Scaffold {
+                Text("Scaffold body")
+            }
+        }
+            .assertWidthIsEqualTo(100.dp)
+            .assertHeightIsEqualTo(100.dp)
+    }
+
+    @Test
+    fun scaffold_onlyContent_stackSlot() {
+        var child1: Offset = Offset.Zero
+        var child2: Offset = Offset.Zero
+        rule.setMaterialContent {
+            Scaffold {
+                Text(
+                    "One",
+                    Modifier.onGloballyPositioned { child1 = it.positionInParent() }
+                )
+                Text(
+                    "Two",
+                    Modifier.onGloballyPositioned { child2 = it.positionInParent() }
+                )
+            }
+        }
+        assertThat(child1.y).isEqualTo(child2.y)
+        assertThat(child1.x).isEqualTo(child2.x)
+    }
+
+    @Test
+    fun scaffold_AppbarAndContent_inColumn() {
+        var appbarPosition: Offset = Offset.Zero
+        var appbarSize: IntSize = IntSize.Zero
+        var contentPosition: Offset = Offset.Zero
+        rule.setMaterialContent {
+            Scaffold(
+                topBar = {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(50.dp)
+                            .background(color = Color.Red)
+                            .onGloballyPositioned { positioned: LayoutCoordinates ->
+                                appbarPosition = positioned.localToWindow(Offset.Zero)
+                                appbarSize = positioned.size
+                            }
+                    )
+                }
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxWidth()
+                        .height(50.dp)
+                        .background(Color.Blue)
+                        .onGloballyPositioned { contentPosition = it.localToWindow(Offset.Zero) }
+                )
+            }
+        }
+        assertThat(appbarPosition.y + appbarSize.height.toFloat())
+            .isEqualTo(contentPosition.y)
+    }
+
+    @Test
+    fun scaffold_bottomBarAndContent_inStack() {
+        var appbarPosition: Offset = Offset.Zero
+        var appbarSize: IntSize = IntSize.Zero
+        var contentPosition: Offset = Offset.Zero
+        var contentSize: IntSize = IntSize.Zero
+        rule.setMaterialContent {
+            Scaffold(
+                bottomBar = {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(50.dp)
+                            .background(color = Color.Red)
+                            .onGloballyPositioned { positioned: LayoutCoordinates ->
+                                appbarPosition = positioned.positionInParent()
+                                appbarSize = positioned.size
+                            }
+                    )
+                }
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .height(50.dp)
+                        .background(color = Color.Blue)
+                        .onGloballyPositioned { positioned: LayoutCoordinates ->
+                            contentPosition = positioned.positionInParent()
+                            contentSize = positioned.size
+                        }
+                )
+            }
+        }
+        val appBarBottom = appbarPosition.y + appbarSize.height
+        val contentBottom = contentPosition.y + contentSize.height
+        assertThat(appBarBottom).isEqualTo(contentBottom)
+    }
+
+    @Test
+    @Ignore("unignore once animation sync is ready (b/147291885)")
+    fun scaffold_drawer_gestures() {
+        var drawerChildPosition: Offset = Offset.Zero
+        val drawerGesturedEnabledState = mutableStateOf(false)
+        rule.setContent {
+            Box(Modifier.testTag(scaffoldTag)) {
+                Scaffold(
+                    drawerContent = {
+                        Box(
+                            Modifier
+                                .fillMaxWidth()
+                                .height(50.dp)
+                                .background(color = Color.Blue)
+                                .onGloballyPositioned { positioned: LayoutCoordinates ->
+                                    drawerChildPosition = positioned.positionInParent()
+                                }
+                        )
+                    },
+                    drawerGesturesEnabled = drawerGesturedEnabledState.value
+                ) {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(50.dp)
+                            .background(color = Color.Blue)
+                    )
+                }
+            }
+        }
+        assertThat(drawerChildPosition.x).isLessThan(0f)
+        rule.onNodeWithTag(scaffoldTag).performTouchInput {
+            swipeRight()
+        }
+        assertThat(drawerChildPosition.x).isLessThan(0f)
+        rule.onNodeWithTag(scaffoldTag).performTouchInput {
+            swipeLeft()
+        }
+        assertThat(drawerChildPosition.x).isLessThan(0f)
+
+        rule.runOnUiThread {
+            drawerGesturedEnabledState.value = true
+        }
+
+        rule.onNodeWithTag(scaffoldTag).performTouchInput {
+            swipeRight()
+        }
+        assertThat(drawerChildPosition.x).isEqualTo(0f)
+        rule.onNodeWithTag(scaffoldTag).performTouchInput {
+            swipeLeft()
+        }
+        assertThat(drawerChildPosition.x).isLessThan(0f)
+    }
+
+    @Test
+    @Ignore("unignore once animation sync is ready (b/147291885)")
+    fun scaffold_drawer_manualControl(): Unit = runBlocking {
+        var drawerChildPosition: Offset = Offset.Zero
+        lateinit var scaffoldState: ScaffoldState
+        rule.setContent {
+            scaffoldState = rememberScaffoldState()
+            Box(Modifier.testTag(scaffoldTag)) {
+                Scaffold(
+                    scaffoldState = scaffoldState,
+                    drawerContent = {
+                        Box(
+                            Modifier
+                                .fillMaxWidth()
+                                .height(50.dp)
+                                .background(color = Color.Blue)
+                                .onGloballyPositioned { positioned: LayoutCoordinates ->
+                                    drawerChildPosition = positioned.positionInParent()
+                                }
+                        )
+                    }
+                ) {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(50.dp)
+                            .background(color = Color.Blue)
+                    )
+                }
+            }
+        }
+        assertThat(drawerChildPosition.x).isLessThan(0f)
+        scaffoldState.drawerState.open()
+        assertThat(drawerChildPosition.x).isLessThan(0f)
+        scaffoldState.drawerState.close()
+        assertThat(drawerChildPosition.x).isLessThan(0f)
+    }
+
+    @Test
+    fun scaffold_startDockedFab_position() {
+        var fabPosition: Offset = Offset.Zero
+        var fabSize: IntSize = IntSize.Zero
+        var bottomBarPosition: Offset = Offset.Zero
+        rule.setContent {
+            Scaffold(
+                floatingActionButton = {
+                    FloatingActionButton(
+                        modifier = Modifier.onGloballyPositioned { positioned ->
+                            fabSize = positioned.size
+                            fabPosition = positioned.positionInRoot()
+                        },
+                        onClick = {}
+                    ) {
+                        Icon(Icons.Filled.Favorite, null)
+                    }
+                },
+                floatingActionButtonPosition = FabPosition.Start,
+                isFloatingActionButtonDocked = true,
+                bottomBar = {
+                    BottomAppBar(
+                        Modifier
+                            .onGloballyPositioned { positioned: LayoutCoordinates ->
+                                bottomBarPosition = positioned.positionInRoot()
+                            }
+                    ) {}
+                }
+            ) {
+                Text("body")
+            }
+        }
+        with(rule.density) {
+            assertThat(fabPosition.x).isWithin(1f).of(fabSpacing.toPx())
+        }
+        val expectedFabY = bottomBarPosition.y - (fabSize.height / 2)
+        assertThat(fabPosition.y).isEqualTo(expectedFabY)
+    }
+
+    @Test
+    fun scaffold_centerDockedFab_position() {
+        var fabPosition: Offset = Offset.Zero
+        var fabSize: IntSize = IntSize.Zero
+        var bottomBarPosition: Offset = Offset.Zero
+        rule.setContent {
+            Scaffold(
+                floatingActionButton = {
+                    FloatingActionButton(
+                        modifier = Modifier.onGloballyPositioned { positioned ->
+                            fabSize = positioned.size
+                            fabPosition = positioned.positionInRoot()
+                        },
+                        onClick = {}
+                    ) {
+                        Icon(Icons.Filled.Favorite, null)
+                    }
+                },
+                floatingActionButtonPosition = FabPosition.Center,
+                isFloatingActionButtonDocked = true,
+                bottomBar = {
+                    BottomAppBar(
+                        Modifier
+                            .onGloballyPositioned { positioned: LayoutCoordinates ->
+                                bottomBarPosition = positioned.positionInRoot()
+                            }
+                    ) {}
+                }
+            ) {
+                Text("body")
+            }
+        }
+        with(rule.density) {
+            assertThat(fabPosition.x).isWithin(1f).of(
+                (rule.rootWidth().toPx() - fabSize.width) / 2f
+            )
+        }
+        val expectedFabY = bottomBarPosition.y - (fabSize.height / 2)
+        assertThat(fabPosition.y).isEqualTo(expectedFabY)
+    }
+
+    @Test
+    fun scaffold_endDockedFab_position() {
+        var fabPosition: Offset = Offset.Zero
+        var fabSize: IntSize = IntSize.Zero
+        var bottomBarPosition: Offset = Offset.Zero
+        rule.setContent {
+            Scaffold(
+                floatingActionButton = {
+                    FloatingActionButton(
+                        modifier = Modifier.onGloballyPositioned { positioned ->
+                            fabSize = positioned.size
+                            fabPosition = positioned.positionInRoot()
+                        },
+                        onClick = {}
+                    ) {
+                        Icon(Icons.Filled.Favorite, null)
+                    }
+                },
+                floatingActionButtonPosition = FabPosition.End,
+                isFloatingActionButtonDocked = true,
+                bottomBar = {
+                    BottomAppBar(
+                        Modifier
+                            .onGloballyPositioned { positioned: LayoutCoordinates ->
+                                bottomBarPosition = positioned.positionInRoot()
+                            }
+                    ) {}
+                }
+            ) {
+                Text("body")
+            }
+        }
+        with(rule.density) {
+            assertThat(fabPosition.x).isWithin(1f).of(
+                rule.rootWidth().toPx() - fabSize.width - fabSpacing.toPx()
+            )
+        }
+        val expectedFabY = bottomBarPosition.y - (fabSize.height / 2)
+        assertThat(fabPosition.y).isEqualTo(expectedFabY)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_topAppBarIsDrawnOnTopOfContent() {
+        rule.setContent {
+            Box(
+                Modifier
+                    .requiredSize(10.dp, 20.dp)
+                    .semantics(mergeDescendants = true) {}
+                    .testTag("Scaffold")
+            ) {
+                Scaffold(
+                    topBar = {
+                        Box(
+                            Modifier
+                                .requiredSize(10.dp)
+                                .shadow(4.dp)
+                                .zIndex(4f)
+                                .background(color = Color.White)
+                        )
+                    }
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("Scaffold")
+            .captureToImage().asAndroidBitmap().apply {
+                // asserts the appbar(top half part) has the shadow
+                val yPos = height / 2 + 2
+                assertThat(Color(getPixel(0, yPos))).isNotEqualTo(Color.White)
+                assertThat(Color(getPixel(width / 2, yPos))).isNotEqualTo(Color.White)
+                assertThat(Color(getPixel(width - 1, yPos))).isNotEqualTo(Color.White)
+            }
+    }
+
+    @Test
+    fun scaffold_geometry_fabSize() {
+        var fabSize: IntSize = IntSize.Zero
+        val showFab = mutableStateOf(true)
+        var fabPlacement: FabPlacement? = null
+        rule.setContent {
+            val fab = @Composable {
+                if (showFab.value) {
+                    FloatingActionButton(
+                        modifier = Modifier.onGloballyPositioned { positioned ->
+                            fabSize = positioned.size
+                        },
+                        onClick = {}
+                    ) {
+                        Icon(Icons.Filled.Favorite, null)
+                    }
+                }
+            }
+            Scaffold(
+                floatingActionButton = fab,
+                floatingActionButtonPosition = FabPosition.End,
+                bottomBar = {
+                    fabPlacement = LocalFabPlacement.current
+                }
+            ) {
+                Text("body")
+            }
+        }
+        rule.runOnIdle {
+            assertThat(fabPlacement?.width).isEqualTo(fabSize.width)
+            assertThat(fabPlacement?.height).isEqualTo(fabSize.height)
+            showFab.value = false
+        }
+
+        rule.runOnIdle {
+            assertThat(fabPlacement).isEqualTo(null)
+            assertThat(fabPlacement).isEqualTo(null)
+        }
+    }
+
+    @Test
+    fun scaffold_geometry_animated_fabSize() {
+        val fabTestTag = "FAB TAG"
+        lateinit var showFab: MutableState<Boolean>
+        var actualFabSize: IntSize = IntSize.Zero
+        var actualFabPlacement: FabPlacement? = null
+        rule.setContent {
+            showFab = remember { mutableStateOf(true) }
+            val animatedFab = @Composable {
+                AnimatedVisibility(visible = showFab.value) {
+                    FloatingActionButton(
+                        modifier = Modifier
+                            .onGloballyPositioned { positioned ->
+                                actualFabSize = positioned.size
+                            }
+                            .testTag(fabTestTag),
+                        onClick = {}
+                    ) {
+                        Icon(Icons.Filled.Favorite, null)
+                    }
+                }
+            }
+            Scaffold(
+                floatingActionButton = animatedFab,
+                floatingActionButtonPosition = FabPosition.End,
+                bottomBar = {
+                    actualFabPlacement = LocalFabPlacement.current
+                }
+            ) {
+                Text("body")
+            }
+        }
+
+        val fabNode = rule.onNodeWithTag(fabTestTag)
+
+        fabNode.assertIsDisplayed()
+
+        rule.runOnIdle {
+            assertThat(actualFabPlacement?.width).isEqualTo(actualFabSize.width)
+            assertThat(actualFabPlacement?.height).isEqualTo(actualFabSize.height)
+            actualFabSize = IntSize.Zero
+            actualFabPlacement = null
+            showFab.value = false
+        }
+
+        fabNode.assertDoesNotExist()
+
+        rule.runOnIdle {
+            assertThat(actualFabPlacement).isNull()
+            actualFabSize = IntSize.Zero
+            actualFabPlacement = null
+            showFab.value = true
+        }
+
+        fabNode.assertIsDisplayed()
+
+        rule.runOnIdle {
+            assertThat(actualFabPlacement?.width).isEqualTo(actualFabSize.width)
+            assertThat(actualFabPlacement?.height).isEqualTo(actualFabSize.height)
+        }
+    }
+
+    @Test
+    fun scaffold_innerPadding_lambdaParam() {
+        var bottomBarSize: IntSize = IntSize.Zero
+        lateinit var innerPadding: PaddingValues
+
+        lateinit var scaffoldState: ScaffoldState
+        rule.setContent {
+            scaffoldState = rememberScaffoldState()
+            Scaffold(
+                scaffoldState = scaffoldState,
+                bottomBar = {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(100.dp)
+                            .background(color = Color.Red)
+                            .onGloballyPositioned { positioned: LayoutCoordinates ->
+                                bottomBarSize = positioned.size
+                            }
+                    )
+                }
+            ) {
+                innerPadding = it
+                Text("body")
+            }
+        }
+        rule.runOnIdle {
+            with(rule.density) {
+                assertThat(innerPadding.calculateBottomPadding())
+                    .isEqualTo(bottomBarSize.toSize().height.toDp())
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_respectsConsumedWindowInsets() {
+        rule.setContent {
+            Box(
+                Modifier
+                    .requiredSize(10.dp, 40.dp)
+                    .windowInsetsPadding(WindowInsets(top = 10.dp, bottom = 10.dp))
+            ) {
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 15.dp, bottom = 15.dp)
+                ) { paddingValues ->
+                    // Consumed windowInsetsPadding is omitted. This replicates behavior from
+                    // Modifier.windowInsetsPadding. (15.dp contentPadding - 10.dp consumedPadding)
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 5.dp,
+                        threshold = roundingError
+                    )
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 5.dp,
+                        threshold = roundingError
+                    )
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_providesInsets_respectsTopAppBar() {
+        rule.setContent {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
+                    topBar = {
+                        Box(Modifier.requiredSize(0.dp))
+                    }
+                ) { paddingValues ->
+                    // top is like the collapsed top app bar (i.e. 0dp) + rounding error
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 0.dp,
+                        threshold = roundingError
+                    )
+                    // bottom is like the insets
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 3.dp,
+                        threshold = roundingError
+                    )
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_providesInsets_respectsBottomAppBar() {
+        rule.setContent {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
+                    bottomBar = {
+                        Box(Modifier.requiredSize(10.dp))
+                    }
+                ) { paddingValues ->
+                    // bottom is like bottom app bar + rounding error
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 10.dp,
+                        threshold = roundingError
+                    )
+                    // top is like the insets
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 5.dp,
+                        threshold = roundingError
+                    )
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_insetsTests_snackbarRespectsInsets() {
+        val hostState = SnackbarHostState()
+        var snackbarSize: IntSize? = null
+        var snackbarPosition: Offset? = null
+        var density: Density? = null
+        rule.setContent {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
+                density = LocalDensity.current
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
+                    snackbarHost = {
+                        SnackbarHost(hostState = hostState,
+                            modifier = Modifier
+                                .onGloballyPositioned {
+                                    snackbarSize = it.size
+                                    snackbarPosition = it.positionInRoot()
+                                })
+                    }
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+        val snackbarBottomOffsetDp =
+            with(density!!) { (snackbarPosition!!.y.roundToInt() + snackbarSize!!.height).toDp() }
+        assertThat(rule.rootHeight() - snackbarBottomOffsetDp - 3.dp).isLessThan(1.dp)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_insetsTests_FabRespectsInsets() {
+        var fabSize: IntSize? = null
+        var fabPosition: Offset? = null
+        var density: Density? = null
+        rule.setContent {
+            Box(Modifier.requiredSize(10.dp, 20.dp)) {
+                density = LocalDensity.current
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
+                    floatingActionButton = {
+                        FloatingActionButton(onClick = {},
+                            modifier = Modifier
+                                .onGloballyPositioned {
+                                    fabSize = it.size
+                                    fabPosition = it.positionInRoot()
+                                }) {
+                            Text("Fab")
+                        }
+                    },
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+        val fabBottomOffsetDp =
+            with(density!!) { (fabPosition!!.y.roundToInt() + fabSize!!.height).toDp() }
+        assertThat(rule.rootHeight() - fabBottomOffsetDp - 3.dp).isLessThan(1.dp)
+    }
+
+    // Regression test for b/295536718
+    @Test
+    fun scaffold_onSizeChanged_calledBeforeLookaheadPlace() {
+        var size: IntSize? = null
+        var onSizeChangedCount = 0
+        var onPlaceCount = 0
+
+        rule.setContent {
+            LookaheadScope {
+                Scaffold {
+                    SubcomposeLayout { constraints ->
+                        val measurables = subcompose("second") {
+                            Box(
+                                Modifier
+                                    .size(45.dp)
+                                    .onSizeChanged {
+                                        onSizeChangedCount++
+                                        size = it
+                                    }
+                            )
+                        }
+                        val placeables = measurables.map { it.measure(constraints) }
+
+                        layout(constraints.maxWidth, constraints.maxHeight) {
+                            onPlaceCount++
+                            assertWithMessage("Expected onSizeChangedCount to be >= 1")
+                                .that(onSizeChangedCount).isAtLeast(1)
+                            assertThat(size).isNotNull()
+                            placeables.forEach { it.place(0, 0) }
+                        }
+                    }
+                }
+            }
+        }
+
+        assertWithMessage("Expected placeCount to be >= 1").that(onPlaceCount).isAtLeast(1)
+    }
+
+    @OptIn(ExperimentalMaterialApi::class)
+    @Test
+    fun scaffold_subcomposeInMeasureFix_enabled_measuresChildrenInMeasurement() {
+        ScaffoldSubcomposeInMeasureFix = true
+        var size: IntSize? = null
+        var measured = false
+        rule.setContent {
+            Layout(
+                content = {
+                    Scaffold(
+                        content = {
+                            Box(Modifier.onSizeChanged { size = it })
+                        }
+                    )
+                }
+            ) { measurables, constraints ->
+                measurables.map { it.measure(constraints) }
+                measured = true
+                layout(0, 0) {
+                    // Empty measurement since we only care about placement
+                }
+            }
+        }
+
+        assertWithMessage("Measure should have been executed")
+            .that(measured).isTrue()
+        assertWithMessage("Expected size to be initialized")
+            .that(size).isNotNull()
+    }
+
+    @OptIn(ExperimentalMaterialApi::class)
+    @Test
+    fun scaffold_subcomposeInMeasureFix_disabled_measuresChildrenInPlacement() {
+        ScaffoldSubcomposeInMeasureFix = false
+        var size: IntSize? = null
+        var measured = false
+        var placed = false
+        rule.setContent {
+            Layout(
+                content = {
+                    Scaffold(
+                        content = {
+                            Box(Modifier.onSizeChanged { size = it })
+                        }
+                    )
+                }
+            ) { measurables, constraints ->
+                val placeables = measurables.map { it.measure(constraints) }
+                measured = true
+                assertWithMessage("Expected size to not be initialized in placement")
+                    .that(size).isNull()
+                layout(constraints.maxWidth, constraints.maxHeight) {
+                    placeables.forEach { it.place(0, 0) }
+                    placed = true
+                }
+            }
+        }
+
+        assertWithMessage("Measure should have been executed")
+            .that(measured).isTrue()
+        assertWithMessage("Placement should have been executed")
+            .that(placed).isTrue()
+        assertWithMessage("Expected size to be initialized")
+            .that(size).isNotNull()
+    }
+
+    private fun assertDpIsWithinThreshold(actual: Dp, expected: Dp, threshold: Dp) {
+        assertThat(actual.value).isWithin(threshold.value).of(expected.value)
+    }
+
+    private val roundingError = 0.5.dp
+}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SliderScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SliderScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SliderTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SliderTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SnackbarTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SnackbarTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SurfaceContentColorTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SurfaceContentColorTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SurfaceContentColorTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SurfaceContentColorTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SurfaceTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SurfaceTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SurfaceTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SurfaceTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeToDismissTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SwipeToDismissTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeToDismissTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SwipeToDismissTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SwipeableTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SwipeableTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SwitchTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SwitchTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TabTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TabTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TextTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TextTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableGestureTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableGestureTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableGestureTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableGestureTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableTestValue.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableTestValue.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableTestValue.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableTestValue.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldDecorationBoxTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/textfield/TextFieldDecorationBoxTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldDecorationBoxTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/textfield/TextFieldDecorationBoxTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
similarity index 100%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
rename to compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
diff --git a/compose/material/material/src/test/bottom_app_bar_rounded_edges.png b/compose/material/material/src/androidUnitTest/bottom_app_bar_rounded_edges.png
similarity index 100%
rename from compose/material/material/src/test/bottom_app_bar_rounded_edges.png
rename to compose/material/material/src/androidUnitTest/bottom_app_bar_rounded_edges.png
Binary files differ
diff --git a/compose/material/material/src/test/bottom_app_bar_rounded_edges_graph.py b/compose/material/material/src/androidUnitTest/bottom_app_bar_rounded_edges_graph.py
similarity index 100%
rename from compose/material/material/src/test/bottom_app_bar_rounded_edges_graph.py
rename to compose/material/material/src/androidUnitTest/bottom_app_bar_rounded_edges_graph.py
diff --git a/compose/material/material/src/test/kotlin/androidx/compose/material/BottomAppBarRoundedEdgesTest.kt b/compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/BottomAppBarRoundedEdgesTest.kt
similarity index 100%
rename from compose/material/material/src/test/kotlin/androidx/compose/material/BottomAppBarRoundedEdgesTest.kt
rename to compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/BottomAppBarRoundedEdgesTest.kt
diff --git a/compose/material/material/src/test/kotlin/androidx/compose/material/ButtonPaparazziScreenshotTest.kt b/compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/ButtonPaparazziScreenshotTest.kt
similarity index 100%
rename from compose/material/material/src/test/kotlin/androidx/compose/material/ButtonPaparazziScreenshotTest.kt
rename to compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/ButtonPaparazziScreenshotTest.kt
diff --git a/compose/material/material/src/test/kotlin/androidx/compose/material/DraggableAnchorsTest.kt b/compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/DraggableAnchorsTest.kt
similarity index 100%
rename from compose/material/material/src/test/kotlin/androidx/compose/material/DraggableAnchorsTest.kt
rename to compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/DraggableAnchorsTest.kt
diff --git a/compose/material/material/src/test/kotlin/androidx/compose/material/InternalMutatorMutexTest.kt b/compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/InternalMutatorMutexTest.kt
similarity index 100%
rename from compose/material/material/src/test/kotlin/androidx/compose/material/InternalMutatorMutexTest.kt
rename to compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/InternalMutatorMutexTest.kt
diff --git a/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt b/compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt
similarity index 100%
rename from compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt
rename to compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt
diff --git a/compose/material/material/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/compose/material/material/src/androidUnitTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
similarity index 100%
rename from compose/material/material/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
rename to compose/material/material/src/androidUnitTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
index c3d1131..2318e39 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
@@ -325,14 +325,12 @@
 /**
  * State of the [BottomSheetScaffold] composable.
  *
- * @param drawerState The state of the navigation drawer.
  * @param bottomSheetState The state of the persistent bottom sheet.
  * @param snackbarHostState The [SnackbarHostState] used to show snackbars inside the scaffold.
  */
 @ExperimentalMaterialApi
 @Stable
 class BottomSheetScaffoldState(
-    val drawerState: DrawerState,
     val bottomSheetState: BottomSheetState,
     val snackbarHostState: SnackbarHostState
 )
@@ -340,22 +338,177 @@
 /**
  * Create and [remember] a [BottomSheetScaffoldState].
  *
+ * @param bottomSheetState The state of the persistent bottom sheet.
+ * @param snackbarHostState The [SnackbarHostState] used to show snackbars inside the scaffold.
+ */
+@Composable
+@ExperimentalMaterialApi
+fun rememberBottomSheetScaffoldState(
+    bottomSheetState: BottomSheetState = rememberBottomSheetState(Collapsed),
+    snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
+): BottomSheetScaffoldState {
+    return remember(bottomSheetState, snackbarHostState) {
+        BottomSheetScaffoldState(
+            bottomSheetState = bottomSheetState,
+            snackbarHostState = snackbarHostState
+        )
+    }
+}
+
+/**
+ * State of the [BottomSheetScaffold] composable.
+ *
+ * @param drawerState The state of the navigation drawer.
+ * @param bottomSheetState The state of the persistent bottom sheet.
+ * @param snackbarHostState The [SnackbarHostState] used to show snackbars inside the scaffold.
+ */
+@Suppress("UNUSED_PARAMETER")
+@Deprecated(message = BottomSheetScaffoldWithDrawerDeprecated, level = DeprecationLevel.ERROR)
+@ExperimentalMaterialApi
+fun BottomSheetScaffoldState(
+    drawerState: DrawerState,
+    bottomSheetState: BottomSheetState,
+    snackbarHostState: SnackbarHostState
+): BottomSheetScaffoldState = error(BottomSheetScaffoldWithDrawerDeprecated)
+
+/**
+ * Create and [remember] a [BottomSheetScaffoldState].
+ *
  * @param drawerState The state of the navigation drawer.
  * @param bottomSheetState The state of the persistent bottom sheet.
  * @param snackbarHostState The [SnackbarHostState] used to show snackbars inside the scaffold.
  */
+@Suppress("UNUSED_PARAMETER")
+@Deprecated(message = BottomSheetScaffoldWithDrawerDeprecated, level = DeprecationLevel.ERROR)
 @Composable
 @ExperimentalMaterialApi
 fun rememberBottomSheetScaffoldState(
     drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
     bottomSheetState: BottomSheetState = rememberBottomSheetState(Collapsed),
     snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
-): BottomSheetScaffoldState {
-    return remember(drawerState, bottomSheetState, snackbarHostState) {
-        BottomSheetScaffoldState(
-            drawerState = drawerState,
-            bottomSheetState = bottomSheetState,
-            snackbarHostState = snackbarHostState
+): BottomSheetScaffoldState = error(BottomSheetScaffoldWithDrawerDeprecated)
+
+/**
+ * <a href="https://material.io/components/sheets-bottom#standard-bottom-sheet" class="external" target="_blank">Material Design standard bottom sheet</a>.
+ *
+ * Standard bottom sheets co-exist with the screen’s main UI region and allow for simultaneously
+ * viewing and interacting with both regions. They are commonly used to keep a feature or
+ * secondary content visible on screen when content in main UI region is frequently scrolled or
+ * panned.
+ *
+ * ![Standard bottom sheet image](https://developer.android.com/images/reference/androidx/compose/material/standard-bottom-sheet.png)
+ *
+ * This component provides an API to put together several material components to construct your
+ * screen. For a similar component which implements the basic material design layout strategy
+ * with app bars, floating action buttons and navigation drawers, use the standard [Scaffold].
+ * For similar component that uses a backdrop as the centerpiece of the screen, use
+ * [BackdropScaffold].
+ *
+ * A simple example of a bottom sheet scaffold looks like this:
+ *
+ * @sample androidx.compose.material.samples.BottomSheetScaffoldSample
+ *
+ * @param sheetContent The content of the bottom sheet.
+ * @param modifier An optional [Modifier] for the root of the scaffold.
+ * @param scaffoldState The state of the scaffold.
+ * @param topBar An optional top app bar.
+ * @param snackbarHost The composable hosting the snackbars shown inside the scaffold.
+ * @param floatingActionButton An optional floating action button.
+ * @param floatingActionButtonPosition The position of the floating action button.
+ * @param sheetGesturesEnabled Whether the bottom sheet can be interacted with by gestures.
+ * @param sheetShape The shape of the bottom sheet.
+ * @param sheetElevation The elevation of the bottom sheet.
+ * @param sheetBackgroundColor The background color of the bottom sheet.
+ * @param sheetContentColor The preferred content color provided by the bottom sheet to its
+ * children. Defaults to the matching content color for [sheetBackgroundColor], or if that is
+ * not a color from the theme, this will keep the same content color set above the bottom sheet.
+ * @param sheetPeekHeight The height of the bottom sheet when it is collapsed. If the peek height
+ * equals the sheet's full height, the sheet will only have a collapsed state.
+ * @param backgroundColor The background color of the scaffold body.
+ * @param contentColor The color of the content in scaffold body. Defaults to either the matching
+ * content color for [backgroundColor], or, if it is not a color from the theme, this will keep
+ * the same value set above this Surface.
+ * @param content The main content of the screen. You should use the provided [PaddingValues]
+ * to properly offset the content, so that it is not obstructed by the bottom sheet when collapsed.
+ */
+@Composable
+@ExperimentalMaterialApi
+fun BottomSheetScaffold(
+    sheetContent: @Composable ColumnScope.() -> Unit,
+    modifier: Modifier = Modifier,
+    scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(),
+    topBar: (@Composable () -> Unit)? = null,
+    snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
+    floatingActionButton: (@Composable () -> Unit)? = null,
+    floatingActionButtonPosition: FabPosition = FabPosition.End,
+    sheetGesturesEnabled: Boolean = true,
+    sheetShape: Shape = MaterialTheme.shapes.large,
+    sheetElevation: Dp = BottomSheetScaffoldDefaults.SheetElevation,
+    sheetBackgroundColor: Color = MaterialTheme.colors.surface,
+    sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
+    sheetPeekHeight: Dp = BottomSheetScaffoldDefaults.SheetPeekHeight,
+    backgroundColor: Color = MaterialTheme.colors.background,
+    contentColor: Color = contentColorFor(backgroundColor),
+    content: @Composable (PaddingValues) -> Unit
+) {
+    // b/278692145 Remove this once deprecated methods without density are removed
+    val density = LocalDensity.current
+    SideEffect {
+        scaffoldState.bottomSheetState.density = density
+    }
+
+    val peekHeightPx = with(LocalDensity.current) { sheetPeekHeight.toPx() }
+    Surface(
+        modifier
+            .fillMaxSize(),
+        color = backgroundColor,
+        contentColor = contentColor
+    ) {
+        BottomSheetScaffoldLayout(
+            topBar = topBar,
+            body = content,
+            bottomSheet = { layoutHeight ->
+                val nestedScroll = if (sheetGesturesEnabled) {
+                    Modifier
+                        .nestedScroll(
+                            remember(scaffoldState.bottomSheetState.anchoredDraggableState) {
+                                ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
+                                    state = scaffoldState.bottomSheetState.anchoredDraggableState,
+                                    orientation = Orientation.Vertical
+                                )
+                            }
+                        )
+                } else Modifier
+                BottomSheet(
+                    state = scaffoldState.bottomSheetState,
+                    modifier = nestedScroll
+                        .fillMaxWidth()
+                        .requiredHeightIn(min = sheetPeekHeight),
+                    calculateAnchors = { sheetSize ->
+                        val sheetHeight = sheetSize.height.toFloat()
+                        DraggableAnchors {
+                            Collapsed at layoutHeight - peekHeightPx
+                            if (sheetHeight > 0f && sheetHeight != peekHeightPx) {
+                                Expanded at layoutHeight - sheetHeight
+                            }
+                        }
+                    },
+                    sheetBackgroundColor = sheetBackgroundColor,
+                    sheetContentColor = sheetContentColor,
+                    sheetElevation = sheetElevation,
+                    sheetGesturesEnabled = sheetGesturesEnabled,
+                    sheetShape = sheetShape,
+                    content = sheetContent
+                )
+            },
+            floatingActionButton = floatingActionButton,
+            snackbarHost = {
+                snackbarHost(scaffoldState.snackbarHostState)
+            },
+            sheetOffset = { scaffoldState.bottomSheetState.requireOffset() },
+            sheetPeekHeight = sheetPeekHeight,
+            sheetState = scaffoldState.bottomSheetState,
+            floatingActionButtonPosition = floatingActionButtonPosition
         )
     }
 }
@@ -405,9 +558,15 @@
  * children. Defaults to the matching content color for [drawerBackgroundColor], or if that is
  * not a color from the theme, this will keep the same content color set above the drawer sheet.
  * @param drawerScrimColor The color of the scrim that is applied when the drawer is open.
+ * @param backgroundColor The background color of the scaffold body.
+ * @param contentColor The color of the content in scaffold body. Defaults to either the matching
+ * content color for [backgroundColor], or, if it is not a color from the theme, this will keep
+ * the same value set above this Surface.
  * @param content The main content of the screen. You should use the provided [PaddingValues]
  * to properly offset the content, so that it is not obstructed by the bottom sheet when collapsed.
  */
+@Suppress("UNUSED_PARAMETER")
+@Deprecated(message = BottomSheetScaffoldWithDrawerDeprecated, level = DeprecationLevel.ERROR)
 @Composable
 @ExperimentalMaterialApi
 fun BottomSheetScaffold(
@@ -435,83 +594,7 @@
     contentColor: Color = contentColorFor(backgroundColor),
     content: @Composable (PaddingValues) -> Unit
 ) {
-    // b/278692145 Remove this once deprecated methods without density are removed
-    val density = LocalDensity.current
-    SideEffect {
-        scaffoldState.bottomSheetState.density = density
-    }
-
-    val peekHeightPx = with(LocalDensity.current) { sheetPeekHeight.toPx() }
-    val child = @Composable {
-        BottomSheetScaffoldLayout(
-            topBar = topBar,
-            body = content,
-            bottomSheet = { layoutHeight ->
-                val nestedScroll = if (sheetGesturesEnabled) {
-                    Modifier
-                        .nestedScroll(
-                            remember(scaffoldState.bottomSheetState.anchoredDraggableState) {
-                                ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
-                                    state = scaffoldState.bottomSheetState.anchoredDraggableState,
-                                    orientation = Orientation.Vertical
-                                )
-                            }
-                        )
-                } else Modifier
-                BottomSheet(
-                    state = scaffoldState.bottomSheetState,
-                    modifier = nestedScroll
-                        .fillMaxWidth()
-                        .requiredHeightIn(min = sheetPeekHeight),
-                    calculateAnchors = { sheetSize ->
-                        val sheetHeight = sheetSize.height.toFloat()
-                        DraggableAnchors {
-                            Collapsed at layoutHeight - peekHeightPx
-                            if (sheetHeight > 0f && sheetHeight != peekHeightPx) {
-                                Expanded at layoutHeight - sheetHeight
-                            }
-                        }
-                    },
-                    sheetBackgroundColor = sheetBackgroundColor,
-                    sheetContentColor = sheetContentColor,
-                    sheetElevation = sheetElevation,
-                    sheetGesturesEnabled = sheetGesturesEnabled,
-                    sheetShape = sheetShape,
-                    content = sheetContent
-                )
-            },
-            floatingActionButton = floatingActionButton,
-            snackbarHost = {
-                snackbarHost(scaffoldState.snackbarHostState)
-            },
-            sheetOffset = { scaffoldState.bottomSheetState.requireOffset() },
-            sheetPeekHeight = sheetPeekHeight,
-            sheetState = scaffoldState.bottomSheetState,
-            floatingActionButtonPosition = floatingActionButtonPosition
-        )
-    }
-    Surface(
-        modifier
-            .fillMaxSize(),
-        color = backgroundColor,
-        contentColor = contentColor
-    ) {
-        if (drawerContent == null) {
-            child()
-        } else {
-            ModalDrawer(
-                drawerContent = drawerContent,
-                drawerState = scaffoldState.drawerState,
-                gesturesEnabled = drawerGesturesEnabled,
-                drawerShape = drawerShape,
-                drawerElevation = drawerElevation,
-                drawerBackgroundColor = drawerBackgroundColor,
-                drawerContentColor = drawerContentColor,
-                scrimColor = drawerScrimColor,
-                content = child
-            )
-        }
-    }
+    error(BottomSheetScaffoldWithDrawerDeprecated)
 }
 
 @OptIn(ExperimentalMaterialApi::class)
@@ -723,3 +806,6 @@
 private val FabSpacing = 16.dp
 private val BottomSheetScaffoldPositionalThreshold = 56.dp
 private val BottomSheetScaffoldVelocityThreshold = 125.dp
+private const val BottomSheetScaffoldWithDrawerDeprecated = "BottomSheetScaffold with a drawer " +
+    "has been deprecated. To achieve the same functionality, wrap your BottomSheetScaffold in a" +
+    "ModalDrawer. See BottomSheetScaffoldWithDrawerSample for more details."
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
index 106f8ad..41eaced 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
@@ -29,7 +29,10 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.UiComposable
@@ -78,7 +81,7 @@
 /**
  * The possible positions for a [FloatingActionButton] attached to a [Scaffold].
  */
-@kotlin.jvm.JvmInline
+@JvmInline
 value class FabPosition internal constructor(@Suppress("unused") private val value: Int) {
     companion object {
         /**
@@ -364,6 +367,22 @@
 }
 
 /**
+ * Flag indicating if [Scaffold] should subcompose and measure its children during measurement or
+ * during placement.
+ * Set this flag to false to keep Scaffold's old measurement behavior (measuring in placement).
+ *
+ * <b>This flag will be removed in Compose 1.6.0-beta01.</b> If you encounter any issues with the
+ * new behavior, please file an issue at: issuetracker.google.com/issues/new?component=742043
+ */
+// TODO(b/299621062): Remove flag before beta
+@Suppress("GetterSetterNames", "OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:Suppress("GetterSetterNames")
+@get:ExperimentalMaterialApi
+@set:ExperimentalMaterialApi
+@ExperimentalMaterialApi
+var ScaffoldSubcomposeInMeasureFix by mutableStateOf(true)
+
+/**
  * Layout for a [Scaffold]'s content.
  *
  * @param isFabDocked whether the FAB (if present) is docked to the bottom bar or not
@@ -376,6 +395,7 @@
  * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
  * [content], typically a [BottomAppBar].
  */
+@OptIn(ExperimentalMaterialApi::class)
 @Composable
 @UiComposable
 private fun ScaffoldLayout(
@@ -388,6 +408,46 @@
     contentWindowInsets: WindowInsets,
     bottomBar: @Composable @UiComposable () -> Unit
 ) {
+    if (ScaffoldSubcomposeInMeasureFix) {
+        ScaffoldLayoutWithMeasureFix(
+            isFabDocked = isFabDocked,
+            fabPosition = fabPosition,
+            topBar = topBar,
+            content = content,
+            snackbar = snackbar,
+            fab = fab,
+            contentWindowInsets = contentWindowInsets,
+            bottomBar = bottomBar
+        )
+    } else {
+        LegacyScaffoldLayout(
+            isFabDocked = isFabDocked,
+            fabPosition = fabPosition,
+            topBar = topBar,
+            content = content,
+            snackbar = snackbar,
+            fab = fab,
+            contentWindowInsets = contentWindowInsets,
+            bottomBar = bottomBar
+        )
+    }
+}
+
+/**
+ * Layout for a [Scaffold]'s content, subcomposing and measuring during measurement.
+ */
+@Composable
+@UiComposable
+private fun ScaffoldLayoutWithMeasureFix(
+    isFabDocked: Boolean,
+    fabPosition: FabPosition,
+    topBar: @Composable @UiComposable () -> Unit,
+    content: @Composable @UiComposable (PaddingValues) -> Unit,
+    snackbar: @Composable @UiComposable () -> Unit,
+    fab: @Composable @UiComposable () -> Unit,
+    contentWindowInsets: WindowInsets,
+    bottomBar: @Composable @UiComposable () -> Unit
+) {
     SubcomposeLayout { constraints ->
         val layoutWidth = constraints.maxWidth
         val layoutHeight = constraints.maxHeight
@@ -447,6 +507,7 @@
                             layoutWidth - FabSpacing.roundToPx() - fabWidth
                         }
                     }
+
                     FabPosition.End -> {
                         if (layoutDirection == LayoutDirection.Ltr) {
                             layoutWidth - FabSpacing.roundToPx() - fabWidth
@@ -454,6 +515,7 @@
                             FabSpacing.roundToPx()
                         }
                     }
+
                     else -> (layoutWidth - fabWidth) / 2
                 }
 
@@ -550,6 +612,183 @@
 }
 
 /**
+ * Legacy layout for a [Scaffold]'s content, subcomposing and measuring during placement.
+ */
+@Composable
+@UiComposable
+private fun LegacyScaffoldLayout(
+    isFabDocked: Boolean,
+    fabPosition: FabPosition,
+    topBar: @Composable @UiComposable () -> Unit,
+    content: @Composable @UiComposable (PaddingValues) -> Unit,
+    snackbar: @Composable @UiComposable () -> Unit,
+    fab: @Composable @UiComposable () -> Unit,
+    contentWindowInsets: WindowInsets,
+    bottomBar: @Composable @UiComposable () -> Unit
+) {
+    SubcomposeLayout { constraints ->
+        val layoutWidth = constraints.maxWidth
+        val layoutHeight = constraints.maxHeight
+
+        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+
+        layout(layoutWidth, layoutHeight) {
+            val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
+                it.measure(looseConstraints)
+            }
+
+            val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
+
+            val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
+                // respect only bottom and horizontal for snackbar and fab
+                val leftInset = contentWindowInsets
+                    .getLeft(this@SubcomposeLayout, layoutDirection)
+                val rightInset = contentWindowInsets
+                    .getRight(this@SubcomposeLayout, layoutDirection)
+                val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
+                // offset the snackbar constraints by the insets values
+                it.measure(
+                    looseConstraints.offset(
+                        -leftInset - rightInset,
+                        -bottomInset
+                    )
+                )
+            }
+
+            val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
+
+            val fabPlaceables =
+                subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable ->
+                    // respect only bottom and horizontal for snackbar and fab
+                    val leftInset =
+                        contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
+                    val rightInset =
+                        contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
+                    val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
+                    measurable.measure(
+                        looseConstraints.offset(
+                            -leftInset - rightInset,
+                            -bottomInset
+                        )
+                    )
+                }
+
+            val fabPlacement = if (fabPlaceables.isNotEmpty()) {
+                val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
+                val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
+                // FAB distance from the left of the layout, taking into account LTR / RTL
+                if (fabWidth != 0 && fabHeight != 0) {
+                    val fabLeftOffset = when (fabPosition) {
+                        FabPosition.Start -> {
+                            if (layoutDirection == LayoutDirection.Ltr) {
+                                FabSpacing.roundToPx()
+                            } else {
+                                layoutWidth - FabSpacing.roundToPx() - fabWidth
+                            }
+                        }
+
+                        FabPosition.End -> {
+                            if (layoutDirection == LayoutDirection.Ltr) {
+                                layoutWidth - FabSpacing.roundToPx() - fabWidth
+                            } else {
+                                FabSpacing.roundToPx()
+                            }
+                        }
+
+                        else -> (layoutWidth - fabWidth) / 2
+                    }
+
+                    FabPlacement(
+                        isDocked = isFabDocked,
+                        left = fabLeftOffset,
+                        width = fabWidth,
+                        height = fabHeight
+                    )
+                } else {
+                    null
+                }
+            } else {
+                null
+            }
+
+            val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) {
+                CompositionLocalProvider(
+                    LocalFabPlacement provides fabPlacement,
+                    content = bottomBar
+                )
+            }.fastMap { it.measure(looseConstraints) }
+
+            val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
+            val fabOffsetFromBottom = fabPlacement?.let {
+                if (bottomBarHeight == null) {
+                    it.height + FabSpacing.roundToPx() +
+                        contentWindowInsets.getBottom(this@SubcomposeLayout)
+                } else {
+                    if (isFabDocked) {
+                        // Total height is the bottom bar height + half the FAB height
+                        bottomBarHeight + (it.height / 2)
+                    } else {
+                        // Total height is the bottom bar height + the FAB height + the padding
+                        // between the FAB and bottom bar
+                        bottomBarHeight + it.height + FabSpacing.roundToPx()
+                    }
+                }
+            }
+
+            val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
+                snackbarHeight +
+                    (fabOffsetFromBottom ?: bottomBarHeight
+                    ?: contentWindowInsets.getBottom(this@SubcomposeLayout))
+            } else {
+                0
+            }
+
+            val bodyContentHeight = layoutHeight - topBarHeight
+
+            val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
+                val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout)
+                val innerPadding = PaddingValues(
+                    top =
+                    if (topBarPlaceables.isEmpty()) {
+                        insets.calculateTopPadding()
+                    } else {
+                        0.dp
+                    },
+                    bottom =
+                    if (bottomBarPlaceables.isEmpty() || bottomBarHeight == null) {
+                        insets.calculateBottomPadding()
+                    } else {
+                        bottomBarHeight.toDp()
+                    },
+                    start = insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection),
+                    end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection)
+                )
+                content(innerPadding)
+            }.fastMap { it.measure(looseConstraints.copy(maxHeight = bodyContentHeight)) }
+
+            // Placing to control drawing order to match default elevation of each placeable
+            bodyContentPlaceables.fastForEach {
+                it.place(0, topBarHeight)
+            }
+            topBarPlaceables.fastForEach {
+                it.place(0, 0)
+            }
+            snackbarPlaceables.fastForEach {
+                it.place(0, layoutHeight - snackbarOffsetFromBottom)
+            }
+            // The bottom bar is always at the bottom of the layout
+            bottomBarPlaceables.fastForEach {
+                it.place(0, layoutHeight - (bottomBarHeight ?: 0))
+            }
+            // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
+            fabPlaceables.fastForEach {
+                it.place(fabPlacement?.left ?: 0, layoutHeight - (fabOffsetFromBottom ?: 0))
+            }
+        }
+    }
+}
+
+/**
  * Placement information for a [FloatingActionButton] inside a [Scaffold].
  *
  * @property isDocked whether the FAB should be docked with the bottom bar
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ColorSchemeBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ColorSchemeBenchmark.kt
new file mode 100644
index 0000000..20e04e7
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ColorSchemeBenchmark.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.material3.benchmark
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ColorSchemeBenchmark {
+    @get:Rule
+    val benchmarkRule = ComposeBenchmarkRule()
+
+    private val colorSchemeTestCaseFactory = { ColorSchemeTestCase() }
+
+    @Test
+    fun firstPixel() {
+        benchmarkRule.benchmarkToFirstPixel(colorSchemeTestCaseFactory)
+    }
+}
+
+class ColorSchemeTestCase : LayeredComposeTestCase() {
+
+    @Composable
+    override fun MeasuredContent() {
+        Column {
+            Box(modifier = Modifier.size(1.dp).background(MaterialTheme.colorScheme.surface))
+            Box(modifier = Modifier.size(1.dp).background(
+                MaterialTheme.colorScheme.contentColorFor(MaterialTheme.colorScheme.surface))
+            )
+            Box(modifier = Modifier.size(1.dp).background(MaterialTheme.colorScheme.primary))
+            Box(modifier = Modifier.size(1.dp).background(
+                MaterialTheme.colorScheme.contentColorFor(MaterialTheme.colorScheme.primary))
+            )
+            Box(modifier = Modifier.size(1.dp).background(MaterialTheme.colorScheme.secondary))
+            Box(modifier = Modifier.size(1.dp).background(
+                MaterialTheme.colorScheme.contentColorFor(MaterialTheme.colorScheme.secondary))
+            )
+            Box(modifier = Modifier.size(1.dp).background(MaterialTheme.colorScheme.tertiary))
+            Box(modifier = Modifier.size(1.dp).background(
+                MaterialTheme.colorScheme.contentColorFor(MaterialTheme.colorScheme.tertiary))
+            )
+            Box(modifier = Modifier.size(1.dp).background(MaterialTheme.colorScheme.error))
+            Box(modifier = Modifier.size(1.dp).background(
+                MaterialTheme.colorScheme.contentColorFor(MaterialTheme.colorScheme.error))
+            )
+            Box(modifier = Modifier.size(1.dp).background(
+                MaterialTheme.colorScheme.surfaceContainerLowest)
+            )
+            Box(modifier = Modifier.size(1.dp).background(
+                MaterialTheme.colorScheme.contentColorFor(
+                    MaterialTheme.colorScheme.surfaceContainerLowest)
+                )
+            )
+        }
+    }
+
+    @Composable
+    override fun ContentWrappers(content: @Composable () -> Unit) {
+        MaterialTheme {
+            content()
+        }
+    }
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/RangeSliderBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/RangeSliderBenchmark.kt
index 270c5dd..ae7504d 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/RangeSliderBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/RangeSliderBenchmark.kt
@@ -29,6 +29,7 @@
 import androidx.compose.testutils.LayeredComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
 import androidx.compose.testutils.benchmark.toggleStateBenchmarkComposeMeasureLayout
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
@@ -45,7 +46,7 @@
 
     @Test
     fun firstPixel() {
-        benchmarkRule.benchmarkFirstRenderUntilStable(sliderTestCaseFactory)
+        benchmarkRule.benchmarkToFirstPixel(sliderTestCaseFactory)
     }
 
     @Test
@@ -87,7 +88,7 @@
 
     override fun toggleState() {
         if (state.activeRangeStart == 0f) {
-            state.activeRangeStart = 1f
+            state.activeRangeStart = .7f
         } else {
             state.activeRangeStart = 0f
         }
diff --git a/compose/material3/material3-adaptive/api/current.txt b/compose/material3/material3-adaptive/api/current.txt
index 955380f..c21a89f 100644
--- a/compose/material3/material3-adaptive/api/current.txt
+++ b/compose/material3/material3-adaptive/api/current.txt
@@ -96,15 +96,6 @@
     property public abstract androidx.compose.material3.adaptive.ThreePaneScaffoldValue layoutValue;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public enum NavigationSuiteAlignment {
-    method public static androidx.compose.material3.adaptive.NavigationSuiteAlignment valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
-    method public static androidx.compose.material3.adaptive.NavigationSuiteAlignment[] values();
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment BottomHorizontal;
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment EndVertical;
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment StartVertical;
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment TopHorizontal;
-  }
-
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class NavigationSuiteColors {
     method public long getNavigationBarContainerColor();
     method public long getNavigationBarContentColor();
@@ -121,14 +112,7 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class NavigationSuiteDefaults {
-    method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
-    method public androidx.compose.material3.adaptive.NavigationSuiteAlignment getNavigationBarAlignment();
-    method public androidx.compose.material3.adaptive.NavigationSuiteAlignment getNavigationDrawerAlignment();
-    method public androidx.compose.material3.adaptive.NavigationSuiteAlignment getNavigationRailAlignment();
-    property public final androidx.compose.material3.adaptive.NavigationSuiteAlignment NavigationBarAlignment;
-    property public final androidx.compose.material3.adaptive.NavigationSuiteAlignment NavigationDrawerAlignment;
-    property public final androidx.compose.material3.adaptive.NavigationSuiteAlignment NavigationRailAlignment;
     field public static final androidx.compose.material3.adaptive.NavigationSuiteDefaults INSTANCE;
   }
 
@@ -141,13 +125,15 @@
     property public final androidx.compose.material3.NavigationRailItemColors navigationRailItemColors;
   }
 
-  public final class NavigationSuiteScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuite(androidx.compose.material3.adaptive.NavigationSuiteScaffoldScope, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScaffoldScope,kotlin.Unit> navigationSuite, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class NavigationSuiteScaffoldDefaults {
+    method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
+    field public static final androidx.compose.material3.adaptive.NavigationSuiteScaffoldDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface NavigationSuiteScaffoldScope {
-    method public androidx.compose.ui.Modifier alignment(androidx.compose.ui.Modifier, androidx.compose.material3.adaptive.NavigationSuiteAlignment alignment);
+  public final class NavigationSuiteScaffoldKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface NavigationSuiteScope {
diff --git a/compose/material3/material3-adaptive/api/restricted_current.txt b/compose/material3/material3-adaptive/api/restricted_current.txt
index 955380f..c21a89f 100644
--- a/compose/material3/material3-adaptive/api/restricted_current.txt
+++ b/compose/material3/material3-adaptive/api/restricted_current.txt
@@ -96,15 +96,6 @@
     property public abstract androidx.compose.material3.adaptive.ThreePaneScaffoldValue layoutValue;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public enum NavigationSuiteAlignment {
-    method public static androidx.compose.material3.adaptive.NavigationSuiteAlignment valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
-    method public static androidx.compose.material3.adaptive.NavigationSuiteAlignment[] values();
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment BottomHorizontal;
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment EndVertical;
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment StartVertical;
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment TopHorizontal;
-  }
-
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class NavigationSuiteColors {
     method public long getNavigationBarContainerColor();
     method public long getNavigationBarContentColor();
@@ -121,14 +112,7 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class NavigationSuiteDefaults {
-    method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
-    method public androidx.compose.material3.adaptive.NavigationSuiteAlignment getNavigationBarAlignment();
-    method public androidx.compose.material3.adaptive.NavigationSuiteAlignment getNavigationDrawerAlignment();
-    method public androidx.compose.material3.adaptive.NavigationSuiteAlignment getNavigationRailAlignment();
-    property public final androidx.compose.material3.adaptive.NavigationSuiteAlignment NavigationBarAlignment;
-    property public final androidx.compose.material3.adaptive.NavigationSuiteAlignment NavigationDrawerAlignment;
-    property public final androidx.compose.material3.adaptive.NavigationSuiteAlignment NavigationRailAlignment;
     field public static final androidx.compose.material3.adaptive.NavigationSuiteDefaults INSTANCE;
   }
 
@@ -141,13 +125,15 @@
     property public final androidx.compose.material3.NavigationRailItemColors navigationRailItemColors;
   }
 
-  public final class NavigationSuiteScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuite(androidx.compose.material3.adaptive.NavigationSuiteScaffoldScope, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScaffoldScope,kotlin.Unit> navigationSuite, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class NavigationSuiteScaffoldDefaults {
+    method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
+    field public static final androidx.compose.material3.adaptive.NavigationSuiteScaffoldDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface NavigationSuiteScaffoldScope {
-    method public androidx.compose.ui.Modifier alignment(androidx.compose.ui.Modifier, androidx.compose.material3.adaptive.NavigationSuiteAlignment alignment);
+  public final class NavigationSuiteScaffoldKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface NavigationSuiteScope {
diff --git a/compose/material3/material3-adaptive/build.gradle b/compose/material3/material3-adaptive/build.gradle
index 5c97bb2..aeb1c75 100644
--- a/compose/material3/material3-adaptive/build.gradle
+++ b/compose/material3/material3-adaptive/build.gradle
@@ -86,7 +86,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependsOn(androidMain)
             dependencies {
@@ -97,6 +97,15 @@
                 implementation(libs.truth)
             }
         }
+
+        androidUnitTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.junit)
+                implementation(libs.testRunner)
+                implementation(libs.truth)
+            }
+        }
     }
 }
 
diff --git a/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/NavigationSuiteScaffoldSamples.kt b/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/NavigationSuiteScaffoldSamples.kt
index 51ee44b..01ccec2 100644
--- a/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/NavigationSuiteScaffoldSamples.kt
+++ b/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/NavigationSuiteScaffoldSamples.kt
@@ -23,10 +23,8 @@
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Text
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
-import androidx.compose.material3.adaptive.NavigationSuite
-import androidx.compose.material3.adaptive.NavigationSuiteAlignment
-import androidx.compose.material3.adaptive.NavigationSuiteDefaults
 import androidx.compose.material3.adaptive.NavigationSuiteScaffold
+import androidx.compose.material3.adaptive.NavigationSuiteScaffoldDefaults
 import androidx.compose.material3.adaptive.NavigationSuiteType
 import androidx.compose.material3.adaptive.calculateWindowAdaptiveInfo
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@@ -47,19 +45,17 @@
     var selectedItem by remember { mutableIntStateOf(0) }
     val navItems = listOf("Songs", "Artists", "Playlists")
     val navSuiteType =
-        NavigationSuiteDefaults.calculateFromAdaptiveInfo(calculateWindowAdaptiveInfo())
+        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(calculateWindowAdaptiveInfo())
 
     NavigationSuiteScaffold(
-        navigationSuite = {
-            NavigationSuite {
-                navItems.forEachIndexed { index, navItem ->
-                    item(
-                        icon = { Icon(Icons.Filled.Favorite, contentDescription = navItem) },
-                        label = { Text(navItem) },
-                        selected = selectedItem == index,
-                        onClick = { selectedItem = index }
-                    )
-                }
+        navigationSuiteItems = {
+            navItems.forEachIndexed { index, navItem ->
+                item(
+                    icon = { Icon(Icons.Filled.Favorite, contentDescription = navItem) },
+                    label = { Text(navItem) },
+                    selected = selectedItem == index,
+                    onClick = { selectedItem = index }
+                )
             }
         }
     ) {
@@ -79,36 +75,25 @@
     var selectedItem by remember { mutableIntStateOf(0) }
     val navItems = listOf("Songs", "Artists", "Playlists")
     val adaptiveInfo = calculateWindowAdaptiveInfo()
+    // Custom configuration that shows a navigation drawer in large screens.
     val customNavSuiteType = with(adaptiveInfo) {
         if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded) {
             NavigationSuiteType.NavigationDrawer
-        } else if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) {
-            NavigationSuiteType.NavigationRail
         } else {
-            NavigationSuiteDefaults.calculateFromAdaptiveInfo(adaptiveInfo)
+            NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(adaptiveInfo)
         }
     }
 
-    // Custom configuration that shows nav rail on end of screen in small screens, and navigation
-    // drawer in large screens.
     NavigationSuiteScaffold(
-        navigationSuite = {
-            NavigationSuite(
-                layoutType = customNavSuiteType,
-                modifier = if (customNavSuiteType == NavigationSuiteType.NavigationRail) {
-                    Modifier.alignment(NavigationSuiteAlignment.EndVertical)
-                } else {
-                    Modifier
-                }
-            ) {
-                navItems.forEachIndexed { index, navItem ->
-                    item(
-                        icon = { Icon(Icons.Filled.Favorite, contentDescription = navItem) },
-                        label = { Text(navItem) },
-                        selected = selectedItem == index,
-                        onClick = { selectedItem = index }
-                    )
-                }
+        layoutType = customNavSuiteType,
+        navigationSuiteItems = {
+            navItems.forEachIndexed { index, navItem ->
+                item(
+                    icon = { Icon(Icons.Filled.Favorite, contentDescription = navItem) },
+                    label = { Text(navItem) },
+                    selected = selectedItem == index,
+                    onClick = { selectedItem = index }
+                )
             }
         }
     ) {
diff --git a/compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/CalculatePostureTest.kt b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CalculatePostureTest.kt
similarity index 100%
rename from compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/CalculatePostureTest.kt
rename to compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CalculatePostureTest.kt
diff --git a/compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/CalculateWindowAdaptiveInfoTest.kt b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CalculateWindowAdaptiveInfoTest.kt
similarity index 100%
rename from compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/CalculateWindowAdaptiveInfoTest.kt
rename to compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CalculateWindowAdaptiveInfoTest.kt
diff --git a/compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/FoldingFeaturesAsStateTest.kt b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/FoldingFeaturesAsStateTest.kt
similarity index 100%
rename from compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/FoldingFeaturesAsStateTest.kt
rename to compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/FoldingFeaturesAsStateTest.kt
diff --git a/compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/GoldenCommon.kt b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/GoldenCommon.kt
similarity index 100%
rename from compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/GoldenCommon.kt
rename to compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/GoldenCommon.kt
diff --git a/compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffoldStateTest.kt b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffoldStateTest.kt
similarity index 100%
rename from compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffoldStateTest.kt
rename to compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffoldStateTest.kt
diff --git a/compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldScreenshotTest.kt b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldScreenshotTest.kt
rename to compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldScreenshotTest.kt
diff --git a/compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldTest.kt b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldTest.kt
similarity index 100%
rename from compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldTest.kt
rename to compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldTest.kt
diff --git a/compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/WindowSizeAsStateTest.kt b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/WindowSizeAsStateTest.kt
similarity index 100%
rename from compose/material3/material3-adaptive/src/androidAndroidTest/kotlin/androidx/compose/material3/adaptive/WindowSizeAsStateTest.kt
rename to compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/WindowSizeAsStateTest.kt
diff --git a/compose/material3/material3-adaptive/src/test/kotlin/androidx/compose/material3/adaptive/AdaptiveLayoutDirectiveTest.kt b/compose/material3/material3-adaptive/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/AdaptiveLayoutDirectiveTest.kt
similarity index 100%
rename from compose/material3/material3-adaptive/src/test/kotlin/androidx/compose/material3/adaptive/AdaptiveLayoutDirectiveTest.kt
rename to compose/material3/material3-adaptive/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/AdaptiveLayoutDirectiveTest.kt
diff --git a/compose/material3/material3-adaptive/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffoldTest.kt b/compose/material3/material3-adaptive/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffoldTest.kt
new file mode 100644
index 0000000..359f940
--- /dev/null
+++ b/compose/material3/material3-adaptive/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffoldTest.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.material3.adaptive
+
+import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
+import androidx.compose.material3.windowsizeclass.WindowSizeClass
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3WindowSizeClassApi::class)
+@RunWith(JUnit4::class)
+class NavigationSuiteScaffoldTest {
+
+    @Test
+    fun navigationLayoutTypeTest_compactWidth_compactHeight() {
+        val mockAdaptiveInfo =
+            createMockAdaptiveInfo(
+                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(400.dp, 400.dp))
+            )
+
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+            .isEqualTo(NavigationSuiteType.NavigationBar)
+    }
+
+    @Test
+    fun navigationLayoutTypeTest_compactWidth_mediumHeight() {
+        val mockAdaptiveInfo =
+            createMockAdaptiveInfo(
+                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(400.dp, 800.dp))
+            )
+
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+            .isEqualTo(NavigationSuiteType.NavigationBar)
+    }
+
+    @Test
+    fun navigationLayoutTypeTest_compactWidth_expandedHeight() {
+        val mockAdaptiveInfo =
+            createMockAdaptiveInfo(
+                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(400.dp, 1000.dp))
+            )
+
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+            .isEqualTo(NavigationSuiteType.NavigationBar)
+    }
+
+    @Test
+    fun navigationLayoutTypeTest_mediumWidth_compactHeight() {
+        val mockAdaptiveInfo =
+            createMockAdaptiveInfo(
+                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 400.dp))
+            )
+
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+            .isEqualTo(NavigationSuiteType.NavigationBar)
+    }
+
+    @Test
+    fun navigationLayoutTypeTest_mediumWidth_mediumHeight() {
+        val mockAdaptiveInfo =
+            createMockAdaptiveInfo(
+                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp))
+            )
+
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+            .isEqualTo(NavigationSuiteType.NavigationBar)
+    }
+
+    @Test
+    fun navigationLayoutTypeTest_mediumWidth_expandedHeight() {
+        val mockAdaptiveInfo =
+            createMockAdaptiveInfo(
+                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 1000.dp))
+            )
+
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+            .isEqualTo(NavigationSuiteType.NavigationBar)
+    }
+
+    @Test
+    fun navigationLayoutTypeTest_expandedWidth_compactHeight() {
+        val mockAdaptiveInfo =
+            createMockAdaptiveInfo(
+                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(1000.dp, 400.dp))
+            )
+
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+            .isEqualTo(NavigationSuiteType.NavigationBar)
+    }
+
+    @Test
+    fun navigationLayoutTypeTest_expandedWidth_mediumHeight() {
+        val mockAdaptiveInfo =
+            createMockAdaptiveInfo(
+                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(1000.dp, 800.dp))
+            )
+
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+            .isEqualTo(NavigationSuiteType.NavigationRail)
+    }
+
+    @Test
+    fun navigationLayoutTypeTest_expandedWidth_expandedHeight() {
+        val mockAdaptiveInfo =
+            createMockAdaptiveInfo(
+                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(1000.dp, 1000.dp))
+            )
+
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+            .isEqualTo(NavigationSuiteType.NavigationRail)
+    }
+
+    @Test
+    fun navigationLayoutTypeTest_tableTop() {
+        val mockAdaptiveInfo =
+            createMockAdaptiveInfo(
+                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(400.dp, 400.dp)),
+                isTableTop = true
+            )
+
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+            .isEqualTo(NavigationSuiteType.NavigationBar)
+    }
+
+    @Test
+    fun navigationLayoutTypeTest_tableTop_expandedWidth() {
+        val mockAdaptiveInfo =
+            createMockAdaptiveInfo(
+                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(1000.dp, 1000.dp)),
+                isTableTop = true
+            )
+
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+            .isEqualTo(NavigationSuiteType.NavigationBar)
+    }
+
+    private fun createMockAdaptiveInfo(
+        windowSizeClass: WindowSizeClass,
+        isTableTop: Boolean = false
+    ): WindowAdaptiveInfo {
+        return WindowAdaptiveInfo(
+            windowSizeClass,
+            Posture(isTabletop = isTableTop)
+        )
+    }
+}
diff --git a/compose/material3/material3-adaptive/src/test/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValueTest.kt b/compose/material3/material3-adaptive/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValueTest.kt
similarity index 100%
rename from compose/material3/material3-adaptive/src/test/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValueTest.kt
rename to compose/material3/material3-adaptive/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValueTest.kt
diff --git a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffold.kt b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffold.kt
index 0629248..0d60c95 100644
--- a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffold.kt
+++ b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffold.kt
@@ -53,14 +53,7 @@
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.IntrinsicMeasurable
 import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.node.ParentDataModifierNode
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastFilterNotNull
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMap
 import androidx.compose.ui.util.fastMaxOfOrNull
@@ -74,8 +67,10 @@
  * Example custom configuration usage:
  * @sample androidx.compose.material3.adaptive.samples.NavigationSuiteScaffoldCustomConfigSample
  *
- * @param navigationSuite the navigation component to be displayed, typically [NavigationSuite]
+ * @param navigationSuiteItems the navigation items to be displayed
  * @param modifier the [Modifier] to be applied to the navigation suite scaffold
+ * @param layoutType the current [NavigationSuiteType]. Defaults to
+ * [NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo]
  * @param containerColor the color used for the background of the navigation suite scaffold. Use
  * [Color.Transparent] to have no color
  * @param contentColor the preferred color for content inside the navigation suite scaffold.
@@ -86,112 +81,87 @@
 @ExperimentalMaterial3AdaptiveApi
 @Composable
 fun NavigationSuiteScaffold(
-    navigationSuite: @Composable NavigationSuiteScaffoldScope.() -> Unit,
+    navigationSuiteItems: NavigationSuiteScope.() -> Unit,
     modifier: Modifier = Modifier,
+    layoutType: NavigationSuiteType =
+        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(WindowAdaptiveInfoDefault),
     containerColor: Color = MaterialTheme.colorScheme.background,
     contentColor: Color = contentColorFor(containerColor),
     content: @Composable () -> Unit = {},
 ) {
     Surface(modifier = modifier, color = containerColor, contentColor = contentColor) {
         NavigationSuiteScaffoldLayout(
-            navigationSuite = navigationSuite,
+            navigationSuite = {
+                NavigationSuite(layoutType = layoutType, content = navigationSuiteItems)
+            },
+            layoutType = layoutType,
             content = content
         )
     }
 }
 
 /**
- * Layout for a [NavigationSuiteScaffold]'s content.
+ * Layout for a [NavigationSuiteScaffold]'s content. This function wraps the [content] and places
+ * the [navigationSuite] component according to the given [layoutType].
  *
- * @param navigationSuite the navigation suite of the [NavigationSuiteScaffold]
- * @param content the main body of the [NavigationSuiteScaffold]
- * @throws [IllegalArgumentException] if there is more than one [NavigationSuiteAlignment] for the
- * given navigation component
+ * @param navigationSuite the navigation component to be displayed, typically [NavigationSuite]
+ * @param layoutType the current [NavigationSuiteType]. Defaults to
+ * [NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo]
+ * @param content the content of your screen
  */
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@ExperimentalMaterial3AdaptiveApi
 @Composable
-private fun NavigationSuiteScaffoldLayout(
-    navigationSuite: @Composable NavigationSuiteScaffoldScope.() -> Unit,
+fun NavigationSuiteScaffoldLayout(
+    navigationSuite: @Composable () -> Unit,
+    layoutType: NavigationSuiteType =
+        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(WindowAdaptiveInfoDefault),
     content: @Composable () -> Unit = {}
 ) {
     Layout(
-        contents = listOf({ NavigationSuiteScaffoldScopeImpl.navigationSuite() }, content)
+        contents = listOf(navigationSuite, content)
     ) { (navigationMeasurables, contentMeasurables), constraints ->
         val navigationPlaceables = navigationMeasurables.fastMap { it.measure(constraints) }
-        val alignments = navigationPlaceables.fastMap {
-            (it.parentData as NavigationSuiteParentData).alignment
-        }.fastFilterNotNull()
-        if (alignments.fastAll { alignments[0] != it }) {
-            throw IllegalArgumentException("There should be only one NavigationSuiteAlignment.")
-        }
-        val alignment = alignments.firstOrNull() ?: NavigationSuiteAlignment.StartVertical
+        val isNavigationBar = layoutType == NavigationSuiteType.NavigationBar
         val layoutHeight = constraints.maxHeight
         val layoutWidth = constraints.maxWidth
         val contentPlaceables = contentMeasurables.fastMap { it.measure(
-            if (alignment == NavigationSuiteAlignment.TopHorizontal ||
-                alignment == NavigationSuiteAlignment.BottomHorizontal
-            ) {
+            if (isNavigationBar) {
                 constraints.copy(
-                    minHeight = layoutHeight - navigationPlaceables.fastMaxOfOrNull { it.height }!!,
-                    maxHeight = layoutHeight - navigationPlaceables.fastMaxOfOrNull { it.height }!!
+                    minHeight = layoutHeight - (navigationPlaceables.fastMaxOfOrNull { it.height }
+                        ?: 0),
+                    maxHeight = layoutHeight - (navigationPlaceables.fastMaxOfOrNull { it.height }
+                        ?: 0)
                 )
             } else {
                 constraints.copy(
-                    minWidth = layoutWidth - navigationPlaceables.fastMaxOfOrNull { it.width }!!,
-                    maxWidth = layoutWidth - navigationPlaceables.fastMaxOfOrNull { it.width }!!
+                    minWidth = layoutWidth - (navigationPlaceables.fastMaxOfOrNull { it.width }
+                        ?: 0),
+                    maxWidth = layoutWidth - (navigationPlaceables.fastMaxOfOrNull { it.width }
+                        ?: 0)
                 )
             }
         ) }
 
         layout(layoutWidth, layoutHeight) {
-            when (alignment) {
-                NavigationSuiteAlignment.StartVertical -> {
-                    // Place the navigation component at the start of the screen.
-                    navigationPlaceables.fastForEach {
-                        it.placeRelative(0, 0)
-                    }
-                    // Place content to the side of the navigation component.
-                    contentPlaceables.fastForEach {
-                        it.placeRelative(navigationPlaceables.fastMaxOfOrNull { it.width }!!, 0)
-                    }
+            if (isNavigationBar) {
+                // Place content above the navigation component.
+                contentPlaceables.fastForEach {
+                    it.placeRelative(0, 0)
                 }
-
-                NavigationSuiteAlignment.EndVertical -> {
-                    // Place the navigation component at the end of the screen.
-                    navigationPlaceables.fastForEach {
-                        it.placeRelative(
-                            layoutWidth - navigationPlaceables.fastMaxOfOrNull { it.width }!!,
-                            0
-                        )
-                    }
-                    // Place content at the start of the screen.
-                    contentPlaceables.fastForEach {
-                        it.placeRelative(0, 0)
-                    }
+                // Place the navigation component at the bottom of the screen.
+                navigationPlaceables.fastForEach {
+                    it.placeRelative(
+                        0,
+                        layoutHeight - (navigationPlaceables.fastMaxOfOrNull { it.height } ?: 0))
                 }
-
-                NavigationSuiteAlignment.TopHorizontal -> {
-                    // Place the navigation component at the start of the screen.
-                    navigationPlaceables.fastForEach {
-                        it.placeRelative(0, 0)
-                    }
-                    // Place content below the navigation component.
-                    contentPlaceables.fastForEach {
-                        it.placeRelative(0, navigationPlaceables.fastMaxOfOrNull { it.height }!!)
-                    }
+            } else {
+                // Place the navigation component at the start of the screen.
+                navigationPlaceables.fastForEach {
+                    it.placeRelative(0, 0)
                 }
-
-                NavigationSuiteAlignment.BottomHorizontal -> {
-                    // Place content above the navigation component.
-                    contentPlaceables.fastForEach {
-                        it.placeRelative(0, 0)
-                    }
-                    // Place the navigation component at the bottom of the screen.
-                    navigationPlaceables.fastForEach {
-                        it.placeRelative(
-                            0,
-                            layoutHeight - navigationPlaceables.fastMaxOfOrNull { it.height }!!)
-                    }
+                // Place content to the side of the navigation component.
+                contentPlaceables.fastForEach {
+                    it.placeRelative((navigationPlaceables.fastMaxOfOrNull { it.width } ?: 0), 0)
                 }
             }
         }
@@ -207,7 +177,7 @@
  *
  * @param modifier the [Modifier] to be applied to the navigation component
  * @param layoutType the current [NavigationSuiteType] of the [NavigationSuiteScaffold]. Defaults to
- * [NavigationSuiteDefaults.calculateFromAdaptiveInfo]
+ * [NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo]
  * @param colors [NavigationSuiteColors] that will be used to determine the container (background)
  * color of the navigation component and the preferred color for content inside the navigation
  * component
@@ -216,10 +186,10 @@
  */
 @ExperimentalMaterial3AdaptiveApi
 @Composable
-fun NavigationSuiteScaffoldScope.NavigationSuite(
+fun NavigationSuite(
     modifier: Modifier = Modifier,
     layoutType: NavigationSuiteType =
-        NavigationSuiteDefaults.calculateFromAdaptiveInfo(WindowAdaptiveInfoDefault),
+        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(WindowAdaptiveInfoDefault),
     colors: NavigationSuiteColors = NavigationSuiteDefaults.colors(),
     content: NavigationSuiteScope.() -> Unit
 ) {
@@ -228,7 +198,7 @@
     when (layoutType) {
         NavigationSuiteType.NavigationBar -> {
             NavigationBar(
-                modifier = modifier.alignment(NavigationSuiteDefaults.NavigationBarAlignment),
+                modifier = modifier,
                 containerColor = colors.navigationBarContainerColor,
                 contentColor = colors.navigationBarContentColor
             ) {
@@ -251,7 +221,7 @@
 
         NavigationSuiteType.NavigationRail -> {
             NavigationRail(
-                modifier = modifier.alignment(NavigationSuiteDefaults.NavigationRailAlignment),
+                modifier = modifier,
                 containerColor = colors.navigationRailContainerColor,
                 contentColor = colors.navigationRailContentColor
             ) {
@@ -274,7 +244,7 @@
 
         NavigationSuiteType.NavigationDrawer -> {
             PermanentDrawerSheet(
-                modifier = modifier.alignment(NavigationSuiteDefaults.NavigationDrawerAlignment),
+                modifier = modifier,
                 drawerContainerColor = colors.navigationDrawerContainerColor,
                 drawerContentColor = colors.navigationDrawerContentColor
             ) {
@@ -296,37 +266,7 @@
     }
 }
 
-/** The scope associated with the [NavigationSuiteScaffold]. */
-@ExperimentalMaterial3AdaptiveApi
-interface NavigationSuiteScaffoldScope {
-    /**
-     * [Modifier] that should be applied to the [NavigationSuite] of the [NavigationSuiteScaffold]
-     * in order to determine its alignment on the screen.
-     *
-     * @param alignment the desired [NavigationSuiteAlignment]
-     */
-    fun Modifier.alignment(alignment: NavigationSuiteAlignment): Modifier
-}
-
-/**
- * Represents the alignment of the navigation component of the [NavigationSuiteScaffold].
- *
- * The alignment informs the Navigation Suite Scaffold how to properly place the expected navigation
- * component on the screen in relation to the Navigation Suite Scaffold's content.
- */
-@ExperimentalMaterial3AdaptiveApi
-enum class NavigationSuiteAlignment {
-    /** The navigation component is vertical and positioned at the start of the screen. */
-    StartVertical,
-    /** The navigation component is vertical and positioned at the end of the screen. */
-    EndVertical,
-    /** The navigation component is horizontal and positioned at the top of the screen. */
-    TopHorizontal,
-    /** The navigation component is horizontal and positioned at the bottom of the screen. */
-    BottomHorizontal
-}
-
-/** The scope associated with the [NavigationSuite]. */
+/** The scope associated with the [NavigationSuiteScope]. */
 @ExperimentalMaterial3AdaptiveApi
 interface NavigationSuiteScope {
 
@@ -383,15 +323,16 @@
 
     companion object {
         /**
-         * A navigation suite type that instructs the [NavigationSuite] to expect a [NavigationBar].
+         * A navigation suite type that instructs the [NavigationSuite] to expect a [NavigationBar]
+         * that will be displayed at the bottom of the screen.
          *
          * @see NavigationBar
          */
         val NavigationBar = NavigationSuiteType(description = "NavigationBar")
 
         /**
-         * A navigation suite type that instructs the [NavigationSuite] to expect a
-         * [NavigationRail].
+         * A navigation suite type that instructs the [NavigationSuite] to expect a [NavigationRail]
+         * that will be displayed at the start of the screen.
          *
          * @see NavigationRail
          */
@@ -399,7 +340,7 @@
 
         /**
          * A navigation suite type that instructs the [NavigationSuite] to expect a
-         * [PermanentDrawerSheet].
+         * [PermanentDrawerSheet] that will be displayed at the start of the screen.
          *
          * @see PermanentDrawerSheet
          */
@@ -407,15 +348,15 @@
     }
 }
 
-/** Contains the default values used by the [NavigationSuite]. */
+/** Contains the default values used by the [NavigationSuiteScaffold]. */
 @ExperimentalMaterial3AdaptiveApi
-object NavigationSuiteDefaults {
+object NavigationSuiteScaffoldDefaults {
     /**
      * Returns the expected [NavigationSuiteType] according to the provided [WindowAdaptiveInfo].
-     * Usually used with the [NavigationSuite].
+     * Usually used with the [NavigationSuiteScaffold] and related APIs.
      *
      * @param adaptiveInfo the provided [WindowAdaptiveInfo]
-     * @see NavigationSuite
+     * @see NavigationSuiteScaffold
      */
     fun calculateFromAdaptiveInfo(adaptiveInfo: WindowAdaptiveInfo): NavigationSuiteType {
         return with(adaptiveInfo) {
@@ -428,16 +369,11 @@
             }
         }
     }
+}
 
-    /** Default alignment for the [NavigationSuiteType.NavigationBar]. */
-    val NavigationBarAlignment = NavigationSuiteAlignment.BottomHorizontal
-
-    /** Default alignment for the [NavigationSuiteType.NavigationRail]. */
-    val NavigationRailAlignment = NavigationSuiteAlignment.StartVertical
-
-    /** Default alignment for the [NavigationSuiteType.NavigationDrawer]. */
-    val NavigationDrawerAlignment = NavigationSuiteAlignment.StartVertical
-
+/** Contains the default values used by the [NavigationSuite]. */
+@ExperimentalMaterial3AdaptiveApi
+object NavigationSuiteDefaults {
     /**
      * Creates a [NavigationSuiteColors] with the provided colors for the container color, according
      * to the Material specification.
@@ -526,64 +462,6 @@
 )
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal object NavigationSuiteScaffoldScopeImpl : NavigationSuiteScaffoldScope {
-    override fun Modifier.alignment(alignment: NavigationSuiteAlignment): Modifier {
-        return this.then(
-            AlignmentElement(alignment = alignment)
-        )
-    }
-}
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal class AlignmentElement(
-    val alignment: NavigationSuiteAlignment
-) : ModifierNodeElement<AlignmentNode>() {
-    override fun create(): AlignmentNode {
-        return AlignmentNode(alignment)
-    }
-
-    override fun update(node: AlignmentNode) {
-        node.alignment = alignment
-    }
-
-    override fun InspectorInfo.inspectableProperties() {
-        name = "alignment"
-        value = alignment
-    }
-
-    override fun hashCode(): Int = alignment.hashCode()
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        val otherModifier = other as? AlignmentElement ?: return false
-        return alignment == otherModifier.alignment
-    }
-}
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal class AlignmentNode(
-    var alignment: NavigationSuiteAlignment
-) : ParentDataModifierNode, Modifier.Node() {
-    override fun Density.modifyParentData(parentData: Any?) =
-        ((parentData as? NavigationSuiteParentData) ?: NavigationSuiteParentData()).also {
-            it.alignment = alignment
-        }
-}
-
-/** Parent data associated with children. */
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal data class NavigationSuiteParentData(
-    var alignment: NavigationSuiteAlignment? = null
-)
-
-internal val IntrinsicMeasurable.navigationSuiteParentData: NavigationSuiteParentData?
-    get() = parentData as? NavigationSuiteParentData
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal val NavigationSuiteParentData?.alignment: NavigationSuiteAlignment
-    get() = this?.alignment ?: NavigationSuiteAlignment.StartVertical
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
 internal expect val WindowAdaptiveInfoDefault: WindowAdaptiveInfo
     @Composable
     get
@@ -594,7 +472,7 @@
 }
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
-private class NavigationSuiteItem constructor(
+private class NavigationSuiteItem(
     val selected: Boolean,
     val onClick: () -> Unit,
     val icon: @Composable () -> Unit,
diff --git a/compose/material3/material3-adaptive/src/test/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffoldTest.kt b/compose/material3/material3-adaptive/src/test/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffoldTest.kt
deleted file mode 100644
index 3ab8ee3..0000000
--- a/compose/material3/material3-adaptive/src/test/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffoldTest.kt
+++ /dev/null
@@ -1,164 +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.compose.material3.adaptive
-
-import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
-import androidx.compose.material3.windowsizeclass.WindowSizeClass
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.dp
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3WindowSizeClassApi::class)
-@RunWith(JUnit4::class)
-class NavigationSuiteScaffoldTest {
-
-    @Test
-    fun navigationLayoutTypeTest_compactWidth_compactHeight() {
-        val mockAdaptiveInfo =
-            createMockAdaptiveInfo(
-                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(400.dp, 400.dp))
-            )
-
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
-            .isEqualTo(NavigationSuiteType.NavigationBar)
-    }
-
-    @Test
-    fun navigationLayoutTypeTest_compactWidth_mediumHeight() {
-        val mockAdaptiveInfo =
-            createMockAdaptiveInfo(
-                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(400.dp, 800.dp))
-            )
-
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
-            .isEqualTo(NavigationSuiteType.NavigationBar)
-    }
-
-    @Test
-    fun navigationLayoutTypeTest_compactWidth_expandedHeight() {
-        val mockAdaptiveInfo =
-            createMockAdaptiveInfo(
-                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(400.dp, 1000.dp))
-            )
-
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
-            .isEqualTo(NavigationSuiteType.NavigationBar)
-    }
-
-    @Test
-    fun navigationLayoutTypeTest_mediumWidth_compactHeight() {
-        val mockAdaptiveInfo =
-            createMockAdaptiveInfo(
-                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 400.dp))
-            )
-
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
-            .isEqualTo(NavigationSuiteType.NavigationBar)
-    }
-
-    @Test
-    fun navigationLayoutTypeTest_mediumWidth_mediumHeight() {
-        val mockAdaptiveInfo =
-            createMockAdaptiveInfo(
-                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp))
-            )
-
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
-            .isEqualTo(NavigationSuiteType.NavigationBar)
-    }
-
-    @Test
-    fun navigationLayoutTypeTest_mediumWidth_expandedHeight() {
-        val mockAdaptiveInfo =
-            createMockAdaptiveInfo(
-                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 1000.dp))
-            )
-
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
-            .isEqualTo(NavigationSuiteType.NavigationBar)
-    }
-
-    @Test
-    fun navigationLayoutTypeTest_expandedWidth_compactHeight() {
-        val mockAdaptiveInfo =
-            createMockAdaptiveInfo(
-                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(1000.dp, 400.dp))
-            )
-
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
-            .isEqualTo(NavigationSuiteType.NavigationBar)
-    }
-
-    @Test
-    fun navigationLayoutTypeTest_expandedWidth_mediumHeight() {
-        val mockAdaptiveInfo =
-            createMockAdaptiveInfo(
-                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(1000.dp, 800.dp))
-            )
-
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
-            .isEqualTo(NavigationSuiteType.NavigationRail)
-    }
-
-    @Test
-    fun navigationLayoutTypeTest_expandedWidth_expandedHeight() {
-        val mockAdaptiveInfo =
-            createMockAdaptiveInfo(
-                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(1000.dp, 1000.dp))
-            )
-
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
-            .isEqualTo(NavigationSuiteType.NavigationRail)
-    }
-
-    @Test
-    fun navigationLayoutTypeTest_tableTop() {
-        val mockAdaptiveInfo =
-            createMockAdaptiveInfo(
-                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(400.dp, 400.dp)),
-                isTableTop = true
-            )
-
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
-            .isEqualTo(NavigationSuiteType.NavigationBar)
-    }
-
-    @Test
-    fun navigationLayoutTypeTest_tableTop_expandedWidth() {
-        val mockAdaptiveInfo =
-            createMockAdaptiveInfo(
-                windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(1000.dp, 1000.dp)),
-                isTableTop = true
-            )
-
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
-            .isEqualTo(NavigationSuiteType.NavigationBar)
-    }
-
-    private fun createMockAdaptiveInfo(
-        windowSizeClass: WindowSizeClass,
-        isTableTop: Boolean = false
-    ): WindowAdaptiveInfo {
-        return WindowAdaptiveInfo(
-            windowSizeClass,
-            Posture(isTabletop = isTableTop)
-        )
-    }
-}
diff --git a/compose/material3/material3-window-size-class/build.gradle b/compose/material3/material3-window-size-class/build.gradle
index ae8d2cd..74d0693 100644
--- a/compose/material3/material3-window-size-class/build.gradle
+++ b/compose/material3/material3-window-size-class/build.gradle
@@ -93,7 +93,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:test-utils"))
@@ -105,7 +105,7 @@
             }
         }
 
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.kotlinTest)
diff --git a/compose/material3/material3-window-size-class/src/androidAndroidTest/kotlin/androidx/compose/material3/windowsizeclass/AndroidWindowSizeClassTest.kt b/compose/material3/material3-window-size-class/src/androidInstrumentedTest/kotlin/androidx/compose/material3/windowsizeclass/AndroidWindowSizeClassTest.kt
similarity index 100%
rename from compose/material3/material3-window-size-class/src/androidAndroidTest/kotlin/androidx/compose/material3/windowsizeclass/AndroidWindowSizeClassTest.kt
rename to compose/material3/material3-window-size-class/src/androidInstrumentedTest/kotlin/androidx/compose/material3/windowsizeclass/AndroidWindowSizeClassTest.kt
diff --git a/compose/material3/material3-window-size-class/src/test/kotlin/androidx/compose/material3/windowsizeclass/WindowSizeClassTest.kt b/compose/material3/material3-window-size-class/src/androidUnitTest/kotlin/androidx/compose/material3/windowsizeclass/WindowSizeClassTest.kt
similarity index 100%
rename from compose/material3/material3-window-size-class/src/test/kotlin/androidx/compose/material3/windowsizeclass/WindowSizeClassTest.kt
rename to compose/material3/material3-window-size-class/src/androidUnitTest/kotlin/androidx/compose/material3/windowsizeclass/WindowSizeClassTest.kt
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 51210ad..1c435af 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -29,13 +29,17 @@
   }
 
   public final class AppBarKt {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.BottomAppBarState BottomAppBarState(float initialHeightOffsetLimit, float initialHeightOffset, float initialContentOffset);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SmallTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.BottomAppBarState rememberBottomAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TopAppBarState rememberTopAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
   }
 
@@ -66,6 +70,7 @@
   }
 
   public final class BottomAppBarDefaults {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.BottomAppBarScrollBehavior exitAlwaysScrollBehavior(optional androidx.compose.material3.BottomAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
     method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getContainerElevation();
@@ -79,6 +84,39 @@
     field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
   }
 
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface BottomAppBarScrollBehavior {
+    method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? getFlingAnimationSpec();
+    method public androidx.compose.ui.input.nestedscroll.NestedScrollConnection getNestedScrollConnection();
+    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float>? getSnapAnimationSpec();
+    method public androidx.compose.material3.BottomAppBarState getState();
+    method public boolean isPinned();
+    property public abstract androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec;
+    property public abstract boolean isPinned;
+    property public abstract androidx.compose.ui.input.nestedscroll.NestedScrollConnection nestedScrollConnection;
+    property public abstract androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec;
+    property public abstract androidx.compose.material3.BottomAppBarState state;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public interface BottomAppBarState {
+    method public float getCollapsedFraction();
+    method public float getContentOffset();
+    method public float getHeightOffset();
+    method public float getHeightOffsetLimit();
+    method public void setContentOffset(float);
+    method public void setHeightOffset(float);
+    method public void setHeightOffsetLimit(float);
+    property public abstract float collapsedFraction;
+    property public abstract float contentOffset;
+    property public abstract float heightOffset;
+    property public abstract float heightOffsetLimit;
+    field public static final androidx.compose.material3.BottomAppBarState.Companion Companion;
+  }
+
+  public static final class BottomAppBarState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> Saver;
+  }
+
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class BottomSheetDefaults {
     method @androidx.compose.runtime.Composable public void DragHandle(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional androidx.compose.ui.graphics.Shape shape, optional long color);
     method @androidx.compose.runtime.Composable public long getContainerColor();
@@ -677,9 +715,11 @@
   public static final class FabPosition.Companion {
     method public int getCenter();
     method public int getEnd();
+    method public int getEndOverlay();
     method public int getStart();
     property public final int Center;
     property public final int End;
+    property public final int EndOverlay;
     property public final int Start;
   }
 
@@ -829,6 +869,10 @@
     property @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumTouchTargetEnforcement;
   }
 
+  public final class LabelKt {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Label(kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean isPersistent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
   @androidx.compose.runtime.Immutable public final class ListItemColors {
     ctor public ListItemColors(long containerColor, long headlineColor, long leadingIconColor, long overlineColor, long supportingTextColor, long trailingIconColor, long disabledHeadlineColor, long disabledLeadingIconColor, long disabledTrailingIconColor);
     method public long getContainerColor();
@@ -1130,6 +1174,9 @@
 
   public final class ScaffoldKt {
     method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets contentWindowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static boolean getScaffoldSubcomposeInMeasureFix();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static void setScaffoldSubcomposeInMeasureFix(boolean);
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final boolean ScaffoldSubcomposeInMeasureFix;
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class SearchBarColors {
@@ -1206,22 +1253,22 @@
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SegmentedButtonDefaults {
     method @androidx.compose.runtime.Composable public void ActiveIcon();
-    method @androidx.compose.runtime.Composable public void SegmentedButtonIcon(boolean active, optional kotlin.jvm.functions.Function0<kotlin.Unit> activeContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? inactiveContent);
+    method @androidx.compose.runtime.Composable public void Icon(boolean active, optional kotlin.jvm.functions.Function0<kotlin.Unit> activeContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? inactiveContent);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SegmentedButtonColors colors(optional long activeContainerColor, optional long activeContentColor, optional long activeBorderColor, optional long inactiveContainerColor, optional long inactiveContentColor, optional long inactiveBorderColor, optional long disabledActiveContainerColor, optional long disabledActiveContentColor, optional long disabledActiveBorderColor, optional long disabledInactiveContainerColor, optional long disabledInactiveContentColor, optional long disabledInactiveBorderColor);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.foundation.shape.CornerBasedShape getBaseShape();
     method public androidx.compose.material3.SegmentedButtonBorder getBorder();
     method public float getIconSize();
-    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.foundation.shape.CornerBasedShape getShape();
-    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.graphics.Shape shape(int position, int count, optional androidx.compose.foundation.shape.CornerBasedShape shape);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.graphics.Shape itemShape(int index, int count, optional androidx.compose.foundation.shape.CornerBasedShape baseShape);
     property public final androidx.compose.material3.SegmentedButtonBorder Border;
     property public final float IconSize;
-    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.foundation.shape.CornerBasedShape Shape;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.foundation.shape.CornerBasedShape baseShape;
     field public static final androidx.compose.material3.SegmentedButtonDefaults INSTANCE;
   }
 
   public final class SegmentedButtonKt {
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MultiChoiceSegmentedButtonRow(optional androidx.compose.ui.Modifier modifier, optional float space, kotlin.jvm.functions.Function1<? super androidx.compose.material3.MultiChoiceSegmentedButtonRowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.MultiChoiceSegmentedButtonRowScope, boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.SingleChoiceSegmentedButtonRowScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.MultiChoiceSegmentedButtonRowScope, boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> label);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.SingleChoiceSegmentedButtonRowScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> label);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SingleChoiceSegmentedButtonRow(optional androidx.compose.ui.Modifier modifier, optional float space, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SingleChoiceSegmentedButtonRowScope,kotlin.Unit> content);
   }
 
@@ -1348,7 +1395,7 @@
   @androidx.compose.runtime.Stable public final class SliderDefaults {
     method @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
     method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.RangeSliderState rangeSliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
-    method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
+    method @Deprecated @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderState sliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors(optional long thumbColor, optional long activeTrackColor, optional long activeTickColor, optional long inactiveTrackColor, optional long inactiveTickColor, optional long disabledThumbColor, optional long disabledActiveTrackColor, optional long disabledActiveTickColor, optional long disabledInactiveTrackColor, optional long disabledInactiveTickColor);
     field public static final androidx.compose.material3.SliderDefaults INSTANCE;
@@ -1363,16 +1410,18 @@
     method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
   }
 
-  @androidx.compose.runtime.Stable public final class SliderPositions {
-    ctor public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
-    method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
-    method public float[] getTickFractions();
-    property public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
-    property public final float[] tickFractions;
+  @Deprecated @androidx.compose.runtime.Stable public final class SliderPositions {
+    ctor @Deprecated public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
+    method @Deprecated public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
+    method @Deprecated public float[] getTickFractions();
+    property @Deprecated public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
+    property @Deprecated public final float[] tickFractions;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderState {
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderState implements androidx.compose.foundation.gestures.DraggableState {
     ctor public SliderState(optional float initialValue, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit>? initialOnValueChange, optional @IntRange(from=0L) int steps, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished);
+    method public void dispatchRawDelta(float delta);
+    method public suspend Object? drag(androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public kotlin.jvm.functions.Function0<kotlin.Unit>? getOnValueChangeFinished();
     method public int getSteps();
     method public float getValue();
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 51210ad..1c435af 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -29,13 +29,17 @@
   }
 
   public final class AppBarKt {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.BottomAppBarState BottomAppBarState(float initialHeightOffsetLimit, float initialHeightOffset, float initialContentOffset);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SmallTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.BottomAppBarState rememberBottomAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TopAppBarState rememberTopAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
   }
 
@@ -66,6 +70,7 @@
   }
 
   public final class BottomAppBarDefaults {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.BottomAppBarScrollBehavior exitAlwaysScrollBehavior(optional androidx.compose.material3.BottomAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
     method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getContainerElevation();
@@ -79,6 +84,39 @@
     field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
   }
 
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface BottomAppBarScrollBehavior {
+    method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? getFlingAnimationSpec();
+    method public androidx.compose.ui.input.nestedscroll.NestedScrollConnection getNestedScrollConnection();
+    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float>? getSnapAnimationSpec();
+    method public androidx.compose.material3.BottomAppBarState getState();
+    method public boolean isPinned();
+    property public abstract androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec;
+    property public abstract boolean isPinned;
+    property public abstract androidx.compose.ui.input.nestedscroll.NestedScrollConnection nestedScrollConnection;
+    property public abstract androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec;
+    property public abstract androidx.compose.material3.BottomAppBarState state;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public interface BottomAppBarState {
+    method public float getCollapsedFraction();
+    method public float getContentOffset();
+    method public float getHeightOffset();
+    method public float getHeightOffsetLimit();
+    method public void setContentOffset(float);
+    method public void setHeightOffset(float);
+    method public void setHeightOffsetLimit(float);
+    property public abstract float collapsedFraction;
+    property public abstract float contentOffset;
+    property public abstract float heightOffset;
+    property public abstract float heightOffsetLimit;
+    field public static final androidx.compose.material3.BottomAppBarState.Companion Companion;
+  }
+
+  public static final class BottomAppBarState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> Saver;
+  }
+
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class BottomSheetDefaults {
     method @androidx.compose.runtime.Composable public void DragHandle(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional androidx.compose.ui.graphics.Shape shape, optional long color);
     method @androidx.compose.runtime.Composable public long getContainerColor();
@@ -677,9 +715,11 @@
   public static final class FabPosition.Companion {
     method public int getCenter();
     method public int getEnd();
+    method public int getEndOverlay();
     method public int getStart();
     property public final int Center;
     property public final int End;
+    property public final int EndOverlay;
     property public final int Start;
   }
 
@@ -829,6 +869,10 @@
     property @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumTouchTargetEnforcement;
   }
 
+  public final class LabelKt {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Label(kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean isPersistent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
   @androidx.compose.runtime.Immutable public final class ListItemColors {
     ctor public ListItemColors(long containerColor, long headlineColor, long leadingIconColor, long overlineColor, long supportingTextColor, long trailingIconColor, long disabledHeadlineColor, long disabledLeadingIconColor, long disabledTrailingIconColor);
     method public long getContainerColor();
@@ -1130,6 +1174,9 @@
 
   public final class ScaffoldKt {
     method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets contentWindowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static boolean getScaffoldSubcomposeInMeasureFix();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static void setScaffoldSubcomposeInMeasureFix(boolean);
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final boolean ScaffoldSubcomposeInMeasureFix;
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class SearchBarColors {
@@ -1206,22 +1253,22 @@
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SegmentedButtonDefaults {
     method @androidx.compose.runtime.Composable public void ActiveIcon();
-    method @androidx.compose.runtime.Composable public void SegmentedButtonIcon(boolean active, optional kotlin.jvm.functions.Function0<kotlin.Unit> activeContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? inactiveContent);
+    method @androidx.compose.runtime.Composable public void Icon(boolean active, optional kotlin.jvm.functions.Function0<kotlin.Unit> activeContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? inactiveContent);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SegmentedButtonColors colors(optional long activeContainerColor, optional long activeContentColor, optional long activeBorderColor, optional long inactiveContainerColor, optional long inactiveContentColor, optional long inactiveBorderColor, optional long disabledActiveContainerColor, optional long disabledActiveContentColor, optional long disabledActiveBorderColor, optional long disabledInactiveContainerColor, optional long disabledInactiveContentColor, optional long disabledInactiveBorderColor);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.foundation.shape.CornerBasedShape getBaseShape();
     method public androidx.compose.material3.SegmentedButtonBorder getBorder();
     method public float getIconSize();
-    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.foundation.shape.CornerBasedShape getShape();
-    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.graphics.Shape shape(int position, int count, optional androidx.compose.foundation.shape.CornerBasedShape shape);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.graphics.Shape itemShape(int index, int count, optional androidx.compose.foundation.shape.CornerBasedShape baseShape);
     property public final androidx.compose.material3.SegmentedButtonBorder Border;
     property public final float IconSize;
-    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.foundation.shape.CornerBasedShape Shape;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.foundation.shape.CornerBasedShape baseShape;
     field public static final androidx.compose.material3.SegmentedButtonDefaults INSTANCE;
   }
 
   public final class SegmentedButtonKt {
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MultiChoiceSegmentedButtonRow(optional androidx.compose.ui.Modifier modifier, optional float space, kotlin.jvm.functions.Function1<? super androidx.compose.material3.MultiChoiceSegmentedButtonRowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.MultiChoiceSegmentedButtonRowScope, boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.SingleChoiceSegmentedButtonRowScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.MultiChoiceSegmentedButtonRowScope, boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> label);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.SingleChoiceSegmentedButtonRowScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> label);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SingleChoiceSegmentedButtonRow(optional androidx.compose.ui.Modifier modifier, optional float space, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SingleChoiceSegmentedButtonRowScope,kotlin.Unit> content);
   }
 
@@ -1348,7 +1395,7 @@
   @androidx.compose.runtime.Stable public final class SliderDefaults {
     method @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
     method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.RangeSliderState rangeSliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
-    method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
+    method @Deprecated @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderState sliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors(optional long thumbColor, optional long activeTrackColor, optional long activeTickColor, optional long inactiveTrackColor, optional long inactiveTickColor, optional long disabledThumbColor, optional long disabledActiveTrackColor, optional long disabledActiveTickColor, optional long disabledInactiveTrackColor, optional long disabledInactiveTickColor);
     field public static final androidx.compose.material3.SliderDefaults INSTANCE;
@@ -1363,16 +1410,18 @@
     method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
   }
 
-  @androidx.compose.runtime.Stable public final class SliderPositions {
-    ctor public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
-    method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
-    method public float[] getTickFractions();
-    property public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
-    property public final float[] tickFractions;
+  @Deprecated @androidx.compose.runtime.Stable public final class SliderPositions {
+    ctor @Deprecated public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
+    method @Deprecated public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
+    method @Deprecated public float[] getTickFractions();
+    property @Deprecated public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
+    property @Deprecated public final float[] tickFractions;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderState {
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderState implements androidx.compose.foundation.gestures.DraggableState {
     ctor public SliderState(optional float initialValue, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit>? initialOnValueChange, optional @IntRange(from=0L) int steps, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished);
+    method public void dispatchRawDelta(float delta);
+    method public suspend Object? drag(androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public kotlin.jvm.functions.Function0<kotlin.Unit>? getOnValueChangeFinished();
     method public int getSteps();
     method public float getValue();
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index fe8552b..28b67ea 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -115,7 +115,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
@@ -125,7 +125,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:material3:material3:material3-samples"))
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index c6d720e..2e8c6a2 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -57,6 +57,7 @@
 import androidx.compose.material3.samples.ElevatedFilterChipSample
 import androidx.compose.material3.samples.ElevatedSuggestionChipSample
 import androidx.compose.material3.samples.EnterAlwaysTopAppBar
+import androidx.compose.material3.samples.ExitAlwaysBottomAppBar
 import androidx.compose.material3.samples.ExitUntilCollapsedLargeTopAppBar
 import androidx.compose.material3.samples.ExitUntilCollapsedMediumTopAppBar
 import androidx.compose.material3.samples.ExposedDropdownMenuSample
@@ -465,7 +466,12 @@
         name = ::BottomAppBarWithFAB.name,
         description = BottomAppBarsExampleDescription,
         sourceUrl = BottomAppBarsExampleSourceUrl,
-    ) { BottomAppBarWithFAB() }
+    ) { BottomAppBarWithFAB() },
+    Example(
+        name = ::ExitAlwaysBottomAppBar.name,
+        description = BottomAppBarsExampleDescription,
+        sourceUrl = BottomAppBarsExampleSourceUrl,
+    ) { ExitAlwaysBottomAppBar() }
 )
 
 private const val TopAppBarExampleDescription = "Top app bar examples"
diff --git a/compose/material3/material3/lint-baseline.xml b/compose/material3/material3/lint-baseline.xml
index 787ecd3..bf09410 100644
--- a/compose/material3/material3/lint-baseline.xml
+++ b/compose/material3/material3/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-beta01)" variant="all" version="8.2.0-beta01">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="BanThreadSleep"
@@ -7,7 +7,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/CardScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/CardScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -16,7 +16,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -25,7 +25,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -34,7 +34,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -43,7 +43,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialRippleThemeTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialRippleThemeTest.kt"/>
     </issue>
 
     <issue
@@ -52,7 +52,7 @@
         errorLine1="            Thread.sleep(300)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialRippleThemeTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialRippleThemeTest.kt"/>
     </issue>
 
     <issue
@@ -61,7 +61,7 @@
         errorLine1="            Thread.sleep(300)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -70,7 +70,7 @@
         errorLine1="            Thread.sleep(300)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerItemScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationDrawerItemScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -79,7 +79,7 @@
         errorLine1="            Thread.sleep(300)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -88,7 +88,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -97,7 +97,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -106,7 +106,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -115,7 +115,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -124,7 +124,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -133,7 +133,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -142,7 +142,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -151,7 +151,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -160,7 +160,7 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -169,16 +169,7 @@
         errorLine1="            Thread.sleep(300)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt"/>
-    </issue>
-
-    <issue
-        id="ExperimentalPropertyAnnotation"
-        message="This property does not have all required annotations to correctly mark it as experimental."
-        errorLine1="    @ExperimentalMaterial3Api"
-        errorLine2="    ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt"/>
     </issue>
 
     <issue
@@ -201,15 +192,6 @@
 
     <issue
         id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method BottomSheetScaffoldAnchorChangeHandler has parameter &apos;animateTo&apos; with type Function2&lt;? super SheetValue, ? super Float, Unit>."
-        errorLine1="    animateTo: (target: SheetValue, velocity: Float) -> Unit,"
-        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method DateInputContent has parameter &apos;onDateSelectionChange&apos; with type Function1&lt;? super Long, Unit>."
         errorLine1="    onDateSelectionChange: (dateInMillis: Long?) -> Unit,"
         errorLine2="                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -382,8 +364,8 @@
     <issue
         id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in variable &apos;onDateSelectionChange&apos; with type Function1&lt;? super Long, ? extends Unit>."
-        errorLine1="                    val onDateSelectionChange = { dateInMillis: Long ->"
-        errorLine2="                    ^">
+        errorLine1="        val onDateSelectionChange = { dateInMillis: Long ->"
+        errorLine2="        ^">
         <location
             file="src/commonMain/kotlin/androidx/compose/material3/DateRangePicker.kt"/>
     </issue>
@@ -417,24 +399,6 @@
 
     <issue
         id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method modalBottomSheetSwipeable has parameter &apos;onDragStopped&apos; with type Function2&lt;? super CoroutineScope, ? super Float, Unit>."
-        errorLine1="    onDragStopped: CoroutineScope.(velocity: Float) -> Unit,"
-        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method ModalBottomSheetAnchorChangeHandler has parameter &apos;animateTo&apos; with type Function2&lt;? super SheetValue, ? super Float, Unit>."
-        errorLine1="    animateTo: (target: SheetValue, velocity: Float) -> Unit,"
-        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method Scrim has parameter &apos;fraction&apos; with type Function0&lt;Float>."
         errorLine1="    fraction: () -> Float,"
         errorLine2="              ~~~~~~~~~~~">
@@ -534,24 +498,6 @@
 
     <issue
         id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in constructor SliderDraggableState has parameter &apos;onDelta&apos; with type Function1&lt;? super Float, Unit>."
-        errorLine1="    val onDelta: (Float) -> Unit"
-        errorLine2="                 ~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/material3/Slider.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in return type Function1&lt;Float, Unit> of &apos;getOnDelta&apos;."
-        errorLine1="    val onDelta: (Float) -> Unit"
-        errorLine2="                 ~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/material3/Slider.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in constructor SliderState has parameter &apos;initialOnValueChange&apos; with type Function1&lt;? super Float, Unit>."
         errorLine1="    initialOnValueChange: ((Float) -> Unit)? = null,"
         errorLine2="                          ~~~~~~~~~~~~~~~~~~">
@@ -579,15 +525,6 @@
 
     <issue
         id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in return type Function3&lt;PressGestureScope, Offset, Continuation&lt;? super Unit>, Object> of &apos;getPress$lint_module&apos;."
-        errorLine1="    internal val press: suspend PressGestureScope.(Offset) -> Unit = { pos ->"
-        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/material3/Slider.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in constructor RangeSliderState has parameter &apos;initialOnValueChange&apos; with type Function1&lt;? super FloatRange, Unit>."
         errorLine1="    initialOnValueChange: ((FloatRange) -> Unit)? = null,"
         errorLine2="                          ~~~~~~~~~~~~~~~~~~~~~~~">
@@ -615,15 +552,6 @@
 
     <issue
         id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in return type Function2&lt;Boolean, Float, Unit> of &apos;getOnDrag$lint_module&apos;."
-        errorLine1="    internal val onDrag: (Boolean, Float) -> Unit = { isStart, offset ->"
-        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/material3/Slider.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in variable &apos;isValidDistance&apos; with type Function1&lt;? super Float, ? extends Boolean>."
         errorLine1="        fun Float.isValidDistance(): Boolean {"
         errorLine2="        ^">
@@ -678,15 +606,6 @@
 
     <issue
         id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method swipeAnchors has parameter &apos;calculateAnchor&apos; with type Function2&lt;? super T, ? super IntSize, Float>."
-        errorLine1="    calculateAnchor: (value: T, layoutSize: IntSize) -> Float?,"
-        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in variable &apos;valueToOffset&apos; with type Function1&lt;? super Boolean, ? extends Float>."
         errorLine1="    val valueToOffset = remember&lt;(Boolean) -> Float>(minBound, maxBound) {"
         errorLine2="    ^">
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
index 6cbc0aa..a6538f0 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
@@ -19,6 +19,7 @@
 import androidx.annotation.Sampled
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.icons.Icons
@@ -31,6 +32,7 @@
 import androidx.compose.material3.BottomAppBarDefaults
 import androidx.compose.material3.CenterAlignedTopAppBar
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FabPosition
 import androidx.compose.material3.FloatingActionButton
 import androidx.compose.material3.FloatingActionButtonDefaults
 import androidx.compose.material3.Icon
@@ -418,11 +420,13 @@
 @Sampled
 @Composable
 fun SimpleBottomAppBar() {
-    BottomAppBar {
-        IconButton(onClick = { /* doSomething() */ }) {
-            Icon(Icons.Filled.Menu, contentDescription = "Localized description")
+    BottomAppBar(
+        actions = {
+            IconButton(onClick = { /* doSomething() */ }) {
+                Icon(Icons.Filled.Menu, contentDescription = "Localized description")
+            }
         }
-    }
+    )
 }
 
 @Preview
@@ -452,3 +456,59 @@
         }
     )
 }
+
+/**
+ * A sample for a [BottomAppBar] that collapses when the content is scrolled up, and
+ * appears when the content scrolled down.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun ExitAlwaysBottomAppBar() {
+    val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+    Scaffold(
+        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+        bottomBar = {
+            BottomAppBar(
+                actions = {
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(Icons.Filled.Check, contentDescription = "Localized description")
+                    }
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(Icons.Filled.Edit, contentDescription = "Localized description")
+                    }
+                },
+                scrollBehavior = scrollBehavior
+            )
+        },
+        floatingActionButton = {
+            FloatingActionButton(
+                modifier = Modifier.offset(y = 4.dp),
+                onClick = { /* do something */ },
+                containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
+            ) {
+                Icon(Icons.Filled.Add, "Localized description")
+            }
+        },
+        floatingActionButtonPosition = FabPosition.EndOverlay,
+        content = { innerPadding ->
+            LazyColumn(
+                contentPadding = innerPadding,
+                verticalArrangement = Arrangement.spacedBy(8.dp)
+            ) {
+                val list = (0..75).map { it.toString() }
+                items(count = list.size) {
+                    Text(
+                        text = list[it],
+                        style = MaterialTheme.typography.bodyLarge,
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .padding(horizontal = 16.dp)
+                    )
+                }
+            }
+        }
+    )
+}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SegmentedButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SegmentedButtonSamples.kt
index ba51a05..eb57d09 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SegmentedButtonSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SegmentedButtonSamples.kt
@@ -48,7 +48,7 @@
     SingleChoiceSegmentedButtonRow {
         options.forEachIndexed { index, label ->
             SegmentedButton(
-                shape = SegmentedButtonDefaults.shape(position = index, count = options.size),
+                shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size),
                 onClick = { selectedIndex = index },
                 selected = index == selectedIndex
             ) {
@@ -73,9 +73,9 @@
     MultiChoiceSegmentedButtonRow {
         options.forEachIndexed { index, label ->
             SegmentedButton(
-                shape = SegmentedButtonDefaults.shape(position = index, count = options.size),
+                shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size),
                 icon = {
-                    SegmentedButtonDefaults.SegmentedButtonIcon(active = index in checkedList) {
+                    SegmentedButtonDefaults.Icon(active = index in checkedList) {
                         Icon(
                             imageVector = icons[index],
                             contentDescription = null,
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
index 77929ef..2528da4 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
@@ -19,12 +19,16 @@
 import androidx.annotation.Sampled
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentWidth
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.material3.ButtonDefaults
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
+import androidx.compose.material3.Label
+import androidx.compose.material3.PlainTooltip
 import androidx.compose.material3.RangeSlider
 import androidx.compose.material3.RangeSliderState
 import androidx.compose.material3.Slider
@@ -41,6 +45,8 @@
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
 
 @Preview
 @Sampled
@@ -56,7 +62,6 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
 @Preview
 @Sampled
 @Composable
@@ -84,25 +89,40 @@
 @Composable
 fun SliderWithCustomThumbSample() {
     var sliderPosition by remember { mutableStateOf(0f) }
+    val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
     Column {
-        Text(text = sliderPosition.toString())
         Slider(
             modifier = Modifier.semantics { contentDescription = "Localized Description" },
             value = sliderPosition,
             onValueChange = { sliderPosition = it },
-            valueRange = 0f..5f,
-            steps = 10,
+            valueRange = 0f..100f,
+            interactionSource = interactionSource,
             onValueChangeFinished = {
                 // launch some business logic update with the state you hold
                 // viewModel.updateSelectedSliderValue(sliderPosition)
             },
             thumb = {
-                Icon(
-                    imageVector = Icons.Filled.Favorite,
-                    contentDescription = null,
-                    modifier = Modifier.size(ButtonDefaults.IconSize),
-                    tint = Color.Red
-                )
+                Label(
+                    label = {
+                        PlainTooltip(
+                            modifier = Modifier
+                                .requiredSize(45.dp, 25.dp)
+                                .wrapContentWidth()
+                        ) {
+                            val roundedEnd =
+                                (sliderPosition * 100.0).roundToInt() / 100.0
+                            Text(roundedEnd.toString())
+                        }
+                    },
+                    interactionSource = interactionSource
+                ) {
+                    Icon(
+                        imageVector = Icons.Filled.Favorite,
+                        contentDescription = null,
+                        modifier = Modifier.size(ButtonDefaults.IconSize),
+                        tint = Color.Red
+                    )
+                }
             }
         )
     }
@@ -221,23 +241,52 @@
     )
     val endThumbColors = SliderDefaults.colors(thumbColor = Color.Green)
     Column {
-        Text(text = (rangeSliderState.activeRangeStart..rangeSliderState.activeRangeEnd).toString())
         RangeSlider(
             state = rangeSliderState,
             modifier = Modifier.semantics { contentDescription = "Localized Description" },
             startInteractionSource = startInteractionSource,
             endInteractionSource = endInteractionSource,
             startThumb = {
-                SliderDefaults.Thumb(
-                    interactionSource = startInteractionSource,
-                    colors = startThumbAndTrackColors
-                )
+                Label(
+                    label = {
+                        PlainTooltip(
+                            modifier = Modifier
+                                .requiredSize(45.dp, 25.dp)
+                                .wrapContentWidth()
+                        ) {
+                            val roundedStart =
+                                (rangeSliderState.activeRangeStart * 100.0).roundToInt() / 100.0
+                            Text(roundedStart.toString())
+                        }
+                    },
+                    interactionSource = startInteractionSource
+                ) {
+                    SliderDefaults.Thumb(
+                        interactionSource = startInteractionSource,
+                        colors = startThumbAndTrackColors
+                    )
+                }
             },
             endThumb = {
-                SliderDefaults.Thumb(
-                    interactionSource = endInteractionSource,
-                    colors = endThumbColors
-                )
+                Label(
+                    label = {
+                        PlainTooltip(
+                            modifier = Modifier
+                                .requiredSize(45.dp, 25.dp)
+                                .wrapContentWidth()
+                        ) {
+                            val roundedEnd =
+                                (rangeSliderState.activeRangeEnd * 100.0).roundToInt() / 100.0
+                            Text(roundedEnd.toString())
+                        }
+                    },
+                    interactionSource = endInteractionSource
+                ) {
+                    SliderDefaults.Thumb(
+                        interactionSource = endInteractionSource,
+                        colors = endThumbColors
+                    )
+                }
             },
             track = { rangeSliderState ->
                 SliderDefaults.Track(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
deleted file mode 100644
index ddbb438..0000000
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
+++ /dev/null
@@ -1,423 +0,0 @@
-/*
- * Copyright 2021 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.material3
-
-import android.os.Build
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material3.TopAppBarDefaults.enterAlwaysScrollBehavior
-import androidx.compose.material3.tokens.TopAppBarSmallTokens
-import androidx.compose.testutils.assertAgainstGolden
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.screenshot.AndroidXScreenshotTestRule
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterial3Api::class)
-class AppBarScreenshotTest {
-
-    @get:Rule
-    val composeTestRule = createComposeRule()
-
-    @get:Rule
-    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
-
-    @Test
-    fun smallAppBar_lightTheme() {
-        composeTestRule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                TopAppBar(
-                    navigationIcon = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
-                                contentDescription = "Back"
-                            )
-                        }
-                    },
-                    title = {
-                        Text("Title")
-                    },
-                    actions = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.Filled.Favorite,
-                                contentDescription = "Like"
-                            )
-                        }
-                    }
-                )
-            }
-        }
-
-        assertAppBarAgainstGolden(goldenIdentifier = "smallAppBar_lightTheme")
-    }
-
-    @Test
-    fun smallAppBar_lightTheme_clipsWhenCollapsedWithInsets() {
-        composeTestRule.setMaterialContent(lightColorScheme()) {
-            val behavior = enterAlwaysScrollBehavior(rememberTopAppBarState())
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                TopAppBar(
-                    navigationIcon = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
-                                contentDescription = "Back"
-                            )
-                        }
-                    },
-                    scrollBehavior = behavior,
-                    windowInsets = WindowInsets(top = 30.dp),
-                    title = {
-                        Text("Title")
-                    },
-                    actions = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.Filled.Favorite,
-                                contentDescription = "Like"
-                            )
-                        }
-                    }
-                )
-            }
-        }
-
-        composeTestRule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
-            // start from the bottom so we can drag enough
-            down(bottomCenter - Offset(1f, 1f))
-            moveBy(Offset(0f, -((TopAppBarSmallTokens.ContainerHeight - 10.dp).toPx())))
-        }
-
-        assertAppBarAgainstGolden(
-            goldenIdentifier = "smallAppBar_lightTheme_clipsWhenCollapsedWithInsets"
-        )
-    }
-
-    @Test
-    fun smallAppBar_darkTheme() {
-        composeTestRule.setMaterialContent(darkColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                TopAppBar(
-                    navigationIcon = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
-                                contentDescription = "Back"
-                            )
-                        }
-                    },
-                    title = {
-                        Text("Title")
-                    },
-                    actions = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.Filled.Favorite,
-                                contentDescription = "Like"
-                            )
-                        }
-                    }
-                )
-            }
-        }
-
-        assertAppBarAgainstGolden(goldenIdentifier = "smallAppBar_darkTheme")
-    }
-
-    @Test
-    fun centerAlignedAppBar_lightTheme() {
-        composeTestRule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                CenterAlignedTopAppBar(
-                    navigationIcon = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
-                                contentDescription = "Back"
-                            )
-                        }
-                    },
-                    title = {
-                        Text("Title")
-                    },
-                    actions = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.Filled.Favorite,
-                                contentDescription = "Like"
-                            )
-                        }
-                    }
-                )
-            }
-        }
-
-        assertAppBarAgainstGolden(goldenIdentifier = "centerAlignedAppBar_lightTheme")
-    }
-
-    @Test
-    fun centerAlignedAppBar_darkTheme() {
-        composeTestRule.setMaterialContent(darkColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                CenterAlignedTopAppBar(
-                    navigationIcon = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
-                                contentDescription = "Back"
-                            )
-                        }
-                    },
-                    title = {
-                        Text("Title")
-                    },
-                    actions = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.Filled.Favorite,
-                                contentDescription = "Like"
-                            )
-                        }
-                    }
-                )
-            }
-        }
-
-        assertAppBarAgainstGolden(goldenIdentifier = "centerAlignedAppBar_darkTheme")
-    }
-
-    @Test
-    fun mediumAppBar_lightTheme() {
-        composeTestRule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                MediumTopAppBar(
-                    navigationIcon = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
-                                contentDescription = "Back"
-                            )
-                        }
-                    },
-                    title = {
-                        Text("Title")
-                    },
-                    actions = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.Filled.Favorite,
-                                contentDescription = "Like"
-                            )
-                        }
-                    }
-                )
-            }
-        }
-
-        assertAppBarAgainstGolden(goldenIdentifier = "mediumAppBar_lightTheme")
-    }
-
-    @Test
-    fun mediumAppBar_darkTheme() {
-        composeTestRule.setMaterialContent(darkColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                MediumTopAppBar(
-                    navigationIcon = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
-                                contentDescription = "Back"
-                            )
-                        }
-                    },
-                    title = {
-                        Text("Title")
-                    },
-                    actions = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.Filled.Favorite,
-                                contentDescription = "Like"
-                            )
-                        }
-                    }
-                )
-            }
-        }
-
-        assertAppBarAgainstGolden(goldenIdentifier = "mediumAppBar_darkTheme")
-    }
-
-    @Test
-    fun largeAppBar_lightTheme() {
-        composeTestRule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                LargeTopAppBar(
-                    navigationIcon = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
-                                contentDescription = "Back"
-                            )
-                        }
-                    },
-                    title = {
-                        Text("Title")
-                    },
-                    actions = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.Filled.Favorite,
-                                contentDescription = "Like"
-                            )
-                        }
-                    }
-                )
-            }
-        }
-
-        assertAppBarAgainstGolden(goldenIdentifier = "largeAppBar_lightTheme")
-    }
-
-    @Test
-    fun largeAppBar_darkTheme() {
-        composeTestRule.setMaterialContent(darkColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                LargeTopAppBar(
-                    navigationIcon = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
-                                contentDescription = "Back"
-                            )
-                        }
-                    },
-                    title = {
-                        Text("Title")
-                    },
-                    actions = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.Filled.Favorite,
-                                contentDescription = "Like"
-                            )
-                        }
-                    }
-                )
-            }
-        }
-
-        assertAppBarAgainstGolden(goldenIdentifier = "largeAppBar_darkTheme")
-    }
-
-    @Test
-    fun bottomAppBarWithFAB_lightTheme() {
-        composeTestRule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(BottomAppBarTestTag)) {
-                BottomAppBar(
-                    actions = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.Filled.Menu,
-                                contentDescription = "Menu"
-                            )
-                        }
-                    },
-                    floatingActionButton = {
-                        FloatingActionButton(
-                            onClick = { /* do something */ },
-                            containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
-                            elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
-                        ) {
-                            Icon(Icons.Filled.Add, "Localized description")
-                        }
-                    }
-                )
-            }
-        }
-
-        assertAppBarAgainstGolden(
-            goldenIdentifier = "bottomAppBarWithFAB_lightTheme",
-            testTag = BottomAppBarTestTag
-        )
-    }
-
-    @Test
-    fun bottomAppBarWithFAB_darkTheme() {
-        composeTestRule.setMaterialContent(darkColorScheme()) {
-            Box(Modifier.testTag(BottomAppBarTestTag)) {
-                BottomAppBar(
-                    actions = {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                imageVector = Icons.Filled.Menu,
-                                contentDescription = "Menu"
-                            )
-                        }
-                    },
-                    floatingActionButton = {
-                        FloatingActionButton(
-                            onClick = { /* do something */ },
-                            containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
-                            elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
-                        ) {
-                            Icon(Icons.Filled.Add, "Localized description")
-                        }
-                    }
-                )
-            }
-        }
-
-        assertAppBarAgainstGolden(
-            goldenIdentifier = "bottomAppBarWithFAB_darkTheme",
-            testTag = BottomAppBarTestTag
-        )
-    }
-
-    private fun assertAppBarAgainstGolden(
-        goldenIdentifier: String,
-        testTag: String = TopAppBarTestTag
-    ) {
-        composeTestRule.onNodeWithTag(testTag)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, goldenIdentifier)
-    }
-
-    private val TopAppBarTestTag = "topAppBar"
-    private val BottomAppBarTestTag = "bottomAppBar"
-}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
deleted file mode 100644
index ef6ffe4..0000000
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
+++ /dev/null
@@ -1,1590 +0,0 @@
-/*
- * Copyright 2021 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.material3
-
-import android.os.Build
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material3.tokens.BottomAppBarTokens
-import androidx.compose.material3.tokens.TopAppBarLargeTokens
-import androidx.compose.material3.tokens.TopAppBarMediumTokens
-import androidx.compose.material3.tokens.TopAppBarSmallCenteredTokens
-import androidx.compose.material3.tokens.TopAppBarSmallTokens
-import androidx.compose.runtime.Composable
-import androidx.compose.testutils.assertContainsColor
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.compositeOver
-import androidx.compose.ui.graphics.painter.ColorPainter
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.layout.layout
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.assertCountEquals
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsEqualTo
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.getBoundsInRoot
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.StateRestorationTester
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onAllNodesWithTag
-import androidx.compose.ui.test.onFirst
-import androidx.compose.ui.test.onLast
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.swipeLeft
-import androidx.compose.ui.test.swipeRight
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.height
-import androidx.compose.ui.unit.width
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalMaterial3Api::class)
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class AppBarTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Test
-    fun smallTopAppBar_expandsToScreen() {
-        rule
-            .setMaterialContentForSizeAssertions {
-                TopAppBar(title = { Text("Title") })
-            }
-            .assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
-            .assertWidthIsEqualTo(rule.rootWidth())
-    }
-
-    @Test
-    fun smallTopAppBar_withTitle() {
-        val title = "Title"
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                TopAppBar(title = { Text(title) })
-            }
-        }
-        rule.onNodeWithText(title).assertIsDisplayed()
-    }
-
-    @Test
-    fun smallTopAppBar_default_positioning() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                TopAppBar(
-                    navigationIcon = {
-                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
-                    },
-                    title = {
-                        Text("Title", Modifier.testTag(TitleTestTag))
-                    },
-                    actions = {
-                        FakeIcon(Modifier.testTag(ActionsTestTag))
-                    }
-                )
-            }
-        }
-        assertSmallDefaultPositioning()
-    }
-
-    @Test
-    fun smallTopAppBar_noNavigationIcon_positioning() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                TopAppBar(
-                    title = {
-                        Text("Title", Modifier.testTag(TitleTestTag))
-                    },
-                    actions = {
-                        FakeIcon(Modifier.testTag(ActionsTestTag))
-                    }
-                )
-            }
-        }
-        assertSmallPositioningWithoutNavigation()
-    }
-
-    @Test
-    fun smallTopAppBar_titleDefaultStyle() {
-        var textStyle: TextStyle? = null
-        var expectedTextStyle: TextStyle? = null
-        rule.setMaterialContent(lightColorScheme()) {
-            TopAppBar(title = {
-                Text("Title")
-                textStyle = LocalTextStyle.current
-                expectedTextStyle =
-                    MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont)
-            }
-            )
-        }
-        assertThat(textStyle).isNotNull()
-        assertThat(textStyle).isEqualTo(expectedTextStyle)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun smallTopAppBar_contentColor() {
-        var titleColor: Color = Color.Unspecified
-        var navigationIconColor: Color = Color.Unspecified
-        var actionsColor: Color = Color.Unspecified
-        var expectedTitleColor: Color = Color.Unspecified
-        var expectedNavigationIconColor: Color = Color.Unspecified
-        var expectedActionsColor: Color = Color.Unspecified
-        var expectedContainerColor: Color = Color.Unspecified
-
-        rule.setMaterialContent(lightColorScheme()) {
-            TopAppBar(
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                navigationIcon = {
-                    FakeIcon(Modifier.testTag(NavigationIconTestTag))
-                    navigationIconColor = LocalContentColor.current
-                    expectedNavigationIconColor =
-                        TopAppBarDefaults.topAppBarColors().navigationIconContentColor
-                    // fraction = 0f to indicate no scroll.
-                    expectedContainerColor = TopAppBarDefaults
-                        .topAppBarColors()
-                        .containerColor(colorTransitionFraction = 0f)
-                },
-                title = {
-                    Text("Title", Modifier.testTag(TitleTestTag))
-                    titleColor = LocalContentColor.current
-                    expectedTitleColor = TopAppBarDefaults
-                        .topAppBarColors().titleContentColor
-                },
-                actions = {
-                    FakeIcon(Modifier.testTag(ActionsTestTag))
-                    actionsColor = LocalContentColor.current
-                    expectedActionsColor = TopAppBarDefaults
-                        .topAppBarColors().actionIconContentColor
-                }
-            )
-        }
-        assertThat(navigationIconColor).isNotNull()
-        assertThat(titleColor).isNotNull()
-        assertThat(actionsColor).isNotNull()
-        assertThat(navigationIconColor).isEqualTo(expectedNavigationIconColor)
-        assertThat(titleColor).isEqualTo(expectedTitleColor)
-        assertThat(actionsColor).isEqualTo(expectedActionsColor)
-
-        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
-            .assertContainsColor(expectedContainerColor)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun smallTopAppBar_scrolledContentColor() {
-        var expectedScrolledContainerColor: Color = Color.Unspecified
-        lateinit var scrollBehavior: TopAppBarScrollBehavior
-        rule.setMaterialContent(lightColorScheme()) {
-            scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
-            TopAppBar(
-                title = {
-                    Text("Title", Modifier.testTag(TitleTestTag))
-                    // fraction = 1f to indicate a scroll.
-                    expectedScrolledContainerColor =
-                        TopAppBarDefaults.topAppBarColors()
-                            .containerColor(colorTransitionFraction = 1f)
-                },
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                scrollBehavior = scrollBehavior
-            )
-        }
-
-        // Simulate scrolled content.
-        rule.runOnIdle {
-            scrollBehavior.state.contentOffset = -100f
-        }
-        rule.waitForIdle()
-        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
-            .assertContainsColor(expectedScrolledContainerColor)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun smallTopAppBar_scrolledPositioning() {
-        lateinit var scrollBehavior: TopAppBarScrollBehavior
-        val scrollHeightOffsetDp = 20.dp
-        var scrollHeightOffsetPx = 0f
-
-        rule.setMaterialContent(lightColorScheme()) {
-            scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
-            scrollHeightOffsetPx = with(LocalDensity.current) { scrollHeightOffsetDp.toPx() }
-            TopAppBar(
-                title = { Text("Title", Modifier.testTag(TitleTestTag)) },
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                scrollBehavior = scrollBehavior
-            )
-        }
-
-        // Simulate scrolled content.
-        rule.runOnIdle {
-            scrollBehavior.state.heightOffset = -scrollHeightOffsetPx
-            scrollBehavior.state.contentOffset = -scrollHeightOffsetPx
-        }
-        rule.waitForIdle()
-        rule.onNodeWithTag(TopAppBarTestTag)
-            .assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight - scrollHeightOffsetDp)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun smallTopAppBar_transparentContainerColor() {
-        val expectedColorBehindTopAppBar: Color = Color.Red
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(
-                modifier = Modifier
-                    .wrapContentHeight()
-                    .fillMaxWidth()
-                    .background(color = expectedColorBehindTopAppBar)
-            ) {
-                TopAppBar(
-                    modifier = Modifier.testTag(TopAppBarTestTag),
-                    title = {
-                        Text("Title", Modifier.testTag(TitleTestTag))
-                    },
-                    colors =
-                    TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
-                    scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
-                )
-            }
-        }
-        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
-            .assertContainsColor(expectedColorBehindTopAppBar)
-    }
-
-    @Test
-    fun centerAlignedTopAppBar_expandsToScreen() {
-        rule.setMaterialContentForSizeAssertions {
-            CenterAlignedTopAppBar(title = { Text("Title") })
-        }
-            .assertHeightIsEqualTo(TopAppBarSmallCenteredTokens.ContainerHeight)
-            .assertWidthIsEqualTo(rule.rootWidth())
-    }
-
-    @Test
-    fun centerAlignedTopAppBar_withTitle() {
-        val title = "Title"
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                CenterAlignedTopAppBar(title = { Text(title) })
-            }
-        }
-        rule.onNodeWithText(title).assertIsDisplayed()
-    }
-
-    @Test
-    fun centerAlignedTopAppBar_default_positioning() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                CenterAlignedTopAppBar(
-                    navigationIcon = {
-                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
-                    },
-                    title = {
-                        Text("Title", Modifier.testTag(TitleTestTag))
-                    },
-                    actions = {
-                        FakeIcon(Modifier.testTag(ActionsTestTag))
-                    }
-                )
-            }
-        }
-        assertSmallDefaultPositioning(isCenteredTitle = true)
-    }
-
-    @Test
-    fun centerAlignedTopAppBar_default_positioning_respectsWindowInsets() {
-        val padding = 10.dp
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                CenterAlignedTopAppBar(
-                    navigationIcon = {
-                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
-                    },
-                    title = {
-                        Text("Title", Modifier.testTag(TitleTestTag))
-                    },
-                    actions = {
-                        FakeIcon(Modifier.testTag(ActionsTestTag))
-                    },
-                    windowInsets = WindowInsets(padding, padding, padding, padding)
-                )
-            }
-        }
-        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
-        val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
-
-        rule.onNodeWithTag(NavigationIconTestTag)
-            // Navigation icon should be 4.dp from the start
-            .assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding + padding)
-            // Navigation icon should be centered within the height of the app bar.
-            .assertTopPositionInRootIsEqualTo(
-                appBarBottomEdgeY - AppBarTopAndBottomPadding - padding - FakeIconSize
-            )
-    }
-
-    @Test
-    fun centerAlignedTopAppBar_noNavigationIcon_positioning() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                CenterAlignedTopAppBar(
-                    title = {
-                        Text("Title", Modifier.testTag(TitleTestTag))
-                    },
-                    actions = {
-                        FakeIcon(Modifier.testTag(ActionsTestTag))
-                    }
-                )
-            }
-        }
-        assertSmallPositioningWithoutNavigation(isCenteredTitle = true)
-    }
-
-    @Test
-    fun centerAlignedTopAppBar_longTextDoesNotOverflowToActions() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                CenterAlignedTopAppBar(
-                    title = {
-                        Text(
-                            text = "This is a very very very very long title",
-                            modifier = Modifier.testTag(TitleTestTag),
-                            overflow = TextOverflow.Ellipsis,
-                            maxLines = 1
-                        )
-                    },
-                    actions = {
-                        FakeIcon(Modifier.testTag(ActionsTestTag))
-                    }
-                )
-            }
-        }
-        val actionsBounds = rule.onNodeWithTag(ActionsTestTag).getUnclippedBoundsInRoot()
-        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
-
-        // Check that the title does not render over the actions.
-        assertThat(titleBounds.right).isLessThan(actionsBounds.left)
-    }
-
-    @Test
-    fun centerAlignedTopAppBar_longTextDoesNotOverflowToNavigation() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                CenterAlignedTopAppBar(
-                    navigationIcon = {
-                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
-                    },
-                    title = {
-                        Text(
-                            text = "This is a very very very very long title",
-                            modifier = Modifier.testTag(TitleTestTag),
-                            overflow = TextOverflow.Ellipsis,
-                            maxLines = 1
-                        )
-                    }
-
-                )
-            }
-        }
-        val navigationIconBounds =
-            rule.onNodeWithTag(NavigationIconTestTag).getUnclippedBoundsInRoot()
-        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
-
-        // Check that the title does not render over the navigation icon.
-        assertThat(titleBounds.left).isGreaterThan(navigationIconBounds.right)
-    }
-
-    @Test
-    fun centerAlignedTopAppBar_titleDefaultStyle() {
-        var textStyle: TextStyle? = null
-        var expectedTextStyle: TextStyle? = null
-        rule.setMaterialContent(lightColorScheme()) {
-            CenterAlignedTopAppBar(
-                title = {
-                    Text("Title")
-                    textStyle = LocalTextStyle.current
-                    expectedTextStyle =
-                        MaterialTheme.typography.fromToken(
-                            TopAppBarSmallCenteredTokens.HeadlineFont
-                        )
-                }
-            )
-        }
-        assertThat(textStyle).isNotNull()
-        assertThat(textStyle).isEqualTo(expectedTextStyle)
-    }
-
-    @Test
-    fun centerAlignedTopAppBar_measureWithNonZeroMinWidth() {
-        var appBarSize = IntSize.Zero
-        rule.setMaterialContent(lightColorScheme()) {
-            CenterAlignedTopAppBar(
-                modifier = Modifier.layout { measurable, constraints ->
-                    val placeable = measurable.measure(
-                        constraints.copy(minWidth = constraints.maxWidth)
-                    )
-                    appBarSize = IntSize(placeable.width, placeable.height)
-                    layout(placeable.width, placeable.height) {
-                        placeable.place(0, 0)
-                    }
-                },
-                title = {
-                    Text("Title")
-                }
-            )
-        }
-
-        assertThat(appBarSize).isNotEqualTo(IntSize.Zero)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun centerAlignedTopAppBar_contentColor() {
-        var titleColor: Color = Color.Unspecified
-        var navigationIconColor: Color = Color.Unspecified
-        var actionsColor: Color = Color.Unspecified
-        var expectedTitleColor: Color = Color.Unspecified
-        var expectedNavigationIconColor: Color = Color.Unspecified
-        var expectedActionsColor: Color = Color.Unspecified
-        var expectedContainerColor: Color = Color.Unspecified
-
-        rule.setMaterialContent(lightColorScheme()) {
-            CenterAlignedTopAppBar(
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                navigationIcon = {
-                    FakeIcon(Modifier.testTag(NavigationIconTestTag))
-                    navigationIconColor = LocalContentColor.current
-                    expectedNavigationIconColor =
-                        TopAppBarDefaults.centerAlignedTopAppBarColors()
-                            .navigationIconContentColor
-                    // fraction = 0f to indicate no scroll.
-                    expectedContainerColor =
-                        TopAppBarDefaults.centerAlignedTopAppBarColors()
-                            .containerColor(colorTransitionFraction = 0f)
-                },
-                title = {
-                    Text("Title", Modifier.testTag(TitleTestTag))
-                    titleColor = LocalContentColor.current
-                    expectedTitleColor =
-                        TopAppBarDefaults.centerAlignedTopAppBarColors()
-                            .titleContentColor
-                },
-                actions = {
-                    FakeIcon(Modifier.testTag(ActionsTestTag))
-                    actionsColor = LocalContentColor.current
-                    expectedActionsColor =
-                        TopAppBarDefaults.centerAlignedTopAppBarColors()
-                            .actionIconContentColor
-                }
-            )
-        }
-        assertThat(navigationIconColor).isNotNull()
-        assertThat(titleColor).isNotNull()
-        assertThat(actionsColor).isNotNull()
-        assertThat(navigationIconColor).isEqualTo(expectedNavigationIconColor)
-        assertThat(titleColor).isEqualTo(expectedTitleColor)
-        assertThat(actionsColor).isEqualTo(expectedActionsColor)
-
-        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
-            .assertContainsColor(expectedContainerColor)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun centerAlignedTopAppBar_scrolledContentColor() {
-        var expectedScrolledContainerColor: Color = Color.Unspecified
-        lateinit var scrollBehavior: TopAppBarScrollBehavior
-
-        rule.setMaterialContent(lightColorScheme()) {
-            scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
-            CenterAlignedTopAppBar(
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                title = {
-                    Text("Title", Modifier.testTag(TitleTestTag))
-                    // fraction = 1f to indicate a scroll.
-                    expectedScrolledContainerColor =
-                        TopAppBarDefaults.centerAlignedTopAppBarColors()
-                            .containerColor(colorTransitionFraction = 1f)
-                },
-                scrollBehavior = scrollBehavior
-            )
-        }
-
-        // Simulate scrolled content.
-        rule.runOnIdle {
-            scrollBehavior.state.contentOffset = -100f
-        }
-        rule.waitForIdle()
-        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
-            .assertContainsColor(expectedScrolledContainerColor)
-    }
-
-    @Test
-    fun mediumTopAppBar_expandsToScreen() {
-        rule.setMaterialContentForSizeAssertions {
-            MediumTopAppBar(title = { Text("Medium Title") })
-        }
-            .assertHeightIsEqualTo(TopAppBarMediumTokens.ContainerHeight)
-            .assertWidthIsEqualTo(rule.rootWidth())
-    }
-
-    @Test
-    fun mediumTopAppBar_expanded_positioning() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                MediumTopAppBar(
-                    navigationIcon = {
-                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
-                    },
-                    title = {
-                        Text("Title", Modifier.testTag(TitleTestTag))
-                    },
-                    actions = {
-                        FakeIcon(Modifier.testTag(ActionsTestTag))
-                    }
-                )
-            }
-        }
-
-        // The bottom text baseline should be 24.dp from the bottom of the app bar.
-        assertMediumOrLargeDefaultPositioning(
-            expectedAppBarHeight = TopAppBarMediumTokens.ContainerHeight,
-            bottomTextPadding = 24.dp
-        )
-    }
-
-    @Test
-    fun mediumTopAppBar_scrolled_positioning() {
-        val windowInsets = WindowInsets(13.dp, 13.dp, 13.dp, 13.dp)
-        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                MediumTopAppBar(
-                    navigationIcon = {
-                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
-                    },
-                    title = {
-                        Text("Title", Modifier.testTag(TitleTestTag))
-                    },
-                    actions = {
-                        FakeIcon(Modifier.testTag(ActionsTestTag))
-                    },
-                    scrollBehavior = scrollBehavior,
-                    windowInsets = windowInsets
-                )
-            }
-        }
-        assertMediumOrLargeScrolledHeight(
-            TopAppBarMediumTokens.ContainerHeight,
-            TopAppBarSmallTokens.ContainerHeight,
-            windowInsets,
-            content
-        )
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun mediumTopAppBar_scrolledContainerColor() {
-        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
-            MediumTopAppBar(
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                title = {
-                    Text("Title", Modifier.testTag(TitleTestTag))
-                },
-                scrollBehavior = scrollBehavior
-            )
-        }
-
-        assertMediumOrLargeScrolledColors(
-            appBarMaxHeight = TopAppBarMediumTokens.ContainerHeight,
-            appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
-            titleContentColor = Color.Unspecified,
-            content = content
-        )
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun mediumTopAppBar_scrolledColorsWithCustomTitleTextColor() {
-        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
-            MediumTopAppBar(
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                title = {
-                    Text(
-                        text = "Title", Modifier.testTag(TitleTestTag),
-                        color = Color.Green
-                    )
-                },
-                scrollBehavior = scrollBehavior
-            )
-        }
-        assertMediumOrLargeScrolledColors(
-            appBarMaxHeight = TopAppBarMediumTokens.ContainerHeight,
-            appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
-            titleContentColor = Color.Green,
-            content = content
-        )
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun mediumTopAppBar_semantics() {
-        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
-            MediumTopAppBar(
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                title = {
-                    Text("Title", Modifier.testTag(TitleTestTag))
-                },
-                scrollBehavior = scrollBehavior
-            )
-        }
-
-        assertMediumOrLargeScrolledSemantics(
-            TopAppBarMediumTokens.ContainerHeight,
-            TopAppBarSmallTokens.ContainerHeight,
-            content
-        )
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun largeTopAppBar_semantics() {
-        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
-            LargeTopAppBar(
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                title = {
-                    Text("Title", Modifier.testTag(TitleTestTag))
-                },
-                scrollBehavior = scrollBehavior
-            )
-        }
-        assertMediumOrLargeScrolledSemantics(
-            TopAppBarLargeTokens.ContainerHeight,
-            TopAppBarSmallTokens.ContainerHeight,
-            content
-        )
-    }
-
-    @Test
-    fun largeTopAppBar_expandsToScreen() {
-        rule.setMaterialContentForSizeAssertions {
-            LargeTopAppBar(title = { Text("Large Title") })
-        }
-            .assertHeightIsEqualTo(TopAppBarLargeTokens.ContainerHeight)
-            .assertWidthIsEqualTo(rule.rootWidth())
-    }
-
-    @Test
-    fun largeTopAppBar_expanded_positioning() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                LargeTopAppBar(
-                    navigationIcon = {
-                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
-                    },
-                    title = {
-                        Text("Title", Modifier.testTag(TitleTestTag))
-                    },
-                    actions = {
-                        FakeIcon(Modifier.testTag(ActionsTestTag))
-                    }
-                )
-            }
-        }
-
-        // The bottom text baseline should be 28.dp from the bottom of the app bar.
-        assertMediumOrLargeDefaultPositioning(
-            expectedAppBarHeight = TopAppBarLargeTokens.ContainerHeight,
-            bottomTextPadding = 28.dp
-        )
-    }
-
-    @Test
-    fun largeTopAppBar_scrolled_positioning() {
-        val windowInsets = WindowInsets(4.dp, 4.dp, 4.dp, 4.dp)
-        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
-            Box(Modifier.testTag(TopAppBarTestTag)) {
-                LargeTopAppBar(
-                    navigationIcon = {
-                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
-                    },
-                    title = {
-                        Text("Title", Modifier.testTag(TitleTestTag))
-                    },
-                    actions = {
-                        FakeIcon(Modifier.testTag(ActionsTestTag))
-                    },
-                    scrollBehavior = scrollBehavior,
-                    windowInsets = windowInsets
-                )
-            }
-        }
-        assertMediumOrLargeScrolledHeight(
-            TopAppBarLargeTokens.ContainerHeight,
-            TopAppBarSmallTokens.ContainerHeight,
-            windowInsets,
-            content
-        )
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun largeTopAppBar_scrolledContainerColor() {
-        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
-            LargeTopAppBar(
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                title = {
-                    Text("Title", Modifier.testTag(TitleTestTag))
-                },
-                scrollBehavior = scrollBehavior,
-            )
-        }
-        assertMediumOrLargeScrolledColors(
-            appBarMaxHeight = TopAppBarLargeTokens.ContainerHeight,
-            appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
-            titleContentColor = Color.Unspecified,
-            content = content
-        )
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun largeTopAppBar_scrolledColorsWithCustomTitleTextColor() {
-        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
-            LargeTopAppBar(
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                title = {
-                    Text(
-                        text = "Title", Modifier.testTag(TitleTestTag),
-                        color = Color.Red
-                    )
-                },
-                scrollBehavior = scrollBehavior,
-            )
-        }
-        assertMediumOrLargeScrolledColors(
-            appBarMaxHeight = TopAppBarLargeTokens.ContainerHeight,
-            appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
-            titleContentColor = Color.Red,
-            content = content
-        )
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun topAppBar_enterAlways_allowHorizontalScroll() {
-        lateinit var state: LazyListState
-        rule.setMaterialContent(lightColorScheme()) {
-            state = rememberLazyListState()
-            MultiPageContent(TopAppBarDefaults.enterAlwaysScrollBehavior(), state)
-        }
-
-        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
-        }
-
-        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun topAppBar_exitUntilCollapsed_allowHorizontalScroll() {
-        lateinit var state: LazyListState
-        rule.setMaterialContent(lightColorScheme()) {
-            state = rememberLazyListState()
-            MultiPageContent(TopAppBarDefaults.exitUntilCollapsedScrollBehavior(), state)
-        }
-
-        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
-        }
-
-        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun topAppBar_pinned_allowHorizontalScroll() {
-        lateinit var state: LazyListState
-        rule.setMaterialContent(lightColorScheme()) {
-            state = rememberLazyListState()
-            MultiPageContent(
-                TopAppBarDefaults.pinnedScrollBehavior(),
-                state
-            )
-        }
-
-        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
-        }
-
-        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
-        }
-    }
-
-    @Test
-    fun topAppBar_smallPinnedDraggedAppBar() {
-        rule.setMaterialContentForSizeAssertions {
-            TopAppBar(
-                title = {
-                    Text("Title")
-                },
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
-            )
-        }
-
-        rule.onNodeWithTag(TopAppBarTestTag)
-            .assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
-
-        // Drag the app bar up half its height.
-        rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
-            down(Offset(x = 0f, y = height / 2f))
-            moveTo(Offset(x = 0f, y = 0f))
-        }
-        rule.waitForIdle()
-        // Check that the app bar did not collapse.
-        rule.onNodeWithTag(TopAppBarTestTag)
-            .assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
-    }
-
-    @Test
-    fun topAppBar_mediumDraggedAppBar() {
-        rule.setMaterialContentForSizeAssertions {
-            MediumTopAppBar(
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                title = {
-                    Text("Title")
-                },
-                scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
-            )
-        }
-
-        rule.onNodeWithTag(TopAppBarTestTag)
-            .assertHeightIsEqualTo(TopAppBarMediumTokens.ContainerHeight)
-
-        // Drag up the app bar.
-        rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
-            down(Offset(x = 0f, y = height - 20f))
-            moveTo(Offset(x = 0f, y = 0f))
-        }
-        rule.waitForIdle()
-        // Check that the app bar collapsed to its small size constraints (i.e.
-        // TopAppBarSmallTokens.ContainerHeight).
-        rule.onNodeWithTag(TopAppBarTestTag)
-            .assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
-    }
-
-    @Test
-    fun topAppBar_dragSnapToCollapsed() {
-        rule.setMaterialContentForSizeAssertions {
-            LargeTopAppBar(
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                title = {
-                    Text("Title")
-                },
-                scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
-            )
-        }
-
-        rule.onNodeWithTag(TopAppBarTestTag)
-            .assertHeightIsEqualTo(TopAppBarLargeTokens.ContainerHeight)
-
-        // Slightly drag up the app bar.
-        rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
-            down(Offset(x = 0f, y = height - 20f))
-            moveTo(Offset(x = 0f, y = height - 40f))
-            up()
-        }
-        rule.waitForIdle()
-
-        // Check that the app bar returned to its expanded size (i.e. fully expanded).
-        rule.onNodeWithTag(TopAppBarTestTag)
-            .assertHeightIsEqualTo(TopAppBarLargeTokens.ContainerHeight)
-
-        // Drag up the app bar to the point it should continue to collapse after.
-        rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
-            down(Offset(x = 0f, y = height - 20f))
-            moveTo(Offset(x = 0f, y = 40f))
-            up()
-        }
-        rule.waitForIdle()
-
-        // Check that the app bar collapsed to its small size constraints (i.e.
-        // TopAppBarSmallTokens.ContainerHeight).
-        rule.onNodeWithTag(TopAppBarTestTag)
-            .assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
-    }
-
-    @Test
-    fun topAppBar_dragWithSnapDisabled() {
-        rule.setMaterialContentForSizeAssertions {
-            LargeTopAppBar(
-                modifier = Modifier.testTag(TopAppBarTestTag),
-                title = {
-                    Text("Title")
-                },
-                scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
-                    snapAnimationSpec = null
-                )
-            )
-        }
-
-        // Check that the app bar stayed at its position (i.e. its bounds are with a smaller height)
-        val boundsBefore = rule.onNodeWithTag(TopAppBarTestTag).getBoundsInRoot()
-        TopAppBarLargeTokens.ContainerHeight.assertIsEqualTo(
-            expected = boundsBefore.height,
-            subject = "container height"
-        )
-        // Slightly drag up the app bar.
-        rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
-            down(Offset(x = 100f, y = height - 20f))
-            moveTo(Offset(x = 100f, y = height - 100f))
-            up()
-        }
-        rule.waitForIdle()
-
-        // Check that the app bar did not snap back to its fully expanded height, or collapsed to
-        // its collapsed height.
-        val boundsAfter = rule.onNodeWithTag(TopAppBarTestTag).getBoundsInRoot()
-        assertThat(TopAppBarLargeTokens.ContainerHeight).isGreaterThan(boundsAfter.height)
-        assertThat(TopAppBarSmallTokens.ContainerHeight).isLessThan(boundsAfter.height)
-    }
-
-    @Test
-    fun state_restoresTopAppBarState() {
-        val restorationTester = StateRestorationTester(rule)
-        var topAppBarState: TopAppBarState? = null
-        restorationTester.setContent {
-            topAppBarState = rememberTopAppBarState()
-        }
-
-        rule.runOnIdle {
-            topAppBarState!!.heightOffsetLimit = -350f
-            topAppBarState!!.heightOffset = -300f
-            topAppBarState!!.contentOffset = -550f
-        }
-
-        topAppBarState = null
-
-        restorationTester.emulateSavedInstanceStateRestore()
-
-        rule.runOnIdle {
-            assertThat(topAppBarState!!.heightOffsetLimit).isEqualTo(-350f)
-            assertThat(topAppBarState!!.heightOffset).isEqualTo(-300f)
-            assertThat(topAppBarState!!.contentOffset).isEqualTo(-550f)
-        }
-    }
-
-    @Test
-    fun bottomAppBarWithFAB_heightIsFromSpec() {
-        rule
-            .setMaterialContentForSizeAssertions {
-                BottomAppBar(
-                    actions = {},
-                    floatingActionButton = {
-                        FloatingActionButton(
-                            onClick = { /* do something */ },
-                            containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
-                            elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
-                        ) {
-                            Icon(Icons.Filled.Add, "Localized description")
-                        }
-                    })
-            }
-            .assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight)
-            .assertWidthIsEqualTo(rule.rootWidth())
-    }
-
-    @Test
-    fun bottomAppBarWithFAB_respectsWindowInsets() {
-        rule
-            .setMaterialContentForSizeAssertions {
-                BottomAppBar(
-                    actions = {},
-                    windowInsets = WindowInsets(10.dp, 10.dp, 10.dp, 10.dp),
-                    floatingActionButton = {
-                        FloatingActionButton(
-                            onClick = { /* do something */ },
-                            containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
-                            elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
-                        ) {
-                            Icon(Icons.Filled.Add, "Localized description")
-                        }
-                    })
-            }
-            .assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight + 20.dp)
-            .assertWidthIsEqualTo(rule.rootWidth())
-    }
-
-    @Test
-    fun bottomAppBar_FABshown_whenActionsOverflowRow() {
-        rule.setMaterialContent(lightColorScheme()) {
-            BottomAppBar(
-                actions = {
-                    repeat(20) {
-                        FakeIcon(Modifier)
-                    }
-                },
-                floatingActionButton = {
-                    FloatingActionButton(
-                        onClick = { /* do something */ },
-                        modifier = Modifier.testTag("FAB"),
-                        containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
-                        elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
-                    ) {
-                        Icon(Icons.Filled.Add, "Localized description")
-                    }
-                })
-        }
-        rule.onNodeWithTag("FAB").assertIsDisplayed()
-    }
-
-    @Test
-    fun bottomAppBar_widthExpandsToScreen() {
-        rule
-            .setMaterialContentForSizeAssertions {
-                BottomAppBar {}
-            }
-            .assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight)
-            .assertWidthIsEqualTo(rule.rootWidth())
-    }
-
-    @Test
-    fun bottomAppBar_default_positioning() {
-        rule.setMaterialContent(lightColorScheme()) {
-            BottomAppBar(Modifier.testTag("bar")) {
-                FakeIcon(Modifier.testTag("icon"))
-            }
-        }
-
-        val appBarBounds = rule.onNodeWithTag("bar").getUnclippedBoundsInRoot()
-        val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
-
-        val defaultPadding = BottomAppBarDefaults.ContentPadding
-        rule.onNodeWithTag("icon")
-            // Child icon should be 4.dp from the start
-            .assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
-            // Child icon should be 10.dp from the top
-            .assertTopPositionInRootIsEqualTo(
-                defaultPadding.calculateTopPadding() +
-                    (appBarBottomEdgeY - defaultPadding.calculateTopPadding() - FakeIconSize) / 2
-            )
-    }
-
-    @Test
-    fun bottomAppBar_default_positioning_respectsContentPadding() {
-        val topPadding = 5.dp
-        rule.setMaterialContent(lightColorScheme()) {
-            BottomAppBar(
-                Modifier.testTag("bar"),
-                contentPadding = PaddingValues(top = topPadding, start = 3.dp)
-            ) {
-                FakeIcon(Modifier.testTag("icon"))
-            }
-        }
-
-        val appBarBounds = rule.onNodeWithTag("bar").getUnclippedBoundsInRoot()
-        val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
-
-        rule.onNodeWithTag("icon")
-            // Child icon should be 4.dp from the start
-            .assertLeftPositionInRootIsEqualTo(3.dp)
-            // Child icon should be 10.dp from the top
-            .assertTopPositionInRootIsEqualTo(
-                (appBarBottomEdgeY - topPadding - FakeIconSize) / 2 + 5.dp
-            )
-    }
-
-    @Test
-    fun bottomAppBarWithFAB_default_positioning() {
-        rule.setMaterialContent(lightColorScheme()) {
-            BottomAppBar(
-                actions = {},
-                Modifier.testTag("bar"),
-                floatingActionButton = {
-                    FloatingActionButton(
-                        onClick = { /* do something */ },
-                        modifier = Modifier.testTag("FAB"),
-                        containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
-                        elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
-                    ) {
-                        Icon(Icons.Filled.Add, "Localized description")
-                    }
-                })
-        }
-
-        val appBarBounds = rule.onNodeWithTag("bar").getUnclippedBoundsInRoot()
-
-        val fabBounds = rule.onNodeWithTag("FAB").getUnclippedBoundsInRoot()
-
-        rule.onNodeWithTag("FAB")
-            // FAB should be 16.dp from the end
-            .assertLeftPositionInRootIsEqualTo(appBarBounds.width - 16.dp - fabBounds.width)
-            // FAB should be 12.dp from the top
-            .assertTopPositionInRootIsEqualTo(12.dp)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Composable
-    private fun MultiPageContent(scrollBehavior: TopAppBarScrollBehavior, state: LazyListState) {
-        Scaffold(
-            modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
-            topBar = {
-                TopAppBar(
-                    title = { Text(text = "Title") },
-                    modifier = Modifier.testTag(TopAppBarTestTag),
-                    scrollBehavior = scrollBehavior
-                )
-            }
-        ) { contentPadding ->
-            LazyRow(
-                Modifier
-                    .fillMaxSize()
-                    .testTag(LazyListTag), state
-            ) {
-                items(2) { page ->
-                    LazyColumn(
-                        modifier = Modifier.fillParentMaxSize(),
-                        contentPadding = contentPadding
-                    ) {
-                        items(50) {
-                            Text(
-                                modifier = Modifier.fillParentMaxWidth(),
-                                text = "Item #$page x $it"
-                            )
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Checks the app bar's components positioning when it's a [TopAppBar], a
-     * [CenterAlignedTopAppBar], or a larger app bar that is scrolled up and collapsed into a small
-     * configuration and there is no navigation icon.
-     */
-    private fun assertSmallPositioningWithoutNavigation(isCenteredTitle: Boolean = false) {
-        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
-        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
-
-        val titleNode = rule.onNodeWithTag(TitleTestTag)
-        // Title should be vertically centered
-        titleNode.assertTopPositionInRootIsEqualTo((appBarBounds.height - titleBounds.height) / 2)
-        if (isCenteredTitle) {
-            // Title should be horizontally centered
-            titleNode.assertLeftPositionInRootIsEqualTo(
-                (appBarBounds.width - titleBounds.width) / 2
-            )
-        } else {
-            // Title should now be placed 16.dp from the start, as there is no navigation icon
-            // 4.dp padding for the whole app bar + 12.dp inset
-            titleNode.assertLeftPositionInRootIsEqualTo(4.dp + 12.dp)
-        }
-
-        rule.onNodeWithTag(ActionsTestTag)
-            // Action should still be placed at the end
-            .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
-    }
-
-    /**
-     * Checks the app bar's components positioning when it's a [TopAppBar] or a
-     * [CenterAlignedTopAppBar].
-     */
-    private fun assertSmallDefaultPositioning(isCenteredTitle: Boolean = false) {
-        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
-        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
-        val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
-
-        rule.onNodeWithTag(NavigationIconTestTag)
-            // Navigation icon should be 4.dp from the start
-            .assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
-            // Navigation icon should be centered within the height of the app bar.
-            .assertTopPositionInRootIsEqualTo(
-                appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
-            )
-
-        val titleNode = rule.onNodeWithTag(TitleTestTag)
-        // Title should be vertically centered
-        titleNode.assertTopPositionInRootIsEqualTo((appBarBounds.height - titleBounds.height) / 2)
-        if (isCenteredTitle) {
-            // Title should be horizontally centered
-            titleNode.assertLeftPositionInRootIsEqualTo(
-                (appBarBounds.width - titleBounds.width) / 2
-            )
-        } else {
-            // Title should be 56.dp from the start
-            // 4.dp padding for the whole app bar + 48.dp icon size + 4.dp title padding.
-            titleNode.assertLeftPositionInRootIsEqualTo(4.dp + FakeIconSize + 4.dp)
-        }
-
-        rule.onNodeWithTag(ActionsTestTag)
-            // Action should be placed at the end
-            .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
-            // Action should be 8.dp from the top
-            .assertTopPositionInRootIsEqualTo(
-                appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
-            )
-    }
-
-    /**
-     * Checks the app bar's components positioning when it's a [MediumTopAppBar] or a
-     * [LargeTopAppBar].
-     */
-    private fun assertMediumOrLargeDefaultPositioning(
-        expectedAppBarHeight: Dp,
-        bottomTextPadding: Dp
-    ) {
-        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
-        appBarBounds.height.assertIsEqualTo(expectedAppBarHeight, "top app bar height")
-
-        // Expecting the title composable to be reused for the top and bottom rows of the top app
-        // bar, so obtaining the node with the title tag should return two nodes, one for each row.
-        val allTitleNodes = rule.onAllNodesWithTag(TitleTestTag, true)
-        allTitleNodes.assertCountEquals(2)
-        val topTitleNode = allTitleNodes.onFirst()
-        val bottomTitleNode = allTitleNodes.onLast()
-
-        val topTitleBounds = topTitleNode.getUnclippedBoundsInRoot()
-        val bottomTitleBounds = bottomTitleNode.getUnclippedBoundsInRoot()
-        val topAppBarBottomEdgeY = appBarBounds.top + TopAppBarSmallTokens.ContainerHeight
-        val bottomAppBarBottomEdgeY = appBarBounds.top + appBarBounds.height
-
-        rule.onNodeWithTag(NavigationIconTestTag)
-            // Navigation icon should be 4.dp from the start
-            .assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
-            // Navigation icon should be centered within the height of the top part of the app bar.
-            .assertTopPositionInRootIsEqualTo(
-                topAppBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
-            )
-
-        rule.onNodeWithTag(ActionsTestTag)
-            // Action should be placed at the end
-            .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
-            // Action should be 8.dp from the top
-            .assertTopPositionInRootIsEqualTo(
-                topAppBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
-            )
-
-        topTitleNode
-            // Top title should be 56.dp from the start
-            // 4.dp padding for the whole app bar + 48.dp icon size + 4.dp title padding.
-            .assertLeftPositionInRootIsEqualTo(4.dp + FakeIconSize + 4.dp)
-            // Title should be vertically centered in the top part, which has a height of a small
-            // app bar.
-            .assertTopPositionInRootIsEqualTo((topAppBarBottomEdgeY - topTitleBounds.height) / 2)
-
-        bottomTitleNode
-            // Bottom title should be 16.dp from the start.
-            .assertLeftPositionInRootIsEqualTo(16.dp)
-
-        // Check if the bottom text baseline is at the expected distance from the bottom of the
-        // app bar.
-        val bottomTextBaselineY = bottomTitleBounds.top + bottomTitleNode.getLastBaselinePosition()
-        (bottomAppBarBottomEdgeY - bottomTextBaselineY).assertIsEqualTo(
-            bottomTextPadding,
-            "text baseline distance from the bottom"
-        )
-    }
-
-    /**
-     * Checks that changing values at a [MediumTopAppBar] or a [LargeTopAppBar] scroll behavior
-     * affects the height of the app bar.
-     *
-     * This check partially and fully collapses the app bar to test its height.
-     *
-     * @param appBarMaxHeight the max height of the app bar [content]
-     * @param appBarMinHeight the min height of the app bar [content]
-     * @param content a Composable that adds a MediumTopAppBar or a LargeTopAppBar
-     */
-    @OptIn(ExperimentalMaterial3Api::class)
-    private fun assertMediumOrLargeScrolledHeight(
-        appBarMaxHeight: Dp,
-        appBarMinHeight: Dp,
-        windowInsets: WindowInsets,
-        content: @Composable (TopAppBarScrollBehavior?) -> Unit
-    ) {
-        val (topInset, bottomInset) = with(rule.density) {
-            windowInsets.getTop(this).toDp() to windowInsets.getBottom(this).toDp()
-        }
-        val fullyCollapsedOffsetDp = appBarMaxHeight - appBarMinHeight
-        val partiallyCollapsedOffsetDp = fullyCollapsedOffsetDp / 3
-        var partiallyCollapsedHeightOffsetPx = 0f
-        var fullyCollapsedHeightOffsetPx = 0f
-        lateinit var scrollBehavior: TopAppBarScrollBehavior
-        rule.setMaterialContent(lightColorScheme()) {
-            scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
-            with(LocalDensity.current) {
-                partiallyCollapsedHeightOffsetPx = partiallyCollapsedOffsetDp.toPx()
-                fullyCollapsedHeightOffsetPx = fullyCollapsedOffsetDp.toPx()
-            }
-
-            content(scrollBehavior)
-        }
-
-        // Simulate a partially collapsed app bar.
-        rule.runOnIdle {
-            scrollBehavior.state.heightOffset = -partiallyCollapsedHeightOffsetPx
-            scrollBehavior.state.contentOffset = -partiallyCollapsedHeightOffsetPx
-        }
-        rule.waitForIdle()
-        rule.onNodeWithTag(TopAppBarTestTag)
-            .assertHeightIsEqualTo(
-                appBarMaxHeight - partiallyCollapsedOffsetDp + topInset + bottomInset
-            )
-
-        // Simulate a fully collapsed app bar.
-        rule.runOnIdle {
-            scrollBehavior.state.heightOffset = -fullyCollapsedHeightOffsetPx
-            // Simulate additional content scroll beyond the max offset scroll.
-            scrollBehavior.state.contentOffset =
-                -fullyCollapsedHeightOffsetPx - partiallyCollapsedHeightOffsetPx
-        }
-        rule.waitForIdle()
-        // Check that the app bar collapsed to its min height.
-        rule.onNodeWithTag(TopAppBarTestTag).assertHeightIsEqualTo(
-            appBarMinHeight + topInset + bottomInset
-        )
-    }
-
-    /**
-     * Checks that changing values at a [MediumTopAppBar] or a [LargeTopAppBar] scroll behavior
-     * affects the container color and the title's content color of the app bar.
-     *
-     * @param appBarMaxHeight the max height of the app bar [content]
-     * @param appBarMinHeight the min height of the app bar [content]
-     * @param titleContentColor text content color expected for the app bar's title.
-     * @param content a Composable that adds a MediumTopAppBar or a LargeTopAppBar
-     */
-    @OptIn(ExperimentalMaterial3Api::class)
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    private fun assertMediumOrLargeScrolledColors(
-        appBarMaxHeight: Dp,
-        appBarMinHeight: Dp,
-        titleContentColor: Color,
-        content: @Composable (TopAppBarScrollBehavior?) -> Unit
-    ) {
-        // Note: This value is specifically picked to avoid precision issues when asserting the
-        // color values further down this test.
-        val fullyCollapsedOffsetDp = appBarMaxHeight - appBarMinHeight
-        var fullyCollapsedHeightOffsetPx = 0f
-        var fullyCollapsedContainerColor: Color = Color.Unspecified
-        var expandedAppBarBackgroundColor: Color = Color.Unspecified
-        var titleColor = titleContentColor
-        lateinit var scrollBehavior: TopAppBarScrollBehavior
-        rule.setMaterialContent(lightColorScheme()) {
-            scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
-            // Using the mediumTopAppBarColors for both Medium and Large top app bars, as the
-            // current content color settings are the same.
-            expandedAppBarBackgroundColor = TopAppBarMediumTokens.ContainerColor.value
-            fullyCollapsedContainerColor =
-                TopAppBarDefaults.mediumTopAppBarColors()
-                    .containerColor(colorTransitionFraction = 1f)
-
-            // Resolve the title's content color. The default implementation returns the same color
-            // regardless of the fraction, and the color is applied later with alpha.
-            if (titleColor == Color.Unspecified) {
-                titleColor =
-                    TopAppBarDefaults.mediumTopAppBarColors().titleContentColor
-            }
-
-            with(LocalDensity.current) {
-                fullyCollapsedHeightOffsetPx = fullyCollapsedOffsetDp.toPx()
-            }
-
-            content(scrollBehavior)
-        }
-
-        // Expecting the title composable to be reused for the top and bottom rows of the top app
-        // bar, so obtaining the node with the title tag should return two nodes, one for each row.
-        val allTitleNodes = rule.onAllNodesWithTag(TitleTestTag, true)
-        allTitleNodes.assertCountEquals(2)
-        val topTitleNode = allTitleNodes.onFirst()
-        val bottomTitleNode = allTitleNodes.onLast()
-
-        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
-            .assertContainsColor(expandedAppBarBackgroundColor)
-
-        // Assert the content color at the top and bottom parts of the expanded app bar.
-        topTitleNode.captureToImage()
-            .assertContainsColor(
-                titleColor.copy(alpha = TopTitleAlphaEasing.transform(0f))
-                    .compositeOver(expandedAppBarBackgroundColor)
-            )
-        bottomTitleNode.captureToImage()
-            .assertContainsColor(
-                titleColor.compositeOver(expandedAppBarBackgroundColor)
-            )
-
-        // Simulate fully collapsed content.
-        rule.runOnIdle {
-            scrollBehavior.state.heightOffset = -fullyCollapsedHeightOffsetPx
-            scrollBehavior.state.contentOffset = -fullyCollapsedHeightOffsetPx
-        }
-        rule.waitForIdle()
-        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
-            .assertContainsColor(fullyCollapsedContainerColor)
-        topTitleNode.captureToImage()
-            .assertContainsColor(
-                titleColor.copy(alpha = TopTitleAlphaEasing.transform(1f))
-                    .compositeOver(fullyCollapsedContainerColor)
-            )
-        // Only the top title should be visible in the collapsed form.
-        bottomTitleNode.assertIsNotDisplayed()
-    }
-
-    /**
-     * Checks that changing values at a [MediumTopAppBar] or a [LargeTopAppBar] scroll behavior
-     * affects the title's semantics.
-     *
-     * This check partially and fully collapses the app bar to test the semantics.
-     *
-     * @param appBarMaxHeight the max height of the app bar [content]
-     * @param appBarMinHeight the min height of the app bar [content]
-     * @param content a Composable that adds a MediumTopAppBar or a LargeTopAppBar
-     */
-    @OptIn(ExperimentalMaterial3Api::class)
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    private fun assertMediumOrLargeScrolledSemantics(
-        appBarMaxHeight: Dp,
-        appBarMinHeight: Dp,
-        content: @Composable (TopAppBarScrollBehavior?) -> Unit
-    ) {
-        val fullyCollapsedOffsetDp = appBarMaxHeight - appBarMinHeight
-        val oneThirdCollapsedOffsetDp = fullyCollapsedOffsetDp / 3
-        var fullyCollapsedHeightOffsetPx = 0f
-        var oneThirdCollapsedHeightOffsetPx = 0f
-        lateinit var scrollBehavior: TopAppBarScrollBehavior
-        rule.setMaterialContent(lightColorScheme()) {
-            scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
-            with(LocalDensity.current) {
-                oneThirdCollapsedHeightOffsetPx = oneThirdCollapsedOffsetDp.toPx()
-                fullyCollapsedHeightOffsetPx = fullyCollapsedOffsetDp.toPx()
-            }
-
-            content(scrollBehavior)
-        }
-
-        // Asserting that only one semantic title node is returned after the clearAndSetSemantics is
-        // applied to the merged tree according to the alpha values of the titles.
-        assertSingleTitleSemanticNode()
-
-        // Simulate 1/3 collapsed content.
-        rule.runOnIdle {
-            scrollBehavior.state.heightOffset = -oneThirdCollapsedHeightOffsetPx
-            scrollBehavior.state.contentOffset = -oneThirdCollapsedHeightOffsetPx
-        }
-        rule.waitForIdle()
-
-        // Assert that only one semantic title node is available while scrolling the app bar.
-        assertSingleTitleSemanticNode()
-
-        // Simulate fully collapsed content.
-        rule.runOnIdle {
-            scrollBehavior.state.heightOffset = -fullyCollapsedHeightOffsetPx
-            scrollBehavior.state.contentOffset = -fullyCollapsedHeightOffsetPx
-        }
-        rule.waitForIdle()
-
-        // Assert that only one semantic title node is available.
-        assertSingleTitleSemanticNode()
-    }
-
-    /**
-     * Asserts that only one semantic node exists at app bar title when the tree is merged.
-     */
-    private fun assertSingleTitleSemanticNode() {
-        val unmergedTitleNodes = rule.onAllNodesWithTag(TitleTestTag, useUnmergedTree = true)
-        unmergedTitleNodes.assertCountEquals(2)
-
-        val mergedTitleNodes = rule.onAllNodesWithTag(TitleTestTag, useUnmergedTree = false)
-        mergedTitleNodes.assertCountEquals(1)
-    }
-
-    /**
-     * An [IconButton] with an [Icon] inside for testing positions.
-     *
-     * An [IconButton] is defaulted to be 48X48dp, while its child [Icon] is defaulted to 24x24dp.
-     */
-    private val FakeIcon = @Composable { modifier: Modifier ->
-        IconButton(
-            onClick = { /* doSomething() */ },
-            modifier = modifier.semantics(mergeDescendants = true) {}
-        ) {
-            Icon(ColorPainter(Color.Red), null)
-        }
-    }
-
-    private fun expectedActionPosition(appBarWidth: Dp): Dp =
-        appBarWidth - AppBarStartAndEndPadding - FakeIconSize
-
-    private val FakeIconSize = 48.dp
-    private val AppBarStartAndEndPadding = 4.dp
-    private val AppBarTopAndBottomPadding =
-        (TopAppBarSmallTokens.ContainerHeight - FakeIconSize) / 2
-
-    private val LazyListTag = "lazyList"
-    private val TopAppBarTestTag = "bar"
-    private val NavigationIconTestTag = "navigationIcon"
-    private val TitleTestTag = "title"
-    private val ActionsTestTag = "actions"
-}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
deleted file mode 100644
index bbae528..0000000
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
+++ /dev/null
@@ -1,658 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3
-
-import android.app.Activity
-import android.content.Context
-import android.content.ContextWrapper
-import android.view.WindowManager
-import android.widget.FrameLayout
-import androidx.compose.foundation.ScrollState
-import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.boundsInRoot
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsFocused
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertTextContains
-import androidx.compose.ui.test.hasText
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.swipe
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.test.filters.MediumTest
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.UiDevice
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import org.junit.Assume.assumeNotNull
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@OptIn(ExperimentalMaterial3Api::class)
-@MediumTest
-@RunWith(Parameterized::class)
-class ExposedDropdownMenuTest(
-    private val softInputMode: SoftInputMode,
-) {
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "{0}")
-        fun parameters() = SoftInputMode.values()
-    }
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    private val TFTag = "TextFieldTag"
-    private val TrailingIconTag = "TrailingIconTag"
-    private val EDMTag = "ExposedDropdownMenuTag"
-    private val MenuItemTag = "MenuItemTag"
-    private val OptionName = "Option 1"
-
-    @Test
-    fun edm_expandsOnClick_andCollapsesOnClickOutside() {
-        var textFieldBounds = Rect.Zero
-        rule.setMaterialContent(lightColorScheme()) {
-            SoftInputMode(softInputMode)
-            var expanded by remember { mutableStateOf(false) }
-            ExposedDropdownMenuForTest(
-                expanded = expanded,
-                onExpandChange = { expanded = it },
-                onTextFieldBoundsChanged = {
-                    textFieldBounds = it
-                }
-            )
-        }
-
-        rule.onNodeWithTag(TFTag).assertIsDisplayed()
-        rule.onNodeWithTag(EDMTag).assertDoesNotExist()
-
-        // Click on the TextField
-        rule.onNodeWithTag(TFTag).performClick()
-
-        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
-
-        // Click outside EDM
-        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).click(
-            (textFieldBounds.right + 1).toInt(),
-            (textFieldBounds.bottom + 1).toInt(),
-        )
-
-        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
-    }
-
-    @Test
-    fun edm_collapsesOnTextFieldClick() {
-        rule.setMaterialContent(lightColorScheme()) {
-            SoftInputMode(softInputMode)
-            var expanded by remember { mutableStateOf(true) }
-            ExposedDropdownMenuForTest(
-                expanded = expanded,
-                onExpandChange = { expanded = it }
-            )
-        }
-
-        rule.onNodeWithTag(TFTag).assertIsDisplayed()
-        rule.onNodeWithTag(EDMTag).assertIsDisplayed()
-        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
-
-        // Click on the TextField
-        rule.onNodeWithTag(TFTag).performClick()
-
-        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
-    }
-
-    @Test
-    fun edm_doesNotCollapse_whenTypingOnSoftKeyboard() {
-        rule.setMaterialContent(lightColorScheme()) {
-            SoftInputMode(softInputMode)
-            var expanded by remember { mutableStateOf(false) }
-            ExposedDropdownMenuForTest(
-                expanded = expanded,
-                onExpandChange = { expanded = it }
-            )
-        }
-
-        rule.onNodeWithTag(TFTag).performClick()
-
-        rule.onNodeWithTag(TFTag).assertIsDisplayed()
-        rule.onNodeWithTag(TFTag).assertIsFocused()
-        rule.onNodeWithTag(EDMTag).assertIsDisplayed()
-        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
-
-        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
-        val zKey = device.findObject(By.desc("z")) ?: device.findObject(By.text("z"))
-        // Only run the test if we can find a key to type, which might fail for any number of
-        // reasons (keyboard doesn't appear, unexpected locale, etc.)
-        assumeNotNull(zKey)
-
-        repeat(3) {
-            zKey.click()
-            rule.waitForIdle()
-        }
-
-        val matcher = hasText("zzz")
-        rule.waitUntil {
-            matcher.matches(rule.onNodeWithTag(TFTag).fetchSemanticsNode())
-        }
-        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
-    }
-
-    @Test
-    fun edm_expandsAndFocusesTextField_whenTrailingIconClicked() {
-        rule.setMaterialContent(lightColorScheme()) {
-            SoftInputMode(softInputMode)
-            var expanded by remember { mutableStateOf(false) }
-            ExposedDropdownMenuForTest(
-                expanded = expanded,
-                onExpandChange = { expanded = it },
-            )
-        }
-
-        rule.onNodeWithTag(TFTag).assertIsDisplayed()
-        rule.onNodeWithTag(TrailingIconTag, useUnmergedTree = true).assertIsDisplayed()
-
-        // Click on the Trailing Icon
-        rule.onNodeWithTag(TrailingIconTag, useUnmergedTree = true).performClick()
-
-        rule.onNodeWithTag(TFTag).assertIsFocused()
-        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
-    }
-
-    @Test
-    fun edm_doesNotExpand_ifTouchEndsOutsideBounds() {
-        var textFieldBounds = Rect.Zero
-        rule.setMaterialContent(lightColorScheme()) {
-            SoftInputMode(softInputMode)
-            var expanded by remember { mutableStateOf(false) }
-            ExposedDropdownMenuForTest(
-                expanded = expanded,
-                onExpandChange = { expanded = it },
-                onTextFieldBoundsChanged = {
-                    textFieldBounds = it
-                }
-            )
-        }
-
-        rule.onNodeWithTag(TFTag).assertIsDisplayed()
-        rule.onNodeWithTag(EDMTag).assertDoesNotExist()
-
-        // A swipe that ends outside the bounds of the anchor should not expand the menu.
-        rule.onNodeWithTag(TFTag).performTouchInput {
-            swipe(
-                start = this.center,
-                end = Offset(this.centerX, this.centerY + (textFieldBounds.height / 2) + 1),
-                durationMillis = 100
-            )
-        }
-        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
-
-        // A swipe that ends within the bounds of the anchor should expand the menu.
-        rule.onNodeWithTag(TFTag).performTouchInput {
-            swipe(
-                start = this.center,
-                end = Offset(this.centerX, this.centerY + (textFieldBounds.height / 2) - 1),
-                durationMillis = 100
-            )
-        }
-        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
-    }
-
-    @Test
-    fun edm_doesNotExpand_ifTouchIsPartOfScroll() {
-        val testIndex = 2
-        var textFieldSize = IntSize.Zero
-        rule.setMaterialContent(lightColorScheme()) {
-            SoftInputMode(softInputMode)
-            LazyColumn(
-                modifier = Modifier.fillMaxSize(),
-                horizontalAlignment = Alignment.CenterHorizontally,
-            ) {
-                items(50) { index ->
-                    var expanded by remember { mutableStateOf(false) }
-                    var selectedOptionText by remember { mutableStateOf("") }
-
-                    ExposedDropdownMenuBox(
-                        expanded = expanded,
-                        onExpandedChange = { expanded = it },
-                        modifier = Modifier.padding(8.dp),
-                    ) {
-                        TextField(
-                            modifier = Modifier
-                                .menuAnchor()
-                                .then(
-                                    if (index == testIndex) Modifier
-                                        .testTag(TFTag)
-                                        .onSizeChanged {
-                                            textFieldSize = it
-                                        } else {
-                                        Modifier
-                                    }
-                                ),
-                            value = selectedOptionText,
-                            onValueChange = { selectedOptionText = it },
-                            label = { Text("Label") },
-                            trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
-                            colors = ExposedDropdownMenuDefaults.textFieldColors()
-                        )
-                        ExposedDropdownMenu(
-                            modifier = if (index == testIndex) {
-                                Modifier.testTag(EDMTag)
-                            } else { Modifier },
-                            expanded = expanded,
-                            onDismissRequest = { expanded = false }
-                        ) {
-                            DropdownMenuItem(
-                                text = { Text(OptionName) },
-                                onClick = {
-                                    selectedOptionText = OptionName
-                                    expanded = false
-                                },
-                                modifier = if (index == testIndex) {
-                                    Modifier.testTag(MenuItemTag)
-                                } else { Modifier },
-                            )
-                        }
-                    }
-                }
-            }
-        }
-
-        rule.onNodeWithTag(TFTag).assertIsDisplayed()
-        rule.onNodeWithTag(EDMTag).assertDoesNotExist()
-
-        // A swipe that causes a scroll should not expand the menu, even if it remains within the
-        // bounds of the anchor.
-        rule.onNodeWithTag(TFTag).performTouchInput {
-            swipe(
-                start = this.center,
-                end = Offset(this.centerX, this.centerY - (textFieldSize.height / 2) + 1),
-                durationMillis = 100
-            )
-        }
-        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
-
-        // But a swipe that does not cause a scroll should expand the menu.
-        rule.onNodeWithTag(TFTag).performTouchInput {
-            swipe(
-                start = this.center,
-                end = Offset(this.centerX + (textFieldSize.width / 2) - 1, this.centerY),
-                durationMillis = 100
-            )
-        }
-        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
-    }
-
-    @Test
-    fun edm_doesNotRecomposeOnScroll() {
-        var compositionCount = 0
-        lateinit var scrollState: ScrollState
-        lateinit var scope: CoroutineScope
-        rule.setMaterialContent(lightColorScheme()) {
-            SoftInputMode(softInputMode)
-            scrollState = rememberScrollState()
-            scope = rememberCoroutineScope()
-            Column(Modifier.verticalScroll(scrollState)) {
-                Spacer(Modifier.height(300.dp))
-
-                val expanded = false
-                ExposedDropdownMenuBox(
-                    expanded = expanded,
-                    onExpandedChange = {},
-                ) {
-                    TextField(
-                        modifier = Modifier.menuAnchor(),
-                        readOnly = true,
-                        value = "",
-                        onValueChange = {},
-                        label = { Text("Label") },
-                    )
-                    ExposedDropdownMenu(
-                        expanded = expanded,
-                        onDismissRequest = {},
-                        content = {},
-                    )
-                    SideEffect {
-                        compositionCount++
-                    }
-                }
-
-                Spacer(Modifier.height(300.dp))
-            }
-        }
-
-        assertThat(compositionCount).isEqualTo(1)
-
-        rule.runOnIdle {
-            scope.launch {
-                scrollState.animateScrollBy(500f)
-            }
-        }
-        rule.waitForIdle()
-
-        assertThat(compositionCount).isEqualTo(1)
-    }
-
-    @Test
-    fun edm_widthMatchesTextFieldWidth() {
-        var textFieldBounds by mutableStateOf(Rect.Zero)
-        var menuBounds by mutableStateOf(Rect.Zero)
-        rule.setMaterialContent(lightColorScheme()) {
-            SoftInputMode(softInputMode)
-            var expanded by remember { mutableStateOf(true) }
-            ExposedDropdownMenuForTest(
-                expanded = expanded,
-                onExpandChange = { expanded = it },
-                onTextFieldBoundsChanged = {
-                    textFieldBounds = it
-                },
-                onMenuBoundsChanged = {
-                    menuBounds = it
-                }
-            )
-        }
-
-        rule.onNodeWithTag(TFTag).assertIsDisplayed()
-        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
-
-        rule.runOnIdle {
-            assertThat(menuBounds.width).isEqualTo(textFieldBounds.width)
-        }
-    }
-
-    @Test
-    fun edm_collapsesWithSelection_whenMenuItemClicked() {
-        rule.setMaterialContent(lightColorScheme()) {
-            SoftInputMode(softInputMode)
-            var expanded by remember { mutableStateOf(true) }
-            ExposedDropdownMenuForTest(
-                expanded = expanded,
-                onExpandChange = { expanded = it }
-            )
-        }
-
-        rule.onNodeWithTag(TFTag).assertIsDisplayed()
-        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
-
-        // Choose the option
-        rule.onNodeWithTag(MenuItemTag).performClick()
-
-        // Menu should collapse
-        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
-        rule.onNodeWithTag(TFTag).assertTextContains(OptionName)
-    }
-
-    @Test
-    fun edm_resizesWithinWindowBounds_uponImeAppearance() {
-        var actualMenuSize: IntSize? = null
-        var density: Density? = null
-        val itemSize = 50.dp
-        val itemCount = 10
-
-        rule.setMaterialContent(lightColorScheme()) {
-            density = LocalDensity.current
-            SoftInputMode(softInputMode)
-            Column(Modifier.fillMaxSize()) {
-                // Push the EDM down so opening the keyboard causes a pan/scroll
-                Spacer(Modifier.weight(1f))
-
-                ExposedDropdownMenuBox(
-                    expanded = true,
-                    onExpandedChange = { }
-                ) {
-                    TextField(
-                        modifier = Modifier.menuAnchor(),
-                        value = "",
-                        onValueChange = { },
-                        label = { Text("Label") },
-                    )
-                    ExposedDropdownMenu(
-                        expanded = true,
-                        onDismissRequest = { },
-                        modifier = Modifier.onGloballyPositioned {
-                            actualMenuSize = it.size
-                        }
-                    ) {
-                        repeat(itemCount) {
-                            Box(Modifier.size(itemSize))
-                        }
-                    }
-                }
-            }
-        }
-
-        // This would fit on screen if the keyboard wasn't displayed.
-        val menuPreferredHeight = with(density!!) {
-            (itemSize * itemCount + DropdownMenuVerticalPadding * 2).roundToPx()
-        }
-        // But the keyboard *is* displayed, forcing the actual size to be smaller.
-        assertThat(actualMenuSize!!.height).isLessThan(menuPreferredHeight)
-    }
-
-    @Ignore("b/266109857")
-    @Test
-    fun edm_doesNotCrash_whenAnchorDetachedFirst() {
-        var parent: FrameLayout? = null
-        rule.setMaterialContent(lightColorScheme()) {
-            SoftInputMode(softInputMode)
-            AndroidView(
-                factory = { context ->
-                    FrameLayout(context).apply {
-                        addView(ComposeView(context).apply {
-                            setContent {
-                                ExposedDropdownMenuBox(expanded = true, onExpandedChange = {}) {
-                                    TextField(
-                                        value = "Text",
-                                        onValueChange = {},
-                                        modifier = Modifier.menuAnchor(),
-                                    )
-                                    ExposedDropdownMenu(
-                                        expanded = true,
-                                        onDismissRequest = {},
-                                    ) {
-                                        DropdownMenuItem(
-                                            text = { Text(OptionName) },
-                                            onClick = {},
-                                        )
-                                    }
-                                }
-                            }
-                        })
-                    }.also { parent = it }
-                }
-            )
-        }
-
-        rule.runOnIdle {
-            parent!!.removeAllViews()
-        }
-
-        rule.waitForIdle()
-
-        // Should not have crashed.
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun edm_withScrolledContent() {
-        lateinit var scrollState: ScrollState
-        rule.setMaterialContent(lightColorScheme()) {
-            SoftInputMode(softInputMode)
-            Box(Modifier.fillMaxSize()) {
-                ExposedDropdownMenuBox(
-                    modifier = Modifier.align(Alignment.Center),
-                    expanded = true,
-                    onExpandedChange = { }
-                ) {
-                    scrollState = rememberScrollState()
-                    TextField(
-                        modifier = Modifier.menuAnchor(),
-                        value = "",
-                        onValueChange = { },
-                        label = { Text("Label") },
-                    )
-                    ExposedDropdownMenu(
-                        expanded = true,
-                        onDismissRequest = { },
-                        scrollState = scrollState
-                    ) {
-                        repeat(100) {
-                            Text(
-                                text = "Text ${it + 1}",
-                                modifier = Modifier.testTag("MenuContent ${it + 1}"),
-                            )
-                        }
-                    }
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            runBlocking {
-                scrollState.scrollTo(scrollState.maxValue)
-            }
-        }
-
-        rule.waitForIdle()
-
-        rule.onNodeWithTag("MenuContent 1").assertIsNotDisplayed()
-        rule.onNodeWithTag("MenuContent 100").assertIsDisplayed()
-    }
-
-    @Composable
-    fun ExposedDropdownMenuForTest(
-        expanded: Boolean,
-        onExpandChange: (Boolean) -> Unit,
-        onTextFieldBoundsChanged: ((Rect) -> Unit)? = null,
-        onMenuBoundsChanged: ((Rect) -> Unit)? = null
-    ) {
-        var selectedOptionText by remember { mutableStateOf("") }
-        Box(Modifier.fillMaxSize()) {
-            ExposedDropdownMenuBox(
-                modifier = Modifier.align(Alignment.Center),
-                expanded = expanded,
-                onExpandedChange = { onExpandChange(!expanded) }
-            ) {
-                TextField(
-                    modifier = Modifier
-                        .menuAnchor()
-                        .testTag(TFTag)
-                        .onGloballyPositioned {
-                            onTextFieldBoundsChanged?.invoke(it.boundsInRoot())
-                        },
-                    value = selectedOptionText,
-                    onValueChange = { selectedOptionText = it },
-                    label = { Text("Label") },
-                    trailingIcon = {
-                        Box(
-                            modifier = Modifier.testTag(TrailingIconTag)
-                        ) {
-                            ExposedDropdownMenuDefaults.TrailingIcon(
-                                expanded = expanded
-                            )
-                        }
-                    },
-                    colors = ExposedDropdownMenuDefaults.textFieldColors()
-                )
-                ExposedDropdownMenu(
-                    modifier = Modifier
-                        .testTag(EDMTag)
-                        .onGloballyPositioned {
-                            onMenuBoundsChanged?.invoke(it.boundsInRoot())
-                        },
-                    expanded = expanded,
-                    onDismissRequest = { onExpandChange(false) }
-                ) {
-                    DropdownMenuItem(
-                        text = { Text(OptionName) },
-                        onClick = {
-                            selectedOptionText = OptionName
-                            onExpandChange(false)
-                        },
-                        modifier = Modifier.testTag(MenuItemTag)
-                    )
-                }
-            }
-        }
-    }
-}
-
-enum class SoftInputMode {
-    AdjustResize,
-    AdjustPan
-}
-
-@Suppress("DEPRECATION")
-@Composable
-fun SoftInputMode(mode: SoftInputMode) {
-    val context = LocalContext.current
-    DisposableEffect(mode) {
-        val activity = context.findActivityOrNull() ?: return@DisposableEffect onDispose {}
-        val originalMode = activity.window.attributes.softInputMode
-        activity.window.setSoftInputMode(when (mode) {
-            SoftInputMode.AdjustResize -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
-            SoftInputMode.AdjustPan -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
-        })
-        onDispose {
-            activity.window.setSoftInputMode(originalMode)
-        }
-    }
-}
-
-private tailrec fun Context.findActivityOrNull(): Activity? {
-    return (this as? Activity)
-        ?: (this as? ContextWrapper)?.baseContext?.findActivityOrNull()
-}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
deleted file mode 100644
index 9dfb619..0000000
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
+++ /dev/null
@@ -1,678 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3
-
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.PressInteraction
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material3.tokens.ExtendedFabPrimaryTokens
-import androidx.compose.material3.tokens.FabPrimaryLargeTokens
-import androidx.compose.material3.tokens.FabPrimarySmallTokens
-import androidx.compose.material3.tokens.FabPrimaryTokens
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.boundsInRoot
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.assert
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsEnabled
-import androidx.compose.ui.test.assertTouchHeightIsEqualTo
-import androidx.compose.ui.test.assertTouchWidthIsEqualTo
-import androidx.compose.ui.test.assertWidthIsAtLeast
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import kotlin.math.abs
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class FloatingActionButtonTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Test
-    fun fabDefaultSemantics() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box {
-                FloatingActionButton(modifier = Modifier.testTag("myButton"), onClick = {}) {
-                    Icon(Icons.Filled.Favorite, null)
-                }
-            }
-        }
-
-        rule.onNodeWithTag("myButton")
-            .assertIsEnabled()
-            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button))
-    }
-
-    @Test
-    fun extendedFabFindByTextAndClick() {
-        var counter = 0
-        val onClick: () -> Unit = { ++counter }
-        val text = "myButton"
-
-        rule.setMaterialContent(lightColorScheme()) {
-            Box {
-                ExtendedFloatingActionButton(onClick = onClick, content = { Text(text) })
-            }
-        }
-
-        rule.onNodeWithText(text)
-            .performClick()
-
-        rule.runOnIdle {
-            assertThat(counter).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun fabHasSizeFromSpec() {
-        rule
-            .setMaterialContentForSizeAssertions {
-                FloatingActionButton(onClick = {}) {
-                    Icon(
-                        Icons.Filled.Favorite,
-                        null,
-                        modifier = Modifier.testTag("icon"))
-                }
-            }
-            .assertIsSquareWithSize(FabPrimaryTokens.ContainerHeight)
-
-        rule
-            .onNodeWithTag("icon", useUnmergedTree = true)
-            .assertHeightIsEqualTo(FabPrimaryTokens.IconSize)
-            .assertWidthIsEqualTo(FabPrimaryTokens.IconSize)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun smallFabHasSizeFromSpec() {
-        rule
-            .setMaterialContentForSizeAssertions {
-                CompositionLocalProvider(
-                    LocalMinimumInteractiveComponentEnforcement provides false
-                ) {
-                    SmallFloatingActionButton(onClick = {}) {
-                        Icon(
-                            Icons.Filled.Favorite,
-                            null,
-                            modifier = Modifier.testTag("icon")
-                        )
-                    }
-                }
-            }
-            // Expecting the size to be equal to the token size.
-            .assertIsSquareWithSize(FabPrimarySmallTokens.ContainerHeight)
-
-        rule
-            .onNodeWithTag("icon", useUnmergedTree = true)
-            .assertHeightIsEqualTo(FabPrimarySmallTokens.IconSize)
-            .assertWidthIsEqualTo(FabPrimarySmallTokens.IconSize)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun smallFabHasMinTouchTarget() {
-        rule
-            .setMaterialContentForSizeAssertions {
-                CompositionLocalProvider(
-                    LocalMinimumInteractiveComponentEnforcement provides true
-                ) {
-                    SmallFloatingActionButton(onClick = {}) {
-                        Icon(Icons.Filled.Favorite, null)
-                    }
-                }
-            }
-            // Expecting the size to be equal to the minimum touch target.
-            .assertTouchWidthIsEqualTo(48.dp)
-            .assertTouchHeightIsEqualTo(48.dp)
-            .assertWidthIsEqualTo(48.dp)
-            .assertHeightIsEqualTo(48.dp)
-    }
-
-    @Test
-    fun largeFabHasSizeFromSpec() {
-        rule
-            .setMaterialContentForSizeAssertions {
-                LargeFloatingActionButton(onClick = {}) {
-                    Icon(
-                        Icons.Filled.Favorite,
-                        null,
-                        modifier = Modifier
-                            .size(FloatingActionButtonDefaults.LargeIconSize)
-                            .testTag("icon")
-                    )
-                }
-            }
-            .assertIsSquareWithSize(FabPrimaryLargeTokens.ContainerHeight)
-
-        rule
-            .onNodeWithTag("icon", useUnmergedTree = true)
-            .assertHeightIsEqualTo(FloatingActionButtonDefaults.LargeIconSize)
-            .assertWidthIsEqualTo(FloatingActionButtonDefaults.LargeIconSize)
-    }
-
-    @Test
-    fun extendedFabLongTextHasHeightFromSpec() {
-        rule.setMaterialContent(lightColorScheme()) {
-            ExtendedFloatingActionButton(
-                modifier = Modifier.testTag("FAB"),
-                text = { Text("Extended FAB Text") },
-                icon = { Icon(Icons.Filled.Favorite, null) },
-                onClick = {}
-            )
-        }
-
-        rule.onNodeWithTag("FAB")
-            .assertHeightIsEqualTo(ExtendedFabPrimaryTokens.ContainerHeight)
-            .assertWidthIsAtLeast(FabPrimaryTokens.ContainerHeight)
-    }
-
-    @Test
-    fun extendedFabShortTextHasMinimumSizeFromSpec() {
-        rule.setMaterialContent(lightColorScheme()) {
-            ExtendedFloatingActionButton(
-                modifier = Modifier.testTag("FAB"),
-                onClick = {},
-                content = { Text(".") },
-            )
-        }
-
-        rule.onNodeWithTag("FAB")
-            .assertHeightIsEqualTo(ExtendedFabPrimaryTokens.ContainerHeight)
-            .assertWidthIsEqualTo(80.dp)
-    }
-
-    @Test
-    fun fabHasCorrectTextStyle() {
-        var fontFamily: FontFamily? = null
-        var fontWeight: FontWeight? = null
-        var fontSize: TextUnit? = null
-        var lineHeight: TextUnit? = null
-        var letterSpacing: TextUnit? = null
-        var expectedTextStyle: TextStyle? = null
-
-        rule.setMaterialContent(lightColorScheme()) {
-            FloatingActionButton(onClick = {}) {
-                Icon(Icons.Filled.Favorite, null)
-                Text(
-                    "Normal FAB with Text",
-                    onTextLayout = {
-                        fontFamily = it.layoutInput.style.fontFamily
-                        fontWeight = it.layoutInput.style.fontWeight
-                        fontSize = it.layoutInput.style.fontSize
-                        lineHeight = it.layoutInput.style.lineHeight
-                        letterSpacing = it.layoutInput.style.letterSpacing
-                    }
-                )
-            }
-            expectedTextStyle = MaterialTheme.typography.fromToken(
-                ExtendedFabPrimaryTokens.LabelTextFont
-            )
-        }
-        rule.runOnIdle {
-            assertThat(fontFamily).isEqualTo(expectedTextStyle!!.fontFamily)
-            assertThat(fontWeight).isEqualTo(expectedTextStyle!!.fontWeight)
-            assertThat(fontSize).isEqualTo(expectedTextStyle!!.fontSize)
-            assertThat(lineHeight).isEqualTo(expectedTextStyle!!.lineHeight)
-            assertThat(letterSpacing).isEqualTo(expectedTextStyle!!.letterSpacing)
-        }
-    }
-
-    @Test
-    fun extendedFabHasCorrectTextStyle() {
-        var fontFamily: FontFamily? = null
-        var fontWeight: FontWeight? = null
-        var fontSize: TextUnit? = null
-        var lineHeight: TextUnit? = null
-        var letterSpacing: TextUnit? = null
-        var expectedTextStyle: TextStyle? = null
-
-        rule.setMaterialContent(lightColorScheme()) {
-            ExtendedFloatingActionButton(onClick = {}) {
-                Text(
-                    "Extended FAB",
-                    onTextLayout = {
-                        fontFamily = it.layoutInput.style.fontFamily
-                        fontWeight = it.layoutInput.style.fontWeight
-                        fontSize = it.layoutInput.style.fontSize
-                        lineHeight = it.layoutInput.style.lineHeight
-                        letterSpacing = it.layoutInput.style.letterSpacing
-                    }
-                )
-            }
-            expectedTextStyle = MaterialTheme.typography.fromToken(
-                ExtendedFabPrimaryTokens.LabelTextFont
-            )
-        }
-        rule.runOnIdle {
-            assertThat(fontFamily).isEqualTo(expectedTextStyle!!.fontFamily)
-            assertThat(fontWeight).isEqualTo(expectedTextStyle!!.fontWeight)
-            assertThat(fontSize).isEqualTo(expectedTextStyle!!.fontSize)
-            assertThat(lineHeight).isEqualTo(expectedTextStyle!!.lineHeight)
-            assertThat(letterSpacing).isEqualTo(expectedTextStyle!!.letterSpacing)
-        }
-    }
-
-    @Test
-    fun fabWeightModifier() {
-        var item1Bounds = Rect(0f, 0f, 0f, 0f)
-        var buttonBounds = Rect(0f, 0f, 0f, 0f)
-        rule.setMaterialContent(lightColorScheme()) {
-            Column {
-                Spacer(
-                    Modifier.requiredSize(10.dp).weight(1f).onGloballyPositioned {
-                        item1Bounds = it.boundsInRoot()
-                    }
-                )
-
-                FloatingActionButton(
-                    onClick = {},
-                    modifier = Modifier.weight(1f)
-                        .onGloballyPositioned {
-                            buttonBounds = it.boundsInRoot()
-                        }
-                ) {
-                    Text("Button")
-                }
-
-                Spacer(Modifier.requiredSize(10.dp).weight(1f))
-            }
-        }
-
-        assertThat(item1Bounds.top).isNotEqualTo(0f)
-        assertThat(buttonBounds.left).isEqualTo(0f)
-    }
-
-    @Test
-    fun contentIsWrappedAndCentered() {
-        var buttonCoordinates: LayoutCoordinates? = null
-        var contentCoordinates: LayoutCoordinates? = null
-        rule.setMaterialContent(lightColorScheme()) {
-            Box {
-                FloatingActionButton(
-                    {},
-                    Modifier.onGloballyPositioned {
-                        buttonCoordinates = it
-                    }
-                ) {
-                    Box(
-                        Modifier.size(2.dp)
-                            .onGloballyPositioned { contentCoordinates = it }
-                    )
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            val buttonBounds = buttonCoordinates!!.boundsInRoot()
-            val contentBounds = contentCoordinates!!.boundsInRoot()
-            assertThat(contentBounds.width).isLessThan(buttonBounds.width)
-            assertThat(contentBounds.height).isLessThan(buttonBounds.height)
-            with(rule.density) {
-                assertThat(contentBounds.width).isEqualTo(2.dp.roundToPx().toFloat())
-                assertThat(contentBounds.height).isEqualTo(2.dp.roundToPx().toFloat())
-            }
-            assertWithinOnePixel(buttonBounds.center, contentBounds.center)
-        }
-    }
-
-    @Test
-    fun extendedFabTextIsWrappedAndCentered() {
-        var buttonCoordinates: LayoutCoordinates? = null
-        var contentCoordinates: LayoutCoordinates? = null
-        rule.setMaterialContent(lightColorScheme()) {
-            Box {
-                ExtendedFloatingActionButton(
-                    onClick = {},
-                    modifier = Modifier.onGloballyPositioned { buttonCoordinates = it },
-                ) {
-                    Box(
-                        Modifier.size(2.dp)
-                            .onGloballyPositioned { contentCoordinates = it }
-                    )
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            val buttonBounds = buttonCoordinates!!.boundsInRoot()
-            val contentBounds = contentCoordinates!!.boundsInRoot()
-            assertThat(contentBounds.width).isLessThan(buttonBounds.width)
-            assertThat(contentBounds.height).isLessThan(buttonBounds.height)
-            with(rule.density) {
-                assertThat(contentBounds.width).isEqualTo(2.dp.roundToPx().toFloat())
-                assertThat(contentBounds.height).isEqualTo(2.dp.roundToPx().toFloat())
-            }
-            assertWithinOnePixel(buttonBounds.center, contentBounds.center)
-        }
-    }
-
-    @Test
-    fun extendedFabTextAndIconArePositionedCorrectly() {
-        var buttonCoordinates: LayoutCoordinates? = null
-        var textCoordinates: LayoutCoordinates? = null
-        var iconCoordinates: LayoutCoordinates? = null
-        rule.setMaterialContent(lightColorScheme()) {
-            Box {
-                ExtendedFloatingActionButton(
-                    text = {
-                        Box(
-                            Modifier.size(2.dp)
-                                .onGloballyPositioned { textCoordinates = it }
-                        )
-                    },
-                    icon = {
-                        Box(
-                            Modifier.size(10.dp)
-                                .onGloballyPositioned { iconCoordinates = it }
-                        )
-                    },
-                    onClick = {},
-                    modifier = Modifier.onGloballyPositioned { buttonCoordinates = it }
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            val buttonBounds = buttonCoordinates!!.boundsInRoot()
-            val textBounds = textCoordinates!!.boundsInRoot()
-            val iconBounds = iconCoordinates!!.boundsInRoot()
-            with(rule.density) {
-                assertThat(textBounds.width).isEqualTo(2.dp.roundToPx().toFloat())
-                assertThat(textBounds.height).isEqualTo(2.dp.roundToPx().toFloat())
-                assertThat(iconBounds.width).isEqualTo(10.dp.roundToPx().toFloat())
-                assertThat(iconBounds.height).isEqualTo(10.dp.roundToPx().toFloat())
-
-                assertWithinOnePixel(buttonBounds.center.y, iconBounds.center.y)
-                assertWithinOnePixel(buttonBounds.center.y, textBounds.center.y)
-
-                // Assert expanded fab icon has 16.dp of padding.
-                assertThat(iconBounds.left - buttonBounds.left)
-                    .isEqualTo(16.dp.roundToPx().toFloat())
-
-                val halfPadding = 6.dp.roundToPx().toFloat()
-                assertWithinOnePixel(
-                    iconBounds.center.x + iconBounds.width / 2 + halfPadding,
-                    textBounds.center.x - textBounds.width / 2 - halfPadding
-                )
-                // Assert that text and icon have 12.dp padding between them.
-                assertThat(textBounds.left - iconBounds.right)
-                    .isEqualTo(12.dp.roundToPx().toFloat())
-            }
-        }
-    }
-
-    @Test
-    fun expandedExtendedFabTextAndIconHaveSizeFromSpecAndVisible() {
-        rule.setMaterialContent(lightColorScheme()) {
-            ExtendedFloatingActionButton(
-                expanded = true,
-                onClick = { },
-                icon = {
-                    Icon(
-                        Icons.Filled.Favorite,
-                        "Add",
-                        modifier = Modifier.testTag("icon"),
-                    )
-                },
-                text = { Text(text = "FAB", modifier = Modifier.testTag("text")) },
-                modifier = Modifier.testTag("FAB"),
-            )
-        }
-
-        rule
-            .onNodeWithTag("icon", useUnmergedTree = true)
-            .assertHeightIsEqualTo(ExtendedFabPrimaryTokens.IconSize)
-            .assertWidthIsEqualTo(ExtendedFabPrimaryTokens.IconSize)
-
-        rule.onNodeWithTag("FAB")
-            .assertHeightIsEqualTo(ExtendedFabPrimaryTokens.ContainerHeight)
-            .assertWidthIsAtLeast(80.dp)
-
-        rule.onNodeWithTag("text", useUnmergedTree = true).assertIsDisplayed()
-        rule.onNodeWithTag("icon", useUnmergedTree = true).assertIsDisplayed()
-    }
-
-    @Test
-    fun collapsedExtendedFabTextAndIconHaveSizeFromSpecAndTextNotVisible() {
-        rule.setMaterialContent(lightColorScheme()) {
-            ExtendedFloatingActionButton(
-                expanded = false,
-                onClick = { },
-                icon = {
-                    Icon(
-                        Icons.Filled.Favorite,
-                        "Add",
-                        modifier = Modifier.testTag("icon")
-                    )
-                },
-                text = { Text(text = "FAB", modifier = Modifier.testTag("text")) },
-                modifier = Modifier.testTag("FAB"),
-            )
-        }
-
-        rule.onNodeWithTag("FAB")
-            .assertIsSquareWithSize(FabPrimaryTokens.ContainerHeight)
-
-        rule
-            .onNodeWithTag("icon", useUnmergedTree = true)
-            .assertHeightIsEqualTo(ExtendedFabPrimaryTokens.IconSize)
-            .assertWidthIsEqualTo(ExtendedFabPrimaryTokens.IconSize)
-
-        rule.onNodeWithTag("text", useUnmergedTree = true).assertDoesNotExist()
-        rule.onNodeWithTag("icon", useUnmergedTree = true).assertIsDisplayed()
-    }
-
-    @Test
-    fun extendedFabAnimates() {
-        rule.mainClock.autoAdvance = false
-
-        var expanded by mutableStateOf(true)
-        rule.setMaterialContent(lightColorScheme()) {
-            ExtendedFloatingActionButton(
-                expanded = expanded,
-                onClick = {},
-                icon = {
-                    Icon(
-                        Icons.Filled.Favorite,
-                        "Add",
-                        modifier = Modifier.testTag("icon")
-                    )
-                },
-                text = { Text(text = "FAB", modifier = Modifier.testTag("text")) },
-                modifier = Modifier.testTag("FAB"),
-            )
-        }
-
-        rule.onNodeWithTag("FAB")
-            .assertHeightIsEqualTo(ExtendedFabPrimaryTokens.ContainerHeight)
-            .assertWidthIsAtLeast(80.dp)
-
-        rule.runOnIdle { expanded = false }
-        rule.mainClock.advanceTimeBy(200)
-
-        rule.onNodeWithTag("FAB")
-            .assertIsSquareWithSize(FabPrimaryTokens.ContainerHeight)
-            .assertHeightIsEqualTo(FabPrimaryTokens.ContainerHeight)
-            .assertWidthIsEqualTo(FabPrimaryTokens.ContainerWidth)
-    }
-
-    @Test
-    fun floatingActionButtonElevation_newInteraction() {
-        val interactionSource = MutableInteractionSource()
-        val defaultElevation = 1.dp
-        val pressedElevation = 2.dp
-        val hoveredElevation = 3.dp
-        val focusedElevation = 4.dp
-        lateinit var tonalElevation: State<Dp>
-        lateinit var shadowElevation: State<Dp>
-
-        rule.setMaterialContent(lightColorScheme()) {
-            val fabElevation = FloatingActionButtonDefaults.elevation(
-                defaultElevation = defaultElevation,
-                pressedElevation = pressedElevation,
-                hoveredElevation = hoveredElevation,
-                focusedElevation = focusedElevation
-            )
-
-            tonalElevation = fabElevation.tonalElevation(interactionSource)
-            shadowElevation = fabElevation.shadowElevation(interactionSource)
-        }
-
-        rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(defaultElevation)
-            assertThat(shadowElevation.value).isEqualTo(defaultElevation)
-        }
-
-        rule.runOnIdle {
-            interactionSource.tryEmit(PressInteraction.Press(Offset.Zero))
-        }
-
-        rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(pressedElevation)
-            assertThat(shadowElevation.value).isEqualTo(pressedElevation)
-        }
-    }
-
-    @Test
-    fun floatingActionButtonElevation_newValue() {
-        val interactionSource = MutableInteractionSource()
-        var defaultElevation by mutableStateOf(1.dp)
-        val pressedElevation = 2.dp
-        val hoveredElevation = 3.dp
-        val focusedElevation = 4.dp
-        lateinit var tonalElevation: State<Dp>
-        lateinit var shadowElevation: State<Dp>
-
-        rule.setMaterialContent(lightColorScheme()) {
-            val fabElevation = FloatingActionButtonDefaults.elevation(
-                defaultElevation = defaultElevation,
-                pressedElevation = pressedElevation,
-                hoveredElevation = hoveredElevation,
-                focusedElevation = focusedElevation
-            )
-
-            tonalElevation = fabElevation.tonalElevation(interactionSource)
-            shadowElevation = fabElevation.shadowElevation(interactionSource)
-        }
-
-        rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(defaultElevation)
-            assertThat(shadowElevation.value).isEqualTo(defaultElevation)
-        }
-
-        rule.runOnIdle {
-            defaultElevation = 5.dp
-        }
-
-        rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(5.dp)
-            assertThat(shadowElevation.value).isEqualTo(5.dp)
-        }
-    }
-
-    @Test
-    fun floatingActionButtonElevation_newValueDuringInteraction() {
-        val interactionSource = MutableInteractionSource()
-        val defaultElevation = 1.dp
-        var pressedElevation by mutableStateOf(2.dp)
-        val hoveredElevation = 3.dp
-        val focusedElevation = 4.dp
-        lateinit var tonalElevation: State<Dp>
-        lateinit var shadowElevation: State<Dp>
-
-        rule.setMaterialContent(lightColorScheme()) {
-            val fabElevation = FloatingActionButtonDefaults.elevation(
-                defaultElevation = defaultElevation,
-                pressedElevation = pressedElevation,
-                hoveredElevation = hoveredElevation,
-                focusedElevation = focusedElevation
-            )
-
-            tonalElevation = fabElevation.tonalElevation(interactionSource)
-            shadowElevation = fabElevation.shadowElevation(interactionSource)
-        }
-
-        rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(defaultElevation)
-            assertThat(shadowElevation.value).isEqualTo(defaultElevation)
-        }
-
-        rule.runOnIdle {
-            interactionSource.tryEmit(PressInteraction.Press(Offset.Zero))
-        }
-
-        rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(pressedElevation)
-            assertThat(shadowElevation.value).isEqualTo(pressedElevation)
-        }
-
-        rule.runOnIdle {
-            pressedElevation = 5.dp
-        }
-
-        // We are still pressed, so we should now show the updated value for the pressed state
-        rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(5.dp)
-            assertThat(shadowElevation.value).isEqualTo(5.dp)
-        }
-    }
-}
-
-fun assertWithinOnePixel(expected: Offset, actual: Offset) {
-    assertWithinOnePixel(expected.x, actual.x)
-    assertWithinOnePixel(expected.y, actual.y)
-}
-
-fun assertWithinOnePixel(expected: Float, actual: Float) {
-    val diff = abs(expected - actual)
-    assertThat(diff).isLessThan(1.1f)
-}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
deleted file mode 100644
index 85a2ddc..0000000
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
+++ /dev/null
@@ -1,1182 +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.compose.material3
-
-import android.content.ComponentCallbacks2
-import android.content.pm.ActivityInfo
-import android.content.res.Configuration
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
-import androidx.compose.foundation.ScrollState
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredHeight
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsActions
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.assert
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.isPopup
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onFirst
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onParent
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performSemanticsAction
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.swipeDown
-import androidx.compose.ui.test.swipeUp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.coerceAtMost
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.height
-import androidx.compose.ui.unit.width
-import androidx.test.filters.MediumTest
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.uiautomator.UiDevice
-import com.google.common.truth.Truth.assertThat
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import junit.framework.TestCase.assertFalse
-import junit.framework.TestCase.assertTrue
-import junit.framework.TestCase.fail
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-@OptIn(ExperimentalMaterial3Api::class)
-class ModalBottomSheetTest(private val edgeToEdgeWrapper: EdgeToEdgeWrapper) {
-
-    @get:Rule
-    val rule = createAndroidComposeRule<ComponentActivity>()
-
-    private val sheetHeight = 256.dp
-    private val dragHandleSize = 44.dp
-
-    private val sheetTag = "sheetContentTag"
-    private val dragHandleTag = "dragHandleTag"
-    private val BackTestTag = "Back"
-
-    @Test
-    fun modalBottomSheet_isDismissedOnTapOutside() {
-        var showBottomSheet by mutableStateOf(true)
-        val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
-
-        rule.setContent {
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            if (showBottomSheet) {
-                ModalBottomSheet(
-                    sheetState = sheetState,
-                    onDismissRequest = { showBottomSheet = false },
-                    windowInsets = windowInsets
-                ) {
-                    Box(
-                        Modifier
-                            .size(sheetHeight)
-                            .testTag(sheetTag)
-                    )
-                }
-            }
-        }
-
-        assertThat(sheetState.isVisible).isTrue()
-
-        // Tap Scrim
-        val outsideY = with(rule.density) {
-            rule.onAllNodes(isPopup()).onFirst().getUnclippedBoundsInRoot().height.roundToPx() / 4
-        }
-        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).click(0, outsideY)
-        rule.waitForIdle()
-
-        // Bottom sheet should not exist
-        rule.onNodeWithTag(sheetTag).assertDoesNotExist()
-    }
-
-    @Test
-    fun modalBottomSheet_fillsScreenWidth() {
-        var boxWidth = 0
-        var screenWidth by mutableStateOf(0)
-
-        rule.setContent {
-            val context = LocalContext.current
-            val density = LocalDensity.current
-            val resScreenWidth = context.resources.configuration.screenWidthDp
-            with(density) { screenWidth = resScreenWidth.dp.roundToPx() }
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = {},
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxWidth()
-                        .height(sheetHeight)
-                        .onSizeChanged { boxWidth = it.width }
-                )
-            }
-        }
-        assertThat(boxWidth).isEqualTo(screenWidth)
-    }
-
-    @Test
-    fun modalBottomSheet_wideScreen_sheetRespectsMaxWidthAndIsCentered() {
-        rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
-        val latch = CountDownLatch(1)
-
-        rule.activity.application.registerComponentCallbacks(object : ComponentCallbacks2 {
-            override fun onConfigurationChanged(p0: Configuration) {
-                latch.countDown()
-            }
-
-            override fun onLowMemory() {
-                // NO-OP
-            }
-
-            override fun onTrimMemory(p0: Int) {
-                // NO-OP
-            }
-        })
-
-        try {
-            latch.await(1500, TimeUnit.MILLISECONDS)
-            rule.setContent {
-                val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                    WindowInsets(0) else BottomSheetDefaults.windowInsets
-                ModalBottomSheet(
-                    onDismissRequest = {},
-                    windowInsets = windowInsets
-                ) {
-                    Box(
-                        Modifier
-                            .testTag(sheetTag)
-                            .fillMaxHeight(0.4f)
-                    )
-                }
-            }
-
-            val simulatedRootWidth = rule.onNode(isPopup()).getUnclippedBoundsInRoot().width
-            val maxSheetWidth = 640.dp
-            val expectedSheetWidth = maxSheetWidth.coerceAtMost(simulatedRootWidth)
-            // Our sheet should be max 640 dp but fill the width if the container is less wide
-            val expectedSheetLeft = if (simulatedRootWidth <= expectedSheetWidth) {
-                0.dp
-            } else {
-                (simulatedRootWidth - expectedSheetWidth) / 2
-            }
-
-            rule.onNodeWithTag(sheetTag)
-                .onParent()
-                .assertLeftPositionInRootIsEqualTo(
-                    expectedLeft = expectedSheetLeft
-                )
-                .assertWidthIsEqualTo(expectedSheetWidth)
-        } catch (e: InterruptedException) {
-            fail("Unable to verify sheet width in landscape orientation")
-        } finally {
-            rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
-        }
-    }
-
-    @Test
-    fun modalBottomSheet_defaultStateForSmallContentIsFullExpanded() {
-        lateinit var sheetState: SheetState
-        var height by mutableStateOf(0.dp)
-
-        rule.setContent {
-            val config = LocalContext.current.resources.configuration
-            height = config.screenHeightDp.dp
-            sheetState = rememberModalBottomSheetState()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState,
-                dragHandle = null,
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxWidth()
-                        .testTag(sheetTag)
-                        .height(sheetHeight)
-                )
-            }
-        }
-
-        height = rule.onNode(isPopup()).getUnclippedBoundsInRoot().height
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
-        rule.onNodeWithTag(sheetTag).assertTopPositionInRootIsEqualTo(height - sheetHeight)
-    }
-
-    @Test
-    fun modalBottomSheet_defaultStateForLargeContentIsHalfExpanded() {
-        lateinit var sheetState: SheetState
-        var screenHeightPx by mutableStateOf(0f)
-
-        rule.setContent {
-            sheetState = rememberModalBottomSheetState()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState,
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        // Deliberately use fraction != 1f
-                        .fillMaxSize(0.6f)
-                        .testTag(sheetTag)
-                )
-            }
-        }
-
-        screenHeightPx = with(rule.density) {
-            rule.onNode(isPopup()).getUnclippedBoundsInRoot().height.toPx()
-        }
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
-        assertThat(sheetState.requireOffset())
-            .isWithin(1f)
-            .of(screenHeightPx / 2f)
-    }
-
-    @Test
-    fun modalBottomSheet_shortSheet_isDismissedOnBackPress() {
-        var showBottomSheet by mutableStateOf(true)
-        val sheetState = SheetState(skipPartiallyExpanded = true, density = rule.density)
-
-        rule.setContent {
-            val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-            if (showBottomSheet) {
-                ModalBottomSheet(
-                    sheetState = sheetState,
-                    onDismissRequest = { showBottomSheet = false },
-                    windowInsets = windowInsets
-                ) {
-                    Box(
-                        Modifier
-                            .fillMaxHeight(0.4f)
-                            .testTag(sheetTag)
-                    ) {
-                        Button(
-                            onClick = { dispatcher.onBackPressed() },
-                            modifier = Modifier.testTag(BackTestTag),
-                            content = { Text("Content") },
-                        )
-                    }
-                }
-            }
-        }
-
-        assertThat(sheetState.isVisible).isTrue()
-
-        rule.onNodeWithTag(BackTestTag).performClick()
-
-        rule.onNodeWithTag(BackTestTag).assertDoesNotExist()
-
-        // Popup should not exist
-        rule.onNodeWithTag(sheetTag).assertDoesNotExist()
-    }
-
-    @Test
-    fun modalBottomSheet_tallSheet_isDismissedOnBackPress() {
-        var showBottomSheet by mutableStateOf(true)
-        val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
-
-        rule.setContent {
-            val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-            if (showBottomSheet) {
-                ModalBottomSheet(
-                    sheetState = sheetState,
-                    onDismissRequest = { showBottomSheet = false },
-                    windowInsets = windowInsets
-                ) {
-                    Box(
-                        Modifier
-                            .fillMaxHeight(0.6f)
-                            .testTag(sheetTag)
-                    ) {
-                        Button(
-                            onClick = { dispatcher.onBackPressed() },
-                            modifier = Modifier.testTag(BackTestTag),
-                            content = { Text("Content") },
-                        )
-                    }
-                }
-            }
-        }
-        assertThat(sheetState.isVisible).isTrue()
-
-        rule.onNodeWithTag(BackTestTag).performClick()
-        rule.onNodeWithTag(BackTestTag).assertDoesNotExist()
-
-        // Popup should not exist
-        rule.onNodeWithTag(sheetTag).assertDoesNotExist()
-    }
-
-    @Test
-    fun modalBottomSheet_shortSheet_sizeChanges_snapsToNewTarget() {
-        lateinit var state: SheetState
-        var size by mutableStateOf(56.dp)
-        var screenHeight by mutableStateOf(0.dp)
-        val expectedExpandedAnchor by derivedStateOf {
-            with(rule.density) {
-                (screenHeight - size).toPx()
-            }
-        }
-
-        rule.setContent {
-            val context = LocalContext.current
-            screenHeight = context.resources.configuration.screenHeightDp.dp
-            state = rememberModalBottomSheetState()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = state,
-                dragHandle = null,
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .height(size)
-                        .fillMaxWidth()
-                )
-            }
-        }
-        screenHeight = rule.onNode(isPopup()).getUnclippedBoundsInRoot().height
-        assertThat(state.requireOffset()).isWithin(1f).of(expectedExpandedAnchor)
-
-        size = 100.dp
-        rule.waitForIdle()
-        assertThat(state.requireOffset()).isWithin(1f).of(expectedExpandedAnchor)
-
-        size = 30.dp
-        rule.waitForIdle()
-        assertThat(state.requireOffset()).isWithin(1f).of(expectedExpandedAnchor)
-    }
-
-    @Test
-    fun modalBottomSheet_emptySheet_expandDoesNotAnimate() {
-        lateinit var state: SheetState
-        lateinit var scope: CoroutineScope
-        rule.setContent {
-            state = rememberModalBottomSheetState()
-            scope = rememberCoroutineScope()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = state,
-                dragHandle = null,
-                windowInsets = windowInsets
-            ) {}
-        }
-        assertThat(state.anchoredDraggableState.currentValue).isEqualTo(SheetValue.Hidden)
-        val hiddenOffset = state.requireOffset()
-        scope.launch { state.show() }
-        rule.waitForIdle()
-
-        assertThat(state.anchoredDraggableState.currentValue).isEqualTo(SheetValue.Expanded)
-        val expandedOffset = state.requireOffset()
-
-        assertThat(hiddenOffset).isEqualTo(expandedOffset)
-    }
-
-    @Test
-    fun modalBottomSheet_anchorsChange_retainsCurrentValue() {
-        lateinit var state: SheetState
-        var amountOfItems by mutableStateOf(0)
-        lateinit var scope: CoroutineScope
-        rule.setContent {
-            state = rememberModalBottomSheetState()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = state,
-                dragHandle = null,
-                windowInsets = windowInsets
-            ) {
-                scope = rememberCoroutineScope()
-                LazyColumn {
-                    items(amountOfItems) {
-                        ListItem(headlineContent = { Text("$it") })
-                    }
-                }
-            }
-        }
-
-        assertThat(state.currentValue).isEqualTo(SheetValue.Hidden)
-
-        amountOfItems = 50
-        rule.waitForIdle()
-        scope.launch {
-            state.show()
-        }
-        // The anchors should now be {Hidden, PartiallyExpanded, Expanded}
-
-        rule.waitForIdle()
-        assertThat(state.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
-
-        amountOfItems = 100 // The anchors should now be {Hidden, PartiallyExpanded, Expanded}
-
-        rule.waitForIdle()
-        assertThat(state.currentValue).isEqualTo(SheetValue.PartiallyExpanded) // We should
-        // retain the current value if possible
-        assertTrue(state.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Hidden))
-        assertTrue(state.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.PartiallyExpanded))
-        assertTrue(state.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Expanded))
-
-        amountOfItems = 0 // When the sheet height is 0, we should only have a hidden anchor
-        rule.waitForIdle()
-        assertThat(state.currentValue).isEqualTo(SheetValue.Hidden)
-        assertTrue(state.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Hidden))
-        assertFalse(state.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.PartiallyExpanded))
-        assertFalse(state.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Expanded))
-    }
-
-    @Test
-    fun modalBottomSheet_nestedScroll_consumesWithinBounds_scrollsOutsideBounds() {
-        lateinit var sheetState: SheetState
-        lateinit var scrollState: ScrollState
-        rule.setContent {
-            sheetState = rememberModalBottomSheetState()
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState
-            ) {
-                scrollState = rememberScrollState()
-                Column(
-                    Modifier
-                        .verticalScroll(scrollState)
-                        .testTag(sheetTag)
-                ) {
-                    repeat(100) {
-                        Text(it.toString(), Modifier.requiredHeight(50.dp))
-                    }
-                }
-            }
-        }
-
-        rule.waitForIdle()
-
-        assertThat(scrollState.value).isEqualTo(0)
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
-
-        rule.onNodeWithTag(sheetTag)
-            .performTouchInput {
-                swipeUp(startY = bottom, endY = bottom / 2)
-            }
-        rule.waitForIdle()
-        assertThat(scrollState.value).isEqualTo(0)
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
-
-        rule.onNodeWithTag(sheetTag)
-            .performTouchInput {
-                swipeUp(startY = bottom, endY = top)
-            }
-        rule.waitForIdle()
-        assertThat(scrollState.value).isGreaterThan(0)
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
-
-        rule.onNodeWithTag(sheetTag)
-            .performTouchInput {
-                swipeDown(startY = top, endY = bottom)
-            }
-        rule.waitForIdle()
-        assertThat(scrollState.value).isEqualTo(0)
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
-
-        rule.onNodeWithTag(sheetTag)
-            .performTouchInput {
-                swipeDown(startY = top, endY = bottom / 2)
-            }
-        rule.waitForIdle()
-        assertThat(scrollState.value).isEqualTo(0)
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
-
-        rule.onNodeWithTag(sheetTag)
-            .performTouchInput {
-                swipeDown(startY = bottom / 2, endY = bottom)
-            }
-        rule.waitForIdle()
-        assertThat(scrollState.value).isEqualTo(0)
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
-    }
-
-    @Test
-    fun modalBottomSheet_missingAnchors_findsClosest() {
-        val topTag = "ModalBottomSheetLayout"
-        var showShortContent by mutableStateOf(false)
-        val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
-        lateinit var scope: CoroutineScope
-
-        rule.setContent {
-            scope = rememberCoroutineScope()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = {},
-                modifier = Modifier.testTag(topTag),
-                sheetState = sheetState,
-                windowInsets = windowInsets
-            ) {
-                if (showShortContent) {
-                    Box(
-                        Modifier
-                            .fillMaxWidth()
-                            .height(100.dp)
-                    )
-                } else {
-                    Box(
-                        Modifier
-                            .fillMaxSize()
-                            .testTag(sheetTag)
-                    )
-                }
-            }
-        }
-
-        rule.onNodeWithTag(topTag).performTouchInput {
-            swipeDown()
-            swipeDown()
-        }
-
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
-        }
-
-        showShortContent = true
-        scope.launch { sheetState.show() } // We can't use LaunchedEffect with Swipeable in tests
-        // yet, so we're invoking this outside of composition. See b/254115946.
-
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
-        }
-    }
-
-    @Test
-    fun modalBottomSheet_expandBySwiping() {
-        lateinit var sheetState: SheetState
-        rule.setContent {
-            sheetState = rememberModalBottomSheetState()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState,
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .testTag(sheetTag)
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
-        }
-
-        rule.onNodeWithTag(sheetTag)
-            .performTouchInput { swipeUp() }
-
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
-        }
-    }
-
-    @Test
-    fun modalBottomSheet_respectsConfirmValueChange() {
-        lateinit var sheetState: SheetState
-        rule.setContent {
-            sheetState = rememberModalBottomSheetState(
-                confirmValueChange = { newState ->
-                    newState != SheetValue.Hidden
-                }
-            )
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState,
-                dragHandle = {
-                    Box(
-                        Modifier
-                            .testTag(dragHandleTag)
-                            .size(dragHandleSize)
-                    )
-                },
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .testTag(sheetTag)
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
-        }
-
-        rule.onNodeWithTag(sheetTag)
-            .performTouchInput { swipeDown() }
-
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
-        }
-
-        rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).onParent()
-            .performSemanticsAction(SemanticsActions.Dismiss)
-
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
-        }
-
-        // Tap Scrim
-        val outsideY = with(rule.density) {
-            rule.onAllNodes(isPopup()).onFirst().getUnclippedBoundsInRoot().height.roundToPx() / 4
-        }
-        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).click(0, outsideY)
-        rule.waitForIdle()
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
-        }
-    }
-
-    @Test
-    fun modalBottomSheet_hideBySwiping_tallBottomSheet() {
-        lateinit var sheetState: SheetState
-        lateinit var scope: CoroutineScope
-        rule.setContent {
-            sheetState = rememberModalBottomSheetState()
-            scope = rememberCoroutineScope()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState,
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .testTag(sheetTag)
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
-        }
-
-        scope.launch { sheetState.expand() }
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
-        }
-
-        rule.onNodeWithTag(sheetTag)
-            .performTouchInput { swipeDown() }
-
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
-        }
-    }
-
-    @Test
-    fun modalBottomSheet_hideBySwiping_skipPartiallyExpanded() {
-        lateinit var sheetState: SheetState
-        rule.setContent {
-            sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState,
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxWidth()
-                        .height(sheetHeight)
-                        .testTag(sheetTag)
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
-        }
-
-        rule.onNodeWithTag(sheetTag)
-            .performTouchInput { swipeDown() }
-
-        rule.runOnIdle {
-            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
-        }
-    }
-
-    @Test
-    fun modalBottomSheet_hideManually_skipPartiallyExpanded(): Unit = runBlocking(
-        AutoTestFrameClock()
-    ) {
-        lateinit var sheetState: SheetState
-        rule.setContent {
-            sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState,
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .testTag(sheetTag)
-                )
-            }
-        }
-        assertThat(sheetState.currentValue == SheetValue.Expanded)
-
-        sheetState.hide()
-
-        assertThat(sheetState.currentValue == SheetValue.Hidden)
-    }
-
-    @Test
-    fun modalBottomSheet_testParialExpandReturnsIllegalStateException_whenSkipPartialExpanded() {
-        lateinit var scope: CoroutineScope
-        val bottomSheetState = SheetState(
-            skipPartiallyExpanded = true,
-            density = rule.density
-        )
-        rule.setContent {
-            scope = rememberCoroutineScope()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = bottomSheetState,
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .testTag(sheetTag)
-                )
-            }
-        }
-        scope.launch {
-            val exception =
-                kotlin.runCatching { bottomSheetState.partialExpand() }.exceptionOrNull()
-            assertThat(exception).isNotNull()
-            assertThat(exception).isInstanceOf(IllegalStateException::class.java)
-            assertThat(exception).hasMessageThat().containsMatch(
-                "Attempted to animate to partial expanded when skipPartiallyExpanded was " +
-                    "enabled. Set skipPartiallyExpanded to false to use this function."
-            )
-        }
-    }
-
-    @Test
-    fun modalBottomSheet_testDismissAction_tallBottomSheet_whenPartiallyExpanded() {
-        rule.setContent {
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-            ModalBottomSheet(
-                onDismissRequest = {},
-                dragHandle = {
-                    Box(
-                        Modifier
-                            .testTag(dragHandleTag)
-                            .size(dragHandleSize)
-                    )
-                },
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .testTag(sheetTag)
-                )
-            }
-        }
-
-        rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).onParent()
-            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Collapse))
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Expand))
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
-            .performSemanticsAction(SemanticsActions.Dismiss)
-    }
-
-    @Test
-    fun modalBottomSheet_testExpandAction_tallBottomSheet_whenHalfExpanded() {
-        lateinit var sheetState: SheetState
-        rule.setContent {
-            sheetState = rememberModalBottomSheetState()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState,
-                dragHandle = {
-                    Box(
-                        Modifier
-                            .testTag(dragHandleTag)
-                            .size(dragHandleSize)
-                    )
-                },
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .testTag(sheetTag)
-                )
-            }
-        }
-
-        rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).onParent()
-            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Collapse))
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Expand))
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
-            .performSemanticsAction(SemanticsActions.Expand)
-
-        rule.runOnIdle {
-            assertThat(sheetState.requireOffset()).isEqualTo(0f)
-        }
-    }
-
-    @Test
-    fun modalBottomSheet_testDismissAction_tallBottomSheet_whenExpanded() {
-        lateinit var sheetState: SheetState
-        lateinit var scope: CoroutineScope
-
-        var screenHeightPx by mutableStateOf(0f)
-
-        rule.setContent {
-            sheetState = rememberModalBottomSheetState()
-            scope = rememberCoroutineScope()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState,
-                dragHandle = {
-                    Box(
-                        Modifier
-                            .testTag(dragHandleTag)
-                            .size(dragHandleSize)
-                    )
-                },
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .testTag(sheetTag)
-                )
-            }
-        }
-        screenHeightPx = with(rule.density) {
-            rule.onNode(isPopup()).getUnclippedBoundsInRoot().height.toPx()
-        }
-        scope.launch {
-            sheetState.expand()
-        }
-        rule.waitForIdle()
-
-        rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).onParent()
-            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Expand))
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Collapse))
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
-            .performSemanticsAction(SemanticsActions.Dismiss)
-
-        rule.runOnIdle {
-            assertThat(sheetState.requireOffset()).isWithin(1f).of(screenHeightPx)
-        }
-    }
-
-    @Test
-    fun modalBottomSheet_testCollapseAction_tallBottomSheet_whenExpanded() {
-        lateinit var sheetState: SheetState
-        lateinit var scope: CoroutineScope
-
-        var screenHeightPx by mutableStateOf(0f)
-
-        rule.setContent {
-            sheetState = rememberModalBottomSheetState()
-            scope = rememberCoroutineScope()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState,
-                dragHandle = {
-                    Box(
-                        Modifier
-                            .testTag(dragHandleTag)
-                            .size(dragHandleSize)
-                    )
-                },
-                windowInsets = windowInsets
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .testTag(sheetTag)
-                )
-            }
-        }
-        screenHeightPx = with(rule.density) {
-            rule.onNode(isPopup()).getUnclippedBoundsInRoot().height.toPx()
-        }
-        scope.launch {
-            sheetState.expand()
-        }
-        rule.waitForIdle()
-
-        rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).onParent()
-            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Expand))
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Collapse))
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
-            .performSemanticsAction(SemanticsActions.Collapse)
-
-        rule.runOnIdle {
-            assertThat(sheetState.requireOffset()).isWithin(1f).of(screenHeightPx / 2)
-        }
-    }
-
-    @Test
-    fun modalBottomSheet_shortSheet_anchorChangeHandler_previousTargetNotInAnchors_reconciles() {
-        val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
-        var hasSheetContent by mutableStateOf(false) // Start out with empty sheet content
-        lateinit var scope: CoroutineScope
-        rule.setContent {
-            scope = rememberCoroutineScope()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState,
-                dragHandle = null,
-                windowInsets = windowInsets
-            ) {
-                if (hasSheetContent) {
-                    Box(Modifier.fillMaxHeight(0.4f))
-                }
-            }
-        }
-
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
-        assertFalse(
-            sheetState.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.PartiallyExpanded)
-        )
-        assertFalse(sheetState.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Expanded))
-
-        scope.launch { sheetState.show() }
-        rule.waitForIdle()
-
-        assertThat(sheetState.isVisible).isTrue()
-        assertThat(sheetState.currentValue).isEqualTo(sheetState.targetValue)
-
-        hasSheetContent = true // Recompose with sheet content
-        rule.waitForIdle()
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
-    }
-
-    @Test
-    fun modalBottomSheet_tallSheet_anchorChangeHandler_previousTargetNotInAnchors_reconciles() {
-        val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
-        var hasSheetContent by mutableStateOf(false) // Start out with empty sheet content
-        lateinit var scope: CoroutineScope
-        rule.setContent {
-            scope = rememberCoroutineScope()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = {},
-                sheetState = sheetState,
-                dragHandle = null,
-                windowInsets = windowInsets
-            ) {
-                if (hasSheetContent) {
-                    Box(Modifier.fillMaxHeight(0.6f))
-                }
-            }
-        }
-
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
-        assertFalse(
-            sheetState.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.PartiallyExpanded)
-        )
-        assertFalse(sheetState.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Expanded))
-
-        scope.launch { sheetState.show() }
-        rule.waitForIdle()
-
-        assertThat(sheetState.isVisible).isTrue()
-        assertThat(sheetState.currentValue).isEqualTo(sheetState.targetValue)
-
-        hasSheetContent = true // Recompose with sheet content
-        rule.waitForIdle()
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
-    }
-
-    @Test
-    fun modalBottomSheet_callsOnDismissRequest_onNestedScrollFling() {
-        var callCount by mutableStateOf(0)
-        val expectedCallCount = 1
-        val sheetState = SheetState(skipPartiallyExpanded = true, density = rule.density)
-
-        val nestedScrollDispatcher = NestedScrollDispatcher()
-        val nestedScrollConnection = object : NestedScrollConnection {
-            // No-Op
-        }
-        lateinit var scope: CoroutineScope
-
-        rule.setContent {
-            scope = rememberCoroutineScope()
-            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
-                WindowInsets(0) else BottomSheetDefaults.windowInsets
-
-            ModalBottomSheet(
-                onDismissRequest = { callCount += 1 },
-                sheetState = sheetState,
-                windowInsets = windowInsets
-            ) {
-                Column(
-                    Modifier
-                        .testTag(sheetTag)
-                        .nestedScroll(nestedScrollConnection, nestedScrollDispatcher)
-                ) {
-                    (0..50).forEach {
-                        Text(text = "$it")
-                    }
-                }
-            }
-        }
-
-        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
-        val scrollableContentHeight = rule.onNodeWithTag(sheetTag).fetchSemanticsNode().size.height
-        // Simulate a drag + fling
-        nestedScrollDispatcher.dispatchPostScroll(
-            consumed = Offset.Zero,
-            available = Offset(x = 0f, y = scrollableContentHeight / 2f),
-            source = NestedScrollSource.Drag
-        )
-        scope.launch {
-            nestedScrollDispatcher.dispatchPostFling(
-                consumed = Velocity.Zero,
-                available = Velocity(x = 0f, y = with(rule.density) { 200.dp.toPx() })
-            )
-        }
-
-        rule.waitForIdle()
-        assertThat(sheetState.isVisible).isFalse()
-        assertThat(callCount).isEqualTo(expectedCallCount)
-    }
-
-    @Test
-    fun modalBottomSheet_preservesLayoutDirection() {
-        var value = LayoutDirection.Ltr
-        rule.setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                ModalBottomSheet(onDismissRequest = { /*TODO*/ }) {
-                    value = LocalLayoutDirection.current
-                }
-            }
-        }
-        rule.runOnIdle {
-            assertThat(value).isEqualTo(LayoutDirection.Rtl)
-        }
-    }
-
-    companion object {
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun parameters() = arrayOf(
-            EdgeToEdgeWrapper("EdgeToEdge", true),
-            EdgeToEdgeWrapper("NonEdgeToEdge", false)
-        )
-    }
-
-    class EdgeToEdgeWrapper(val name: String, val edgeToEdgeEnabled: Boolean) {
-        override fun toString(): String {
-            return name
-        }
-    }
-}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
deleted file mode 100644
index 5ab689e..0000000
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
+++ /dev/null
@@ -1,663 +0,0 @@
-/*
- * Copyright 2021 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.material3
-
-import android.os.Build
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.asAndroidBitmap
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.SubcomposeLayout
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.layout.positionInParent
-import androidx.compose.ui.layout.positionInRoot
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.toSize
-import androidx.compose.ui.zIndex
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlin.math.roundToInt
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class ScaffoldTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    private val scaffoldTag = "Scaffold"
-    private val roundingError = 0.5.dp
-    private val fabSpacing = 16.dp
-
-    @Test
-    fun scaffold_onlyContent_takesWholeScreen() {
-        rule.setMaterialContentForSizeAssertions(
-            parentMaxWidth = 100.dp,
-            parentMaxHeight = 100.dp
-        ) {
-            Scaffold {
-                Text("Scaffold body")
-            }
-        }
-            .assertWidthIsEqualTo(100.dp)
-            .assertHeightIsEqualTo(100.dp)
-    }
-
-    @Test
-    fun scaffold_onlyContent_stackSlot() {
-        var child1: Offset = Offset.Zero
-        var child2: Offset = Offset.Zero
-        rule.setMaterialContent(lightColorScheme()) {
-            Scaffold {
-                Text(
-                    "One",
-                    Modifier.onGloballyPositioned { child1 = it.positionInParent() }
-                )
-                Text(
-                    "Two",
-                    Modifier.onGloballyPositioned { child2 = it.positionInParent() }
-                )
-            }
-        }
-        assertThat(child1.y).isEqualTo(child2.y)
-        assertThat(child1.x).isEqualTo(child2.x)
-    }
-
-    @Test
-    fun scaffold_AppbarAndContent_inColumn() {
-        var scaffoldSize: IntSize = IntSize.Zero
-        var appbarPosition: Offset = Offset.Zero
-        var contentPosition: Offset = Offset.Zero
-        var contentSize: IntSize = IntSize.Zero
-        rule.setMaterialContent(lightColorScheme()) {
-            Scaffold(
-                topBar = {
-                    Box(
-                        Modifier
-                            .fillMaxWidth()
-                            .height(50.dp)
-                            .background(color = Color.Red)
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                appbarPosition = positioned.localToWindow(Offset.Zero)
-                            }
-                    )
-                },
-                modifier = Modifier
-                    .onGloballyPositioned { positioned: LayoutCoordinates ->
-                        scaffoldSize = positioned.size
-                    }
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .background(Color.Blue)
-                        .onGloballyPositioned { positioned: LayoutCoordinates ->
-                            contentPosition = positioned.positionInParent()
-                            contentSize = positioned.size
-                        }
-                )
-            }
-        }
-        assertThat(appbarPosition.y).isEqualTo(contentPosition.y)
-        assertThat(scaffoldSize).isEqualTo(contentSize)
-    }
-
-    @Test
-    fun scaffold_bottomBarAndContent_inStack() {
-        var scaffoldSize: IntSize = IntSize.Zero
-        var appbarPosition: Offset = Offset.Zero
-        var appbarSize: IntSize = IntSize.Zero
-        var contentPosition: Offset = Offset.Zero
-        var contentSize: IntSize = IntSize.Zero
-        rule.setMaterialContent(lightColorScheme()) {
-            Scaffold(
-                bottomBar = {
-                    Box(
-                        Modifier
-                            .fillMaxWidth()
-                            .height(50.dp)
-                            .background(color = Color.Red)
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                appbarPosition = positioned.positionInParent()
-                                appbarSize = positioned.size
-                            }
-                    )
-                },
-                modifier = Modifier
-                    .onGloballyPositioned { positioned: LayoutCoordinates ->
-                        scaffoldSize = positioned.size
-                    }
-            ) {
-                Box(
-                    Modifier
-                        .fillMaxSize()
-                        .background(color = Color.Blue)
-                        .onGloballyPositioned { positioned: LayoutCoordinates ->
-                            contentPosition = positioned.positionInParent()
-                            contentSize = positioned.size
-                        }
-                )
-            }
-        }
-        val appBarBottom = appbarPosition.y + appbarSize.height
-        val contentBottom = contentPosition.y + contentSize.height
-        assertThat(appBarBottom).isEqualTo(contentBottom)
-        assertThat(scaffoldSize).isEqualTo(contentSize)
-    }
-
-    @Test
-    fun scaffold_innerPadding_lambdaParam() {
-        var topBarSize: IntSize = IntSize.Zero
-        var bottomBarSize: IntSize = IntSize.Zero
-        lateinit var innerPadding: PaddingValues
-
-        rule.setContent {
-            Scaffold(
-                topBar = {
-                    Box(
-                        Modifier
-                            .fillMaxWidth()
-                            .height(50.dp)
-                            .background(color = Color.Red)
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                topBarSize = positioned.size
-                            }
-                    )
-                },
-                bottomBar = {
-                    Box(
-                        Modifier
-                            .fillMaxWidth()
-                            .height(100.dp)
-                            .background(color = Color.Red)
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                bottomBarSize = positioned.size
-                            }
-                    )
-                }
-            ) {
-                innerPadding = it
-                Text("body")
-            }
-        }
-        rule.runOnIdle {
-            with(rule.density) {
-                assertThat(innerPadding.calculateTopPadding())
-                    .isEqualTo(topBarSize.toSize().height.toDp())
-                assertThat(innerPadding.calculateBottomPadding())
-                    .isEqualTo(bottomBarSize.toSize().height.toDp())
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_topAppBarIsDrawnOnTopOfContent() {
-        rule.setContent {
-            Box(
-                Modifier
-                    .requiredSize(10.dp, 20.dp)
-                    .semantics(mergeDescendants = true) {}
-                    .testTag(scaffoldTag)
-            ) {
-                Scaffold(
-                    topBar = {
-                        Box(
-                            Modifier
-                                .requiredSize(10.dp)
-                                .shadow(4.dp)
-                                .zIndex(4f)
-                                .background(color = Color.White)
-                        )
-                    }
-                ) {
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-
-        rule.onNodeWithTag(scaffoldTag)
-            .captureToImage().asAndroidBitmap().apply {
-                // asserts the appbar(top half part) has the shadow
-                val yPos = height / 2 + 2
-                assertThat(Color(getPixel(0, yPos))).isNotEqualTo(Color.White)
-                assertThat(Color(getPixel(width / 2, yPos))).isNotEqualTo(Color.White)
-                assertThat(Color(getPixel(width - 1, yPos))).isNotEqualTo(Color.White)
-            }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_providesInsets_respectTopAppBar() {
-        rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 40.dp)) {
-                Scaffold(
-                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
-                    topBar = {
-                        Box(Modifier.requiredSize(10.dp))
-                    }
-                ) { paddingValues ->
-                    // top is like top app bar + rounding error
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateTopPadding(),
-                        expected = 10.dp,
-                        threshold = roundingError
-                    )
-                    // bottom is like the insets
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateBottomPadding(),
-                        expected = 3.dp,
-                        threshold = roundingError
-                    )
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_respectsProvidedInsets() {
-        rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 40.dp)) {
-                Scaffold(
-                    contentWindowInsets = WindowInsets(top = 15.dp, bottom = 10.dp),
-                ) { paddingValues ->
-                    // topPadding is equal to provided top window inset
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateTopPadding(),
-                        expected = 15.dp,
-                        threshold = roundingError
-                    )
-                    // bottomPadding is equal to provided bottom window inset
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateBottomPadding(),
-                        expected = 10.dp,
-                        threshold = roundingError
-                    )
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_respectsConsumedWindowInsets() {
-        rule.setContent {
-            Box(
-                Modifier
-                    .requiredSize(10.dp, 40.dp)
-                    .windowInsetsPadding(WindowInsets(top = 10.dp, bottom = 10.dp))
-            ) {
-                Scaffold(
-                    contentWindowInsets = WindowInsets(top = 15.dp, bottom = 15.dp)
-                ) { paddingValues ->
-                    // Consumed windowInsetsPadding is omitted. This replicates behavior from
-                    // Modifier.windowInsetsPadding. (15.dp contentPadding - 10.dp consumedPadding)
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateTopPadding(),
-                        expected = 5.dp,
-                        threshold = roundingError
-                    )
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateBottomPadding(),
-                        expected = 5.dp,
-                        threshold = roundingError
-                    )
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_providesInsets_respectCollapsedTopAppBar() {
-        rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 40.dp)) {
-                Scaffold(
-                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
-                    topBar = {
-                        Box(Modifier.requiredSize(0.dp))
-                    }
-                ) { paddingValues ->
-                    // top is like the collapsed top app bar (i.e. 0dp) + rounding error
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateTopPadding(),
-                        expected = 0.dp,
-                        threshold = roundingError
-                    )
-                    // bottom is like the insets
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateBottomPadding(),
-                        expected = 3.dp,
-                        threshold = roundingError
-                    )
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_providesInsets_respectsBottomAppBar() {
-        rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 40.dp)) {
-                Scaffold(
-                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
-                    bottomBar = {
-                        Box(Modifier.requiredSize(10.dp))
-                    }
-                ) { paddingValues ->
-                    // bottom is like bottom app bar + rounding error
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateBottomPadding(),
-                        expected = 10.dp,
-                        threshold = roundingError
-                    )
-                    // top is like the insets
-                    assertDpIsWithinThreshold(
-                        actual = paddingValues.calculateTopPadding(),
-                        expected = 5.dp,
-                        threshold = roundingError
-                    )
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_insetsTests_snackbarRespectsInsets() {
-        val hostState = SnackbarHostState()
-        var snackbarSize: IntSize? = null
-        var snackbarPosition: Offset? = null
-        var density: Density? = null
-        rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 40.dp)) {
-                density = LocalDensity.current
-                Scaffold(
-                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
-                    snackbarHost = {
-                        SnackbarHost(hostState = hostState,
-                            modifier = Modifier
-                                .onGloballyPositioned {
-                                    snackbarSize = it.size
-                                    snackbarPosition = it.positionInRoot()
-                                })
-                    }
-                ) {
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-        val snackbarBottomOffsetDp =
-            with(density!!) { (snackbarPosition!!.y.roundToInt() + snackbarSize!!.height).toDp() }
-        assertThat(rule.rootHeight() - snackbarBottomOffsetDp - 3.dp).isLessThan(1.dp)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun scaffold_insetsTests_FabRespectsInsets() {
-        var fabSize: IntSize? = null
-        var fabPosition: Offset? = null
-        var density: Density? = null
-        rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 20.dp)) {
-                density = LocalDensity.current
-                Scaffold(
-                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
-                    floatingActionButton = {
-                        FloatingActionButton(onClick = {},
-                            modifier = Modifier
-                                .onGloballyPositioned {
-                                    fabSize = it.size
-                                    fabPosition = it.positionInRoot()
-                                }) {
-                            Text("Fab")
-                        }
-                    },
-                ) {
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-        val fabBottomOffsetDp =
-            with(density!!) { (fabPosition!!.y.roundToInt() + fabSize!!.height).toDp() }
-        assertThat(rule.rootHeight() - fabBottomOffsetDp - 3.dp).isLessThan(1.dp)
-    }
-
-    @Test
-    fun scaffold_fabPosition_start() {
-        var fabSize: IntSize? = null
-        var fabPosition: Offset? = null
-        rule.setContent {
-            Box(Modifier.requiredSize(200.dp, 200.dp)) {
-                Scaffold(
-                    floatingActionButton = {
-                        FloatingActionButton(
-                            onClick = {},
-                            modifier = Modifier
-                                .onGloballyPositioned {
-                                    fabSize = it.size
-                                    fabPosition = it.positionInRoot()
-                                }) {
-                            Text("Fab")
-                        }
-                    },
-                    floatingActionButtonPosition = FabPosition.Start,
-                ) {
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-        with(rule.density) {
-            assertThat(fabPosition!!.x).isWithin(1f).of(fabSpacing.toPx())
-            assertThat(fabPosition!!.y).isWithin(1f).of(
-                200.dp.toPx() - fabSize!!.height - fabSpacing.toPx()
-            )
-        }
-    }
-
-    @Test
-    fun scaffold_fabPosition_center() {
-        var fabSize: IntSize? = null
-        var fabPosition: Offset? = null
-        rule.setContent {
-            Box(Modifier.requiredSize(200.dp, 200.dp)) {
-                Scaffold(
-                    floatingActionButton = {
-                        FloatingActionButton(
-                            onClick = {},
-                            modifier = Modifier
-                                .onGloballyPositioned {
-                                    fabSize = it.size
-                                    fabPosition = it.positionInRoot()
-                                }) {
-                            Text("Fab")
-                        }
-                    },
-                    floatingActionButtonPosition = FabPosition.Center,
-                ) {
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-        with(rule.density) {
-            assertThat(fabPosition!!.x).isWithin(1f).of(
-                (200.dp.toPx() - fabSize!!.width) / 2f
-            )
-            assertThat(fabPosition!!.y).isWithin(1f).of(
-                200.dp.toPx() - fabSize!!.height - fabSpacing.toPx()
-            )
-        }
-    }
-
-    @Test
-    fun scaffold_fabPosition_end() {
-        var fabSize: IntSize? = null
-        var fabPosition: Offset? = null
-        rule.setContent {
-            Box(Modifier.requiredSize(200.dp, 200.dp)) {
-                Scaffold(
-                    floatingActionButton = {
-                        FloatingActionButton(
-                            onClick = {},
-                            modifier = Modifier
-                                .onGloballyPositioned {
-                                    fabSize = it.size
-                                    fabPosition = it.positionInRoot()
-                                }) {
-                            Text("Fab")
-                        }
-                    },
-                    floatingActionButtonPosition = FabPosition.End,
-                ) {
-                    Box(
-                        Modifier
-                            .requiredSize(10.dp)
-                            .background(color = Color.White)
-                    )
-                }
-            }
-        }
-        with(rule.density) {
-            assertThat(fabPosition!!.x).isWithin(1f).of(
-                200.dp.toPx() - fabSize!!.width - fabSpacing.toPx()
-            )
-            assertThat(fabPosition!!.y).isWithin(1f).of(
-                200.dp.toPx() - fabSize!!.height - fabSpacing.toPx()
-            )
-        }
-    }
-
-    // Regression test for b/295536718
-    @Test
-    fun scaffold_onSizeChanged_calledBeforeLookaheadPlace() {
-        var size: IntSize? = null
-        var onSizeChangedCount = 0
-        var onPlaceCount = 0
-
-        rule.setContent {
-            LookaheadScope {
-                Scaffold {
-                    SubcomposeLayout { constraints ->
-                        val measurables = subcompose("second") {
-                            Box(
-                                Modifier
-                                    .size(45.dp)
-                                    .onSizeChanged {
-                                        onSizeChangedCount++
-                                        size = it
-                                    }
-                            )
-                        }
-                        val placeables = measurables.map { it.measure(constraints) }
-
-                        layout(constraints.maxWidth, constraints.maxHeight) {
-                            onPlaceCount++
-                            assertWithMessage("Expected onSizeChangedCount to be >= 1")
-                                .that(onSizeChangedCount).isAtLeast(1)
-                            assertThat(size).isNotNull()
-                            placeables.forEach { it.place(0, 0) }
-                        }
-                    }
-                }
-            }
-        }
-
-        assertWithMessage("Expected placeCount to be >= 1").that(onPlaceCount).isAtLeast(1)
-    }
-
-    private fun assertDpIsWithinThreshold(actual: Dp, expected: Dp, threshold: Dp) {
-        assertThat(actual.value).isWithin(threshold.value).of(expected.value)
-    }
-}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonScreenshotTest.kt
deleted file mode 100644
index 89ae9dd..0000000
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonScreenshotTest.kt
+++ /dev/null
@@ -1,213 +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.compose.material3
-
-import android.os.Build
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Favorite
-import androidx.compose.testutils.assertAgainstGolden
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.screenshot.AndroidXScreenshotTestRule
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterial3Api::class)
-
-class SegmentedButtonScreenshotTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @get:Rule
-    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
-
-    @Test
-    fun all_unselected() {
-        rule.setMaterialContent(lightColorScheme()) {
-            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
-                values.forEach {
-                    SegmentedButton(checked = false, onCheckedChange = {}) {
-                        Text(it)
-                    }
-                }
-            }
-        }
-
-        assertButtonAgainstGolden("all_unselected")
-    }
-
-    @Test
-    fun all_selected() {
-        rule.setMaterialContent(lightColorScheme()) {
-            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
-                values.forEach {
-                    SegmentedButton(checked = true, onCheckedChange = {}) {
-                        Text(it)
-                    }
-                }
-            }
-        }
-
-        assertButtonAgainstGolden("all_selected")
-    }
-
-    @Test
-    fun middle_selected() {
-        rule.setMaterialContent(lightColorScheme()) {
-            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
-                values.forEachIndexed { index, item ->
-                    SegmentedButton(checked = index == 1, onCheckedChange = {}) {
-                        Text(item)
-                    }
-                }
-            }
-        }
-
-        assertButtonAgainstGolden("middle_selected")
-    }
-
-    @Test
-    fun middle_selected_with_icon() {
-        rule.setMaterialContent(lightColorScheme()) {
-            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
-                values.forEachIndexed { index, item ->
-                    SegmentedButton(
-                        checked = index == 1,
-                        onCheckedChange = {},
-                        icon = if (index == 1) {
-                            { SegmentedButtonDefaults.ActiveIcon() }
-                        } else {
-                            {
-                                Icon(
-                                    imageVector = Icons.Outlined.Favorite,
-                                    contentDescription = null,
-                                    modifier = Modifier.size(SegmentedButtonDefaults.IconSize)
-                                )
-                            }
-                        }
-                    ) {
-                        Text(item)
-                    }
-                }
-            }
-        }
-
-        assertButtonAgainstGolden("middle_selected_with_icon")
-    }
-
-    @Test
-    fun stroke_zIndex() {
-        rule.setMaterialContent(lightColorScheme()) {
-            val colors = SegmentedButtonDefaults.colors(
-                activeBorderColor = Color.Blue,
-                inactiveBorderColor = Color.Yellow
-            )
-            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
-                values.forEachIndexed { index, item ->
-                    SegmentedButton(
-                        checked = index == 1,
-                        onCheckedChange = {},
-                        colors = colors
-                    ) {
-                        Text(item)
-                    }
-                }
-            }
-        }
-
-        assertButtonAgainstGolden("stroke_zIndex")
-    }
-
-    @Test
-    fun button_shape() {
-        rule.setMaterialContent(lightColorScheme()) {
-            val colors = SegmentedButtonDefaults.colors(
-                activeBorderColor = Color.Blue,
-                inactiveBorderColor = Color.Yellow
-            )
-            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
-                values.forEachIndexed { index, item ->
-                    val shape = SegmentedButtonDefaults.shape(index, values.size)
-
-                    SegmentedButton(
-                        checked = index == 1,
-                        onCheckedChange = {},
-                        colors = colors,
-                        shape = shape,
-                    ) {
-                        Text(item)
-                    }
-                }
-            }
-        }
-
-        assertButtonAgainstGolden("button_shape")
-    }
-
-    @Test
-    fun all_unselected_darkTheme() {
-        rule.setMaterialContent(darkColorScheme()) {
-            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
-                values.forEach {
-                    SegmentedButton(checked = false, onCheckedChange = {}) {
-                        Text(it)
-                    }
-                }
-            }
-        }
-
-        assertButtonAgainstGolden("all_unselected_darkTheme")
-    }
-
-    @Test
-    fun all_selected_darkTheme() {
-        rule.setMaterialContent(darkColorScheme()) {
-            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
-                values.forEach {
-                    SegmentedButton(checked = true, onCheckedChange = {}) {
-                        Text(it)
-                    }
-                }
-            }
-        }
-
-        assertButtonAgainstGolden("all_selected_darkTheme")
-    }
-
-    private fun assertButtonAgainstGolden(goldenName: String) {
-        rule.onNodeWithTag(testTag)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, goldenName)
-    }
-
-    private val values = listOf("Day", "Month", "Week")
-
-    private val testTag = "button"
-}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonTest.kt
deleted file mode 100644
index 37c5862..0000000
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonTest.kt
+++ /dev/null
@@ -1,192 +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.compose.material3
-
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.tokens.OutlinedSegmentedButtonTokens
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.SolidColor
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.semantics.getOrNull
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsOff
-import androidx.compose.ui.test.assertIsOn
-import androidx.compose.ui.test.assertWidthIsAtLeast
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalMaterial3Api::class)
-class SegmentedButtonTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Test
-    fun toggleableSegmentedButton_itemsDisplay() {
-        val values = listOf("Day", "Month", "Week")
-
-        rule.setMaterialContent(lightColorScheme()) {
-            MultiChoiceSegmentedButtonRow {
-                values.forEach {
-                    SegmentedButton(checked = false, onCheckedChange = {}) {
-                        Text(it)
-                    }
-                }
-            }
-        }
-
-        values.forEach { rule.onNodeWithText(it).assertIsDisplayed() }
-    }
-
-    @Test
-    fun selectableSegmentedButton_itemsDisplay() {
-        val values = listOf("Day", "Month", "Week")
-
-        rule.setMaterialContent(lightColorScheme()) {
-            SingleChoiceSegmentedButtonRow {
-                values.forEach {
-                    SegmentedButton(selected = false, onClick = {}) {
-                        Text(it)
-                    }
-                }
-            }
-        }
-
-        values.forEach { rule.onNodeWithText(it).assertIsDisplayed() }
-    }
-
-    @Test
-    fun segmentedButton_itemsChecked() {
-        var checked by mutableStateOf(true)
-        rule.setMaterialContent(lightColorScheme()) {
-            MultiChoiceSegmentedButtonRow {
-                SegmentedButton(onCheckedChange = { checked = it }, checked = checked) {
-                    Text("Day")
-                }
-                SegmentedButton(onCheckedChange = { checked = it }, checked = !checked) {
-                    Text("Month")
-                }
-            }
-        }
-
-        rule.onNodeWithText("Day").assertIsOn()
-        rule.onNodeWithText("Month").assertIsOff()
-
-        rule.runOnIdle {
-            checked = false
-        }
-
-        rule.onNodeWithText("Day").assertIsOff()
-        rule.onNodeWithText("Month").assertIsOn()
-    }
-
-    @Test
-    fun selectableSegmentedButton_semantics() {
-        rule.setMaterialContent(lightColorScheme()) {
-            SingleChoiceSegmentedButtonRow(modifier = Modifier.testTag("row")) {
-                SegmentedButton(selected = false, onClick = {}) {
-                    Text("Day")
-                }
-                SegmentedButton(selected = false, onClick = {}) {
-                    Text("Month")
-                }
-            }
-        }
-
-        val semanticsNode = rule.onNodeWithTag("row").fetchSemanticsNode()
-        val selectableGroup = semanticsNode.config.getOrNull(SemanticsProperties.SelectableGroup)
-
-        assertThat(selectableGroup).isNotNull()
-    }
-
-    @Test
-    fun segmentedButton_icon() {
-        var checked by mutableStateOf(false)
-        rule.setMaterialContent(lightColorScheme()) {
-            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag("row")) {
-                SegmentedButton(
-                    checked = checked,
-                    onCheckedChange = {},
-                    icon = { Text(if (checked) "checked" else "unchecked") },
-                ) {
-                    Text("Day")
-                }
-            }
-        }
-
-        rule.onNodeWithText("unchecked").assertIsDisplayed()
-
-        rule.runOnIdle { checked = true }
-        rule.waitForIdle()
-
-        rule.onNodeWithText("checked").assertIsDisplayed()
-    }
-
-    @Test
-    fun segmentedButton_Sizing() {
-        val itemSize = 60.dp
-
-        rule.setMaterialContentForSizeAssertions(
-            parentMaxWidth = 300.dp, parentMaxHeight = 100.dp
-        ) {
-            MultiChoiceSegmentedButtonRow {
-                SegmentedButton(checked = false, onCheckedChange = {}) {
-                    Text(modifier = Modifier.width(60.dp), text = "Day")
-                }
-                SegmentedButton(checked = false, onCheckedChange = {}) {
-                    Text(modifier = Modifier.width(30.dp), text = "Month")
-                }
-            }
-        }
-            .assertWidthIsAtLeast((itemSize + 12.dp * 2) * 2)
-            .assertHeightIsEqualTo(OutlinedSegmentedButtonTokens.ContainerHeight)
-    }
-
-    @Test
-    fun segmentedButtonBorder_default_matchesSpec() {
-        lateinit var border: BorderStroke
-        var specColor: Color = Color.Unspecified
-        rule.setMaterialContent(lightColorScheme()) {
-            specColor = OutlinedSegmentedButtonTokens.OutlineColor.value
-            border = SegmentedButtonDefaults.Border.borderStroke(
-                checked = true,
-                enabled = true,
-                colors = SegmentedButtonDefaults.colors()
-            )
-        }
-
-        assertThat((border.brush as SolidColor).value).isEqualTo(specColor)
-        assertThat(border.width).isEqualTo(OutlinedSegmentedButtonTokens.OutlineWidth)
-    }
-}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt
deleted file mode 100644
index 36514dd..0000000
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt
+++ /dev/null
@@ -1,352 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3
-
-import android.os.Build
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.requiredWidth
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.runtime.remember
-import androidx.compose.testutils.assertAgainstGolden
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.screenshot.AndroidXScreenshotTestRule
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-class SliderScreenshotTest {
-    @get:Rule
-    val rule = createComposeRule()
-
-    @get:Rule
-    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
-
-    val wrap = Modifier.requiredWidth(70.dp).wrapContentSize(Alignment.TopStart)
-
-    private val wrapperTestTag = "sliderWrapper"
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun sliderTest_origin() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                Slider(
-                    remember { SliderState(0f) }
-                )
-            }
-        }
-        assertSliderAgainstGolden("slider_origin")
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun sliderTest_origin_disabled() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                Slider(
-                    remember { SliderState(0f) },
-                    enabled = false
-                )
-            }
-        }
-        assertSliderAgainstGolden("slider_origin_disabled")
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun sliderTest_middle() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                Slider(
-                    remember { SliderState(0.5f) }
-                )
-            }
-        }
-        assertSliderAgainstGolden("slider_middle")
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun sliderTest_middle_dark() {
-        rule.setMaterialContent(darkColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                Slider(
-                    remember { SliderState(0.5f) }
-                )
-            }
-        }
-        assertSliderAgainstGolden("slider_middle_dark")
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun sliderTest_middle_dark_disabled() {
-        rule.setMaterialContent(darkColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                Slider(
-                    remember { SliderState(0.5f) },
-                    enabled = false
-                )
-            }
-        }
-        assertSliderAgainstGolden("slider_middle_dark_disabled")
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun sliderTest_end() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                Slider(
-                    remember { SliderState(1f) }
-                )
-            }
-        }
-        assertSliderAgainstGolden("slider_end")
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun sliderTest_middle_steps() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                Slider(
-                    remember { SliderState(0.5f, steps = 5) }
-                )
-            }
-        }
-        assertSliderAgainstGolden("slider_middle_steps")
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun sliderTest_middle_steps_dark() {
-        rule.setMaterialContent(darkColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                Slider(
-                    remember { SliderState(0.5f, steps = 5) }
-                )
-            }
-        }
-        assertSliderAgainstGolden("slider_middle_steps_dark")
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun sliderTest_middle_steps_disabled() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                Slider(
-                    remember { SliderState(0.5f, steps = 5) },
-                    enabled = false
-                )
-            }
-        }
-        assertSliderAgainstGolden("slider_middle_steps_disabled")
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun sliderTest_customColors() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                Slider(
-                    remember { SliderState(0.5f, steps = 5) },
-                    colors = SliderDefaults.colors(
-                        thumbColor = Color.Red,
-                        activeTrackColor = Color.Blue,
-                        activeTickColor = Color.Yellow,
-                        inactiveTickColor = Color.Magenta
-                    )
-
-                )
-            }
-        }
-        assertSliderAgainstGolden("slider_customColors")
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun sliderTest_customColors_disabled() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                Slider(
-                    remember { SliderState(0.5f, steps = 5) },
-                    enabled = false,
-                    // this is intentionally made to appear as enabled in disabled state for a
-                    // brighter test
-                    colors = SliderDefaults.colors(
-                        disabledThumbColor = Color.Blue,
-                        disabledActiveTrackColor = Color.Red,
-                        disabledInactiveTrackColor = Color.Yellow,
-                        disabledActiveTickColor = Color.Magenta,
-                        disabledInactiveTickColor = Color.Cyan
-                    )
-
-                )
-            }
-        }
-        assertSliderAgainstGolden("slider_customColors_disabled")
-    }
-
-    @Test
-    @ExperimentalMaterial3Api
-    fun rangeSliderTest_middle_steps_disabled() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                RangeSlider(
-                    remember {
-                        RangeSliderState(0.5f, 1f, steps = 5)
-                    },
-                    enabled = false
-                )
-            }
-        }
-        assertSliderAgainstGolden("rangeSlider_middle_steps_disabled")
-    }
-
-    @Test
-    @ExperimentalMaterial3Api
-    fun rangeSliderTest_middle_steps_enabled() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                RangeSlider(
-                    remember {
-                        RangeSliderState(
-                            0.5f,
-                            1f,
-                            steps = 5
-                        )
-                    }
-                )
-            }
-        }
-        assertSliderAgainstGolden("rangeSlider_middle_steps_enabled")
-    }
-
-    @Test
-    @ExperimentalMaterial3Api
-    fun rangeSliderTest_middle_steps_dark_enabled() {
-        rule.setMaterialContent(darkColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                RangeSlider(
-                    remember {
-                        RangeSliderState(
-                            0.5f,
-                            1f,
-                            steps = 5
-                        )
-                    }
-                )
-            }
-        }
-        assertSliderAgainstGolden("rangeSlider_middle_steps_dark_enabled")
-    }
-
-    @Test
-    @ExperimentalMaterial3Api
-    fun rangeSliderTest_middle_steps_dark_disabled() {
-        rule.setMaterialContent(darkColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                RangeSlider(
-                    remember {
-                        RangeSliderState(0.5f, 1f, steps = 5)
-                    },
-                    enabled = false
-                )
-            }
-        }
-        assertSliderAgainstGolden("rangeSlider_middle_steps_dark_disabled")
-    }
-
-    @Test
-    @ExperimentalMaterial3Api
-    fun rangeSliderTest_overlapingThumbs() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                RangeSlider(
-                    remember {
-                        RangeSliderState(0.5f, 0.51f)
-                    }
-                )
-            }
-        }
-        assertSliderAgainstGolden("rangeSlider_overlapingThumbs")
-    }
-
-    @Test
-    @ExperimentalMaterial3Api
-    fun rangeSliderTest_fullRange() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                RangeSlider(
-                    remember {
-                        RangeSliderState(0f, 1f)
-                    }
-                )
-            }
-        }
-        assertSliderAgainstGolden("rangeSlider_fullRange")
-    }
-
-    @Test
-    @ExperimentalMaterial3Api
-    fun rangeSliderTest_steps_customColors() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(wrap.testTag(wrapperTestTag)) {
-                val state = remember {
-                    RangeSliderState(
-                        30f,
-                        70f,
-                        steps = 9,
-                        valueRange = 0f..100f
-                    )
-                }
-                RangeSlider(
-                    state = state,
-                    colors = SliderDefaults.colors(
-                        thumbColor = Color.Blue,
-                        activeTrackColor = Color.Red,
-                        inactiveTrackColor = Color.Yellow,
-                        activeTickColor = Color.Magenta,
-                        inactiveTickColor = Color.Cyan
-                    )
-                )
-            }
-        }
-        assertSliderAgainstGolden("rangeSlider_steps_customColors")
-    }
-
-    private fun assertSliderAgainstGolden(goldenName: String) {
-        rule.onNodeWithTag(wrapperTestTag)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, goldenName)
-    }
-}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
deleted file mode 100644
index 8696ba5..0000000
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
+++ /dev/null
@@ -1,1366 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.rememberScrollableState
-import androidx.compose.foundation.gestures.scrollable
-import androidx.compose.foundation.interaction.DragInteraction
-import androidx.compose.foundation.interaction.Interaction
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.requiredWidth
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.tokens.SliderTokens
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.testutils.expectAssertionError
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.boundsInParent
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalViewConfiguration
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.ProgressBarRangeInfo
-import androidx.compose.ui.semantics.SemanticsActions
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.assert
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertRangeInfoEquals
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.click
-import androidx.compose.ui.test.isFocusable
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performSemanticsAction
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class SliderTest {
-    private val tag = "slider"
-    private val SliderTolerance = 0.003f
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun sliderPosition_valueCoercion() {
-        val state = SliderState(0f)
-        rule.setContent {
-            Slider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-        rule.runOnIdle {
-            state.value = 2f
-        }
-        rule.onNodeWithTag(tag).assertRangeInfoEquals(ProgressBarRangeInfo(1f, 0f..1f, 0))
-        rule.runOnIdle {
-            state.value = -123145f
-        }
-        rule.onNodeWithTag(tag).assertRangeInfoEquals(ProgressBarRangeInfo(0f, 0f..1f, 0))
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test(expected = IllegalArgumentException::class)
-    fun sliderPosition_stepsThrowWhenLessThanZero() {
-        rule.setContent {
-            Slider(SliderState(initialValue = 0f, initialOnValueChange = {}, steps = -1))
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_semantics_continuous() {
-        val state = SliderState(0f)
-
-        rule.setMaterialContent(lightColorScheme()) {
-            Slider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.onNodeWithTag(tag)
-            .assertRangeInfoEquals(ProgressBarRangeInfo(0f, 0f..1f, 0))
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.SetProgress))
-
-        rule.runOnUiThread {
-            state.value = 0.5f
-        }
-
-        rule.onNodeWithTag(tag).assertRangeInfoEquals(ProgressBarRangeInfo(0.5f, 0f..1f, 0))
-
-        rule.onNodeWithTag(tag)
-            .performSemanticsAction(SemanticsActions.SetProgress) { it(0.7f) }
-
-        rule.onNodeWithTag(tag).assertRangeInfoEquals(ProgressBarRangeInfo(0.7f, 0f..1f, 0))
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_semantics_stepped() {
-        val state = SliderState(0f, steps = 4)
-
-        rule.setMaterialContent(lightColorScheme()) {
-            Slider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.onNodeWithTag(tag)
-            .assertRangeInfoEquals(ProgressBarRangeInfo(0f, 0f..1f, 4))
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.SetProgress))
-
-        rule.runOnUiThread {
-            state.value = 0.6f
-        }
-
-        rule.onNodeWithTag(tag).assertRangeInfoEquals(ProgressBarRangeInfo(0.6f, 0f..1f, 4))
-
-        rule.onNodeWithTag(tag)
-            .performSemanticsAction(SemanticsActions.SetProgress) { it(0.75f) }
-
-        rule.onNodeWithTag(tag).assertRangeInfoEquals(ProgressBarRangeInfo(0.8f, 0f..1f, 4))
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_semantics_focusable() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Slider(
-                SliderState(0f),
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.onNodeWithTag(tag)
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Focused))
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_semantics_disabled() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Slider(
-                state = SliderState(0f),
-                modifier = Modifier.testTag(tag),
-                enabled = false
-            )
-        }
-
-        rule.onNodeWithTag(tag)
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Disabled))
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_drag() {
-        val state = SliderState(0f)
-        var slop = 0f
-
-        rule.setMaterialContent(lightColorScheme()) {
-            slop = LocalViewConfiguration.current.touchSlop
-            Slider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.runOnUiThread {
-            Truth.assertThat(state.value).isEqualTo(0f)
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(100f, 0f))
-                up()
-                expected = calculateFraction(left, right, centerX + 100 - slop)
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_drag_out_of_bounds() {
-        val state = SliderState(0f)
-        var slop = 0f
-
-        rule.setMaterialContent(lightColorScheme()) {
-            slop = LocalViewConfiguration.current.touchSlop
-            Slider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.runOnUiThread {
-            Truth.assertThat(state.value).isEqualTo(0f)
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(width.toFloat(), 0f))
-                moveBy(Offset(-width.toFloat(), 0f))
-                moveBy(Offset(-width.toFloat(), 0f))
-                moveBy(Offset(width.toFloat() + 100f, 0f))
-                up()
-                expected = calculateFraction(left, right, centerX + 100 - slop)
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_tap() {
-        val state = SliderState(0f)
-
-        rule.setMaterialContent(lightColorScheme()) {
-            Slider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.runOnUiThread {
-            Truth.assertThat(state.value).isEqualTo(0f)
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(Offset(centerX + 50, centerY))
-                up()
-                expected = calculateFraction(left, right, centerX + 50)
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    /**
-     * Guarantee slider doesn't move as we scroll, tapping still works
-     */
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_scrollableContainer() {
-        val state = SliderState(0f)
-        val offset = mutableStateOf(0f)
-
-        rule.setContent {
-            Column(
-                modifier = Modifier
-                    .height(2000.dp)
-                    .scrollable(
-                        orientation = Orientation.Vertical,
-                        state = rememberScrollableState { delta ->
-                            offset.value += delta
-                            delta
-                        })
-            ) {
-                Slider(
-                    state = state,
-                    modifier = Modifier.testTag(tag)
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            Truth.assertThat(offset.value).isEqualTo(0f)
-        }
-
-        // Just scroll
-        rule.onNodeWithTag(tag, useUnmergedTree = true)
-            .performTouchInput {
-                down(Offset(centerX, centerY))
-                moveBy(Offset(0f, 500f))
-                up()
-            }
-
-        rule.runOnIdle {
-            Truth.assertThat(offset.value).isGreaterThan(0f)
-            Truth.assertThat(state.value).isEqualTo(0f)
-        }
-
-        // Tap
-        var expected = 0f
-        rule.onNodeWithTag(tag, useUnmergedTree = true)
-            .performTouchInput {
-                click(Offset(centerX, centerY))
-                expected = calculateFraction(left, right, centerX)
-            }
-
-        rule.runOnIdle {
-            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_tap_rangeChange() {
-        val rangeEnd = mutableStateOf(0.25f)
-        lateinit var state: SliderState
-
-        rule.setMaterialContent(lightColorScheme()) {
-            state = remember(rangeEnd.value) {
-                SliderState(0f, valueRange = 0f..rangeEnd.value)
-            }
-            Slider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        // change to 1 since [calculateFraction] coerces between 0..1
-        rule.runOnUiThread {
-            rangeEnd.value = 1f
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                click(Offset(centerX + 50, centerY))
-                expected = calculateFraction(left, right, centerX + 50)
-            }
-
-        rule.runOnIdle {
-            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_drag_rtl() {
-        val state = SliderState(0f)
-        var slop = 0f
-
-        rule.setMaterialContent(lightColorScheme()) {
-            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                slop = LocalViewConfiguration.current.touchSlop
-                Slider(
-                    state = state,
-                    modifier = Modifier.testTag(tag)
-                )
-            }
-        }
-
-        rule.runOnUiThread {
-            Truth.assertThat(state.value).isEqualTo(0f)
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(100f, 0f))
-                up()
-                // subtract here as we're in rtl and going in the opposite direction
-                expected = calculateFraction(left, right, centerX - 100 + slop)
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_tap_rtl() {
-        val state = SliderState(0f)
-
-        rule.setMaterialContent(lightColorScheme()) {
-            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                Slider(
-                    state = state,
-                    modifier = Modifier.testTag(tag)
-                )
-            }
-        }
-
-        rule.runOnUiThread {
-            Truth.assertThat(state.value).isEqualTo(0f)
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(Offset(centerX + 50, centerY))
-                up()
-                expected = calculateFraction(left, right, centerX - 50)
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    private fun calculateFraction(left: Float, right: Float, pos: Float) = with(rule.density) {
-        val offset = (ThumbWidth / 2).toPx()
-        val start = left + offset
-        val end = right - offset
-        ((pos - start) / (end - start)).coerceIn(0f, 1f)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_sizes() {
-        val state = SliderState(0f)
-        rule
-            .setMaterialContentForSizeAssertions(
-                parentMaxWidth = 100.dp,
-                parentMaxHeight = 100.dp
-            ) { Slider(state) }
-            .assertHeightIsEqualTo(48.dp)
-            .assertWidthIsEqualTo(100.dp)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_sizes_within_row() {
-        val rowWidth = 100.dp
-        val spacerWidth = 10.dp
-
-        rule.setMaterialContent(lightColorScheme()) {
-            Row(modifier = Modifier.requiredWidth(rowWidth)) {
-                Spacer(Modifier.width(spacerWidth))
-                Slider(
-                    state = SliderState(0f, {}),
-                    modifier = Modifier.testTag(tag).weight(1f)
-                )
-                Spacer(Modifier.width(spacerWidth))
-            }
-        }
-
-        rule.onNodeWithTag(tag)
-            .assertWidthIsEqualTo(rowWidth - spacerWidth.times(2))
-            .assertHeightIsEqualTo(SliderTokens.HandleHeight)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_min_size() {
-        rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.requiredSize(0.dp)) {
-                Slider(
-                    state = SliderState(0f, {}),
-                    modifier = Modifier.testTag(tag)
-                )
-            }
-        }
-
-        rule.onNodeWithTag(tag)
-            .assertWidthIsEqualTo(SliderTokens.HandleWidth)
-            .assertHeightIsEqualTo(SliderTokens.HandleHeight)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_noUnwantedCallbackCalls() {
-        val callCount = mutableStateOf(0f)
-        val state = SliderState(0f, { callCount.value += 1 })
-
-        rule.setMaterialContent(lightColorScheme()) {
-            Slider(
-                state = state,
-                modifier = Modifier.testTag(tag),
-            )
-        }
-
-        rule.runOnIdle {
-            Truth.assertThat(callCount.value).isEqualTo(0f)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_valueChangeFinished_calledOnce() {
-        val callCount = mutableStateOf(0f)
-        val state = SliderState(0f, onValueChangeFinished = { callCount.value += 1 })
-
-        rule.setMaterialContent(lightColorScheme()) {
-            Slider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.runOnIdle {
-            Truth.assertThat(callCount.value).isEqualTo(0)
-        }
-
-        rule.onNodeWithTag(tag).performTouchInput {
-            down(center)
-            moveBy(Offset(50f, 50f))
-            up()
-        }
-
-        rule.runOnIdle {
-            Truth.assertThat(callCount.value).isEqualTo(1)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_setProgress_callsOnValueChangeFinished() {
-        val callCount = mutableStateOf(0)
-        val state = SliderState(0f, onValueChangeFinished = { callCount.value += 1 })
-
-        rule.setMaterialContent(lightColorScheme()) {
-            Slider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.runOnIdle {
-            Truth.assertThat(callCount.value).isEqualTo(0)
-        }
-
-        rule.onNodeWithTag(tag)
-            .performSemanticsAction(SemanticsActions.SetProgress) { it(0.8f) }
-
-        rule.runOnIdle {
-            Truth.assertThat(callCount.value).isEqualTo(1)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_interactionSource_resetWhenDisposed() {
-        val interactionSource = MutableInteractionSource()
-        var emitSlider by mutableStateOf(true)
-
-        var scope: CoroutineScope? = null
-
-        rule.setContent {
-            scope = rememberCoroutineScope()
-            Box {
-                if (emitSlider) {
-                    Slider(
-                        state = SliderState(0.5f, {}),
-                        modifier = Modifier.testTag(tag),
-                        interactionSource = interactionSource
-                    )
-                }
-            }
-        }
-
-        val interactions = mutableListOf<Interaction>()
-
-        scope!!.launch {
-            interactionSource.interactions.collect { interactions.add(it) }
-        }
-
-        rule.runOnIdle {
-            Truth.assertThat(interactions).isEmpty()
-        }
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(100f, 0f))
-            }
-
-        rule.runOnIdle {
-            Truth.assertThat(interactions).hasSize(1)
-            Truth.assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
-        }
-
-        // Dispose
-        rule.runOnIdle {
-            emitSlider = false
-        }
-
-        rule.runOnIdle {
-            Truth.assertThat(interactions).hasSize(2)
-            Truth.assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
-            Truth.assertThat(interactions[1]).isInstanceOf(DragInteraction.Cancel::class.java)
-            Truth.assertThat((interactions[1] as DragInteraction.Cancel).start)
-                .isEqualTo(interactions[0])
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_onValueChangedFinish_afterTap() {
-        var changedFlag = false
-        rule.setContent {
-            Slider(
-                state = SliderState(0f, {}, onValueChangeFinished = { changedFlag = true }),
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                click(center)
-            }
-
-        rule.runOnIdle {
-            Truth.assertThat(changedFlag).isTrue()
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_zero_width() {
-        rule.setMaterialContentForSizeAssertions(
-            parentMaxHeight = 0.dp,
-            parentMaxWidth = 0.dp
-        ) { Slider(SliderState(1f, {})) }
-            .assertHeightIsEqualTo(0.dp)
-            .assertWidthIsEqualTo(0.dp)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_thumb_recomposition() {
-        val state = SliderState(0f)
-        val recompositionCounter = SliderRecompositionCounter()
-
-        rule.setContent {
-            Slider(
-                state = state,
-                modifier = Modifier.testTag(tag),
-                thumb = { sliderState -> recompositionCounter.OuterContent(sliderState) }
-            )
-        }
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(100f, 0f))
-                moveBy(Offset(-100f, 0f))
-                moveBy(Offset(100f, 0f))
-            }
-        rule.runOnIdle {
-            Truth.assertThat(recompositionCounter.outerRecomposition).isEqualTo(1)
-            Truth.assertThat(recompositionCounter.innerRecomposition).isEqualTo(4)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_track_recomposition() {
-        val state = SliderState(0f)
-        val recompositionCounter = SliderRecompositionCounter()
-
-        rule.setContent {
-            Slider(
-                state = state,
-                modifier = Modifier.testTag(tag),
-                track = { sliderState -> recompositionCounter.OuterContent(sliderState) }
-            )
-        }
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(100f, 0f))
-                moveBy(Offset(-100f, 0f))
-                moveBy(Offset(100f, 0f))
-            }
-        rule.runOnIdle {
-            Truth.assertThat(recompositionCounter.outerRecomposition).isEqualTo(1)
-            Truth.assertThat(recompositionCounter.innerRecomposition).isEqualTo(4)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_parentWithInfiniteWidth_minWidth() {
-        val state = SliderState(0f)
-        rule.setMaterialContentForSizeAssertions {
-            Box(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
-                Slider(state)
-            }
-        }.assertWidthIsEqualTo(48.dp)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun slider_rowWithInfiniteWidth() {
-        expectAssertionError(false) {
-            rule.setContent {
-                Row(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
-                    Slider(
-                        state = SliderState(0f),
-                        modifier = Modifier.weight(1f)
-                    )
-                }
-            }
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_dragThumb() {
-        val state = RangeSliderState(0f, 1f)
-        var slop = 0f
-
-        rule.setMaterialContent(lightColorScheme()) {
-            slop = LocalViewConfiguration.current.touchSlop
-            RangeSlider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.runOnUiThread {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
-            Truth.assertThat(state.activeRangeEnd).isEqualTo(1f)
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(slop, 0f))
-                moveBy(Offset(100f, 0f))
-                expected = calculateFraction(left, right, centerX + 100)
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
-            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_drag_out_of_bounds() {
-        val state = RangeSliderState(0f, 1f)
-        var slop = 0f
-
-        rule.setMaterialContent(lightColorScheme()) {
-            slop = LocalViewConfiguration.current.touchSlop
-            RangeSlider(
-                state = state,
-                modifier = Modifier.testTag(tag),
-            )
-        }
-
-        rule.runOnUiThread {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
-            Truth.assertThat(state.activeRangeEnd).isEqualTo(1f)
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(slop, 0f))
-                moveBy(Offset(width.toFloat(), 0f))
-                moveBy(Offset(-width.toFloat(), 0f))
-                moveBy(Offset(-width.toFloat(), 0f))
-                moveBy(Offset(width.toFloat() + 100f, 0f))
-                up()
-                expected = calculateFraction(left, right, centerX + 100)
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
-            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_drag_overlap_thumbs() {
-        val state = RangeSliderState(0.5f, 1f)
-        var slop = 0f
-
-        rule.setMaterialContent(lightColorScheme()) {
-            slop = LocalViewConfiguration.current.touchSlop
-            RangeSlider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(centerRight)
-                moveBy(Offset(-slop, 0f))
-                moveBy(Offset(-width.toFloat(), 0f))
-                up()
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0.5f)
-            Truth.assertThat(state.activeRangeEnd).isEqualTo(0.5f)
-        }
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(-slop, 0f))
-                moveBy(Offset(-width.toFloat(), 0f))
-                up()
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
-            Truth.assertThat(state.activeRangeEnd).isEqualTo(0.5f)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_tap() {
-        val state = RangeSliderState(0f, 1f)
-
-        rule.setMaterialContent(lightColorScheme()) {
-            RangeSlider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.runOnUiThread {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
-            Truth.assertThat(state.activeRangeEnd).isEqualTo(1f)
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(Offset(centerX + 50, centerY))
-                up()
-                expected = calculateFraction(left, right, centerX + 50)
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_tap_rangeChange() {
-        val rangeEnd = mutableStateOf(0.25f)
-        lateinit var state: RangeSliderState
-
-        rule.setMaterialContent(lightColorScheme()) {
-            state = remember(rangeEnd.value) {
-                RangeSliderState(
-                    0f,
-                    25f,
-                    valueRange = 0f..rangeEnd.value
-                )
-            }
-            RangeSlider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-        // change to 1 since [calculateFraction] coerces between 0..1
-        rule.runOnUiThread {
-            rangeEnd.value = 1f
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(Offset(centerX + 50, centerY))
-                up()
-                expected = calculateFraction(left, right, centerX + 50)
-            }
-
-        rule.runOnIdle {
-            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_drag_rtl() {
-        val state = RangeSliderState(0f, 1f)
-        var slop = 0f
-
-        rule.setMaterialContent(lightColorScheme()) {
-            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                slop = LocalViewConfiguration.current.touchSlop
-                RangeSlider(
-                    state = state,
-                    modifier = Modifier.testTag(tag)
-                )
-            }
-        }
-
-        rule.runOnUiThread {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
-            Truth.assertThat(state.activeRangeEnd).isEqualTo(1f)
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(slop, 0f))
-                moveBy(Offset(100f, 0f))
-                up()
-                // subtract here as we're in rtl and going in the opposite direction
-                expected = calculateFraction(left, right, centerX - 100)
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
-            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_drag_out_of_bounds_rtl() {
-        val state = RangeSliderState(0f, 1f)
-        var slop = 0f
-
-        rule.setMaterialContent(lightColorScheme()) {
-            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                slop = LocalViewConfiguration.current.touchSlop
-                RangeSlider(
-                    state = state,
-                    modifier = Modifier.testTag(tag)
-                )
-            }
-        }
-
-        rule.runOnUiThread {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
-            Truth.assertThat(state.activeRangeEnd).isEqualTo(1f)
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(slop, 0f))
-                moveBy(Offset(width.toFloat(), 0f))
-                moveBy(Offset(-width.toFloat(), 0f))
-                moveBy(Offset(-width.toFloat(), 0f))
-                moveBy(Offset(width.toFloat() + 100f, 0f))
-                up()
-                // subtract here as we're in rtl and going in the opposite direction
-                expected = calculateFraction(left, right, centerX - 100)
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
-            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_closeThumbs_dragRight() {
-        val state = RangeSliderState(0.5f, 0.5f)
-        var slop = 0f
-
-        rule.setMaterialContent(lightColorScheme()) {
-            slop = LocalViewConfiguration.current.touchSlop
-            RangeSlider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.runOnUiThread {
-
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0.5f)
-            Truth.assertThat(state.activeRangeEnd).isEqualTo(0.5f)
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(slop, 0f))
-                moveBy(Offset(100f, 0f))
-                up()
-                // subtract here as we're in rtl and going in the opposite direction
-                expected = calculateFraction(left, right, centerX + 100)
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0.5f)
-            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_closeThumbs_dragLeft() {
-        val state = RangeSliderState(0.5f, 0.5f)
-        var slop = 0f
-
-        rule.setMaterialContent(lightColorScheme()) {
-            slop = LocalViewConfiguration.current.touchSlop
-            RangeSlider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.runOnUiThread {
-            Truth.assertThat(state.activeRangeStart).isEqualTo(0.5f)
-            Truth.assertThat(state.activeRangeEnd).isEqualTo(0.5f)
-        }
-
-        var expected = 0f
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(-slop - 1, 0f))
-                moveBy(Offset(-100f, 0f))
-                up()
-                // subtract here as we're in rtl and going in the opposite direction
-                expected = calculateFraction(left, right, centerX - 100)
-            }
-        rule.runOnIdle {
-            Truth.assertThat(state.activeRangeStart).isWithin(SliderTolerance).of(expected)
-            Truth.assertThat(state.activeRangeEnd).isEqualTo(0.5f)
-        }
-    }
-
-    /**
-     * Regression test for bug: 210289161 where RangeSlider was ignoring some modifiers like weight.
-     */
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_weightModifier() {
-        var sliderBounds = Rect(0f, 0f, 0f, 0f)
-        val state = RangeSliderState(0f, 0.5f, {})
-        rule.setMaterialContent(lightColorScheme()) {
-            with(LocalDensity.current) {
-                Row(Modifier.width(500.toDp())) {
-                    Spacer(Modifier.requiredSize(100.toDp()))
-                    RangeSlider(
-                        state = state,
-                        modifier = Modifier
-                            .testTag(tag)
-                            .weight(1f)
-                            .onGloballyPositioned {
-                                sliderBounds = it.boundsInParent()
-                            }
-                    )
-                    Spacer(Modifier.requiredSize(100.toDp()))
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            Truth.assertThat(sliderBounds.left).isEqualTo(100)
-            Truth.assertThat(sliderBounds.right).isEqualTo(400)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_semantics_continuous() {
-        val state = RangeSliderState(0f, 1f)
-
-        rule.setMaterialContent(lightColorScheme()) {
-            RangeSlider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.onAllNodes(isFocusable(), true)[0]
-            .assertRangeInfoEquals(ProgressBarRangeInfo(0f, 0f..1f, 0))
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.SetProgress))
-
-        rule.onAllNodes(isFocusable(), true)[1]
-            .assertRangeInfoEquals(ProgressBarRangeInfo(1f, 0f..1f, 0))
-            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.SetProgress))
-
-        rule.runOnUiThread {
-            state.activeRangeStart = 0.5f
-            state.activeRangeEnd = 0.75f
-        }
-
-        rule.onAllNodes(isFocusable(), true)[0].assertRangeInfoEquals(
-            ProgressBarRangeInfo(
-                0.5f,
-                0f..0.75f,
-                0
-            )
-        )
-
-        rule.onAllNodes(isFocusable(), true)[1].assertRangeInfoEquals(
-            ProgressBarRangeInfo(
-                0.75f,
-                0.5f..1f,
-                0
-            )
-        )
-
-        rule.onAllNodes(isFocusable(), true)[0]
-            .performSemanticsAction(SemanticsActions.SetProgress) { it(0.6f) }
-
-        rule.onAllNodes(isFocusable(), true)[1]
-            .performSemanticsAction(SemanticsActions.SetProgress) { it(0.8f) }
-
-        rule.onAllNodes(isFocusable(), true)[0]
-            .assertRangeInfoEquals(ProgressBarRangeInfo(0.6f, 0f..0.8f, 0))
-
-        rule.onAllNodes(isFocusable(), true)[1]
-            .assertRangeInfoEquals(ProgressBarRangeInfo(0.8f, 0.6f..1f, 0))
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_semantics_stepped() {
-        val state = RangeSliderState(
-            0f,
-            20f,
-            steps = 3,
-            valueRange = 0f..20f
-        )
-        // Slider with [0,5,10,15,20] possible values
-        rule.setMaterialContent(lightColorScheme()) {
-            RangeSlider(
-                state = state,
-                modifier = Modifier.testTag(tag)
-            )
-        }
-
-        rule.runOnUiThread {
-            state.activeRangeStart = 5f
-            state.activeRangeEnd = 10f
-        }
-
-        rule.onAllNodes(isFocusable(), true)[0].assertRangeInfoEquals(
-            ProgressBarRangeInfo(
-                5f,
-                0f..10f,
-                1
-            )
-        )
-
-        rule.onAllNodes(isFocusable(), true)[1].assertRangeInfoEquals(
-            ProgressBarRangeInfo(
-                10f,
-                5f..20f,
-                2,
-            )
-        )
-
-        rule.onAllNodes(isFocusable(), true)[0]
-            .performSemanticsAction(SemanticsActions.SetProgress) { it(10f) }
-
-        rule.onAllNodes(isFocusable(), true)[1]
-            .performSemanticsAction(SemanticsActions.SetProgress) { it(15f) }
-
-        rule.onAllNodes(isFocusable(), true)[0]
-            .assertRangeInfoEquals(ProgressBarRangeInfo(10f, 0f..15f, 2))
-
-        rule.onAllNodes(isFocusable(), true)[1]
-            .assertRangeInfoEquals(ProgressBarRangeInfo(15f, 10f..20f, 1))
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_thumb_recomposition() {
-        val state = RangeSliderState(
-            0f,
-            100f,
-            valueRange = 0f..100f
-        )
-        val startRecompositionCounter = RangeSliderRecompositionCounter()
-        val endRecompositionCounter = RangeSliderRecompositionCounter()
-
-        rule.setContent {
-            RangeSlider(
-                state = state,
-                modifier = Modifier.testTag(tag),
-                startThumb = { rangeSliderState ->
-                    startRecompositionCounter.OuterContent(rangeSliderState)
-                },
-                endThumb = { rangeSliderState ->
-                    endRecompositionCounter.OuterContent(rangeSliderState)
-                }
-            )
-        }
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(100f, 0f))
-                moveBy(Offset(-100f, 0f))
-                moveBy(Offset(100f, 0f))
-            }
-
-        rule.runOnIdle {
-            Truth.assertThat(startRecompositionCounter.outerRecomposition).isEqualTo(1)
-            Truth.assertThat(startRecompositionCounter.innerRecomposition).isEqualTo(3)
-            Truth.assertThat(endRecompositionCounter.outerRecomposition).isEqualTo(1)
-            Truth.assertThat(endRecompositionCounter.innerRecomposition).isEqualTo(3)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_track_recomposition() {
-        val state = RangeSliderState(
-            0f,
-            100f,
-            valueRange = 0f..100f
-        )
-        val recompositionCounter = RangeSliderRecompositionCounter()
-
-        rule.setContent {
-            RangeSlider(
-                state = state,
-                modifier = Modifier.testTag(tag),
-                track = { rangeSliderState ->
-                    recompositionCounter.OuterContent(rangeSliderState)
-                }
-            )
-        }
-
-        rule.onNodeWithTag(tag)
-            .performTouchInput {
-                down(center)
-                moveBy(Offset(100f, 0f))
-                moveBy(Offset(-100f, 0f))
-                moveBy(Offset(100f, 0f))
-            }
-
-        rule.runOnIdle {
-            Truth.assertThat(recompositionCounter.outerRecomposition).isEqualTo(1)
-            Truth.assertThat(recompositionCounter.innerRecomposition).isEqualTo(3)
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_parentWithInfiniteWidth_minWidth() {
-        val state = RangeSliderState(0f, 1f)
-        rule.setMaterialContentForSizeAssertions {
-            Box(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
-                RangeSlider(state)
-            }
-        }.assertWidthIsEqualTo(48.dp)
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Test
-    fun rangeSlider_rowWithInfiniteWidth() {
-        val state = RangeSliderState(0f, 1f)
-        expectAssertionError(false) {
-            rule.setContent {
-                Row(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
-                    RangeSlider(
-                        state = state,
-                        modifier = Modifier.weight(1f)
-                    )
-                }
-            }
-        }
-    }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Stable
-class SliderRecompositionCounter {
-    var innerRecomposition = 0
-    var outerRecomposition = 0
-
-    @Composable
-    fun OuterContent(state: SliderState) {
-        SideEffect { ++outerRecomposition }
-        Column {
-            Text("OuterContent")
-            InnerContent(state)
-        }
-    }
-
-    @Composable
-    private fun InnerContent(state: SliderState) {
-        SideEffect { ++innerRecomposition }
-        Text("InnerContent: ${state.value}")
-    }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Stable
-class RangeSliderRecompositionCounter {
-    var innerRecomposition = 0
-    var outerRecomposition = 0
-
-    @Composable
-    fun OuterContent(state: RangeSliderState) {
-        SideEffect {
-            ++outerRecomposition
-        }
-        Column {
-            Text("OuterContent")
-            InnerContent(state)
-        }
-    }
-
-    @Composable
-    private fun InnerContent(state: RangeSliderState) {
-        SideEffect { ++innerRecomposition }
-        Text("InnerContent: ${state.activeRangeStart..state.activeRangeEnd}")
-    }
-}
diff --git a/compose/material3/material3/src/androidAndroidTest/AndroidManifest.xml b/compose/material3/material3/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/AndroidManifest.xml
rename to compose/material3/material3/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AlertDialogScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AlertDialogScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
new file mode 100644
index 0000000..7b8d1a5
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
@@ -0,0 +1,424 @@
+/*
+ * Copyright 2021 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.material3
+
+import android.os.Build
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.BottomAppBarDefaults.bottomAppBarFabColor
+import androidx.compose.material3.TopAppBarDefaults.enterAlwaysScrollBehavior
+import androidx.compose.material3.tokens.TopAppBarSmallTokens
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalMaterial3Api::class)
+class AppBarScreenshotTest {
+
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+    @Test
+    fun smallAppBar_lightTheme() {
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                TopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
+                        }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        assertAppBarAgainstGolden(goldenIdentifier = "smallAppBar_lightTheme")
+    }
+
+    @Test
+    fun smallAppBar_lightTheme_clipsWhenCollapsedWithInsets() {
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            val behavior = enterAlwaysScrollBehavior(rememberTopAppBarState())
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                TopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
+                        }
+                    },
+                    scrollBehavior = behavior,
+                    windowInsets = WindowInsets(top = 30.dp),
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        composeTestRule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
+            // start from the bottom so we can drag enough
+            down(bottomCenter - Offset(1f, 1f))
+            moveBy(Offset(0f, -((TopAppBarSmallTokens.ContainerHeight - 10.dp).toPx())))
+        }
+
+        assertAppBarAgainstGolden(
+            goldenIdentifier = "smallAppBar_lightTheme_clipsWhenCollapsedWithInsets"
+        )
+    }
+
+    @Test
+    fun smallAppBar_darkTheme() {
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                TopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
+                        }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        assertAppBarAgainstGolden(goldenIdentifier = "smallAppBar_darkTheme")
+    }
+
+    @Test
+    fun centerAlignedAppBar_lightTheme() {
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                CenterAlignedTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
+                        }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        assertAppBarAgainstGolden(goldenIdentifier = "centerAlignedAppBar_lightTheme")
+    }
+
+    @Test
+    fun centerAlignedAppBar_darkTheme() {
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                CenterAlignedTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
+                        }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        assertAppBarAgainstGolden(goldenIdentifier = "centerAlignedAppBar_darkTheme")
+    }
+
+    @Test
+    fun mediumAppBar_lightTheme() {
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                MediumTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
+                        }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        assertAppBarAgainstGolden(goldenIdentifier = "mediumAppBar_lightTheme")
+    }
+
+    @Test
+    fun mediumAppBar_darkTheme() {
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                MediumTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
+                        }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        assertAppBarAgainstGolden(goldenIdentifier = "mediumAppBar_darkTheme")
+    }
+
+    @Test
+    fun largeAppBar_lightTheme() {
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                LargeTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
+                        }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        assertAppBarAgainstGolden(goldenIdentifier = "largeAppBar_lightTheme")
+    }
+
+    @Test
+    fun largeAppBar_darkTheme() {
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                LargeTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
+                        }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        assertAppBarAgainstGolden(goldenIdentifier = "largeAppBar_darkTheme")
+    }
+
+    @Test
+    fun bottomAppBarWithFAB_lightTheme() {
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(BottomAppBarTestTag)) {
+                BottomAppBar(
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Menu,
+                                contentDescription = "Menu"
+                            )
+                        }
+                    },
+                    floatingActionButton = {
+                        FloatingActionButton(
+                            onClick = { /* do something */ },
+                            containerColor = bottomAppBarFabColor,
+                            elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
+                        ) {
+                            Icon(Icons.Filled.Add, "Localized description")
+                        }
+                    }
+                )
+            }
+        }
+
+        assertAppBarAgainstGolden(
+            goldenIdentifier = "bottomAppBarWithFAB_lightTheme",
+            testTag = BottomAppBarTestTag
+        )
+    }
+
+    @Test
+    fun bottomAppBarWithFAB_darkTheme() {
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            Box(Modifier.testTag(BottomAppBarTestTag)) {
+                BottomAppBar(
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Menu,
+                                contentDescription = "Menu"
+                            )
+                        }
+                    },
+                    floatingActionButton = {
+                        FloatingActionButton(
+                            onClick = { /* do something */ },
+                            containerColor = bottomAppBarFabColor,
+                            elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
+                        ) {
+                            Icon(Icons.Filled.Add, "Localized description")
+                        }
+                    }
+                )
+            }
+        }
+
+        assertAppBarAgainstGolden(
+            goldenIdentifier = "bottomAppBarWithFAB_darkTheme",
+            testTag = BottomAppBarTestTag
+        )
+    }
+
+    private fun assertAppBarAgainstGolden(
+        goldenIdentifier: String,
+        testTag: String = TopAppBarTestTag
+    ) {
+        composeTestRule.onNodeWithTag(testTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, goldenIdentifier)
+    }
+
+    private val TopAppBarTestTag = "topAppBar"
+    private val BottomAppBarTestTag = "bottomAppBar"
+}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt
new file mode 100644
index 0000000..a913ceb
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt
@@ -0,0 +1,1724 @@
+/*
+ * Copyright 2021 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.material3
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material3.tokens.BottomAppBarTokens
+import androidx.compose.material3.tokens.TopAppBarLargeTokens
+import androidx.compose.material3.tokens.TopAppBarMediumTokens
+import androidx.compose.material3.tokens.TopAppBarSmallCenteredTokens
+import androidx.compose.material3.tokens.TopAppBarSmallTokens
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertContainsColor
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.getBoundsInRoot
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onAllNodesWithTag
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onLast
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalMaterial3Api::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class AppBarTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun smallTopAppBar_expandsToScreen() {
+        rule
+            .setMaterialContentForSizeAssertions {
+                TopAppBar(title = { Text("Title") })
+            }
+            .assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
+            .assertWidthIsEqualTo(rule.rootWidth())
+    }
+
+    @Test
+    fun smallTopAppBar_withTitle() {
+        val title = "Title"
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                TopAppBar(title = { Text(title) })
+            }
+        }
+        rule.onNodeWithText(title).assertIsDisplayed()
+    }
+
+    @Test
+    fun smallTopAppBar_default_positioning() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                TopAppBar(
+                    navigationIcon = {
+                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    },
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    }
+                )
+            }
+        }
+        assertSmallDefaultPositioning()
+    }
+
+    @Test
+    fun smallTopAppBar_noNavigationIcon_positioning() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                TopAppBar(
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    }
+                )
+            }
+        }
+        assertSmallPositioningWithoutNavigation()
+    }
+
+    @Test
+    fun smallTopAppBar_titleDefaultStyle() {
+        var textStyle: TextStyle? = null
+        var expectedTextStyle: TextStyle? = null
+        rule.setMaterialContent(lightColorScheme()) {
+            TopAppBar(title = {
+                Text("Title")
+                textStyle = LocalTextStyle.current
+                expectedTextStyle =
+                    MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont)
+            }
+            )
+        }
+        assertThat(textStyle).isNotNull()
+        assertThat(textStyle).isEqualTo(expectedTextStyle)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun smallTopAppBar_contentColor() {
+        var titleColor: Color = Color.Unspecified
+        var navigationIconColor: Color = Color.Unspecified
+        var actionsColor: Color = Color.Unspecified
+        var expectedTitleColor: Color = Color.Unspecified
+        var expectedNavigationIconColor: Color = Color.Unspecified
+        var expectedActionsColor: Color = Color.Unspecified
+        var expectedContainerColor: Color = Color.Unspecified
+
+        rule.setMaterialContent(lightColorScheme()) {
+            TopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                navigationIcon = {
+                    FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    navigationIconColor = LocalContentColor.current
+                    expectedNavigationIconColor =
+                        TopAppBarDefaults.topAppBarColors().navigationIconContentColor
+                    // fraction = 0f to indicate no scroll.
+                    expectedContainerColor = TopAppBarDefaults
+                        .topAppBarColors()
+                        .containerColor(colorTransitionFraction = 0f)
+                },
+                title = {
+                    Text("Title", Modifier.testTag(TitleTestTag))
+                    titleColor = LocalContentColor.current
+                    expectedTitleColor = TopAppBarDefaults
+                        .topAppBarColors().titleContentColor
+                },
+                actions = {
+                    FakeIcon(Modifier.testTag(ActionsTestTag))
+                    actionsColor = LocalContentColor.current
+                    expectedActionsColor = TopAppBarDefaults
+                        .topAppBarColors().actionIconContentColor
+                }
+            )
+        }
+        assertThat(navigationIconColor).isNotNull()
+        assertThat(titleColor).isNotNull()
+        assertThat(actionsColor).isNotNull()
+        assertThat(navigationIconColor).isEqualTo(expectedNavigationIconColor)
+        assertThat(titleColor).isEqualTo(expectedTitleColor)
+        assertThat(actionsColor).isEqualTo(expectedActionsColor)
+
+        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
+            .assertContainsColor(expectedContainerColor)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun smallTopAppBar_scrolledContentColor() {
+        var expectedScrolledContainerColor: Color = Color.Unspecified
+        lateinit var scrollBehavior: TopAppBarScrollBehavior
+        rule.setMaterialContent(lightColorScheme()) {
+            scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+            TopAppBar(
+                title = {
+                    Text("Title", Modifier.testTag(TitleTestTag))
+                    // fraction = 1f to indicate a scroll.
+                    expectedScrolledContainerColor =
+                        TopAppBarDefaults.topAppBarColors()
+                            .containerColor(colorTransitionFraction = 1f)
+                },
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                scrollBehavior = scrollBehavior
+            )
+        }
+
+        // Simulate scrolled content.
+        rule.runOnIdle {
+            scrollBehavior.state.contentOffset = -100f
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
+            .assertContainsColor(expectedScrolledContainerColor)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun smallTopAppBar_scrolledPositioning() {
+        lateinit var scrollBehavior: TopAppBarScrollBehavior
+        val scrollHeightOffsetDp = 20.dp
+        var scrollHeightOffsetPx = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
+            scrollHeightOffsetPx = with(LocalDensity.current) { scrollHeightOffsetDp.toPx() }
+            TopAppBar(
+                title = { Text("Title", Modifier.testTag(TitleTestTag)) },
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                scrollBehavior = scrollBehavior
+            )
+        }
+
+        // Simulate scrolled content.
+        rule.runOnIdle {
+            scrollBehavior.state.heightOffset = -scrollHeightOffsetPx
+            scrollBehavior.state.contentOffset = -scrollHeightOffsetPx
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag(TopAppBarTestTag)
+            .assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight - scrollHeightOffsetDp)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun smallTopAppBar_transparentContainerColor() {
+        val expectedColorBehindTopAppBar: Color = Color.Red
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(
+                modifier = Modifier
+                    .wrapContentHeight()
+                    .fillMaxWidth()
+                    .background(color = expectedColorBehindTopAppBar)
+            ) {
+                TopAppBar(
+                    modifier = Modifier.testTag(TopAppBarTestTag),
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    colors =
+                    TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
+                    scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
+                )
+            }
+        }
+        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
+            .assertContainsColor(expectedColorBehindTopAppBar)
+    }
+
+    @Test
+    fun centerAlignedTopAppBar_expandsToScreen() {
+        rule.setMaterialContentForSizeAssertions {
+            CenterAlignedTopAppBar(title = { Text("Title") })
+        }
+            .assertHeightIsEqualTo(TopAppBarSmallCenteredTokens.ContainerHeight)
+            .assertWidthIsEqualTo(rule.rootWidth())
+    }
+
+    @Test
+    fun centerAlignedTopAppBar_withTitle() {
+        val title = "Title"
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                CenterAlignedTopAppBar(title = { Text(title) })
+            }
+        }
+        rule.onNodeWithText(title).assertIsDisplayed()
+    }
+
+    @Test
+    fun centerAlignedTopAppBar_default_positioning() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                CenterAlignedTopAppBar(
+                    navigationIcon = {
+                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    },
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    }
+                )
+            }
+        }
+        assertSmallDefaultPositioning(isCenteredTitle = true)
+    }
+
+    @Test
+    fun centerAlignedTopAppBar_default_positioning_respectsWindowInsets() {
+        val padding = 10.dp
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                CenterAlignedTopAppBar(
+                    navigationIcon = {
+                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    },
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    },
+                    windowInsets = WindowInsets(padding, padding, padding, padding)
+                )
+            }
+        }
+        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
+        val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
+
+        rule.onNodeWithTag(NavigationIconTestTag)
+            // Navigation icon should be 4.dp from the start
+            .assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding + padding)
+            // Navigation icon should be centered within the height of the app bar.
+            .assertTopPositionInRootIsEqualTo(
+                appBarBottomEdgeY - AppBarTopAndBottomPadding - padding - FakeIconSize
+            )
+    }
+
+    @Test
+    fun centerAlignedTopAppBar_noNavigationIcon_positioning() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                CenterAlignedTopAppBar(
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    }
+                )
+            }
+        }
+        assertSmallPositioningWithoutNavigation(isCenteredTitle = true)
+    }
+
+    @Test
+    fun centerAlignedTopAppBar_longTextDoesNotOverflowToActions() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                CenterAlignedTopAppBar(
+                    title = {
+                        Text(
+                            text = "This is a very very very very long title",
+                            modifier = Modifier.testTag(TitleTestTag),
+                            overflow = TextOverflow.Ellipsis,
+                            maxLines = 1
+                        )
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    }
+                )
+            }
+        }
+        val actionsBounds = rule.onNodeWithTag(ActionsTestTag).getUnclippedBoundsInRoot()
+        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
+
+        // Check that the title does not render over the actions.
+        assertThat(titleBounds.right).isLessThan(actionsBounds.left)
+    }
+
+    @Test
+    fun centerAlignedTopAppBar_longTextDoesNotOverflowToNavigation() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                CenterAlignedTopAppBar(
+                    navigationIcon = {
+                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    },
+                    title = {
+                        Text(
+                            text = "This is a very very very very long title",
+                            modifier = Modifier.testTag(TitleTestTag),
+                            overflow = TextOverflow.Ellipsis,
+                            maxLines = 1
+                        )
+                    }
+
+                )
+            }
+        }
+        val navigationIconBounds =
+            rule.onNodeWithTag(NavigationIconTestTag).getUnclippedBoundsInRoot()
+        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
+
+        // Check that the title does not render over the navigation icon.
+        assertThat(titleBounds.left).isGreaterThan(navigationIconBounds.right)
+    }
+
+    @Test
+    fun centerAlignedTopAppBar_titleDefaultStyle() {
+        var textStyle: TextStyle? = null
+        var expectedTextStyle: TextStyle? = null
+        rule.setMaterialContent(lightColorScheme()) {
+            CenterAlignedTopAppBar(
+                title = {
+                    Text("Title")
+                    textStyle = LocalTextStyle.current
+                    expectedTextStyle =
+                        MaterialTheme.typography.fromToken(
+                            TopAppBarSmallCenteredTokens.HeadlineFont
+                        )
+                }
+            )
+        }
+        assertThat(textStyle).isNotNull()
+        assertThat(textStyle).isEqualTo(expectedTextStyle)
+    }
+
+    @Test
+    fun centerAlignedTopAppBar_measureWithNonZeroMinWidth() {
+        var appBarSize = IntSize.Zero
+        rule.setMaterialContent(lightColorScheme()) {
+            CenterAlignedTopAppBar(
+                modifier = Modifier.layout { measurable, constraints ->
+                    val placeable = measurable.measure(
+                        constraints.copy(minWidth = constraints.maxWidth)
+                    )
+                    appBarSize = IntSize(placeable.width, placeable.height)
+                    layout(placeable.width, placeable.height) {
+                        placeable.place(0, 0)
+                    }
+                },
+                title = {
+                    Text("Title")
+                }
+            )
+        }
+
+        assertThat(appBarSize).isNotEqualTo(IntSize.Zero)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun centerAlignedTopAppBar_contentColor() {
+        var titleColor: Color = Color.Unspecified
+        var navigationIconColor: Color = Color.Unspecified
+        var actionsColor: Color = Color.Unspecified
+        var expectedTitleColor: Color = Color.Unspecified
+        var expectedNavigationIconColor: Color = Color.Unspecified
+        var expectedActionsColor: Color = Color.Unspecified
+        var expectedContainerColor: Color = Color.Unspecified
+
+        rule.setMaterialContent(lightColorScheme()) {
+            CenterAlignedTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                navigationIcon = {
+                    FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    navigationIconColor = LocalContentColor.current
+                    expectedNavigationIconColor =
+                        TopAppBarDefaults.centerAlignedTopAppBarColors()
+                            .navigationIconContentColor
+                    // fraction = 0f to indicate no scroll.
+                    expectedContainerColor =
+                        TopAppBarDefaults.centerAlignedTopAppBarColors()
+                            .containerColor(colorTransitionFraction = 0f)
+                },
+                title = {
+                    Text("Title", Modifier.testTag(TitleTestTag))
+                    titleColor = LocalContentColor.current
+                    expectedTitleColor =
+                        TopAppBarDefaults.centerAlignedTopAppBarColors()
+                            .titleContentColor
+                },
+                actions = {
+                    FakeIcon(Modifier.testTag(ActionsTestTag))
+                    actionsColor = LocalContentColor.current
+                    expectedActionsColor =
+                        TopAppBarDefaults.centerAlignedTopAppBarColors()
+                            .actionIconContentColor
+                }
+            )
+        }
+        assertThat(navigationIconColor).isNotNull()
+        assertThat(titleColor).isNotNull()
+        assertThat(actionsColor).isNotNull()
+        assertThat(navigationIconColor).isEqualTo(expectedNavigationIconColor)
+        assertThat(titleColor).isEqualTo(expectedTitleColor)
+        assertThat(actionsColor).isEqualTo(expectedActionsColor)
+
+        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
+            .assertContainsColor(expectedContainerColor)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun centerAlignedTopAppBar_scrolledContentColor() {
+        var expectedScrolledContainerColor: Color = Color.Unspecified
+        lateinit var scrollBehavior: TopAppBarScrollBehavior
+
+        rule.setMaterialContent(lightColorScheme()) {
+            scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+            CenterAlignedTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                title = {
+                    Text("Title", Modifier.testTag(TitleTestTag))
+                    // fraction = 1f to indicate a scroll.
+                    expectedScrolledContainerColor =
+                        TopAppBarDefaults.centerAlignedTopAppBarColors()
+                            .containerColor(colorTransitionFraction = 1f)
+                },
+                scrollBehavior = scrollBehavior
+            )
+        }
+
+        // Simulate scrolled content.
+        rule.runOnIdle {
+            scrollBehavior.state.contentOffset = -100f
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
+            .assertContainsColor(expectedScrolledContainerColor)
+    }
+
+    @Test
+    fun mediumTopAppBar_expandsToScreen() {
+        rule.setMaterialContentForSizeAssertions {
+            MediumTopAppBar(title = { Text("Medium Title") })
+        }
+            .assertHeightIsEqualTo(TopAppBarMediumTokens.ContainerHeight)
+            .assertWidthIsEqualTo(rule.rootWidth())
+    }
+
+    @Test
+    fun mediumTopAppBar_expanded_positioning() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                MediumTopAppBar(
+                    navigationIcon = {
+                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    },
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    }
+                )
+            }
+        }
+
+        // The bottom text baseline should be 24.dp from the bottom of the app bar.
+        assertMediumOrLargeDefaultPositioning(
+            expectedAppBarHeight = TopAppBarMediumTokens.ContainerHeight,
+            bottomTextPadding = 24.dp
+        )
+    }
+
+    @Test
+    fun mediumTopAppBar_scrolled_positioning() {
+        val windowInsets = WindowInsets(13.dp, 13.dp, 13.dp, 13.dp)
+        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                MediumTopAppBar(
+                    navigationIcon = {
+                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    },
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    },
+                    scrollBehavior = scrollBehavior,
+                    windowInsets = windowInsets
+                )
+            }
+        }
+        assertMediumOrLargeScrolledHeight(
+            TopAppBarMediumTokens.ContainerHeight,
+            TopAppBarSmallTokens.ContainerHeight,
+            windowInsets,
+            content
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun mediumTopAppBar_scrolledContainerColor() {
+        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+            MediumTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                title = {
+                    Text("Title", Modifier.testTag(TitleTestTag))
+                },
+                scrollBehavior = scrollBehavior
+            )
+        }
+
+        assertMediumOrLargeScrolledColors(
+            appBarMaxHeight = TopAppBarMediumTokens.ContainerHeight,
+            appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
+            titleContentColor = Color.Unspecified,
+            content = content
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun mediumTopAppBar_scrolledColorsWithCustomTitleTextColor() {
+        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+            MediumTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                title = {
+                    Text(
+                        text = "Title", Modifier.testTag(TitleTestTag),
+                        color = Color.Green
+                    )
+                },
+                scrollBehavior = scrollBehavior
+            )
+        }
+        assertMediumOrLargeScrolledColors(
+            appBarMaxHeight = TopAppBarMediumTokens.ContainerHeight,
+            appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
+            titleContentColor = Color.Green,
+            content = content
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun mediumTopAppBar_semantics() {
+        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+            MediumTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                title = {
+                    Text("Title", Modifier.testTag(TitleTestTag))
+                },
+                scrollBehavior = scrollBehavior
+            )
+        }
+
+        assertMediumOrLargeScrolledSemantics(
+            TopAppBarMediumTokens.ContainerHeight,
+            TopAppBarSmallTokens.ContainerHeight,
+            content
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun largeTopAppBar_semantics() {
+        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+            LargeTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                title = {
+                    Text("Title", Modifier.testTag(TitleTestTag))
+                },
+                scrollBehavior = scrollBehavior
+            )
+        }
+        assertMediumOrLargeScrolledSemantics(
+            TopAppBarLargeTokens.ContainerHeight,
+            TopAppBarSmallTokens.ContainerHeight,
+            content
+        )
+    }
+
+    @Test
+    fun largeTopAppBar_expandsToScreen() {
+        rule.setMaterialContentForSizeAssertions {
+            LargeTopAppBar(title = { Text("Large Title") })
+        }
+            .assertHeightIsEqualTo(TopAppBarLargeTokens.ContainerHeight)
+            .assertWidthIsEqualTo(rule.rootWidth())
+    }
+
+    @Test
+    fun largeTopAppBar_expanded_positioning() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                LargeTopAppBar(
+                    navigationIcon = {
+                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    },
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    }
+                )
+            }
+        }
+
+        // The bottom text baseline should be 28.dp from the bottom of the app bar.
+        assertMediumOrLargeDefaultPositioning(
+            expectedAppBarHeight = TopAppBarLargeTokens.ContainerHeight,
+            bottomTextPadding = 28.dp
+        )
+    }
+
+    @Test
+    fun largeTopAppBar_scrolled_positioning() {
+        val windowInsets = WindowInsets(4.dp, 4.dp, 4.dp, 4.dp)
+        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                LargeTopAppBar(
+                    navigationIcon = {
+                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    },
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    },
+                    scrollBehavior = scrollBehavior,
+                    windowInsets = windowInsets
+                )
+            }
+        }
+        assertMediumOrLargeScrolledHeight(
+            TopAppBarLargeTokens.ContainerHeight,
+            TopAppBarSmallTokens.ContainerHeight,
+            windowInsets,
+            content
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun largeTopAppBar_scrolledContainerColor() {
+        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+            LargeTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                title = {
+                    Text("Title", Modifier.testTag(TitleTestTag))
+                },
+                scrollBehavior = scrollBehavior,
+            )
+        }
+        assertMediumOrLargeScrolledColors(
+            appBarMaxHeight = TopAppBarLargeTokens.ContainerHeight,
+            appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
+            titleContentColor = Color.Unspecified,
+            content = content
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun largeTopAppBar_scrolledColorsWithCustomTitleTextColor() {
+        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+            LargeTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                title = {
+                    Text(
+                        text = "Title", Modifier.testTag(TitleTestTag),
+                        color = Color.Red
+                    )
+                },
+                scrollBehavior = scrollBehavior,
+            )
+        }
+        assertMediumOrLargeScrolledColors(
+            appBarMaxHeight = TopAppBarLargeTokens.ContainerHeight,
+            appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
+            titleContentColor = Color.Red,
+            content = content
+        )
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun topAppBar_enterAlways_allowHorizontalScroll() {
+        lateinit var state: LazyListState
+        rule.setMaterialContent(lightColorScheme()) {
+            state = rememberLazyListState()
+            MultiPageContent(TopAppBarDefaults.enterAlwaysScrollBehavior(), state)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun topAppBar_exitUntilCollapsed_allowHorizontalScroll() {
+        lateinit var state: LazyListState
+        rule.setMaterialContent(lightColorScheme()) {
+            state = rememberLazyListState()
+            MultiPageContent(TopAppBarDefaults.exitUntilCollapsedScrollBehavior(), state)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun topAppBar_pinned_allowHorizontalScroll() {
+        lateinit var state: LazyListState
+        rule.setMaterialContent(lightColorScheme()) {
+            state = rememberLazyListState()
+            MultiPageContent(
+                TopAppBarDefaults.pinnedScrollBehavior(),
+                state
+            )
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun topAppBar_smallPinnedDraggedAppBar() {
+        rule.setMaterialContentForSizeAssertions {
+            TopAppBar(
+                title = {
+                    Text("Title")
+                },
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+            )
+        }
+
+        rule.onNodeWithTag(TopAppBarTestTag)
+            .assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
+
+        // Drag the app bar up half its height.
+        rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
+            down(Offset(x = 0f, y = height / 2f))
+            moveTo(Offset(x = 0f, y = 0f))
+        }
+        rule.waitForIdle()
+        // Check that the app bar did not collapse.
+        rule.onNodeWithTag(TopAppBarTestTag)
+            .assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
+    }
+
+    @Test
+    fun topAppBar_mediumDraggedAppBar() {
+        rule.setMaterialContentForSizeAssertions {
+            MediumTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                title = {
+                    Text("Title")
+                },
+                scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+            )
+        }
+
+        rule.onNodeWithTag(TopAppBarTestTag)
+            .assertHeightIsEqualTo(TopAppBarMediumTokens.ContainerHeight)
+
+        // Drag up the app bar.
+        rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
+            down(Offset(x = 0f, y = height - 20f))
+            moveTo(Offset(x = 0f, y = 0f))
+        }
+        rule.waitForIdle()
+        // Check that the app bar collapsed to its small size constraints (i.e.
+        // TopAppBarSmallTokens.ContainerHeight).
+        rule.onNodeWithTag(TopAppBarTestTag)
+            .assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
+    }
+
+    @Test
+    fun topAppBar_dragSnapToCollapsed() {
+        rule.setMaterialContentForSizeAssertions {
+            LargeTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                title = {
+                    Text("Title")
+                },
+                scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+            )
+        }
+
+        rule.onNodeWithTag(TopAppBarTestTag)
+            .assertHeightIsEqualTo(TopAppBarLargeTokens.ContainerHeight)
+
+        // Slightly drag up the app bar.
+        rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
+            down(Offset(x = 0f, y = height - 20f))
+            moveTo(Offset(x = 0f, y = height - 40f))
+            up()
+        }
+        rule.waitForIdle()
+
+        // Check that the app bar returned to its expanded size (i.e. fully expanded).
+        rule.onNodeWithTag(TopAppBarTestTag)
+            .assertHeightIsEqualTo(TopAppBarLargeTokens.ContainerHeight)
+
+        // Drag up the app bar to the point it should continue to collapse after.
+        rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
+            down(Offset(x = 0f, y = height - 20f))
+            moveTo(Offset(x = 0f, y = 40f))
+            up()
+        }
+        rule.waitForIdle()
+
+        // Check that the app bar collapsed to its small size constraints (i.e.
+        // TopAppBarSmallTokens.ContainerHeight).
+        rule.onNodeWithTag(TopAppBarTestTag)
+            .assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
+    }
+
+    @Test
+    fun topAppBar_dragWithSnapDisabled() {
+        rule.setMaterialContentForSizeAssertions {
+            LargeTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                title = {
+                    Text("Title")
+                },
+                scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
+                    snapAnimationSpec = null
+                )
+            )
+        }
+
+        // Check that the app bar stayed at its position (i.e. its bounds are with a smaller height)
+        val boundsBefore = rule.onNodeWithTag(TopAppBarTestTag).getBoundsInRoot()
+        TopAppBarLargeTokens.ContainerHeight.assertIsEqualTo(
+            expected = boundsBefore.height,
+            subject = "container height"
+        )
+        // Slightly drag up the app bar.
+        rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
+            down(Offset(x = 100f, y = height - 20f))
+            moveTo(Offset(x = 100f, y = height - 100f))
+            up()
+        }
+        rule.waitForIdle()
+
+        // Check that the app bar did not snap back to its fully expanded height, or collapsed to
+        // its collapsed height.
+        val boundsAfter = rule.onNodeWithTag(TopAppBarTestTag).getBoundsInRoot()
+        assertThat(TopAppBarLargeTokens.ContainerHeight).isGreaterThan(boundsAfter.height)
+        assertThat(TopAppBarSmallTokens.ContainerHeight).isLessThan(boundsAfter.height)
+    }
+
+    @Test
+    fun state_restoresTopAppBarState() {
+        val restorationTester = StateRestorationTester(rule)
+        var topAppBarState: TopAppBarState? = null
+        restorationTester.setContent {
+            topAppBarState = rememberTopAppBarState()
+        }
+
+        rule.runOnIdle {
+            topAppBarState!!.heightOffsetLimit = -350f
+            topAppBarState!!.heightOffset = -300f
+            topAppBarState!!.contentOffset = -550f
+        }
+
+        topAppBarState = null
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(topAppBarState!!.heightOffsetLimit).isEqualTo(-350f)
+            assertThat(topAppBarState!!.heightOffset).isEqualTo(-300f)
+            assertThat(topAppBarState!!.contentOffset).isEqualTo(-550f)
+        }
+    }
+
+    @Test
+    fun bottomAppBarWithFAB_heightIsFromSpec() {
+        rule
+            .setMaterialContentForSizeAssertions {
+                BottomAppBar(
+                    actions = {},
+                    floatingActionButton = {
+                        FloatingActionButton(
+                            onClick = { /* do something */ },
+                            containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                            elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
+                        ) {
+                            Icon(Icons.Filled.Add, "Localized description")
+                        }
+                    })
+            }
+            .assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight)
+            .assertWidthIsEqualTo(rule.rootWidth())
+    }
+
+    @Test
+    fun bottomAppBarWithFAB_respectsWindowInsets() {
+        rule
+            .setMaterialContentForSizeAssertions {
+                BottomAppBar(
+                    actions = {},
+                    windowInsets = WindowInsets(10.dp, 10.dp, 10.dp, 10.dp),
+                    floatingActionButton = {
+                        FloatingActionButton(
+                            onClick = { /* do something */ },
+                            containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                            elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
+                        ) {
+                            Icon(Icons.Filled.Add, "Localized description")
+                        }
+                    })
+            }
+            .assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight + 20.dp)
+            .assertWidthIsEqualTo(rule.rootWidth())
+    }
+
+    @Test
+    fun bottomAppBar_FABshown_whenActionsOverflowRow() {
+        rule.setMaterialContent(lightColorScheme()) {
+            BottomAppBar(
+                actions = {
+                    repeat(20) {
+                        FakeIcon(Modifier)
+                    }
+                },
+                floatingActionButton = {
+                    FloatingActionButton(
+                        onClick = { /* do something */ },
+                        modifier = Modifier.testTag("FAB"),
+                        containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                        elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
+                    ) {
+                        Icon(Icons.Filled.Add, "Localized description")
+                    }
+                })
+        }
+        rule.onNodeWithTag("FAB").assertIsDisplayed()
+    }
+
+    @Test
+    fun bottomAppBar_widthExpandsToScreen() {
+        rule
+            .setMaterialContentForSizeAssertions {
+                BottomAppBar {}
+            }
+            .assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight)
+            .assertWidthIsEqualTo(rule.rootWidth())
+    }
+
+    @Test
+    fun bottomAppBar_default_positioning() {
+        rule.setMaterialContent(lightColorScheme()) {
+            BottomAppBar(Modifier.testTag("bar")) {
+                FakeIcon(Modifier.testTag("icon"))
+            }
+        }
+
+        val appBarBounds = rule.onNodeWithTag("bar").getUnclippedBoundsInRoot()
+        val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
+
+        val defaultPadding = BottomAppBarDefaults.ContentPadding
+        rule.onNodeWithTag("icon")
+            // Child icon should be 4.dp from the start
+            .assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
+            // Child icon should be 10.dp from the top
+            .assertTopPositionInRootIsEqualTo(
+                defaultPadding.calculateTopPadding() +
+                    (appBarBottomEdgeY - defaultPadding.calculateTopPadding() - FakeIconSize) / 2
+            )
+    }
+
+    @Test
+    fun bottomAppBar_default_positioning_respectsContentPadding() {
+        val topPadding = 5.dp
+        rule.setMaterialContent(lightColorScheme()) {
+            BottomAppBar(
+                Modifier.testTag("bar"),
+                contentPadding = PaddingValues(top = topPadding, start = 3.dp)
+            ) {
+                FakeIcon(Modifier.testTag("icon"))
+            }
+        }
+
+        val appBarBounds = rule.onNodeWithTag("bar").getUnclippedBoundsInRoot()
+        val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
+
+        rule.onNodeWithTag("icon")
+            // Child icon should be 4.dp from the start
+            .assertLeftPositionInRootIsEqualTo(3.dp)
+            // Child icon should be 10.dp from the top
+            .assertTopPositionInRootIsEqualTo(
+                (appBarBottomEdgeY - topPadding - FakeIconSize) / 2 + 5.dp
+            )
+    }
+
+    @Test
+    fun bottomAppBarWithFAB_default_positioning() {
+        rule.setMaterialContent(lightColorScheme()) {
+            BottomAppBar(
+                actions = {},
+                Modifier.testTag("bar"),
+                floatingActionButton = {
+                    FloatingActionButton(
+                        onClick = { /* do something */ },
+                        modifier = Modifier.testTag("FAB"),
+                        containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                        elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
+                    ) {
+                        Icon(Icons.Filled.Add, "Localized description")
+                    }
+                })
+        }
+
+        val appBarBounds = rule.onNodeWithTag("bar").getUnclippedBoundsInRoot()
+
+        val fabBounds = rule.onNodeWithTag("FAB").getUnclippedBoundsInRoot()
+
+        rule.onNodeWithTag("FAB")
+            // FAB should be 16.dp from the end
+            .assertLeftPositionInRootIsEqualTo(appBarBounds.width - 16.dp - fabBounds.width)
+            // FAB should be 12.dp from the top
+            .assertTopPositionInRootIsEqualTo(12.dp)
+    }
+
+    @Test
+    fun bottomAppBar_exitAlways_scaffoldWithFAB_default_positioning() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+            Scaffold(
+                modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+                bottomBar = {
+                    BottomAppBar(
+                        modifier = Modifier.testTag(BottomAppBarTestTag),
+                        scrollBehavior = scrollBehavior
+                    ) {}
+                },
+                floatingActionButton = {
+                    FloatingActionButton(
+                        modifier = Modifier
+                            .testTag("FAB")
+                            .offset(y = 4.dp),
+                        onClick = { /* do something */ },
+                    ) {}
+                },
+                floatingActionButtonPosition = FabPosition.EndOverlay
+            ) {}
+        }
+
+        val appBarBounds = rule.onNodeWithTag(BottomAppBarTestTag).getUnclippedBoundsInRoot()
+        val fabBounds = rule.onNodeWithTag("FAB").getUnclippedBoundsInRoot()
+        rule.onNodeWithTag("FAB")
+            // FAB should be 16.dp from the end
+            .assertLeftPositionInRootIsEqualTo(appBarBounds.width - 16.dp - fabBounds.width)
+            // FAB should be 12.dp from the bottom
+            .assertTopPositionInRootIsEqualTo(rule.rootHeight() - 12.dp - fabBounds.height)
+    }
+
+    @Test
+    fun bottomAppBar_exitAlways_scaffoldWithFAB_scrolled_positioning() {
+        lateinit var scrollBehavior: BottomAppBarScrollBehavior
+        val scrollHeightOffsetDp = 20.dp
+        var scrollHeightOffsetPx = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+            scrollHeightOffsetPx = with(LocalDensity.current) { scrollHeightOffsetDp.toPx() }
+            Scaffold(
+                modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+                bottomBar = {
+                    BottomAppBar(
+                        modifier = Modifier.testTag(BottomAppBarTestTag),
+                        scrollBehavior = scrollBehavior
+                    ) {}
+                },
+                floatingActionButton = {
+                    FloatingActionButton(
+                        modifier = Modifier
+                            .testTag("FAB")
+                            .offset(y = 4.dp),
+                        onClick = { /* do something */ },
+                    ) {}
+                },
+                floatingActionButtonPosition = FabPosition.EndOverlay
+            ) {}
+        }
+
+        // Simulate scrolled content.
+        rule.runOnIdle {
+            scrollBehavior.state.heightOffset = -scrollHeightOffsetPx
+            scrollBehavior.state.contentOffset = -scrollHeightOffsetPx
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag(BottomAppBarTestTag)
+            .assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight - scrollHeightOffsetDp)
+
+        val appBarBounds = rule.onNodeWithTag(BottomAppBarTestTag).getUnclippedBoundsInRoot()
+        val fabBounds = rule.onNodeWithTag("FAB").getUnclippedBoundsInRoot()
+        rule.onNodeWithTag("FAB")
+            // FAB should be 16.dp from the end
+            .assertLeftPositionInRootIsEqualTo(appBarBounds.width - 16.dp - fabBounds.width)
+            // FAB should be 12.dp from the bottom
+            .assertTopPositionInRootIsEqualTo(rule.rootHeight() - 12.dp - fabBounds.height)
+    }
+
+    @Test
+    fun bottomAppBar_exitAlways_allowHorizontalScroll() {
+        lateinit var state: LazyListState
+        rule.setMaterialContent(lightColorScheme()) {
+            state = rememberLazyListState()
+            MultiPageContent(BottomAppBarDefaults.exitAlwaysScrollBehavior(), state)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Composable
+    private fun MultiPageContent(scrollBehavior: TopAppBarScrollBehavior, state: LazyListState) {
+        Scaffold(
+            modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+            topBar = {
+                TopAppBar(
+                    title = { Text(text = "Title") },
+                    modifier = Modifier.testTag(TopAppBarTestTag),
+                    scrollBehavior = scrollBehavior
+                )
+            }
+        ) { contentPadding ->
+            LazyRow(
+                Modifier
+                    .fillMaxSize()
+                    .testTag(LazyListTag), state
+            ) {
+                items(2) { page ->
+                    LazyColumn(
+                        modifier = Modifier.fillParentMaxSize(),
+                        contentPadding = contentPadding
+                    ) {
+                        items(50) {
+                            Text(
+                                modifier = Modifier.fillParentMaxWidth(),
+                                text = "Item #$page x $it"
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @Composable
+    private fun MultiPageContent(scrollBehavior: BottomAppBarScrollBehavior, state: LazyListState) {
+        Scaffold(
+            modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+            bottomBar = {
+                BottomAppBar(
+                    modifier = Modifier.testTag(BottomAppBarTestTag),
+                    scrollBehavior = scrollBehavior
+                ) {}
+            }
+        ) { contentPadding ->
+            LazyRow(
+                Modifier
+                    .fillMaxSize()
+                    .testTag(LazyListTag), state
+            ) {
+                items(2) { page ->
+                    LazyColumn(
+                        modifier = Modifier.fillParentMaxSize(),
+                        contentPadding = contentPadding
+                    ) {
+                        items(50) {
+                            Text(
+                                modifier = Modifier.fillParentMaxWidth(),
+                                text = "Item #$page x $it"
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks the app bar's components positioning when it's a [TopAppBar], a
+     * [CenterAlignedTopAppBar], or a larger app bar that is scrolled up and collapsed into a small
+     * configuration and there is no navigation icon.
+     */
+    private fun assertSmallPositioningWithoutNavigation(isCenteredTitle: Boolean = false) {
+        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
+        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
+
+        val titleNode = rule.onNodeWithTag(TitleTestTag)
+        // Title should be vertically centered
+        titleNode.assertTopPositionInRootIsEqualTo((appBarBounds.height - titleBounds.height) / 2)
+        if (isCenteredTitle) {
+            // Title should be horizontally centered
+            titleNode.assertLeftPositionInRootIsEqualTo(
+                (appBarBounds.width - titleBounds.width) / 2
+            )
+        } else {
+            // Title should now be placed 16.dp from the start, as there is no navigation icon
+            // 4.dp padding for the whole app bar + 12.dp inset
+            titleNode.assertLeftPositionInRootIsEqualTo(4.dp + 12.dp)
+        }
+
+        rule.onNodeWithTag(ActionsTestTag)
+            // Action should still be placed at the end
+            .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
+    }
+
+    /**
+     * Checks the app bar's components positioning when it's a [TopAppBar] or a
+     * [CenterAlignedTopAppBar].
+     */
+    private fun assertSmallDefaultPositioning(isCenteredTitle: Boolean = false) {
+        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
+        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
+        val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
+
+        rule.onNodeWithTag(NavigationIconTestTag)
+            // Navigation icon should be 4.dp from the start
+            .assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
+            // Navigation icon should be centered within the height of the app bar.
+            .assertTopPositionInRootIsEqualTo(
+                appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
+            )
+
+        val titleNode = rule.onNodeWithTag(TitleTestTag)
+        // Title should be vertically centered
+        titleNode.assertTopPositionInRootIsEqualTo((appBarBounds.height - titleBounds.height) / 2)
+        if (isCenteredTitle) {
+            // Title should be horizontally centered
+            titleNode.assertLeftPositionInRootIsEqualTo(
+                (appBarBounds.width - titleBounds.width) / 2
+            )
+        } else {
+            // Title should be 56.dp from the start
+            // 4.dp padding for the whole app bar + 48.dp icon size + 4.dp title padding.
+            titleNode.assertLeftPositionInRootIsEqualTo(4.dp + FakeIconSize + 4.dp)
+        }
+
+        rule.onNodeWithTag(ActionsTestTag)
+            // Action should be placed at the end
+            .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
+            // Action should be 8.dp from the top
+            .assertTopPositionInRootIsEqualTo(
+                appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
+            )
+    }
+
+    /**
+     * Checks the app bar's components positioning when it's a [MediumTopAppBar] or a
+     * [LargeTopAppBar].
+     */
+    private fun assertMediumOrLargeDefaultPositioning(
+        expectedAppBarHeight: Dp,
+        bottomTextPadding: Dp
+    ) {
+        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
+        appBarBounds.height.assertIsEqualTo(expectedAppBarHeight, "top app bar height")
+
+        // Expecting the title composable to be reused for the top and bottom rows of the top app
+        // bar, so obtaining the node with the title tag should return two nodes, one for each row.
+        val allTitleNodes = rule.onAllNodesWithTag(TitleTestTag, true)
+        allTitleNodes.assertCountEquals(2)
+        val topTitleNode = allTitleNodes.onFirst()
+        val bottomTitleNode = allTitleNodes.onLast()
+
+        val topTitleBounds = topTitleNode.getUnclippedBoundsInRoot()
+        val bottomTitleBounds = bottomTitleNode.getUnclippedBoundsInRoot()
+        val topAppBarBottomEdgeY = appBarBounds.top + TopAppBarSmallTokens.ContainerHeight
+        val bottomAppBarBottomEdgeY = appBarBounds.top + appBarBounds.height
+
+        rule.onNodeWithTag(NavigationIconTestTag)
+            // Navigation icon should be 4.dp from the start
+            .assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
+            // Navigation icon should be centered within the height of the top part of the app bar.
+            .assertTopPositionInRootIsEqualTo(
+                topAppBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
+            )
+
+        rule.onNodeWithTag(ActionsTestTag)
+            // Action should be placed at the end
+            .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
+            // Action should be 8.dp from the top
+            .assertTopPositionInRootIsEqualTo(
+                topAppBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
+            )
+
+        topTitleNode
+            // Top title should be 56.dp from the start
+            // 4.dp padding for the whole app bar + 48.dp icon size + 4.dp title padding.
+            .assertLeftPositionInRootIsEqualTo(4.dp + FakeIconSize + 4.dp)
+            // Title should be vertically centered in the top part, which has a height of a small
+            // app bar.
+            .assertTopPositionInRootIsEqualTo((topAppBarBottomEdgeY - topTitleBounds.height) / 2)
+
+        bottomTitleNode
+            // Bottom title should be 16.dp from the start.
+            .assertLeftPositionInRootIsEqualTo(16.dp)
+
+        // Check if the bottom text baseline is at the expected distance from the bottom of the
+        // app bar.
+        val bottomTextBaselineY = bottomTitleBounds.top + bottomTitleNode.getLastBaselinePosition()
+        (bottomAppBarBottomEdgeY - bottomTextBaselineY).assertIsEqualTo(
+            bottomTextPadding,
+            "text baseline distance from the bottom"
+        )
+    }
+
+    /**
+     * Checks that changing values at a [MediumTopAppBar] or a [LargeTopAppBar] scroll behavior
+     * affects the height of the app bar.
+     *
+     * This check partially and fully collapses the app bar to test its height.
+     *
+     * @param appBarMaxHeight the max height of the app bar [content]
+     * @param appBarMinHeight the min height of the app bar [content]
+     * @param content a Composable that adds a MediumTopAppBar or a LargeTopAppBar
+     */
+    @OptIn(ExperimentalMaterial3Api::class)
+    private fun assertMediumOrLargeScrolledHeight(
+        appBarMaxHeight: Dp,
+        appBarMinHeight: Dp,
+        windowInsets: WindowInsets,
+        content: @Composable (TopAppBarScrollBehavior?) -> Unit
+    ) {
+        val (topInset, bottomInset) = with(rule.density) {
+            windowInsets.getTop(this).toDp() to windowInsets.getBottom(this).toDp()
+        }
+        val fullyCollapsedOffsetDp = appBarMaxHeight - appBarMinHeight
+        val partiallyCollapsedOffsetDp = fullyCollapsedOffsetDp / 3
+        var partiallyCollapsedHeightOffsetPx = 0f
+        var fullyCollapsedHeightOffsetPx = 0f
+        lateinit var scrollBehavior: TopAppBarScrollBehavior
+        rule.setMaterialContent(lightColorScheme()) {
+            scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+            with(LocalDensity.current) {
+                partiallyCollapsedHeightOffsetPx = partiallyCollapsedOffsetDp.toPx()
+                fullyCollapsedHeightOffsetPx = fullyCollapsedOffsetDp.toPx()
+            }
+
+            content(scrollBehavior)
+        }
+
+        // Simulate a partially collapsed app bar.
+        rule.runOnIdle {
+            scrollBehavior.state.heightOffset = -partiallyCollapsedHeightOffsetPx
+            scrollBehavior.state.contentOffset = -partiallyCollapsedHeightOffsetPx
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag(TopAppBarTestTag)
+            .assertHeightIsEqualTo(
+                appBarMaxHeight - partiallyCollapsedOffsetDp + topInset + bottomInset
+            )
+
+        // Simulate a fully collapsed app bar.
+        rule.runOnIdle {
+            scrollBehavior.state.heightOffset = -fullyCollapsedHeightOffsetPx
+            // Simulate additional content scroll beyond the max offset scroll.
+            scrollBehavior.state.contentOffset =
+                -fullyCollapsedHeightOffsetPx - partiallyCollapsedHeightOffsetPx
+        }
+        rule.waitForIdle()
+        // Check that the app bar collapsed to its min height.
+        rule.onNodeWithTag(TopAppBarTestTag).assertHeightIsEqualTo(
+            appBarMinHeight + topInset + bottomInset
+        )
+    }
+
+    /**
+     * Checks that changing values at a [MediumTopAppBar] or a [LargeTopAppBar] scroll behavior
+     * affects the container color and the title's content color of the app bar.
+     *
+     * @param appBarMaxHeight the max height of the app bar [content]
+     * @param appBarMinHeight the min height of the app bar [content]
+     * @param titleContentColor text content color expected for the app bar's title.
+     * @param content a Composable that adds a MediumTopAppBar or a LargeTopAppBar
+     */
+    @OptIn(ExperimentalMaterial3Api::class)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    private fun assertMediumOrLargeScrolledColors(
+        appBarMaxHeight: Dp,
+        appBarMinHeight: Dp,
+        titleContentColor: Color,
+        content: @Composable (TopAppBarScrollBehavior?) -> Unit
+    ) {
+        // Note: This value is specifically picked to avoid precision issues when asserting the
+        // color values further down this test.
+        val fullyCollapsedOffsetDp = appBarMaxHeight - appBarMinHeight
+        var fullyCollapsedHeightOffsetPx = 0f
+        var fullyCollapsedContainerColor: Color = Color.Unspecified
+        var expandedAppBarBackgroundColor: Color = Color.Unspecified
+        var titleColor = titleContentColor
+        lateinit var scrollBehavior: TopAppBarScrollBehavior
+        rule.setMaterialContent(lightColorScheme()) {
+            scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+            // Using the mediumTopAppBarColors for both Medium and Large top app bars, as the
+            // current content color settings are the same.
+            expandedAppBarBackgroundColor = TopAppBarMediumTokens.ContainerColor.value
+            fullyCollapsedContainerColor =
+                TopAppBarDefaults.mediumTopAppBarColors()
+                    .containerColor(colorTransitionFraction = 1f)
+
+            // Resolve the title's content color. The default implementation returns the same color
+            // regardless of the fraction, and the color is applied later with alpha.
+            if (titleColor == Color.Unspecified) {
+                titleColor =
+                    TopAppBarDefaults.mediumTopAppBarColors().titleContentColor
+            }
+
+            with(LocalDensity.current) {
+                fullyCollapsedHeightOffsetPx = fullyCollapsedOffsetDp.toPx()
+            }
+
+            content(scrollBehavior)
+        }
+
+        // Expecting the title composable to be reused for the top and bottom rows of the top app
+        // bar, so obtaining the node with the title tag should return two nodes, one for each row.
+        val allTitleNodes = rule.onAllNodesWithTag(TitleTestTag, true)
+        allTitleNodes.assertCountEquals(2)
+        val topTitleNode = allTitleNodes.onFirst()
+        val bottomTitleNode = allTitleNodes.onLast()
+
+        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
+            .assertContainsColor(expandedAppBarBackgroundColor)
+
+        // Assert the content color at the top and bottom parts of the expanded app bar.
+        topTitleNode.captureToImage()
+            .assertContainsColor(
+                titleColor.copy(alpha = TopTitleAlphaEasing.transform(0f))
+                    .compositeOver(expandedAppBarBackgroundColor)
+            )
+        bottomTitleNode.captureToImage()
+            .assertContainsColor(
+                titleColor.compositeOver(expandedAppBarBackgroundColor)
+            )
+
+        // Simulate fully collapsed content.
+        rule.runOnIdle {
+            scrollBehavior.state.heightOffset = -fullyCollapsedHeightOffsetPx
+            scrollBehavior.state.contentOffset = -fullyCollapsedHeightOffsetPx
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
+            .assertContainsColor(fullyCollapsedContainerColor)
+        topTitleNode.captureToImage()
+            .assertContainsColor(
+                titleColor.copy(alpha = TopTitleAlphaEasing.transform(1f))
+                    .compositeOver(fullyCollapsedContainerColor)
+            )
+        // Only the top title should be visible in the collapsed form.
+        bottomTitleNode.assertIsNotDisplayed()
+    }
+
+    /**
+     * Checks that changing values at a [MediumTopAppBar] or a [LargeTopAppBar] scroll behavior
+     * affects the title's semantics.
+     *
+     * This check partially and fully collapses the app bar to test the semantics.
+     *
+     * @param appBarMaxHeight the max height of the app bar [content]
+     * @param appBarMinHeight the min height of the app bar [content]
+     * @param content a Composable that adds a MediumTopAppBar or a LargeTopAppBar
+     */
+    @OptIn(ExperimentalMaterial3Api::class)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    private fun assertMediumOrLargeScrolledSemantics(
+        appBarMaxHeight: Dp,
+        appBarMinHeight: Dp,
+        content: @Composable (TopAppBarScrollBehavior?) -> Unit
+    ) {
+        val fullyCollapsedOffsetDp = appBarMaxHeight - appBarMinHeight
+        val oneThirdCollapsedOffsetDp = fullyCollapsedOffsetDp / 3
+        var fullyCollapsedHeightOffsetPx = 0f
+        var oneThirdCollapsedHeightOffsetPx = 0f
+        lateinit var scrollBehavior: TopAppBarScrollBehavior
+        rule.setMaterialContent(lightColorScheme()) {
+            scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+            with(LocalDensity.current) {
+                oneThirdCollapsedHeightOffsetPx = oneThirdCollapsedOffsetDp.toPx()
+                fullyCollapsedHeightOffsetPx = fullyCollapsedOffsetDp.toPx()
+            }
+
+            content(scrollBehavior)
+        }
+
+        // Asserting that only one semantic title node is returned after the clearAndSetSemantics is
+        // applied to the merged tree according to the alpha values of the titles.
+        assertSingleTitleSemanticNode()
+
+        // Simulate 1/3 collapsed content.
+        rule.runOnIdle {
+            scrollBehavior.state.heightOffset = -oneThirdCollapsedHeightOffsetPx
+            scrollBehavior.state.contentOffset = -oneThirdCollapsedHeightOffsetPx
+        }
+        rule.waitForIdle()
+
+        // Assert that only one semantic title node is available while scrolling the app bar.
+        assertSingleTitleSemanticNode()
+
+        // Simulate fully collapsed content.
+        rule.runOnIdle {
+            scrollBehavior.state.heightOffset = -fullyCollapsedHeightOffsetPx
+            scrollBehavior.state.contentOffset = -fullyCollapsedHeightOffsetPx
+        }
+        rule.waitForIdle()
+
+        // Assert that only one semantic title node is available.
+        assertSingleTitleSemanticNode()
+    }
+
+    /**
+     * Asserts that only one semantic node exists at app bar title when the tree is merged.
+     */
+    private fun assertSingleTitleSemanticNode() {
+        val unmergedTitleNodes = rule.onAllNodesWithTag(TitleTestTag, useUnmergedTree = true)
+        unmergedTitleNodes.assertCountEquals(2)
+
+        val mergedTitleNodes = rule.onAllNodesWithTag(TitleTestTag, useUnmergedTree = false)
+        mergedTitleNodes.assertCountEquals(1)
+    }
+
+    /**
+     * An [IconButton] with an [Icon] inside for testing positions.
+     *
+     * An [IconButton] is defaulted to be 48X48dp, while its child [Icon] is defaulted to 24x24dp.
+     */
+    private val FakeIcon = @Composable { modifier: Modifier ->
+        IconButton(
+            onClick = { /* doSomething() */ },
+            modifier = modifier.semantics(mergeDescendants = true) {}
+        ) {
+            Icon(ColorPainter(Color.Red), null)
+        }
+    }
+
+    private fun expectedActionPosition(appBarWidth: Dp): Dp =
+        appBarWidth - AppBarStartAndEndPadding - FakeIconSize
+
+    private val FakeIconSize = 48.dp
+    private val AppBarStartAndEndPadding = 4.dp
+    private val AppBarTopAndBottomPadding =
+        (TopAppBarSmallTokens.ContainerHeight - FakeIconSize) / 2
+
+    private val LazyListTag = "lazyList"
+    private val TopAppBarTestTag = "topAppBar"
+    private val BottomAppBarTestTag = "bottomAppBar"
+    private val NavigationIconTestTag = "navigationIcon"
+    private val TitleTestTag = "title"
+    private val ActionsTestTag = "actions"
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AutoTestFrameClock.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AutoTestFrameClock.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AutoTestFrameClock.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AutoTestFrameClock.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BadgeScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BadgeScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BadgeTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BadgeTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ButtonTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CardScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/CardScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CardScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/CardScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CardTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/CardTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CardTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/CardTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/CheckboxTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/CheckboxTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ChipTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ChipTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ColorSchemeScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ColorSchemeScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ColorSchemeScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ColorSchemeScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ColorSchemeTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ColorSchemeTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ColorSchemeTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ColorSchemeTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ColorUtilTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ColorUtilTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ColorUtilTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ColorUtilTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DateInputScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DateInputScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DateInputTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DateInputTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DatePickerScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DatePickerScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DatePickerTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DatePickerTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangeInputScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DateRangeInputScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangeInputScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DateRangeInputScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangeInputTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DateRangeInputTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangeInputTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DateRangeInputTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DateRangePickerScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DateRangePickerScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DateRangePickerTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DateRangePickerTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DividerTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DividerTest.kt
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
new file mode 100644
index 0000000..347ce70
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
@@ -0,0 +1,659 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import android.view.WindowManager
+import android.widget.FrameLayout
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertTextContains
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipe
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Assume.assumeNotNull
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalMaterial3Api::class)
+@MediumTest
+@RunWith(Parameterized::class)
+class ExposedDropdownMenuTest(
+    private val softInputMode: SoftInputMode,
+) {
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun parameters() = SoftInputMode.values()
+    }
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val TFTag = "TextFieldTag"
+    private val TrailingIconTag = "TrailingIconTag"
+    private val EDMTag = "ExposedDropdownMenuTag"
+    private val MenuItemTag = "MenuItemTag"
+    private val OptionName = "Option 1"
+
+    @Test
+    fun edm_expandsOnClick_andCollapsesOnClickOutside() {
+        var textFieldBounds = Rect.Zero
+        rule.setMaterialContent(lightColorScheme()) {
+            SoftInputMode(softInputMode)
+            var expanded by remember { mutableStateOf(false) }
+            ExposedDropdownMenuForTest(
+                expanded = expanded,
+                onExpandChange = { expanded = it },
+                onTextFieldBoundsChanged = {
+                    textFieldBounds = it
+                }
+            )
+        }
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(EDMTag).assertDoesNotExist()
+
+        // Click on the TextField
+        rule.onNodeWithTag(TFTag).performClick()
+
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+
+        // Click outside EDM
+        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).click(
+            (textFieldBounds.right + 1).toInt(),
+            (textFieldBounds.bottom + 1).toInt(),
+        )
+
+        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
+    }
+
+    @Test
+    fun edm_collapsesOnTextFieldClick() {
+        rule.setMaterialContent(lightColorScheme()) {
+            SoftInputMode(softInputMode)
+            var expanded by remember { mutableStateOf(true) }
+            ExposedDropdownMenuForTest(
+                expanded = expanded,
+                onExpandChange = { expanded = it }
+            )
+        }
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(EDMTag).assertIsDisplayed()
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+
+        // Click on the TextField
+        rule.onNodeWithTag(TFTag).performClick()
+
+        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
+    }
+
+    @Test
+    fun edm_doesNotCollapse_whenTypingOnSoftKeyboard() {
+        rule.setMaterialContent(lightColorScheme()) {
+            SoftInputMode(softInputMode)
+            var expanded by remember { mutableStateOf(false) }
+            ExposedDropdownMenuForTest(
+                expanded = expanded,
+                onExpandChange = { expanded = it }
+            )
+        }
+
+        rule.onNodeWithTag(TFTag).performClick()
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(TFTag).assertIsFocused()
+        rule.onNodeWithTag(EDMTag).assertIsDisplayed()
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+
+        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+        val zKey = device.findObject(By.desc("z")) ?: device.findObject(By.text("z"))
+        // Only run the test if we can find a key to type, which might fail for any number of
+        // reasons (keyboard doesn't appear, unexpected locale, etc.)
+        assumeNotNull(zKey)
+
+        repeat(3) {
+            zKey.click()
+            rule.waitForIdle()
+        }
+
+        val matcher = hasText("zzz")
+        rule.waitUntil {
+            matcher.matches(rule.onNodeWithTag(TFTag).fetchSemanticsNode())
+        }
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+    }
+
+    @Test
+    fun edm_expandsAndFocusesTextField_whenTrailingIconClicked() {
+        rule.setMaterialContent(lightColorScheme()) {
+            SoftInputMode(softInputMode)
+            var expanded by remember { mutableStateOf(false) }
+            ExposedDropdownMenuForTest(
+                expanded = expanded,
+                onExpandChange = { expanded = it },
+            )
+        }
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(TrailingIconTag, useUnmergedTree = true).assertIsDisplayed()
+
+        // Click on the Trailing Icon
+        rule.onNodeWithTag(TrailingIconTag, useUnmergedTree = true).performClick()
+
+        rule.onNodeWithTag(TFTag).assertIsFocused()
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+    }
+
+    @Test
+    fun edm_doesNotExpand_ifTouchEndsOutsideBounds() {
+        var textFieldBounds = Rect.Zero
+        rule.setMaterialContent(lightColorScheme()) {
+            SoftInputMode(softInputMode)
+            var expanded by remember { mutableStateOf(false) }
+            ExposedDropdownMenuForTest(
+                expanded = expanded,
+                onExpandChange = { expanded = it },
+                onTextFieldBoundsChanged = {
+                    textFieldBounds = it
+                }
+            )
+        }
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(EDMTag).assertDoesNotExist()
+
+        // A swipe that ends outside the bounds of the anchor should not expand the menu.
+        rule.onNodeWithTag(TFTag).performTouchInput {
+            swipe(
+                start = this.center,
+                end = Offset(this.centerX, this.centerY + (textFieldBounds.height / 2) + 1),
+                durationMillis = 100
+            )
+        }
+        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
+
+        // A swipe that ends within the bounds of the anchor should expand the menu.
+        rule.onNodeWithTag(TFTag).performTouchInput {
+            swipe(
+                start = this.center,
+                end = Offset(this.centerX, this.centerY + (textFieldBounds.height / 2) - 1),
+                durationMillis = 100
+            )
+        }
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+    }
+
+    @Test
+    fun edm_doesNotExpand_ifTouchIsPartOfScroll() {
+        val testIndex = 2
+        var textFieldSize = IntSize.Zero
+        rule.setMaterialContent(lightColorScheme()) {
+            SoftInputMode(softInputMode)
+            LazyColumn(
+                modifier = Modifier.fillMaxSize(),
+                horizontalAlignment = Alignment.CenterHorizontally,
+            ) {
+                items(50) { index ->
+                    var expanded by remember { mutableStateOf(false) }
+                    var selectedOptionText by remember { mutableStateOf("") }
+
+                    ExposedDropdownMenuBox(
+                        expanded = expanded,
+                        onExpandedChange = { expanded = it },
+                        modifier = Modifier.padding(8.dp),
+                    ) {
+                        TextField(
+                            modifier = Modifier
+                                .menuAnchor()
+                                .then(
+                                    if (index == testIndex) Modifier
+                                        .testTag(TFTag)
+                                        .onSizeChanged {
+                                            textFieldSize = it
+                                        } else {
+                                        Modifier
+                                    }
+                                ),
+                            value = selectedOptionText,
+                            onValueChange = { selectedOptionText = it },
+                            label = { Text("Label") },
+                            trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
+                            colors = ExposedDropdownMenuDefaults.textFieldColors()
+                        )
+                        ExposedDropdownMenu(
+                            modifier = if (index == testIndex) {
+                                Modifier.testTag(EDMTag)
+                            } else { Modifier },
+                            expanded = expanded,
+                            onDismissRequest = { expanded = false }
+                        ) {
+                            DropdownMenuItem(
+                                text = { Text(OptionName) },
+                                onClick = {
+                                    selectedOptionText = OptionName
+                                    expanded = false
+                                },
+                                modifier = if (index == testIndex) {
+                                    Modifier.testTag(MenuItemTag)
+                                } else { Modifier },
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(EDMTag).assertDoesNotExist()
+
+        // A swipe that causes a scroll should not expand the menu, even if it remains within the
+        // bounds of the anchor.
+        rule.onNodeWithTag(TFTag).performTouchInput {
+            swipe(
+                start = this.center,
+                end = Offset(this.centerX, this.centerY - (textFieldSize.height / 2) + 1),
+                durationMillis = 100
+            )
+        }
+        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
+
+        // But a swipe that does not cause a scroll should expand the menu.
+        rule.onNodeWithTag(TFTag).performTouchInput {
+            swipe(
+                start = this.center,
+                end = Offset(this.centerX + (textFieldSize.width / 2) - 1, this.centerY),
+                durationMillis = 100
+            )
+        }
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+    }
+
+    @Test
+    fun edm_doesNotRecomposeOnScroll() {
+        var compositionCount = 0
+        lateinit var scrollState: ScrollState
+        lateinit var scope: CoroutineScope
+        rule.setMaterialContent(lightColorScheme()) {
+            SoftInputMode(softInputMode)
+            scrollState = rememberScrollState()
+            scope = rememberCoroutineScope()
+            Column(Modifier.verticalScroll(scrollState)) {
+                Spacer(Modifier.height(300.dp))
+
+                val expanded = false
+                ExposedDropdownMenuBox(
+                    expanded = expanded,
+                    onExpandedChange = {},
+                ) {
+                    TextField(
+                        modifier = Modifier.menuAnchor(),
+                        readOnly = true,
+                        value = "",
+                        onValueChange = {},
+                        label = { Text("Label") },
+                    )
+                    ExposedDropdownMenu(
+                        expanded = expanded,
+                        onDismissRequest = {},
+                        content = {},
+                    )
+                    SideEffect {
+                        compositionCount++
+                    }
+                }
+
+                Spacer(Modifier.height(300.dp))
+            }
+        }
+
+        assertThat(compositionCount).isEqualTo(1)
+
+        rule.runOnIdle {
+            scope.launch {
+                scrollState.animateScrollBy(500f)
+            }
+        }
+        rule.waitForIdle()
+
+        assertThat(compositionCount).isEqualTo(1)
+    }
+
+    @Test
+    fun edm_widthMatchesTextFieldWidth() {
+        var textFieldBounds by mutableStateOf(Rect.Zero)
+        var menuBounds by mutableStateOf(Rect.Zero)
+        rule.setMaterialContent(lightColorScheme()) {
+            SoftInputMode(softInputMode)
+            var expanded by remember { mutableStateOf(true) }
+            ExposedDropdownMenuForTest(
+                expanded = expanded,
+                onExpandChange = { expanded = it },
+                onTextFieldBoundsChanged = {
+                    textFieldBounds = it
+                },
+                onMenuBoundsChanged = {
+                    menuBounds = it
+                }
+            )
+        }
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+
+        rule.runOnIdle {
+            assertThat(menuBounds.width).isEqualTo(textFieldBounds.width)
+        }
+    }
+
+    @Test
+    fun edm_collapsesWithSelection_whenMenuItemClicked() {
+        rule.setMaterialContent(lightColorScheme()) {
+            SoftInputMode(softInputMode)
+            var expanded by remember { mutableStateOf(true) }
+            ExposedDropdownMenuForTest(
+                expanded = expanded,
+                onExpandChange = { expanded = it }
+            )
+        }
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+
+        // Choose the option
+        rule.onNodeWithTag(MenuItemTag).performClick()
+
+        // Menu should collapse
+        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
+        rule.onNodeWithTag(TFTag).assertTextContains(OptionName)
+    }
+
+    @Test
+    fun edm_resizesWithinWindowBounds_uponImeAppearance() {
+        var actualMenuSize: IntSize? = null
+        var density: Density? = null
+        val itemSize = 50.dp
+        val itemCount = 10
+
+        rule.setMaterialContent(lightColorScheme()) {
+            density = LocalDensity.current
+            SoftInputMode(softInputMode)
+            Column(Modifier.fillMaxSize()) {
+                // Push the EDM down so opening the keyboard causes a pan/scroll
+                Spacer(Modifier.weight(1f))
+
+                ExposedDropdownMenuBox(
+                    expanded = true,
+                    onExpandedChange = { }
+                ) {
+                    TextField(
+                        modifier = Modifier.menuAnchor(),
+                        value = "",
+                        onValueChange = { },
+                        label = { Text("Label") },
+                    )
+                    ExposedDropdownMenu(
+                        expanded = true,
+                        onDismissRequest = { },
+                        modifier = Modifier.onGloballyPositioned {
+                            actualMenuSize = it.size
+                        }
+                    ) {
+                        repeat(itemCount) {
+                            Box(Modifier.size(itemSize))
+                        }
+                    }
+                }
+            }
+        }
+
+        // This would fit on screen if the keyboard wasn't displayed.
+        val menuPreferredHeight = with(density!!) {
+            (itemSize * itemCount + DropdownMenuVerticalPadding * 2).roundToPx()
+        }
+        // But the keyboard *is* displayed, forcing the actual size to be smaller.
+        assertThat(actualMenuSize!!.height).isLessThan(menuPreferredHeight)
+    }
+
+    @Ignore("b/266109857")
+    @Test
+    fun edm_doesNotCrash_whenAnchorDetachedFirst() {
+        var parent: FrameLayout? = null
+        rule.setMaterialContent(lightColorScheme()) {
+            SoftInputMode(softInputMode)
+            AndroidView(
+                factory = { context ->
+                    FrameLayout(context).apply {
+                        addView(ComposeView(context).apply {
+                            setContent {
+                                ExposedDropdownMenuBox(expanded = true, onExpandedChange = {}) {
+                                    TextField(
+                                        value = "Text",
+                                        onValueChange = {},
+                                        modifier = Modifier.menuAnchor(),
+                                    )
+                                    ExposedDropdownMenu(
+                                        expanded = true,
+                                        onDismissRequest = {},
+                                    ) {
+                                        DropdownMenuItem(
+                                            text = { Text(OptionName) },
+                                            onClick = {},
+                                        )
+                                    }
+                                }
+                            }
+                        })
+                    }.also { parent = it }
+                }
+            )
+        }
+
+        rule.runOnIdle {
+            parent!!.removeAllViews()
+        }
+
+        rule.waitForIdle()
+
+        // Should not have crashed.
+    }
+
+    @Ignore("b/297059209")
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun edm_withScrolledContent() {
+        lateinit var scrollState: ScrollState
+        rule.setMaterialContent(lightColorScheme()) {
+            SoftInputMode(softInputMode)
+            Box(Modifier.fillMaxSize()) {
+                ExposedDropdownMenuBox(
+                    modifier = Modifier.align(Alignment.Center),
+                    expanded = true,
+                    onExpandedChange = { }
+                ) {
+                    scrollState = rememberScrollState()
+                    TextField(
+                        modifier = Modifier.menuAnchor(),
+                        value = "",
+                        onValueChange = { },
+                        label = { Text("Label") },
+                    )
+                    ExposedDropdownMenu(
+                        expanded = true,
+                        onDismissRequest = { },
+                        scrollState = scrollState
+                    ) {
+                        repeat(100) {
+                            Text(
+                                text = "Text ${it + 1}",
+                                modifier = Modifier.testTag("MenuContent ${it + 1}"),
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                scrollState.scrollTo(scrollState.maxValue)
+            }
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("MenuContent 1").assertIsNotDisplayed()
+        rule.onNodeWithTag("MenuContent 100").assertIsDisplayed()
+    }
+
+    @Composable
+    fun ExposedDropdownMenuForTest(
+        expanded: Boolean,
+        onExpandChange: (Boolean) -> Unit,
+        onTextFieldBoundsChanged: ((Rect) -> Unit)? = null,
+        onMenuBoundsChanged: ((Rect) -> Unit)? = null
+    ) {
+        var selectedOptionText by remember { mutableStateOf("") }
+        Box(Modifier.fillMaxSize()) {
+            ExposedDropdownMenuBox(
+                modifier = Modifier.align(Alignment.Center),
+                expanded = expanded,
+                onExpandedChange = { onExpandChange(!expanded) }
+            ) {
+                TextField(
+                    modifier = Modifier
+                        .menuAnchor()
+                        .testTag(TFTag)
+                        .onGloballyPositioned {
+                            onTextFieldBoundsChanged?.invoke(it.boundsInRoot())
+                        },
+                    value = selectedOptionText,
+                    onValueChange = { selectedOptionText = it },
+                    label = { Text("Label") },
+                    trailingIcon = {
+                        Box(
+                            modifier = Modifier.testTag(TrailingIconTag)
+                        ) {
+                            ExposedDropdownMenuDefaults.TrailingIcon(
+                                expanded = expanded
+                            )
+                        }
+                    },
+                    colors = ExposedDropdownMenuDefaults.textFieldColors()
+                )
+                ExposedDropdownMenu(
+                    modifier = Modifier
+                        .testTag(EDMTag)
+                        .onGloballyPositioned {
+                            onMenuBoundsChanged?.invoke(it.boundsInRoot())
+                        },
+                    expanded = expanded,
+                    onDismissRequest = { onExpandChange(false) }
+                ) {
+                    DropdownMenuItem(
+                        text = { Text(OptionName) },
+                        onClick = {
+                            selectedOptionText = OptionName
+                            onExpandChange(false)
+                        },
+                        modifier = Modifier.testTag(MenuItemTag)
+                    )
+                }
+            }
+        }
+    }
+}
+
+enum class SoftInputMode {
+    AdjustResize,
+    AdjustPan
+}
+
+@Suppress("DEPRECATION")
+@Composable
+fun SoftInputMode(mode: SoftInputMode) {
+    val context = LocalContext.current
+    DisposableEffect(mode) {
+        val activity = context.findActivityOrNull() ?: return@DisposableEffect onDispose {}
+        val originalMode = activity.window.attributes.softInputMode
+        activity.window.setSoftInputMode(when (mode) {
+            SoftInputMode.AdjustResize -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+            SoftInputMode.AdjustPan -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
+        })
+        onDispose {
+            activity.window.setSoftInputMode(originalMode)
+        }
+    }
+}
+
+private tailrec fun Context.findActivityOrNull(): Activity? {
+    return (this as? Activity)
+        ?: (this as? ContextWrapper)?.baseContext?.findActivityOrNull()
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
new file mode 100644
index 0000000..925e620
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
@@ -0,0 +1,678 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material3.tokens.ExtendedFabPrimaryTokens
+import androidx.compose.material3.tokens.FabPrimaryLargeTokens
+import androidx.compose.material3.tokens.FabPrimarySmallTokens
+import androidx.compose.material3.tokens.FabPrimaryTokens
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertTouchHeightIsEqualTo
+import androidx.compose.ui.test.assertTouchWidthIsEqualTo
+import androidx.compose.ui.test.assertWidthIsAtLeast
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.abs
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class FloatingActionButtonTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun fabDefaultSemantics() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box {
+                FloatingActionButton(modifier = Modifier.testTag("myButton"), onClick = {}) {
+                    Icon(Icons.Filled.Favorite, null)
+                }
+            }
+        }
+
+        rule.onNodeWithTag("myButton")
+            .assertIsEnabled()
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button))
+    }
+
+    @Test
+    fun extendedFabFindByTextAndClick() {
+        var counter = 0
+        val onClick: () -> Unit = { ++counter }
+        val text = "myButton"
+
+        rule.setMaterialContent(lightColorScheme()) {
+            Box {
+                ExtendedFloatingActionButton(onClick = onClick, content = { Text(text) })
+            }
+        }
+
+        rule.onNodeWithText(text)
+            .performClick()
+
+        rule.runOnIdle {
+            assertThat(counter).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun fabHasSizeFromSpec() {
+        rule
+            .setMaterialContentForSizeAssertions {
+                FloatingActionButton(onClick = {}) {
+                    Icon(
+                        Icons.Filled.Favorite,
+                        null,
+                        modifier = Modifier.testTag("icon"))
+                }
+            }
+            .assertIsSquareWithSize(FabPrimaryTokens.ContainerHeight)
+
+        rule
+            .onNodeWithTag("icon", useUnmergedTree = true)
+            .assertHeightIsEqualTo(FabPrimaryTokens.IconSize)
+            .assertWidthIsEqualTo(FabPrimaryTokens.IconSize)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun smallFabHasSizeFromSpec() {
+        rule
+            .setMaterialContentForSizeAssertions {
+                CompositionLocalProvider(
+                    LocalMinimumInteractiveComponentEnforcement provides false
+                ) {
+                    SmallFloatingActionButton(onClick = {}) {
+                        Icon(
+                            Icons.Filled.Favorite,
+                            null,
+                            modifier = Modifier.testTag("icon")
+                        )
+                    }
+                }
+            }
+            // Expecting the size to be equal to the token size.
+            .assertIsSquareWithSize(FabPrimarySmallTokens.ContainerHeight)
+
+        rule
+            .onNodeWithTag("icon", useUnmergedTree = true)
+            .assertHeightIsEqualTo(FabPrimarySmallTokens.IconSize)
+            .assertWidthIsEqualTo(FabPrimarySmallTokens.IconSize)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun smallFabHasMinTouchTarget() {
+        rule
+            .setMaterialContentForSizeAssertions {
+                CompositionLocalProvider(
+                    LocalMinimumInteractiveComponentEnforcement provides true
+                ) {
+                    SmallFloatingActionButton(onClick = {}) {
+                        Icon(Icons.Filled.Favorite, null)
+                    }
+                }
+            }
+            // Expecting the size to be equal to the minimum touch target.
+            .assertTouchWidthIsEqualTo(48.dp)
+            .assertTouchHeightIsEqualTo(48.dp)
+            .assertWidthIsEqualTo(48.dp)
+            .assertHeightIsEqualTo(48.dp)
+    }
+
+    @Test
+    fun largeFabHasSizeFromSpec() {
+        rule
+            .setMaterialContentForSizeAssertions {
+                LargeFloatingActionButton(onClick = {}) {
+                    Icon(
+                        Icons.Filled.Favorite,
+                        null,
+                        modifier = Modifier
+                            .size(FloatingActionButtonDefaults.LargeIconSize)
+                            .testTag("icon")
+                    )
+                }
+            }
+            .assertIsSquareWithSize(FabPrimaryLargeTokens.ContainerHeight)
+
+        rule
+            .onNodeWithTag("icon", useUnmergedTree = true)
+            .assertHeightIsEqualTo(FloatingActionButtonDefaults.LargeIconSize)
+            .assertWidthIsEqualTo(FloatingActionButtonDefaults.LargeIconSize)
+    }
+
+    @Test
+    fun extendedFabLongTextHasHeightFromSpec() {
+        rule.setMaterialContent(lightColorScheme()) {
+            ExtendedFloatingActionButton(
+                modifier = Modifier.testTag("FAB"),
+                text = { Text("Extended FAB Text") },
+                icon = { Icon(Icons.Filled.Favorite, null) },
+                onClick = {}
+            )
+        }
+
+        rule.onNodeWithTag("FAB")
+            .assertHeightIsEqualTo(ExtendedFabPrimaryTokens.ContainerHeight)
+            .assertWidthIsAtLeast(FabPrimaryTokens.ContainerHeight)
+    }
+
+    @Test
+    fun extendedFabShortTextHasMinimumSizeFromSpec() {
+        rule.setMaterialContent(lightColorScheme()) {
+            ExtendedFloatingActionButton(
+                modifier = Modifier.testTag("FAB"),
+                onClick = {},
+                content = { Text(".") },
+            )
+        }
+
+        rule.onNodeWithTag("FAB")
+            .assertHeightIsEqualTo(ExtendedFabPrimaryTokens.ContainerHeight)
+            .assertWidthIsEqualTo(80.dp)
+    }
+
+    @Test
+    fun fabHasCorrectTextStyle() {
+        var fontFamily: FontFamily? = null
+        var fontWeight: FontWeight? = null
+        var fontSize: TextUnit? = null
+        var lineHeight: TextUnit? = null
+        var letterSpacing: TextUnit? = null
+        var expectedTextStyle: TextStyle? = null
+
+        rule.setMaterialContent(lightColorScheme()) {
+            FloatingActionButton(onClick = {}) {
+                Icon(Icons.Filled.Favorite, null)
+                Text(
+                    "Normal FAB with Text",
+                    onTextLayout = {
+                        fontFamily = it.layoutInput.style.fontFamily
+                        fontWeight = it.layoutInput.style.fontWeight
+                        fontSize = it.layoutInput.style.fontSize
+                        lineHeight = it.layoutInput.style.lineHeight
+                        letterSpacing = it.layoutInput.style.letterSpacing
+                    }
+                )
+            }
+            expectedTextStyle = MaterialTheme.typography.fromToken(
+                ExtendedFabPrimaryTokens.LabelTextFont
+            )
+        }
+        rule.runOnIdle {
+            assertThat(fontFamily).isEqualTo(expectedTextStyle!!.fontFamily)
+            assertThat(fontWeight).isEqualTo(expectedTextStyle!!.fontWeight)
+            assertThat(fontSize).isEqualTo(expectedTextStyle!!.fontSize)
+            assertThat(lineHeight).isEqualTo(expectedTextStyle!!.lineHeight)
+            assertThat(letterSpacing).isEqualTo(expectedTextStyle!!.letterSpacing)
+        }
+    }
+
+    @Test
+    fun extendedFabHasCorrectTextStyle() {
+        var fontFamily: FontFamily? = null
+        var fontWeight: FontWeight? = null
+        var fontSize: TextUnit? = null
+        var lineHeight: TextUnit? = null
+        var letterSpacing: TextUnit? = null
+        var expectedTextStyle: TextStyle? = null
+
+        rule.setMaterialContent(lightColorScheme()) {
+            ExtendedFloatingActionButton(onClick = {}) {
+                Text(
+                    "Extended FAB",
+                    onTextLayout = {
+                        fontFamily = it.layoutInput.style.fontFamily
+                        fontWeight = it.layoutInput.style.fontWeight
+                        fontSize = it.layoutInput.style.fontSize
+                        lineHeight = it.layoutInput.style.lineHeight
+                        letterSpacing = it.layoutInput.style.letterSpacing
+                    }
+                )
+            }
+            expectedTextStyle = MaterialTheme.typography.fromToken(
+                ExtendedFabPrimaryTokens.LabelTextFont
+            )
+        }
+        rule.runOnIdle {
+            assertThat(fontFamily).isEqualTo(expectedTextStyle!!.fontFamily)
+            assertThat(fontWeight).isEqualTo(expectedTextStyle!!.fontWeight)
+            assertThat(fontSize).isEqualTo(expectedTextStyle!!.fontSize)
+            assertThat(lineHeight).isEqualTo(expectedTextStyle!!.lineHeight)
+            assertThat(letterSpacing).isEqualTo(expectedTextStyle!!.letterSpacing)
+        }
+    }
+
+    @Test
+    fun fabWeightModifier() {
+        var item1Bounds = Rect(0f, 0f, 0f, 0f)
+        var buttonBounds = Rect(0f, 0f, 0f, 0f)
+        rule.setMaterialContent(lightColorScheme()) {
+            Column {
+                Spacer(
+                    Modifier.requiredSize(10.dp).weight(1f).onGloballyPositioned {
+                        item1Bounds = it.boundsInRoot()
+                    }
+                )
+
+                FloatingActionButton(
+                    onClick = {},
+                    modifier = Modifier.weight(1f)
+                        .onGloballyPositioned {
+                            buttonBounds = it.boundsInRoot()
+                        }
+                ) {
+                    Text("Button")
+                }
+
+                Spacer(Modifier.requiredSize(10.dp).weight(1f))
+            }
+        }
+
+        assertThat(item1Bounds.top).isNotEqualTo(0f)
+        assertThat(buttonBounds.left).isEqualTo(0f)
+    }
+
+    @Test
+    fun contentIsWrappedAndCentered() {
+        var buttonCoordinates: LayoutCoordinates? = null
+        var contentCoordinates: LayoutCoordinates? = null
+        rule.setMaterialContent(lightColorScheme()) {
+            Box {
+                FloatingActionButton(
+                    {},
+                    Modifier.onGloballyPositioned {
+                        buttonCoordinates = it
+                    }
+                ) {
+                    Box(
+                        Modifier.size(2.dp)
+                            .onGloballyPositioned { contentCoordinates = it }
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            val buttonBounds = buttonCoordinates!!.boundsInRoot()
+            val contentBounds = contentCoordinates!!.boundsInRoot()
+            assertThat(contentBounds.width).isLessThan(buttonBounds.width)
+            assertThat(contentBounds.height).isLessThan(buttonBounds.height)
+            with(rule.density) {
+                assertThat(contentBounds.width).isEqualTo(2.dp.roundToPx().toFloat())
+                assertThat(contentBounds.height).isEqualTo(2.dp.roundToPx().toFloat())
+            }
+            assertWithinOnePixel(buttonBounds.center, contentBounds.center)
+        }
+    }
+
+    @Test
+    fun extendedFabTextIsWrappedAndCentered() {
+        var buttonCoordinates: LayoutCoordinates? = null
+        var contentCoordinates: LayoutCoordinates? = null
+        rule.setMaterialContent(lightColorScheme()) {
+            Box {
+                ExtendedFloatingActionButton(
+                    onClick = {},
+                    modifier = Modifier.onGloballyPositioned { buttonCoordinates = it },
+                ) {
+                    Box(
+                        Modifier.size(2.dp)
+                            .onGloballyPositioned { contentCoordinates = it }
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            val buttonBounds = buttonCoordinates!!.boundsInRoot()
+            val contentBounds = contentCoordinates!!.boundsInRoot()
+            assertThat(contentBounds.width).isLessThan(buttonBounds.width)
+            assertThat(contentBounds.height).isLessThan(buttonBounds.height)
+            with(rule.density) {
+                assertThat(contentBounds.width).isEqualTo(2.dp.roundToPx().toFloat())
+                assertThat(contentBounds.height).isEqualTo(2.dp.roundToPx().toFloat())
+            }
+            assertWithinOnePixel(buttonBounds.center, contentBounds.center)
+        }
+    }
+
+    @Test
+    fun extendedFabTextAndIconArePositionedCorrectly() {
+        var buttonCoordinates: LayoutCoordinates? = null
+        var textCoordinates: LayoutCoordinates? = null
+        var iconCoordinates: LayoutCoordinates? = null
+        rule.setMaterialContent(lightColorScheme()) {
+            Box {
+                ExtendedFloatingActionButton(
+                    text = {
+                        Box(
+                            Modifier.size(2.dp)
+                                .onGloballyPositioned { textCoordinates = it }
+                        )
+                    },
+                    icon = {
+                        Box(
+                            Modifier.size(10.dp)
+                                .onGloballyPositioned { iconCoordinates = it }
+                        )
+                    },
+                    onClick = {},
+                    modifier = Modifier.onGloballyPositioned { buttonCoordinates = it }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            val buttonBounds = buttonCoordinates!!.boundsInRoot()
+            val textBounds = textCoordinates!!.boundsInRoot()
+            val iconBounds = iconCoordinates!!.boundsInRoot()
+            with(rule.density) {
+                assertThat(textBounds.width).isEqualTo(2.dp.roundToPx().toFloat())
+                assertThat(textBounds.height).isEqualTo(2.dp.roundToPx().toFloat())
+                assertThat(iconBounds.width).isEqualTo(10.dp.roundToPx().toFloat())
+                assertThat(iconBounds.height).isEqualTo(10.dp.roundToPx().toFloat())
+
+                assertWithinOnePixel(buttonBounds.center.y, iconBounds.center.y)
+                assertWithinOnePixel(buttonBounds.center.y, textBounds.center.y)
+
+                // Assert expanded fab icon has 16.dp of padding.
+                assertThat(iconBounds.left - buttonBounds.left)
+                    .isEqualTo(16.dp.roundToPx().toFloat())
+
+                val halfPadding = 6.dp.roundToPx().toFloat()
+                assertWithinOnePixel(
+                    iconBounds.center.x + iconBounds.width / 2 + halfPadding,
+                    textBounds.center.x - textBounds.width / 2 - halfPadding
+                )
+                // Assert that text and icon have 12.dp padding between them.
+                assertThat(textBounds.left - iconBounds.right)
+                    .isEqualTo(12.dp.roundToPx().toFloat())
+            }
+        }
+    }
+
+    @Test
+    fun expandedExtendedFabTextAndIconHaveSizeFromSpecAndVisible() {
+        rule.setMaterialContent(lightColorScheme()) {
+            ExtendedFloatingActionButton(
+                expanded = true,
+                onClick = { },
+                icon = {
+                    Icon(
+                        Icons.Filled.Favorite,
+                        "Add",
+                        modifier = Modifier.testTag("icon"),
+                    )
+                },
+                text = { Text(text = "FAB", modifier = Modifier.testTag("text")) },
+                modifier = Modifier.testTag("FAB"),
+            )
+        }
+
+        rule
+            .onNodeWithTag("icon", useUnmergedTree = true)
+            .assertHeightIsEqualTo(ExtendedFabPrimaryTokens.IconSize)
+            .assertWidthIsEqualTo(ExtendedFabPrimaryTokens.IconSize)
+
+        rule.onNodeWithTag("FAB")
+            .assertHeightIsEqualTo(ExtendedFabPrimaryTokens.ContainerHeight)
+            .assertWidthIsAtLeast(80.dp)
+
+        rule.onNodeWithTag("text", useUnmergedTree = true).assertIsDisplayed()
+        rule.onNodeWithTag("icon", useUnmergedTree = true).assertIsDisplayed()
+    }
+
+    @Test
+    fun collapsedExtendedFabTextAndIconHaveSizeFromSpecAndTextNotVisible() {
+        rule.setMaterialContent(lightColorScheme()) {
+            ExtendedFloatingActionButton(
+                expanded = false,
+                onClick = { },
+                icon = {
+                    Icon(
+                        Icons.Filled.Favorite,
+                        "Add",
+                        modifier = Modifier.testTag("icon")
+                    )
+                },
+                text = { Text(text = "FAB", modifier = Modifier.testTag("text")) },
+                modifier = Modifier.testTag("FAB"),
+            )
+        }
+
+        rule.onNodeWithTag("FAB")
+            .assertIsSquareWithSize(FabPrimaryTokens.ContainerHeight)
+
+        rule
+            .onNodeWithTag("icon", useUnmergedTree = true)
+            .assertHeightIsEqualTo(ExtendedFabPrimaryTokens.IconSize)
+            .assertWidthIsEqualTo(ExtendedFabPrimaryTokens.IconSize)
+
+        rule.onNodeWithTag("text", useUnmergedTree = true).assertDoesNotExist()
+        rule.onNodeWithTag("icon", useUnmergedTree = true).assertIsDisplayed()
+    }
+
+    @Test
+    fun extendedFabAnimates() {
+        rule.mainClock.autoAdvance = false
+
+        var expanded by mutableStateOf(true)
+        rule.setMaterialContent(lightColorScheme()) {
+            ExtendedFloatingActionButton(
+                expanded = expanded,
+                onClick = {},
+                icon = {
+                    Icon(
+                        Icons.Filled.Favorite,
+                        "Add",
+                        modifier = Modifier.testTag("icon")
+                    )
+                },
+                text = { Text(text = "FAB", modifier = Modifier.testTag("text")) },
+                modifier = Modifier.testTag("FAB"),
+            )
+        }
+
+        rule.onNodeWithTag("FAB")
+            .assertHeightIsEqualTo(ExtendedFabPrimaryTokens.ContainerHeight)
+            .assertWidthIsAtLeast(80.dp)
+
+        rule.runOnIdle { expanded = false }
+        rule.mainClock.advanceTimeBy(200)
+
+        rule.onNodeWithTag("FAB")
+            .assertIsSquareWithSize(FabPrimaryTokens.ContainerHeight)
+            .assertHeightIsEqualTo(FabPrimaryTokens.ContainerHeight)
+            .assertWidthIsEqualTo(FabPrimaryTokens.ContainerWidth)
+    }
+
+    @Test
+    fun floatingActionButtonElevation_newInteraction() {
+        val interactionSource = MutableInteractionSource()
+        val defaultElevation = 1.dp
+        val pressedElevation = 2.dp
+        val hoveredElevation = 3.dp
+        val focusedElevation = 4.dp
+        var tonalElevation: Dp = Dp.Unspecified
+        lateinit var shadowElevation: State<Dp>
+
+        rule.setMaterialContent(lightColorScheme()) {
+            val fabElevation = FloatingActionButtonDefaults.elevation(
+                defaultElevation = defaultElevation,
+                pressedElevation = pressedElevation,
+                hoveredElevation = hoveredElevation,
+                focusedElevation = focusedElevation
+            )
+
+            tonalElevation = fabElevation.tonalElevation()
+            shadowElevation = fabElevation.shadowElevation(interactionSource)
+        }
+
+        rule.runOnIdle {
+            assertThat(tonalElevation).isEqualTo(defaultElevation)
+            assertThat(shadowElevation.value).isEqualTo(defaultElevation)
+        }
+
+        rule.runOnIdle {
+            interactionSource.tryEmit(PressInteraction.Press(Offset.Zero))
+        }
+
+        rule.runOnIdle {
+            assertThat(tonalElevation).isEqualTo(defaultElevation)
+            assertThat(shadowElevation.value).isEqualTo(pressedElevation)
+        }
+    }
+
+    @Test
+    fun floatingActionButtonElevation_newValue() {
+        val interactionSource = MutableInteractionSource()
+        var defaultElevation by mutableStateOf(1.dp)
+        val pressedElevation = 2.dp
+        val hoveredElevation = 3.dp
+        val focusedElevation = 4.dp
+        var tonalElevation: Dp = Dp.Unspecified
+        lateinit var shadowElevation: State<Dp>
+
+        rule.setMaterialContent(lightColorScheme()) {
+            val fabElevation = FloatingActionButtonDefaults.elevation(
+                defaultElevation = defaultElevation,
+                pressedElevation = pressedElevation,
+                hoveredElevation = hoveredElevation,
+                focusedElevation = focusedElevation
+            )
+
+            tonalElevation = fabElevation.tonalElevation()
+            shadowElevation = fabElevation.shadowElevation(interactionSource)
+        }
+
+        rule.runOnIdle {
+            assertThat(tonalElevation).isEqualTo(defaultElevation)
+            assertThat(shadowElevation.value).isEqualTo(defaultElevation)
+        }
+
+        rule.runOnIdle {
+            defaultElevation = 5.dp
+        }
+
+        rule.runOnIdle {
+            assertThat(tonalElevation).isEqualTo(5.dp)
+            assertThat(shadowElevation.value).isEqualTo(5.dp)
+        }
+    }
+
+    @Test
+    fun floatingActionButtonElevation_newValueDuringInteraction() {
+        val interactionSource = MutableInteractionSource()
+        val defaultElevation = 1.dp
+        var pressedElevation by mutableStateOf(2.dp)
+        val hoveredElevation = 3.dp
+        val focusedElevation = 4.dp
+        var tonalElevation: Dp = Dp.Unspecified
+        lateinit var shadowElevation: State<Dp>
+
+        rule.setMaterialContent(lightColorScheme()) {
+            val fabElevation = FloatingActionButtonDefaults.elevation(
+                defaultElevation = defaultElevation,
+                pressedElevation = pressedElevation,
+                hoveredElevation = hoveredElevation,
+                focusedElevation = focusedElevation
+            )
+
+            tonalElevation = fabElevation.tonalElevation()
+            shadowElevation = fabElevation.shadowElevation(interactionSource)
+        }
+
+        rule.runOnIdle {
+            assertThat(tonalElevation).isEqualTo(defaultElevation)
+            assertThat(shadowElevation.value).isEqualTo(defaultElevation)
+        }
+
+        rule.runOnIdle {
+            interactionSource.tryEmit(PressInteraction.Press(Offset.Zero))
+        }
+
+        rule.runOnIdle {
+            assertThat(tonalElevation).isEqualTo(defaultElevation)
+            assertThat(shadowElevation.value).isEqualTo(pressedElevation)
+        }
+
+        rule.runOnIdle {
+            pressedElevation = 5.dp
+        }
+
+        // We are still pressed, so we should now show the updated value for the pressed state
+        rule.runOnIdle {
+            assertThat(tonalElevation).isEqualTo(defaultElevation)
+            assertThat(shadowElevation.value).isEqualTo(5.dp)
+        }
+    }
+}
+
+fun assertWithinOnePixel(expected: Offset, actual: Offset) {
+    assertWithinOnePixel(expected.x, actual.x)
+    assertWithinOnePixel(expected.y, actual.y)
+}
+
+fun assertWithinOnePixel(expected: Float, actual: Float) {
+    val diff = abs(expected - actual)
+    assertThat(diff).isLessThan(1.1f)
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/GoldenCommon.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/GoldenCommon.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/GoldenCommon.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/GoldenCommon.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ListItemScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ListItemScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ListItemScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ListItemScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ListItemTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ListItemTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ListItemTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ListItemTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialComponentsInsetSupportTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialComponentsInsetSupportTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialComponentsInsetSupportTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialComponentsInsetSupportTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialRippleThemeTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialRippleThemeTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialRippleThemeTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialRippleThemeTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialWindowInsetsActivity.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialWindowInsetsActivity.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialWindowInsetsActivity.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialWindowInsetsActivity.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuPositionTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MenuPositionTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuPositionTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MenuPositionTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MenuScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MenuScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MenuTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MenuTest.kt
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
new file mode 100644
index 0000000..859ae81
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -0,0 +1,1215 @@
+/*
+ * 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.material3
+
+import android.content.ComponentCallbacks2
+import android.content.pm.ActivityInfo
+import android.content.res.Configuration
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.isPopup
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.coerceAtMost
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import junit.framework.TestCase.fail
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+@OptIn(ExperimentalMaterial3Api::class)
+class ModalBottomSheetTest(private val edgeToEdgeWrapper: EdgeToEdgeWrapper) {
+
+    @get:Rule
+    val rule = createAndroidComposeRule<ComponentActivity>()
+
+    private val sheetHeight = 256.dp
+    private val dragHandleSize = 44.dp
+
+    private val sheetTag = "sheetContentTag"
+    private val dragHandleTag = "dragHandleTag"
+    private val BackTestTag = "Back"
+
+    @Test
+    fun modalBottomSheet_isDismissedOnTapOutside() {
+        var showBottomSheet by mutableStateOf(true)
+        val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
+
+        rule.setContent {
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            if (showBottomSheet) {
+                ModalBottomSheet(
+                    sheetState = sheetState,
+                    onDismissRequest = { showBottomSheet = false },
+                    windowInsets = windowInsets
+                ) {
+                    Box(
+                        Modifier
+                            .size(sheetHeight)
+                            .testTag(sheetTag)
+                    )
+                }
+            }
+        }
+
+        assertThat(sheetState.isVisible).isTrue()
+
+        // Tap Scrim
+        val outsideY = with(rule.density) {
+            rule.onAllNodes(isPopup()).onFirst().getUnclippedBoundsInRoot().height.roundToPx() / 4
+        }
+        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).click(0, outsideY)
+        rule.waitForIdle()
+
+        // Bottom sheet should not exist
+        rule.onNodeWithTag(sheetTag).assertDoesNotExist()
+    }
+
+    @Test
+    fun modalBottomSheet_isDismissedOnSwipeDown() {
+        var showBottomSheet by mutableStateOf(true)
+        val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
+
+        rule.setContent {
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            if (showBottomSheet) {
+                ModalBottomSheet(
+                    sheetState = sheetState,
+                    onDismissRequest = { showBottomSheet = false },
+                    windowInsets = windowInsets
+                ) {
+                    Box(
+                        Modifier
+                            .size(sheetHeight)
+                            .testTag(sheetTag)
+                    )
+                }
+            }
+        }
+
+        assertThat(sheetState.isVisible).isTrue()
+
+        // Swipe Down
+        rule.onNodeWithTag(sheetTag).performTouchInput {
+            swipeDown()
+        }
+        rule.waitForIdle()
+
+        // Bottom sheet should not exist
+        rule.onNodeWithTag(sheetTag).assertDoesNotExist()
+    }
+
+    @Test
+    fun modalBottomSheet_fillsScreenWidth() {
+        var boxWidth = 0
+        var screenWidth by mutableStateOf(0)
+
+        rule.setContent {
+            val context = LocalContext.current
+            screenWidth = context.resources.displayMetrics.widthPixels
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = {},
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxWidth()
+                        .height(sheetHeight)
+                        .onSizeChanged { boxWidth = it.width }
+                )
+            }
+        }
+        assertThat(boxWidth).isEqualTo(screenWidth)
+    }
+
+    @Test
+    fun modalBottomSheet_wideScreen_sheetRespectsMaxWidthAndIsCentered() {
+        rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+        val latch = CountDownLatch(1)
+
+        rule.activity.application.registerComponentCallbacks(object : ComponentCallbacks2 {
+            override fun onConfigurationChanged(p0: Configuration) {
+                latch.countDown()
+            }
+
+            override fun onLowMemory() {
+                // NO-OP
+            }
+
+            override fun onTrimMemory(p0: Int) {
+                // NO-OP
+            }
+        })
+
+        try {
+            latch.await(1500, TimeUnit.MILLISECONDS)
+            rule.setContent {
+                val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                    WindowInsets(0) else BottomSheetDefaults.windowInsets
+                ModalBottomSheet(
+                    onDismissRequest = {},
+                    windowInsets = windowInsets
+                ) {
+                    Box(
+                        Modifier
+                            .testTag(sheetTag)
+                            .fillMaxHeight(0.4f)
+                    )
+                }
+            }
+
+            val simulatedRootWidth = rule.onNode(isPopup()).getUnclippedBoundsInRoot().width
+            val maxSheetWidth = 640.dp
+            val expectedSheetWidth = maxSheetWidth.coerceAtMost(simulatedRootWidth)
+            // Our sheet should be max 640 dp but fill the width if the container is less wide
+            val expectedSheetLeft = if (simulatedRootWidth <= expectedSheetWidth) {
+                0.dp
+            } else {
+                (simulatedRootWidth - expectedSheetWidth) / 2
+            }
+
+            rule.onNodeWithTag(sheetTag)
+                .onParent()
+                .assertLeftPositionInRootIsEqualTo(
+                    expectedLeft = expectedSheetLeft
+                )
+                .assertWidthIsEqualTo(expectedSheetWidth)
+        } catch (e: InterruptedException) {
+            fail("Unable to verify sheet width in landscape orientation")
+        } finally {
+            rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_defaultStateForSmallContentIsFullExpanded() {
+        lateinit var sheetState: SheetState
+        var height by mutableStateOf(0.dp)
+
+        rule.setContent {
+            val config = LocalContext.current.resources.configuration
+            height = config.screenHeightDp.dp
+            sheetState = rememberModalBottomSheetState()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                dragHandle = null,
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxWidth()
+                        .testTag(sheetTag)
+                        .height(sheetHeight)
+                )
+            }
+        }
+
+        height = rule.onNode(isPopup()).getUnclippedBoundsInRoot().height
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+        rule.onNodeWithTag(sheetTag).assertTopPositionInRootIsEqualTo(height - sheetHeight)
+    }
+
+    @Test
+    fun modalBottomSheet_defaultStateForLargeContentIsHalfExpanded() {
+        lateinit var sheetState: SheetState
+        var screenHeightPx by mutableStateOf(0f)
+
+        rule.setContent {
+            sheetState = rememberModalBottomSheetState()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        // Deliberately use fraction != 1f
+                        .fillMaxSize(0.6f)
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        screenHeightPx = with(rule.density) {
+            rule.onNode(isPopup()).getUnclippedBoundsInRoot().height.toPx()
+        }
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+        assertThat(sheetState.requireOffset())
+            .isWithin(1f)
+            .of(screenHeightPx / 2f)
+    }
+
+    @Test
+    fun modalBottomSheet_shortSheet_isDismissedOnBackPress() {
+        var showBottomSheet by mutableStateOf(true)
+        val sheetState = SheetState(skipPartiallyExpanded = true, density = rule.density)
+
+        rule.setContent {
+            val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+            if (showBottomSheet) {
+                ModalBottomSheet(
+                    sheetState = sheetState,
+                    onDismissRequest = { showBottomSheet = false },
+                    windowInsets = windowInsets
+                ) {
+                    Box(
+                        Modifier
+                            .fillMaxHeight(0.4f)
+                            .testTag(sheetTag)
+                    ) {
+                        Button(
+                            onClick = { dispatcher.onBackPressed() },
+                            modifier = Modifier.testTag(BackTestTag),
+                            content = { Text("Content") },
+                        )
+                    }
+                }
+            }
+        }
+
+        assertThat(sheetState.isVisible).isTrue()
+
+        rule.onNodeWithTag(BackTestTag).performClick()
+
+        rule.onNodeWithTag(BackTestTag).assertDoesNotExist()
+
+        // Popup should not exist
+        rule.onNodeWithTag(sheetTag).assertDoesNotExist()
+    }
+
+    @Test
+    fun modalBottomSheet_tallSheet_isDismissedOnBackPress() {
+        var showBottomSheet by mutableStateOf(true)
+        val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
+
+        rule.setContent {
+            val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+            if (showBottomSheet) {
+                ModalBottomSheet(
+                    sheetState = sheetState,
+                    onDismissRequest = { showBottomSheet = false },
+                    windowInsets = windowInsets
+                ) {
+                    Box(
+                        Modifier
+                            .fillMaxHeight(0.6f)
+                            .testTag(sheetTag)
+                    ) {
+                        Button(
+                            onClick = { dispatcher.onBackPressed() },
+                            modifier = Modifier.testTag(BackTestTag),
+                            content = { Text("Content") },
+                        )
+                    }
+                }
+            }
+        }
+        assertThat(sheetState.isVisible).isTrue()
+
+        rule.onNodeWithTag(BackTestTag).performClick()
+        rule.onNodeWithTag(BackTestTag).assertDoesNotExist()
+
+        // Popup should not exist
+        rule.onNodeWithTag(sheetTag).assertDoesNotExist()
+    }
+
+    @Test
+    fun modalBottomSheet_shortSheet_sizeChanges_snapsToNewTarget() {
+        lateinit var state: SheetState
+        var size by mutableStateOf(56.dp)
+        var screenHeight by mutableStateOf(0.dp)
+        val expectedExpandedAnchor by derivedStateOf {
+            with(rule.density) {
+                (screenHeight - size).toPx()
+            }
+        }
+
+        rule.setContent {
+            val context = LocalContext.current
+            screenHeight = context.resources.configuration.screenHeightDp.dp
+            state = rememberModalBottomSheetState()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = state,
+                dragHandle = null,
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .height(size)
+                        .fillMaxWidth()
+                )
+            }
+        }
+        screenHeight = rule.onNode(isPopup()).getUnclippedBoundsInRoot().height
+        assertThat(state.requireOffset()).isWithin(1f).of(expectedExpandedAnchor)
+
+        size = 100.dp
+        rule.waitForIdle()
+        assertThat(state.requireOffset()).isWithin(1f).of(expectedExpandedAnchor)
+
+        size = 30.dp
+        rule.waitForIdle()
+        assertThat(state.requireOffset()).isWithin(1f).of(expectedExpandedAnchor)
+    }
+
+    @Test
+    fun modalBottomSheet_emptySheet_expandDoesNotAnimate() {
+        lateinit var state: SheetState
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            state = rememberModalBottomSheetState()
+            scope = rememberCoroutineScope()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = state,
+                dragHandle = null,
+                windowInsets = windowInsets
+            ) {}
+        }
+        assertThat(state.anchoredDraggableState.currentValue).isEqualTo(SheetValue.Hidden)
+        val hiddenOffset = state.requireOffset()
+        scope.launch { state.show() }
+        rule.waitForIdle()
+
+        assertThat(state.anchoredDraggableState.currentValue).isEqualTo(SheetValue.Expanded)
+        val expandedOffset = state.requireOffset()
+
+        assertThat(hiddenOffset).isEqualTo(expandedOffset)
+    }
+
+    @Test
+    fun modalBottomSheet_anchorsChange_retainsCurrentValue() {
+        lateinit var state: SheetState
+        var amountOfItems by mutableStateOf(0)
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            state = rememberModalBottomSheetState()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = state,
+                dragHandle = null,
+                windowInsets = windowInsets
+            ) {
+                scope = rememberCoroutineScope()
+                LazyColumn {
+                    items(amountOfItems) {
+                        ListItem(headlineContent = { Text("$it") })
+                    }
+                }
+            }
+        }
+
+        assertThat(state.currentValue).isEqualTo(SheetValue.Hidden)
+
+        amountOfItems = 50
+        rule.waitForIdle()
+        scope.launch {
+            state.show()
+        }
+        // The anchors should now be {Hidden, PartiallyExpanded, Expanded}
+
+        rule.waitForIdle()
+        assertThat(state.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+
+        amountOfItems = 100 // The anchors should now be {Hidden, PartiallyExpanded, Expanded}
+
+        rule.waitForIdle()
+        assertThat(state.currentValue).isEqualTo(SheetValue.PartiallyExpanded) // We should
+        // retain the current value if possible
+        assertTrue(state.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Hidden))
+        assertTrue(state.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.PartiallyExpanded))
+        assertTrue(state.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Expanded))
+
+        amountOfItems = 0 // When the sheet height is 0, we should only have a hidden anchor
+        rule.waitForIdle()
+        assertThat(state.currentValue).isEqualTo(SheetValue.Hidden)
+        assertTrue(state.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Hidden))
+        assertFalse(state.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.PartiallyExpanded))
+        assertFalse(state.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Expanded))
+    }
+
+    @Test
+    fun modalBottomSheet_nestedScroll_consumesWithinBounds_scrollsOutsideBounds() {
+        lateinit var sheetState: SheetState
+        lateinit var scrollState: ScrollState
+        rule.setContent {
+            sheetState = rememberModalBottomSheetState()
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState
+            ) {
+                scrollState = rememberScrollState()
+                Column(
+                    Modifier
+                        .verticalScroll(scrollState)
+                        .testTag(sheetTag)
+                ) {
+                    repeat(100) {
+                        Text(it.toString(), Modifier.requiredHeight(50.dp))
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+
+        assertThat(scrollState.value).isEqualTo(0)
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput {
+                swipeUp(startY = bottom, endY = bottom / 2)
+            }
+        rule.waitForIdle()
+        assertThat(scrollState.value).isEqualTo(0)
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput {
+                swipeUp(startY = bottom, endY = top)
+            }
+        rule.waitForIdle()
+        assertThat(scrollState.value).isGreaterThan(0)
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput {
+                swipeDown(startY = top, endY = bottom)
+            }
+        rule.waitForIdle()
+        assertThat(scrollState.value).isEqualTo(0)
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput {
+                swipeDown(startY = top, endY = bottom / 2)
+            }
+        rule.waitForIdle()
+        assertThat(scrollState.value).isEqualTo(0)
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput {
+                swipeDown(startY = bottom / 2, endY = bottom)
+            }
+        rule.waitForIdle()
+        assertThat(scrollState.value).isEqualTo(0)
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+    }
+
+    @Test
+    fun modalBottomSheet_missingAnchors_findsClosest() {
+        val topTag = "ModalBottomSheetLayout"
+        var showShortContent by mutableStateOf(false)
+        val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
+        lateinit var scope: CoroutineScope
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = {},
+                modifier = Modifier.testTag(topTag),
+                sheetState = sheetState,
+                windowInsets = windowInsets
+            ) {
+                if (showShortContent) {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(100.dp)
+                    )
+                } else {
+                    Box(
+                        Modifier
+                            .fillMaxSize()
+                            .testTag(sheetTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(topTag).performTouchInput {
+            swipeDown()
+            swipeDown()
+        }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+        }
+
+        showShortContent = true
+        scope.launch { sheetState.show() } // We can't use LaunchedEffect with Swipeable in tests
+        // yet, so we're invoking this outside of composition. See b/254115946.
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_expandBySwiping() {
+        lateinit var sheetState: SheetState
+        rule.setContent {
+            sheetState = rememberModalBottomSheetState()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+        }
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput { swipeUp() }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_respectsConfirmValueChange() {
+        lateinit var sheetState: SheetState
+        rule.setContent {
+            sheetState = rememberModalBottomSheetState(
+                confirmValueChange = { newState ->
+                    newState != SheetValue.Hidden
+                }
+            )
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                dragHandle = {
+                    Box(
+                        Modifier
+                            .testTag(dragHandleTag)
+                            .size(dragHandleSize)
+                    )
+                },
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+        }
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput { swipeDown() }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+        }
+
+        rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).onParent()
+            .performSemanticsAction(SemanticsActions.Dismiss)
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+        }
+
+        // Tap Scrim
+        val outsideY = with(rule.density) {
+            rule.onAllNodes(isPopup()).onFirst().getUnclippedBoundsInRoot().height.roundToPx() / 4
+        }
+        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).click(0, outsideY)
+        rule.waitForIdle()
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_hideBySwiping_tallBottomSheet() {
+        lateinit var sheetState: SheetState
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            sheetState = rememberModalBottomSheetState()
+            scope = rememberCoroutineScope()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+        }
+
+        scope.launch { sheetState.expand() }
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+        }
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput { swipeDown() }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_hideBySwiping_skipPartiallyExpanded() {
+        lateinit var sheetState: SheetState
+        rule.setContent {
+            sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxWidth()
+                        .height(sheetHeight)
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+        }
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput { swipeDown() }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_hideManually_skipPartiallyExpanded(): Unit = runBlocking(
+        AutoTestFrameClock()
+    ) {
+        lateinit var sheetState: SheetState
+        rule.setContent {
+            sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+        assertThat(sheetState.currentValue == SheetValue.Expanded)
+
+        sheetState.hide()
+
+        assertThat(sheetState.currentValue == SheetValue.Hidden)
+    }
+
+    @Test
+    fun modalBottomSheet_testParialExpandReturnsIllegalStateException_whenSkipPartialExpanded() {
+        lateinit var scope: CoroutineScope
+        val bottomSheetState = SheetState(
+            skipPartiallyExpanded = true,
+            density = rule.density
+        )
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = bottomSheetState,
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+        scope.launch {
+            val exception =
+                kotlin.runCatching { bottomSheetState.partialExpand() }.exceptionOrNull()
+            assertThat(exception).isNotNull()
+            assertThat(exception).isInstanceOf(IllegalStateException::class.java)
+            assertThat(exception).hasMessageThat().containsMatch(
+                "Attempted to animate to partial expanded when skipPartiallyExpanded was " +
+                    "enabled. Set skipPartiallyExpanded to false to use this function."
+            )
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_testDismissAction_tallBottomSheet_whenPartiallyExpanded() {
+        rule.setContent {
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+            ModalBottomSheet(
+                onDismissRequest = {},
+                dragHandle = {
+                    Box(
+                        Modifier
+                            .testTag(dragHandleTag)
+                            .size(dragHandleSize)
+                    )
+                },
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).onParent()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Collapse))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Expand))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+            .performSemanticsAction(SemanticsActions.Dismiss)
+    }
+
+    @Test
+    fun modalBottomSheet_testExpandAction_tallBottomSheet_whenHalfExpanded() {
+        lateinit var sheetState: SheetState
+        rule.setContent {
+            sheetState = rememberModalBottomSheetState()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                dragHandle = {
+                    Box(
+                        Modifier
+                            .testTag(dragHandleTag)
+                            .size(dragHandleSize)
+                    )
+                },
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).onParent()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Collapse))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Expand))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+            .performSemanticsAction(SemanticsActions.Expand)
+
+        rule.runOnIdle {
+            assertThat(sheetState.requireOffset()).isEqualTo(0f)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_testDismissAction_tallBottomSheet_whenExpanded() {
+        lateinit var sheetState: SheetState
+        lateinit var scope: CoroutineScope
+
+        var screenHeightPx by mutableStateOf(0f)
+
+        rule.setContent {
+            sheetState = rememberModalBottomSheetState()
+            scope = rememberCoroutineScope()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                dragHandle = {
+                    Box(
+                        Modifier
+                            .testTag(dragHandleTag)
+                            .size(dragHandleSize)
+                    )
+                },
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+        screenHeightPx = with(rule.density) {
+            rule.onNode(isPopup()).getUnclippedBoundsInRoot().height.toPx()
+        }
+        scope.launch {
+            sheetState.expand()
+        }
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).onParent()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Expand))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Collapse))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+            .performSemanticsAction(SemanticsActions.Dismiss)
+
+        rule.runOnIdle {
+            assertThat(sheetState.requireOffset()).isWithin(1f).of(screenHeightPx)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_testCollapseAction_tallBottomSheet_whenExpanded() {
+        lateinit var sheetState: SheetState
+        lateinit var scope: CoroutineScope
+
+        var screenHeightPx by mutableStateOf(0f)
+
+        rule.setContent {
+            sheetState = rememberModalBottomSheetState()
+            scope = rememberCoroutineScope()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                dragHandle = {
+                    Box(
+                        Modifier
+                            .testTag(dragHandleTag)
+                            .size(dragHandleSize)
+                    )
+                },
+                windowInsets = windowInsets
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+        screenHeightPx = with(rule.density) {
+            rule.onNode(isPopup()).getUnclippedBoundsInRoot().height.toPx()
+        }
+        scope.launch {
+            sheetState.expand()
+        }
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).onParent()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Expand))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Collapse))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+            .performSemanticsAction(SemanticsActions.Collapse)
+
+        rule.runOnIdle {
+            assertThat(sheetState.requireOffset()).isWithin(1f).of(screenHeightPx / 2)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_shortSheet_anchorChangeHandler_previousTargetNotInAnchors_reconciles() {
+        val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
+        var hasSheetContent by mutableStateOf(false) // Start out with empty sheet content
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                dragHandle = null,
+                windowInsets = windowInsets
+            ) {
+                if (hasSheetContent) {
+                    Box(Modifier.fillMaxHeight(0.4f))
+                }
+            }
+        }
+
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+        assertFalse(
+            sheetState.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.PartiallyExpanded)
+        )
+        assertFalse(sheetState.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Expanded))
+
+        scope.launch { sheetState.show() }
+        rule.waitForIdle()
+
+        assertThat(sheetState.isVisible).isTrue()
+        assertThat(sheetState.currentValue).isEqualTo(sheetState.targetValue)
+
+        hasSheetContent = true // Recompose with sheet content
+        rule.waitForIdle()
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+    }
+
+    @Test
+    fun modalBottomSheet_tallSheet_anchorChangeHandler_previousTargetNotInAnchors_reconciles() {
+        val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
+        var hasSheetContent by mutableStateOf(false) // Start out with empty sheet content
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                dragHandle = null,
+                windowInsets = windowInsets
+            ) {
+                if (hasSheetContent) {
+                    Box(Modifier.fillMaxHeight(0.6f))
+                }
+            }
+        }
+
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+        assertFalse(
+            sheetState.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.PartiallyExpanded)
+        )
+        assertFalse(sheetState.anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Expanded))
+
+        scope.launch { sheetState.show() }
+        rule.waitForIdle()
+
+        assertThat(sheetState.isVisible).isTrue()
+        assertThat(sheetState.currentValue).isEqualTo(sheetState.targetValue)
+
+        hasSheetContent = true // Recompose with sheet content
+        rule.waitForIdle()
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+    }
+
+    @Test
+    fun modalBottomSheet_callsOnDismissRequest_onNestedScrollFling() {
+        var callCount by mutableStateOf(0)
+        val expectedCallCount = 1
+        val sheetState = SheetState(skipPartiallyExpanded = true, density = rule.density)
+
+        val nestedScrollDispatcher = NestedScrollDispatcher()
+        val nestedScrollConnection = object : NestedScrollConnection {
+            // No-Op
+        }
+        lateinit var scope: CoroutineScope
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            ModalBottomSheet(
+                onDismissRequest = { callCount += 1 },
+                sheetState = sheetState,
+                windowInsets = windowInsets
+            ) {
+                Column(
+                    Modifier
+                        .testTag(sheetTag)
+                        .nestedScroll(nestedScrollConnection, nestedScrollDispatcher)
+                ) {
+                    (0..50).forEach {
+                        Text(text = "$it")
+                    }
+                }
+            }
+        }
+
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+        val scrollableContentHeight = rule.onNodeWithTag(sheetTag).fetchSemanticsNode().size.height
+        // Simulate a drag + fling
+        nestedScrollDispatcher.dispatchPostScroll(
+            consumed = Offset.Zero,
+            available = Offset(x = 0f, y = scrollableContentHeight / 2f),
+            source = NestedScrollSource.Drag
+        )
+        scope.launch {
+            nestedScrollDispatcher.dispatchPostFling(
+                consumed = Velocity.Zero,
+                available = Velocity(x = 0f, y = with(rule.density) { 200.dp.toPx() })
+            )
+        }
+
+        rule.waitForIdle()
+        assertThat(sheetState.isVisible).isFalse()
+        assertThat(callCount).isEqualTo(expectedCallCount)
+    }
+
+    @Test
+    fun modalBottomSheet_preservesLayoutDirection() {
+        var value = LayoutDirection.Ltr
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                ModalBottomSheet(onDismissRequest = { /*TODO*/ }) {
+                    value = LocalLayoutDirection.current
+                }
+            }
+        }
+        rule.runOnIdle {
+            assertThat(value).isEqualTo(LayoutDirection.Rtl)
+        }
+    }
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun parameters() = arrayOf(
+            EdgeToEdgeWrapper("EdgeToEdge", true),
+            EdgeToEdgeWrapper("NonEdgeToEdge", false)
+        )
+    }
+
+    class EdgeToEdgeWrapper(val name: String, val edgeToEdgeEnabled: Boolean) {
+        override fun toString(): String {
+            return name
+        }
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerItemScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationDrawerItemScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerItemScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationDrawerItemScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerItemTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationDrawerItemTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerItemTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationDrawerItemTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationRailTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationRailTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/PermanentNavigationDrawerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/PermanentNavigationDrawerTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/PermanentNavigationDrawerTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/PermanentNavigationDrawerTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ProgressIndicatorScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ProgressIndicatorScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ProgressIndicatorScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ProgressIndicatorScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ProgressIndicatorTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ProgressIndicatorTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ProgressIndicatorTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ProgressIndicatorTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/RadioButtonTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/RadioButtonTest.kt
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ScaffoldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
new file mode 100644
index 0000000..ab6f9b9
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
@@ -0,0 +1,730 @@
+/*
+ * Copyright 2021 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.material3
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.zIndex
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.math.roundToInt
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ScaffoldTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val scaffoldTag = "Scaffold"
+    private val roundingError = 0.5.dp
+    private val fabSpacing = 16.dp
+
+    @Test
+    fun scaffold_onlyContent_takesWholeScreen() {
+        rule.setMaterialContentForSizeAssertions(
+            parentMaxWidth = 100.dp,
+            parentMaxHeight = 100.dp
+        ) {
+            Scaffold {
+                Text("Scaffold body")
+            }
+        }
+            .assertWidthIsEqualTo(100.dp)
+            .assertHeightIsEqualTo(100.dp)
+    }
+
+    @Test
+    fun scaffold_onlyContent_stackSlot() {
+        var child1: Offset = Offset.Zero
+        var child2: Offset = Offset.Zero
+        rule.setMaterialContent(lightColorScheme()) {
+            Scaffold {
+                Text(
+                    "One",
+                    Modifier.onGloballyPositioned { child1 = it.positionInParent() }
+                )
+                Text(
+                    "Two",
+                    Modifier.onGloballyPositioned { child2 = it.positionInParent() }
+                )
+            }
+        }
+        assertThat(child1.y).isEqualTo(child2.y)
+        assertThat(child1.x).isEqualTo(child2.x)
+    }
+
+    @Test
+    fun scaffold_AppbarAndContent_inColumn() {
+        var scaffoldSize: IntSize = IntSize.Zero
+        var appbarPosition: Offset = Offset.Zero
+        var contentPosition: Offset = Offset.Zero
+        var contentSize: IntSize = IntSize.Zero
+        rule.setMaterialContent(lightColorScheme()) {
+            Scaffold(
+                topBar = {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(50.dp)
+                            .background(color = Color.Red)
+                            .onGloballyPositioned { positioned: LayoutCoordinates ->
+                                appbarPosition = positioned.localToWindow(Offset.Zero)
+                            }
+                    )
+                },
+                modifier = Modifier
+                    .onGloballyPositioned { positioned: LayoutCoordinates ->
+                        scaffoldSize = positioned.size
+                    }
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .background(Color.Blue)
+                        .onGloballyPositioned { positioned: LayoutCoordinates ->
+                            contentPosition = positioned.positionInParent()
+                            contentSize = positioned.size
+                        }
+                )
+            }
+        }
+        assertThat(appbarPosition.y).isEqualTo(contentPosition.y)
+        assertThat(scaffoldSize).isEqualTo(contentSize)
+    }
+
+    @Test
+    fun scaffold_bottomBarAndContent_inStack() {
+        var scaffoldSize: IntSize = IntSize.Zero
+        var appbarPosition: Offset = Offset.Zero
+        var appbarSize: IntSize = IntSize.Zero
+        var contentPosition: Offset = Offset.Zero
+        var contentSize: IntSize = IntSize.Zero
+        rule.setMaterialContent(lightColorScheme()) {
+            Scaffold(
+                bottomBar = {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(50.dp)
+                            .background(color = Color.Red)
+                            .onGloballyPositioned { positioned: LayoutCoordinates ->
+                                appbarPosition = positioned.positionInParent()
+                                appbarSize = positioned.size
+                            }
+                    )
+                },
+                modifier = Modifier
+                    .onGloballyPositioned { positioned: LayoutCoordinates ->
+                        scaffoldSize = positioned.size
+                    }
+            ) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .background(color = Color.Blue)
+                        .onGloballyPositioned { positioned: LayoutCoordinates ->
+                            contentPosition = positioned.positionInParent()
+                            contentSize = positioned.size
+                        }
+                )
+            }
+        }
+        val appBarBottom = appbarPosition.y + appbarSize.height
+        val contentBottom = contentPosition.y + contentSize.height
+        assertThat(appBarBottom).isEqualTo(contentBottom)
+        assertThat(scaffoldSize).isEqualTo(contentSize)
+    }
+
+    @Test
+    fun scaffold_innerPadding_lambdaParam() {
+        var topBarSize: IntSize = IntSize.Zero
+        var bottomBarSize: IntSize = IntSize.Zero
+        lateinit var innerPadding: PaddingValues
+
+        rule.setContent {
+            Scaffold(
+                topBar = {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(50.dp)
+                            .background(color = Color.Red)
+                            .onGloballyPositioned { positioned: LayoutCoordinates ->
+                                topBarSize = positioned.size
+                            }
+                    )
+                },
+                bottomBar = {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(100.dp)
+                            .background(color = Color.Red)
+                            .onGloballyPositioned { positioned: LayoutCoordinates ->
+                                bottomBarSize = positioned.size
+                            }
+                    )
+                }
+            ) {
+                innerPadding = it
+                Text("body")
+            }
+        }
+        rule.runOnIdle {
+            with(rule.density) {
+                assertThat(innerPadding.calculateTopPadding())
+                    .isEqualTo(topBarSize.toSize().height.toDp())
+                assertThat(innerPadding.calculateBottomPadding())
+                    .isEqualTo(bottomBarSize.toSize().height.toDp())
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_topAppBarIsDrawnOnTopOfContent() {
+        rule.setContent {
+            Box(
+                Modifier
+                    .requiredSize(10.dp, 20.dp)
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(scaffoldTag)
+            ) {
+                Scaffold(
+                    topBar = {
+                        Box(
+                            Modifier
+                                .requiredSize(10.dp)
+                                .shadow(4.dp)
+                                .zIndex(4f)
+                                .background(color = Color.White)
+                        )
+                    }
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(scaffoldTag)
+            .captureToImage().asAndroidBitmap().apply {
+                // asserts the appbar(top half part) has the shadow
+                val yPos = height / 2 + 2
+                assertThat(Color(getPixel(0, yPos))).isNotEqualTo(Color.White)
+                assertThat(Color(getPixel(width / 2, yPos))).isNotEqualTo(Color.White)
+                assertThat(Color(getPixel(width - 1, yPos))).isNotEqualTo(Color.White)
+            }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_providesInsets_respectTopAppBar() {
+        rule.setContent {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
+                    topBar = {
+                        Box(Modifier.requiredSize(10.dp))
+                    }
+                ) { paddingValues ->
+                    // top is like top app bar + rounding error
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 10.dp,
+                        threshold = roundingError
+                    )
+                    // bottom is like the insets
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 3.dp,
+                        threshold = roundingError
+                    )
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_respectsProvidedInsets() {
+        rule.setContent {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 15.dp, bottom = 10.dp),
+                ) { paddingValues ->
+                    // topPadding is equal to provided top window inset
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 15.dp,
+                        threshold = roundingError
+                    )
+                    // bottomPadding is equal to provided bottom window inset
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 10.dp,
+                        threshold = roundingError
+                    )
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_respectsConsumedWindowInsets() {
+        rule.setContent {
+            Box(
+                Modifier
+                    .requiredSize(10.dp, 40.dp)
+                    .windowInsetsPadding(WindowInsets(top = 10.dp, bottom = 10.dp))
+            ) {
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 15.dp, bottom = 15.dp)
+                ) { paddingValues ->
+                    // Consumed windowInsetsPadding is omitted. This replicates behavior from
+                    // Modifier.windowInsetsPadding. (15.dp contentPadding - 10.dp consumedPadding)
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 5.dp,
+                        threshold = roundingError
+                    )
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 5.dp,
+                        threshold = roundingError
+                    )
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_providesInsets_respectCollapsedTopAppBar() {
+        rule.setContent {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
+                    topBar = {
+                        Box(Modifier.requiredSize(0.dp))
+                    }
+                ) { paddingValues ->
+                    // top is like the collapsed top app bar (i.e. 0dp) + rounding error
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 0.dp,
+                        threshold = roundingError
+                    )
+                    // bottom is like the insets
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 3.dp,
+                        threshold = roundingError
+                    )
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_providesInsets_respectsBottomAppBar() {
+        rule.setContent {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
+                    bottomBar = {
+                        Box(Modifier.requiredSize(10.dp))
+                    }
+                ) { paddingValues ->
+                    // bottom is like bottom app bar + rounding error
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 10.dp,
+                        threshold = roundingError
+                    )
+                    // top is like the insets
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 5.dp,
+                        threshold = roundingError
+                    )
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_insetsTests_snackbarRespectsInsets() {
+        val hostState = SnackbarHostState()
+        var snackbarSize: IntSize? = null
+        var snackbarPosition: Offset? = null
+        var density: Density? = null
+        rule.setContent {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
+                density = LocalDensity.current
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
+                    snackbarHost = {
+                        SnackbarHost(hostState = hostState,
+                            modifier = Modifier
+                                .onGloballyPositioned {
+                                    snackbarSize = it.size
+                                    snackbarPosition = it.positionInRoot()
+                                })
+                    }
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+        val snackbarBottomOffsetDp =
+            with(density!!) { (snackbarPosition!!.y.roundToInt() + snackbarSize!!.height).toDp() }
+        assertThat(rule.rootHeight() - snackbarBottomOffsetDp - 3.dp).isLessThan(1.dp)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_insetsTests_FabRespectsInsets() {
+        var fabSize: IntSize? = null
+        var fabPosition: Offset? = null
+        var density: Density? = null
+        rule.setContent {
+            Box(Modifier.requiredSize(10.dp, 20.dp)) {
+                density = LocalDensity.current
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
+                    floatingActionButton = {
+                        FloatingActionButton(onClick = {},
+                            modifier = Modifier
+                                .onGloballyPositioned {
+                                    fabSize = it.size
+                                    fabPosition = it.positionInRoot()
+                                }) {
+                            Text("Fab")
+                        }
+                    },
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+        val fabBottomOffsetDp =
+            with(density!!) { (fabPosition!!.y.roundToInt() + fabSize!!.height).toDp() }
+        assertThat(rule.rootHeight() - fabBottomOffsetDp - 3.dp).isLessThan(1.dp)
+    }
+
+    @Test
+    fun scaffold_fabPosition_start() {
+        var fabSize: IntSize? = null
+        var fabPosition: Offset? = null
+        rule.setContent {
+            Box(Modifier.requiredSize(200.dp, 200.dp)) {
+                Scaffold(
+                    floatingActionButton = {
+                        FloatingActionButton(
+                            onClick = {},
+                            modifier = Modifier
+                                .onGloballyPositioned {
+                                    fabSize = it.size
+                                    fabPosition = it.positionInRoot()
+                                }) {
+                            Text("Fab")
+                        }
+                    },
+                    floatingActionButtonPosition = FabPosition.Start,
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+        with(rule.density) {
+            assertThat(fabPosition!!.x).isWithin(1f).of(fabSpacing.toPx())
+            assertThat(fabPosition!!.y).isWithin(1f).of(
+                200.dp.toPx() - fabSize!!.height - fabSpacing.toPx()
+            )
+        }
+    }
+
+    @Test
+    fun scaffold_fabPosition_center() {
+        var fabSize: IntSize? = null
+        var fabPosition: Offset? = null
+        rule.setContent {
+            Box(Modifier.requiredSize(200.dp, 200.dp)) {
+                Scaffold(
+                    floatingActionButton = {
+                        FloatingActionButton(
+                            onClick = {},
+                            modifier = Modifier
+                                .onGloballyPositioned {
+                                    fabSize = it.size
+                                    fabPosition = it.positionInRoot()
+                                }) {
+                            Text("Fab")
+                        }
+                    },
+                    floatingActionButtonPosition = FabPosition.Center,
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+        with(rule.density) {
+            assertThat(fabPosition!!.x).isWithin(1f).of(
+                (200.dp.toPx() - fabSize!!.width) / 2f
+            )
+            assertThat(fabPosition!!.y).isWithin(1f).of(
+                200.dp.toPx() - fabSize!!.height - fabSpacing.toPx()
+            )
+        }
+    }
+
+    @Test
+    fun scaffold_fabPosition_end() {
+        var fabSize: IntSize? = null
+        var fabPosition: Offset? = null
+        rule.setContent {
+            Box(Modifier.requiredSize(200.dp, 200.dp)) {
+                Scaffold(
+                    floatingActionButton = {
+                        FloatingActionButton(
+                            onClick = {},
+                            modifier = Modifier
+                                .onGloballyPositioned {
+                                    fabSize = it.size
+                                    fabPosition = it.positionInRoot()
+                                }) {
+                            Text("Fab")
+                        }
+                    },
+                    floatingActionButtonPosition = FabPosition.End,
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+        with(rule.density) {
+            assertThat(fabPosition!!.x).isWithin(1f).of(
+                200.dp.toPx() - fabSize!!.width - fabSpacing.toPx()
+            )
+            assertThat(fabPosition!!.y).isWithin(1f).of(
+                200.dp.toPx() - fabSize!!.height - fabSpacing.toPx()
+            )
+        }
+    }
+
+    // Regression test for b/295536718
+    @Test
+    fun scaffold_onSizeChanged_calledBeforeLookaheadPlace() {
+        var size: IntSize? = null
+        var onSizeChangedCount = 0
+        var onPlaceCount = 0
+
+        rule.setContent {
+            LookaheadScope {
+                Scaffold {
+                    SubcomposeLayout { constraints ->
+                        val measurables = subcompose("second") {
+                            Box(
+                                Modifier
+                                    .size(45.dp)
+                                    .onSizeChanged {
+                                        onSizeChangedCount++
+                                        size = it
+                                    }
+                            )
+                        }
+                        val placeables = measurables.map { it.measure(constraints) }
+
+                        layout(constraints.maxWidth, constraints.maxHeight) {
+                            onPlaceCount++
+                            assertWithMessage("Expected onSizeChangedCount to be >= 1")
+                                .that(onSizeChangedCount).isAtLeast(1)
+                            assertThat(size).isNotNull()
+                            placeables.forEach { it.place(0, 0) }
+                        }
+                    }
+                }
+            }
+        }
+
+        assertWithMessage("Expected placeCount to be >= 1").that(onPlaceCount).isAtLeast(1)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun scaffold_subcomposeInMeasureFix_enabled_measuresChildrenInMeasurement() {
+        ScaffoldSubcomposeInMeasureFix = true
+        var size: IntSize? = null
+        var measured = false
+        rule.setContent {
+            Layout(
+                content = {
+                    Scaffold(
+                        content = {
+                            Box(Modifier.onSizeChanged { size = it })
+                        }
+                    )
+                }
+            ) { measurables, constraints ->
+                measurables.map { it.measure(constraints) }
+                measured = true
+                layout(0, 0) {
+                    // Empty measurement since we only care about placement
+                }
+            }
+        }
+
+        assertWithMessage("Measure should have been executed")
+            .that(measured).isTrue()
+        assertWithMessage("Expected size to be initialized")
+            .that(size).isNotNull()
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun scaffold_subcomposeInMeasureFix_disabled_measuresChildrenInPlacement() {
+        ScaffoldSubcomposeInMeasureFix = false
+        var size: IntSize? = null
+        var measured = false
+        var placed = false
+        rule.setContent {
+            Layout(
+                content = {
+                    Scaffold(
+                        content = {
+                            Box(Modifier.onSizeChanged { size = it })
+                        }
+                    )
+                }
+            ) { measurables, constraints ->
+                val placeables = measurables.map { it.measure(constraints) }
+                measured = true
+                assertWithMessage("Expected size to not be initialized in placement")
+                    .that(size).isNull()
+                layout(constraints.maxWidth, constraints.maxHeight) {
+                    placeables.forEach { it.place(0, 0) }
+                    placed = true
+                }
+            }
+        }
+
+        assertWithMessage("Measure should have been executed")
+            .that(measured).isTrue()
+        assertWithMessage("Placement should have been executed")
+            .that(placed).isTrue()
+        assertWithMessage("Expected size to be initialized")
+            .that(size).isNotNull()
+    }
+
+    private fun assertDpIsWithinThreshold(actual: Dp, expected: Dp, threshold: Dp) {
+        assertThat(actual.value).isWithin(threshold.value).of(expected.value)
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SearchBarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SearchBarTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarTest.kt
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SegmentedButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SegmentedButtonScreenshotTest.kt
new file mode 100644
index 0000000..e079c56
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SegmentedButtonScreenshotTest.kt
@@ -0,0 +1,228 @@
+/*
+ * 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.material3
+
+import android.os.Build
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Favorite
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalMaterial3Api::class)
+
+class SegmentedButtonScreenshotTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+    @Test
+    fun all_unselected() {
+        rule.setMaterialContent(lightColorScheme()) {
+            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
+                values.forEach {
+                    SegmentedButton(
+                        checked = false,
+                        onCheckedChange = {},
+                        shape = RectangleShape,
+                    ) {
+                        Text(it)
+                    }
+                }
+            }
+        }
+
+        assertButtonAgainstGolden("all_unselected")
+    }
+
+    @Test
+    fun all_selected() {
+        rule.setMaterialContent(lightColorScheme()) {
+            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
+                values.forEach {
+                    SegmentedButton(checked = true, onCheckedChange = {}, shape = RectangleShape) {
+                        Text(it)
+                    }
+                }
+            }
+        }
+
+        assertButtonAgainstGolden("all_selected")
+    }
+
+    @Test
+    fun middle_selected() {
+        rule.setMaterialContent(lightColorScheme()) {
+            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
+                values.forEachIndexed { index, item ->
+                    SegmentedButton(
+                        checked = index == 1,
+                        onCheckedChange = {},
+                        shape = RectangleShape,
+                    ) {
+                        Text(item)
+                    }
+                }
+            }
+        }
+
+        assertButtonAgainstGolden("middle_selected")
+    }
+
+    @Test
+    fun middle_selected_with_icon() {
+        rule.setMaterialContent(lightColorScheme()) {
+            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
+                values.forEachIndexed { index, item ->
+                    SegmentedButton(
+                        checked = index == 1,
+                        onCheckedChange = {},
+                        icon = if (index == 1) {
+                            { SegmentedButtonDefaults.ActiveIcon() }
+                        } else {
+                            {
+                                Icon(
+                                    imageVector = Icons.Outlined.Favorite,
+                                    contentDescription = null,
+                                    modifier = Modifier.size(SegmentedButtonDefaults.IconSize)
+                                )
+                            }
+                        },
+                        shape = RectangleShape,
+                    ) {
+                        Text(item)
+                    }
+                }
+            }
+        }
+
+        assertButtonAgainstGolden("middle_selected_with_icon")
+    }
+
+    @Test
+    fun stroke_zIndex() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val colors = SegmentedButtonDefaults.colors(
+                activeBorderColor = Color.Blue,
+                inactiveBorderColor = Color.Yellow
+            )
+            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
+                values.forEachIndexed { index, item ->
+                    SegmentedButton(
+                        checked = index == 1,
+                        onCheckedChange = {},
+                        colors = colors,
+                        shape = RectangleShape,
+                    ) {
+                        Text(item)
+                    }
+                }
+            }
+        }
+
+        assertButtonAgainstGolden("stroke_zIndex")
+    }
+
+    @Test
+    fun button_shape() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val colors = SegmentedButtonDefaults.colors(
+                activeBorderColor = Color.Blue,
+                inactiveBorderColor = Color.Yellow
+            )
+            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
+                values.forEachIndexed { index, item ->
+                    val shape = SegmentedButtonDefaults.itemShape(index, values.size)
+
+                    SegmentedButton(
+                        checked = index == 1,
+                        onCheckedChange = {},
+                        colors = colors,
+                        shape = shape,
+                    ) {
+                        Text(item)
+                    }
+                }
+            }
+        }
+
+        assertButtonAgainstGolden("button_shape")
+    }
+
+    @Test
+    fun all_unselected_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
+                values.forEach {
+                    SegmentedButton(
+                        checked = false,
+                        onCheckedChange = {},
+                        shape = RectangleShape,
+                    ) {
+                        Text(it)
+                    }
+                }
+            }
+        }
+
+        assertButtonAgainstGolden("all_unselected_darkTheme")
+    }
+
+    @Test
+    fun all_selected_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
+                values.forEach {
+                    SegmentedButton(checked = true, onCheckedChange = {}, shape = RectangleShape) {
+                        Text(it)
+                    }
+                }
+            }
+        }
+
+        assertButtonAgainstGolden("all_selected_darkTheme")
+    }
+
+    private fun assertButtonAgainstGolden(goldenName: String) {
+        rule.onNodeWithTag(testTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, goldenName)
+    }
+
+    private val values = listOf("Day", "Month", "Week")
+
+    private val testTag = "button"
+}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SegmentedButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SegmentedButtonTest.kt
new file mode 100644
index 0000000..8f1a178
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SegmentedButtonTest.kt
@@ -0,0 +1,218 @@
+/*
+ * 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.material3
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.tokens.OutlinedSegmentedButtonTokens
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsOff
+import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.assertWidthIsAtLeast
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalMaterial3Api::class)
+class SegmentedButtonTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun toggleableSegmentedButton_itemsDisplay() {
+        val values = listOf("Day", "Month", "Week")
+
+        rule.setMaterialContent(lightColorScheme()) {
+            MultiChoiceSegmentedButtonRow {
+                values.forEach {
+                    SegmentedButton(
+                        shape = RectangleShape,
+                        checked = false,
+                        onCheckedChange = {}
+                    ) {
+                        Text(it)
+                    }
+                }
+            }
+        }
+
+        values.forEach { rule.onNodeWithText(it).assertIsDisplayed() }
+    }
+
+    @Test
+    fun selectableSegmentedButton_itemsDisplay() {
+        val values = listOf("Day", "Month", "Week")
+
+        rule.setMaterialContent(lightColorScheme()) {
+            SingleChoiceSegmentedButtonRow {
+                values.forEach {
+                    SegmentedButton(
+                        shape = RectangleShape,
+                        selected = false,
+                        onClick = {}
+                    ) {
+                        Text(it)
+                    }
+                }
+            }
+        }
+
+        values.forEach { rule.onNodeWithText(it).assertIsDisplayed() }
+    }
+
+    @Test
+    fun segmentedButton_itemsChecked() {
+        var checked by mutableStateOf(true)
+        rule.setMaterialContent(lightColorScheme()) {
+            MultiChoiceSegmentedButtonRow {
+                SegmentedButton(
+                    onCheckedChange = { checked = it },
+                    checked = checked,
+                    shape = RectangleShape,
+                ) {
+                    Text("Day")
+                }
+                SegmentedButton(
+                    onCheckedChange = { checked = it },
+                    checked = !checked,
+                    shape = RectangleShape,
+                ) {
+                    Text("Month")
+                }
+            }
+        }
+
+        rule.onNodeWithText("Day").assertIsOn()
+        rule.onNodeWithText("Month").assertIsOff()
+
+        rule.runOnIdle {
+            checked = false
+        }
+
+        rule.onNodeWithText("Day").assertIsOff()
+        rule.onNodeWithText("Month").assertIsOn()
+    }
+
+    @Test
+    fun selectableSegmentedButton_semantics() {
+        rule.setMaterialContent(lightColorScheme()) {
+            SingleChoiceSegmentedButtonRow(modifier = Modifier.testTag("row")) {
+                SegmentedButton(
+                    selected = false,
+                    onClick = {},
+                    shape = RectangleShape,
+                ) {
+                    Text("Day")
+                }
+                SegmentedButton(
+                    selected = false,
+                    onClick = {},
+                    shape = RectangleShape,
+                ) {
+                    Text("Month")
+                }
+            }
+        }
+
+        val semanticsNode = rule.onNodeWithTag("row").fetchSemanticsNode()
+        val selectableGroup = semanticsNode.config.getOrNull(SemanticsProperties.SelectableGroup)
+
+        assertThat(selectableGroup).isNotNull()
+    }
+
+    @Test
+    fun segmentedButton_icon() {
+        var checked by mutableStateOf(false)
+        rule.setMaterialContent(lightColorScheme()) {
+            MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag("row")) {
+                SegmentedButton(
+                    checked = checked,
+                    onCheckedChange = {},
+                    icon = { Text(if (checked) "checked" else "unchecked") },
+                    shape = RectangleShape,
+                ) {
+                    Text("Day")
+                }
+            }
+        }
+
+        rule.onNodeWithText("unchecked").assertIsDisplayed()
+
+        rule.runOnIdle { checked = true }
+        rule.waitForIdle()
+
+        rule.onNodeWithText("checked").assertIsDisplayed()
+    }
+
+    @Test
+    fun segmentedButton_Sizing() {
+        val itemSize = 60.dp
+
+        rule.setMaterialContentForSizeAssertions(
+            parentMaxWidth = 300.dp, parentMaxHeight = 100.dp
+        ) {
+            MultiChoiceSegmentedButtonRow {
+                SegmentedButton(checked = false, onCheckedChange = {}, shape = RectangleShape) {
+                    Text(modifier = Modifier.width(60.dp), text = "Day")
+                }
+                SegmentedButton(checked = false, onCheckedChange = {}, shape = RectangleShape) {
+                    Text(modifier = Modifier.width(30.dp), text = "Month")
+                }
+            }
+        }
+            .assertWidthIsAtLeast((itemSize + 12.dp * 2) * 2)
+            .assertHeightIsEqualTo(OutlinedSegmentedButtonTokens.ContainerHeight)
+    }
+
+    @Test
+    fun segmentedButtonBorder_default_matchesSpec() {
+        lateinit var border: BorderStroke
+        var specColor: Color = Color.Unspecified
+        rule.setMaterialContent(lightColorScheme()) {
+            specColor = OutlinedSegmentedButtonTokens.OutlineColor.value
+            border = SegmentedButtonDefaults.Border.borderStroke(
+                checked = true,
+                enabled = true,
+                colors = SegmentedButtonDefaults.colors()
+            )
+        }
+
+        assertThat((border.brush as SolidColor).value).isEqualTo(specColor)
+        assertThat(border.width).isEqualTo(OutlinedSegmentedButtonTokens.OutlineWidth)
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ShapesScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShapesScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ShapesScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShapesScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt
new file mode 100644
index 0000000..883e39b
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt
@@ -0,0 +1,352 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import android.os.Build
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.remember
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class SliderScreenshotTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+    val wrap = Modifier.requiredWidth(70.dp).wrapContentSize(Alignment.TopStart)
+
+    private val wrapperTestTag = "sliderWrapper"
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun sliderTest_origin() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Slider(
+                    remember { SliderState(0f) }
+                )
+            }
+        }
+        assertSliderAgainstGolden("slider_origin")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun sliderTest_origin_disabled() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Slider(
+                    remember { SliderState(0f) },
+                    enabled = false
+                )
+            }
+        }
+        assertSliderAgainstGolden("slider_origin_disabled")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun sliderTest_middle() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Slider(
+                    remember { SliderState(0.5f) }
+                )
+            }
+        }
+        assertSliderAgainstGolden("slider_middle")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun sliderTest_middle_dark() {
+        rule.setMaterialContent(darkColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Slider(
+                    remember { SliderState(0.5f) }
+                )
+            }
+        }
+        assertSliderAgainstGolden("slider_middle_dark")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun sliderTest_middle_dark_disabled() {
+        rule.setMaterialContent(darkColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Slider(
+                    remember { SliderState(0.5f) },
+                    enabled = false
+                )
+            }
+        }
+        assertSliderAgainstGolden("slider_middle_dark_disabled")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun sliderTest_end() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Slider(
+                    remember { SliderState(1f) }
+                )
+            }
+        }
+        assertSliderAgainstGolden("slider_end")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun sliderTest_middle_steps() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Slider(
+                    remember { SliderState(0.5f, steps = 5) }
+                )
+            }
+        }
+        assertSliderAgainstGolden("slider_middle_steps")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun sliderTest_middle_steps_dark() {
+        rule.setMaterialContent(darkColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Slider(
+                    remember { SliderState(0.5f, steps = 5) }
+                )
+            }
+        }
+        assertSliderAgainstGolden("slider_middle_steps_dark")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun sliderTest_middle_steps_disabled() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Slider(
+                    remember { SliderState(0.5f, steps = 5) },
+                    enabled = false
+                )
+            }
+        }
+        assertSliderAgainstGolden("slider_middle_steps_disabled")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun sliderTest_customColors() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Slider(
+                    remember { SliderState(0.5f, steps = 5) },
+                    colors = SliderDefaults.colors(
+                        thumbColor = Color.Red,
+                        activeTrackColor = Color.Blue,
+                        activeTickColor = Color.Yellow,
+                        inactiveTickColor = Color.Magenta
+                    )
+
+                )
+            }
+        }
+        assertSliderAgainstGolden("slider_customColors")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun sliderTest_customColors_disabled() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Slider(
+                    remember { SliderState(0.5f, steps = 5) },
+                    enabled = false,
+                    // this is intentionally made to appear as enabled in disabled state for a
+                    // brighter test
+                    colors = SliderDefaults.colors(
+                        disabledThumbColor = Color.Blue,
+                        disabledActiveTrackColor = Color.Red,
+                        disabledInactiveTrackColor = Color.Yellow,
+                        disabledActiveTickColor = Color.Magenta,
+                        disabledInactiveTickColor = Color.Cyan
+                    )
+
+                )
+            }
+        }
+        assertSliderAgainstGolden("slider_customColors_disabled")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSliderTest_middle_steps_disabled() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RangeSlider(
+                    remember {
+                        RangeSliderState(0.5f, 1f, steps = 5)
+                    },
+                    enabled = false
+                )
+            }
+        }
+        assertSliderAgainstGolden("rangeSlider_middle_steps_disabled")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSliderTest_middle_steps_enabled() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RangeSlider(
+                    remember {
+                        RangeSliderState(
+                            0.5f,
+                            1f,
+                            steps = 5
+                        )
+                    }
+                )
+            }
+        }
+        assertSliderAgainstGolden("rangeSlider_middle_steps_enabled")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSliderTest_middle_steps_dark_enabled() {
+        rule.setMaterialContent(darkColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RangeSlider(
+                    remember {
+                        RangeSliderState(
+                            0.5f,
+                            1f,
+                            steps = 5
+                        )
+                    }
+                )
+            }
+        }
+        assertSliderAgainstGolden("rangeSlider_middle_steps_dark_enabled")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSliderTest_middle_steps_dark_disabled() {
+        rule.setMaterialContent(darkColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RangeSlider(
+                    remember {
+                        RangeSliderState(0.5f, 1f, steps = 5)
+                    },
+                    enabled = false
+                )
+            }
+        }
+        assertSliderAgainstGolden("rangeSlider_middle_steps_dark_disabled")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSliderTest_overlapingThumbs() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RangeSlider(
+                    remember {
+                        RangeSliderState(0.5f, 0.51f)
+                    }
+                )
+            }
+        }
+        assertSliderAgainstGolden("rangeSlider_overlapingThumbs")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSliderTest_fullRange() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RangeSlider(
+                    remember {
+                        RangeSliderState(0f, 1f)
+                    }
+                )
+            }
+        }
+        assertSliderAgainstGolden("rangeSlider_fullRange")
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSliderTest_steps_customColors() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                val state = remember {
+                    RangeSliderState(
+                        30f,
+                        70f,
+                        steps = 9,
+                        valueRange = 0f..100f
+                    )
+                }
+                RangeSlider(
+                    state = state,
+                    colors = SliderDefaults.colors(
+                        thumbColor = Color.Blue,
+                        activeTrackColor = Color.Red,
+                        inactiveTrackColor = Color.Yellow,
+                        activeTickColor = Color.Magenta,
+                        inactiveTickColor = Color.Cyan
+                    )
+                )
+            }
+        }
+        assertSliderAgainstGolden("rangeSlider_steps_customColors")
+    }
+
+    private fun assertSliderAgainstGolden(goldenName: String) {
+        rule.onNodeWithTag(wrapperTestTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, goldenName)
+    }
+}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderTest.kt
new file mode 100644
index 0000000..a06c97d
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderTest.kt
@@ -0,0 +1,1368 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.interaction.DragInteraction
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.tokens.SliderTokens
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.expectAssertionError
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.boundsInParent
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertRangeInfoEquals
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.isFocusable
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class SliderTest {
+    private val tag = "slider"
+    private val SliderTolerance = 0.003f
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun sliderPosition_valueCoercion() {
+        val state = SliderState(0f)
+        rule.setContent {
+            Slider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+        rule.runOnIdle {
+            state.value = 2f
+        }
+        rule.onNodeWithTag(tag).assertRangeInfoEquals(ProgressBarRangeInfo(1f, 0f..1f, 0))
+        rule.runOnIdle {
+            state.value = -123145f
+        }
+        rule.onNodeWithTag(tag).assertRangeInfoEquals(ProgressBarRangeInfo(0f, 0f..1f, 0))
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test(expected = IllegalArgumentException::class)
+    fun sliderPosition_stepsThrowWhenLessThanZero() {
+        rule.setContent {
+            Slider(SliderState(initialValue = 0f, initialOnValueChange = {}, steps = -1))
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_semantics_continuous() {
+        val state = SliderState(0f)
+
+        rule.setMaterialContent(lightColorScheme()) {
+            Slider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .assertRangeInfoEquals(ProgressBarRangeInfo(0f, 0f..1f, 0))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.SetProgress))
+
+        rule.runOnUiThread {
+            state.value = 0.5f
+        }
+
+        rule.onNodeWithTag(tag).assertRangeInfoEquals(ProgressBarRangeInfo(0.5f, 0f..1f, 0))
+
+        rule.onNodeWithTag(tag)
+            .performSemanticsAction(SemanticsActions.SetProgress) { it(0.7f) }
+
+        rule.onNodeWithTag(tag).assertRangeInfoEquals(ProgressBarRangeInfo(0.7f, 0f..1f, 0))
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_semantics_stepped() {
+        val state = SliderState(0f, steps = 4)
+
+        rule.setMaterialContent(lightColorScheme()) {
+            Slider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .assertRangeInfoEquals(ProgressBarRangeInfo(0f, 0f..1f, 4))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.SetProgress))
+
+        rule.runOnUiThread {
+            state.value = 0.6f
+        }
+
+        rule.onNodeWithTag(tag).assertRangeInfoEquals(ProgressBarRangeInfo(0.6f, 0f..1f, 4))
+
+        rule.onNodeWithTag(tag)
+            .performSemanticsAction(SemanticsActions.SetProgress) { it(0.75f) }
+
+        rule.onNodeWithTag(tag).assertRangeInfoEquals(ProgressBarRangeInfo(0.8f, 0f..1f, 4))
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_semantics_focusable() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Slider(
+                SliderState(0f),
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Focused))
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_semantics_disabled() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Slider(
+                state = SliderState(0f),
+                modifier = Modifier.testTag(tag),
+                enabled = false
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Disabled))
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_drag() {
+        val state = SliderState(0f)
+        var slop = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            slop = LocalViewConfiguration.current.touchSlop
+            Slider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.runOnUiThread {
+            Truth.assertThat(state.value).isEqualTo(0f)
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(100f, 0f))
+                up()
+                expected = calculateFraction(left, right, centerX + 100 - slop)
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_drag_out_of_bounds() {
+        val state = SliderState(0f)
+        var slop = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            slop = LocalViewConfiguration.current.touchSlop
+            Slider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.runOnUiThread {
+            Truth.assertThat(state.value).isEqualTo(0f)
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(width.toFloat(), 0f))
+                moveBy(Offset(-width.toFloat(), 0f))
+                moveBy(Offset(-width.toFloat(), 0f))
+                moveBy(Offset(width.toFloat() + 100f, 0f))
+                up()
+                expected = calculateFraction(left, right, centerX + 100 - slop)
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_tap() {
+        val state = SliderState(0f)
+
+        rule.setMaterialContent(lightColorScheme()) {
+            Slider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.runOnUiThread {
+            Truth.assertThat(state.value).isEqualTo(0f)
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(Offset(centerX + 50, centerY))
+                up()
+                expected = calculateFraction(left, right, centerX + 50)
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    /**
+     * Guarantee slider doesn't move as we scroll, tapping still works
+     */
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_scrollableContainer() {
+        val state = SliderState(0f)
+        val offset = mutableStateOf(0f)
+
+        rule.setContent {
+            Column(
+                modifier = Modifier
+                    .height(2000.dp)
+                    .scrollable(
+                        orientation = Orientation.Vertical,
+                        state = rememberScrollableState { delta ->
+                            offset.value += delta
+                            delta
+                        })
+            ) {
+                Slider(
+                    state = state,
+                    modifier = Modifier.testTag(tag)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(offset.value).isEqualTo(0f)
+        }
+
+        // Just scroll
+        rule.onNodeWithTag(tag, useUnmergedTree = true)
+            .performTouchInput {
+                down(Offset(centerX, centerY))
+                moveBy(Offset(0f, 500f))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(offset.value).isGreaterThan(0f)
+            Truth.assertThat(state.value).isEqualTo(0f)
+        }
+
+        // Tap
+        var expected = 0f
+        rule.onNodeWithTag(tag, useUnmergedTree = true)
+            .performTouchInput {
+                click(Offset(centerX, centerY))
+                expected = calculateFraction(left, right, centerX)
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_tap_rangeChange() {
+        val rangeEnd = mutableStateOf(0.25f)
+        lateinit var state: SliderState
+
+        rule.setMaterialContent(lightColorScheme()) {
+            state = remember(rangeEnd.value) {
+                SliderState(0f, valueRange = 0f..rangeEnd.value)
+            }
+            Slider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        // change to 1 since [calculateFraction] coerces between 0..1
+        rule.runOnUiThread {
+            rangeEnd.value = 1f
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                click(Offset(centerX + 50, centerY))
+                expected = calculateFraction(left, right, centerX + 50)
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_drag_rtl() {
+        val state = SliderState(0f)
+        var slop = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                slop = LocalViewConfiguration.current.touchSlop
+                Slider(
+                    state = state,
+                    modifier = Modifier.testTag(tag)
+                )
+            }
+        }
+
+        rule.runOnUiThread {
+            Truth.assertThat(state.value).isEqualTo(0f)
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(100f, 0f))
+                up()
+                // subtract here as we're in rtl and going in the opposite direction
+                expected = calculateFraction(left, right, centerX - 100 + slop)
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_tap_rtl() {
+        val state = SliderState(0f)
+
+        rule.setMaterialContent(lightColorScheme()) {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                Slider(
+                    state = state,
+                    modifier = Modifier.testTag(tag)
+                )
+            }
+        }
+
+        rule.runOnUiThread {
+            Truth.assertThat(state.value).isEqualTo(0f)
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(Offset(centerX + 50, centerY))
+                up()
+                expected = calculateFraction(left, right, centerX - 50)
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.value).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    private fun calculateFraction(left: Float, right: Float, pos: Float) = with(rule.density) {
+        val offset = (ThumbWidth / 2).toPx()
+        val start = left + offset
+        val end = right - offset
+        ((pos - start) / (end - start)).coerceIn(0f, 1f)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_sizes() {
+        val state = SliderState(0f)
+        rule
+            .setMaterialContentForSizeAssertions(
+                parentMaxWidth = 100.dp,
+                parentMaxHeight = 100.dp
+            ) { Slider(state) }
+            .assertHeightIsEqualTo(48.dp)
+            .assertWidthIsEqualTo(100.dp)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_sizes_within_row() {
+        val rowWidth = 100.dp
+        val spacerWidth = 10.dp
+
+        rule.setMaterialContent(lightColorScheme()) {
+            Row(modifier = Modifier.requiredWidth(rowWidth)) {
+                Spacer(Modifier.width(spacerWidth))
+                Slider(
+                    state = SliderState(0f, {}),
+                    modifier = Modifier
+                        .testTag(tag)
+                        .weight(1f)
+                )
+                Spacer(Modifier.width(spacerWidth))
+            }
+        }
+
+        rule.onNodeWithTag(tag)
+            .assertWidthIsEqualTo(rowWidth - spacerWidth.times(2))
+            .assertHeightIsEqualTo(SliderTokens.HandleHeight)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_min_size() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.requiredSize(0.dp)) {
+                Slider(
+                    state = SliderState(0f, {}),
+                    modifier = Modifier.testTag(tag)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(tag)
+            .assertWidthIsEqualTo(SliderTokens.HandleWidth)
+            .assertHeightIsEqualTo(SliderTokens.HandleHeight)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_noUnwantedCallbackCalls() {
+        val callCount = mutableStateOf(0f)
+        val state = SliderState(0f, { callCount.value += 1 })
+
+        rule.setMaterialContent(lightColorScheme()) {
+            Slider(
+                state = state,
+                modifier = Modifier.testTag(tag),
+            )
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(callCount.value).isEqualTo(0f)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_valueChangeFinished_calledOnce() {
+        val callCount = mutableStateOf(0f)
+        val state = SliderState(0f, onValueChangeFinished = { callCount.value += 1 })
+
+        rule.setMaterialContent(lightColorScheme()) {
+            Slider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(callCount.value).isEqualTo(0)
+        }
+
+        rule.onNodeWithTag(tag).performTouchInput {
+            down(center)
+            moveBy(Offset(50f, 50f))
+            up()
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(callCount.value).isEqualTo(1)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_setProgress_callsOnValueChangeFinished() {
+        val callCount = mutableStateOf(0)
+        val state = SliderState(0f, onValueChangeFinished = { callCount.value += 1 })
+
+        rule.setMaterialContent(lightColorScheme()) {
+            Slider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(callCount.value).isEqualTo(0)
+        }
+
+        rule.onNodeWithTag(tag)
+            .performSemanticsAction(SemanticsActions.SetProgress) { it(0.8f) }
+
+        rule.runOnIdle {
+            Truth.assertThat(callCount.value).isEqualTo(1)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_interactionSource_resetWhenDisposed() {
+        val interactionSource = MutableInteractionSource()
+        var emitSlider by mutableStateOf(true)
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box {
+                if (emitSlider) {
+                    Slider(
+                        state = SliderState(0.5f, {}),
+                        modifier = Modifier.testTag(tag),
+                        interactionSource = interactionSource
+                    )
+                }
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope!!.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(100f, 0f))
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).hasSize(1)
+            Truth.assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
+        }
+
+        // Dispose
+        rule.runOnIdle {
+            emitSlider = false
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).hasSize(2)
+            Truth.assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
+            Truth.assertThat(interactions[1]).isInstanceOf(DragInteraction.Cancel::class.java)
+            Truth.assertThat((interactions[1] as DragInteraction.Cancel).start)
+                .isEqualTo(interactions[0])
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_onValueChangedFinish_afterTap() {
+        var changedFlag = false
+        rule.setContent {
+            Slider(
+                state = SliderState(0f, {}, onValueChangeFinished = { changedFlag = true }),
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                click(center)
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(changedFlag).isTrue()
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_zero_width() {
+        rule.setMaterialContentForSizeAssertions(
+            parentMaxHeight = 0.dp,
+            parentMaxWidth = 0.dp
+        ) { Slider(SliderState(1f, {})) }
+            .assertHeightIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(0.dp)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_thumb_recomposition() {
+        val state = SliderState(0f)
+        val recompositionCounter = SliderRecompositionCounter()
+
+        rule.setContent {
+            Slider(
+                state = state,
+                modifier = Modifier.testTag(tag),
+                thumb = { sliderState -> recompositionCounter.OuterContent(sliderState) }
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(100f, 0f))
+                moveBy(Offset(-100f, 0f))
+                moveBy(Offset(100f, 0f))
+            }
+        rule.runOnIdle {
+            Truth.assertThat(recompositionCounter.outerRecomposition).isEqualTo(1)
+            Truth.assertThat(recompositionCounter.innerRecomposition).isEqualTo(4)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_track_recomposition() {
+        val state = SliderState(0f)
+        val recompositionCounter = SliderRecompositionCounter()
+
+        rule.setContent {
+            Slider(
+                state = state,
+                modifier = Modifier.testTag(tag),
+                track = { sliderState -> recompositionCounter.OuterContent(sliderState) }
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(100f, 0f))
+                moveBy(Offset(-100f, 0f))
+                moveBy(Offset(100f, 0f))
+            }
+        rule.runOnIdle {
+            Truth.assertThat(recompositionCounter.outerRecomposition).isEqualTo(1)
+            Truth.assertThat(recompositionCounter.innerRecomposition).isEqualTo(4)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_parentWithInfiniteWidth_minWidth() {
+        val state = SliderState(0f)
+        rule.setMaterialContentForSizeAssertions {
+            Box(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
+                Slider(state)
+            }
+        }.assertWidthIsEqualTo(48.dp)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun slider_rowWithInfiniteWidth() {
+        expectAssertionError(false) {
+            rule.setContent {
+                Row(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
+                    Slider(
+                        state = SliderState(0f),
+                        modifier = Modifier.weight(1f)
+                    )
+                }
+            }
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_dragThumb() {
+        val state = RangeSliderState(0f, 1f)
+        var slop = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            slop = LocalViewConfiguration.current.touchSlop
+            RangeSlider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.runOnUiThread {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
+            Truth.assertThat(state.activeRangeEnd).isEqualTo(1f)
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(slop, 0f))
+                moveBy(Offset(100f, 0f))
+                expected = calculateFraction(left, right, centerX + 100)
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
+            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_drag_out_of_bounds() {
+        val state = RangeSliderState(0f, 1f)
+        var slop = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            slop = LocalViewConfiguration.current.touchSlop
+            RangeSlider(
+                state = state,
+                modifier = Modifier.testTag(tag),
+            )
+        }
+
+        rule.runOnUiThread {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
+            Truth.assertThat(state.activeRangeEnd).isEqualTo(1f)
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(slop, 0f))
+                moveBy(Offset(width.toFloat(), 0f))
+                moveBy(Offset(-width.toFloat(), 0f))
+                moveBy(Offset(-width.toFloat(), 0f))
+                moveBy(Offset(width.toFloat() + 100f, 0f))
+                up()
+                expected = calculateFraction(left, right, centerX + 100)
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
+            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_drag_overlap_thumbs() {
+        val state = RangeSliderState(0.5f, 1f)
+        var slop = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            slop = LocalViewConfiguration.current.touchSlop
+            RangeSlider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(centerRight)
+                moveBy(Offset(-slop, 0f))
+                moveBy(Offset(-width.toFloat(), 0f))
+                up()
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0.5f)
+            Truth.assertThat(state.activeRangeEnd).isEqualTo(0.5f)
+        }
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(-slop, 0f))
+                moveBy(Offset(-width.toFloat(), 0f))
+                up()
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
+            Truth.assertThat(state.activeRangeEnd).isEqualTo(0.5f)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_tap() {
+        val state = RangeSliderState(0f, 1f)
+
+        rule.setMaterialContent(lightColorScheme()) {
+            RangeSlider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.runOnUiThread {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
+            Truth.assertThat(state.activeRangeEnd).isEqualTo(1f)
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(Offset(centerX + 50, centerY))
+                up()
+                expected = calculateFraction(left, right, centerX + 50)
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_tap_rangeChange() {
+        val rangeEnd = mutableStateOf(0.25f)
+        lateinit var state: RangeSliderState
+
+        rule.setMaterialContent(lightColorScheme()) {
+            state = remember(rangeEnd.value) {
+                RangeSliderState(
+                    0f,
+                    25f,
+                    valueRange = 0f..rangeEnd.value
+                )
+            }
+            RangeSlider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+        // change to 1 since [calculateFraction] coerces between 0..1
+        rule.runOnUiThread {
+            rangeEnd.value = 1f
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(Offset(centerX + 50, centerY))
+                up()
+                expected = calculateFraction(left, right, centerX + 50)
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_drag_rtl() {
+        val state = RangeSliderState(0f, 1f)
+        var slop = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                slop = LocalViewConfiguration.current.touchSlop
+                RangeSlider(
+                    state = state,
+                    modifier = Modifier.testTag(tag)
+                )
+            }
+        }
+
+        rule.runOnUiThread {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
+            Truth.assertThat(state.activeRangeEnd).isEqualTo(1f)
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(slop, 0f))
+                moveBy(Offset(100f, 0f))
+                up()
+                // subtract here as we're in rtl and going in the opposite direction
+                expected = calculateFraction(left, right, centerX - 100)
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
+            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_drag_out_of_bounds_rtl() {
+        val state = RangeSliderState(0f, 1f)
+        var slop = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                slop = LocalViewConfiguration.current.touchSlop
+                RangeSlider(
+                    state = state,
+                    modifier = Modifier.testTag(tag)
+                )
+            }
+        }
+
+        rule.runOnUiThread {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
+            Truth.assertThat(state.activeRangeEnd).isEqualTo(1f)
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(slop, 0f))
+                moveBy(Offset(width.toFloat(), 0f))
+                moveBy(Offset(-width.toFloat(), 0f))
+                moveBy(Offset(-width.toFloat(), 0f))
+                moveBy(Offset(width.toFloat() + 100f, 0f))
+                up()
+                // subtract here as we're in rtl and going in the opposite direction
+                expected = calculateFraction(left, right, centerX - 100)
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0f)
+            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_closeThumbs_dragRight() {
+        val state = RangeSliderState(0.5f, 0.5f)
+        var slop = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            slop = LocalViewConfiguration.current.touchSlop
+            RangeSlider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.runOnUiThread {
+
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0.5f)
+            Truth.assertThat(state.activeRangeEnd).isEqualTo(0.5f)
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(slop, 0f))
+                moveBy(Offset(100f, 0f))
+                up()
+                // subtract here as we're in rtl and going in the opposite direction
+                expected = calculateFraction(left, right, centerX + 100)
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0.5f)
+            Truth.assertThat(state.activeRangeEnd).isWithin(SliderTolerance).of(expected)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_closeThumbs_dragLeft() {
+        val state = RangeSliderState(0.5f, 0.5f)
+        var slop = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            slop = LocalViewConfiguration.current.touchSlop
+            RangeSlider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.runOnUiThread {
+            Truth.assertThat(state.activeRangeStart).isEqualTo(0.5f)
+            Truth.assertThat(state.activeRangeEnd).isEqualTo(0.5f)
+        }
+
+        var expected = 0f
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(-slop - 1, 0f))
+                moveBy(Offset(-100f, 0f))
+                up()
+                // subtract here as we're in rtl and going in the opposite direction
+                expected = calculateFraction(left, right, centerX - 100)
+            }
+        rule.runOnIdle {
+            Truth.assertThat(state.activeRangeStart).isWithin(SliderTolerance).of(expected)
+            Truth.assertThat(state.activeRangeEnd).isEqualTo(0.5f)
+        }
+    }
+
+    /**
+     * Regression test for bug: 210289161 where RangeSlider was ignoring some modifiers like weight.
+     */
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_weightModifier() {
+        var sliderBounds = Rect(0f, 0f, 0f, 0f)
+        val state = RangeSliderState(0f, 0.5f, {})
+        rule.setMaterialContent(lightColorScheme()) {
+            with(LocalDensity.current) {
+                Row(Modifier.width(500.toDp())) {
+                    Spacer(Modifier.requiredSize(100.toDp()))
+                    RangeSlider(
+                        state = state,
+                        modifier = Modifier
+                            .testTag(tag)
+                            .weight(1f)
+                            .onGloballyPositioned {
+                                sliderBounds = it.boundsInParent()
+                            }
+                    )
+                    Spacer(Modifier.requiredSize(100.toDp()))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(sliderBounds.left).isEqualTo(100)
+            Truth.assertThat(sliderBounds.right).isEqualTo(400)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_semantics_continuous() {
+        val state = RangeSliderState(0f, 1f)
+
+        rule.setMaterialContent(lightColorScheme()) {
+            RangeSlider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.onAllNodes(isFocusable(), true)[0]
+            .assertRangeInfoEquals(ProgressBarRangeInfo(0f, 0f..1f, 0))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.SetProgress))
+
+        rule.onAllNodes(isFocusable(), true)[1]
+            .assertRangeInfoEquals(ProgressBarRangeInfo(1f, 0f..1f, 0))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.SetProgress))
+
+        rule.runOnUiThread {
+            state.activeRangeStart = 0.5f
+            state.activeRangeEnd = 0.75f
+        }
+
+        rule.onAllNodes(isFocusable(), true)[0].assertRangeInfoEquals(
+            ProgressBarRangeInfo(
+                0.5f,
+                0f..0.75f,
+                0
+            )
+        )
+
+        rule.onAllNodes(isFocusable(), true)[1].assertRangeInfoEquals(
+            ProgressBarRangeInfo(
+                0.75f,
+                0.5f..1f,
+                0
+            )
+        )
+
+        rule.onAllNodes(isFocusable(), true)[0]
+            .performSemanticsAction(SemanticsActions.SetProgress) { it(0.6f) }
+
+        rule.onAllNodes(isFocusable(), true)[1]
+            .performSemanticsAction(SemanticsActions.SetProgress) { it(0.8f) }
+
+        rule.onAllNodes(isFocusable(), true)[0]
+            .assertRangeInfoEquals(ProgressBarRangeInfo(0.6f, 0f..0.8f, 0))
+
+        rule.onAllNodes(isFocusable(), true)[1]
+            .assertRangeInfoEquals(ProgressBarRangeInfo(0.8f, 0.6f..1f, 0))
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_semantics_stepped() {
+        val state = RangeSliderState(
+            0f,
+            20f,
+            steps = 3,
+            valueRange = 0f..20f
+        )
+        // Slider with [0,5,10,15,20] possible values
+        rule.setMaterialContent(lightColorScheme()) {
+            RangeSlider(
+                state = state,
+                modifier = Modifier.testTag(tag)
+            )
+        }
+
+        rule.runOnUiThread {
+            state.activeRangeStart = 5f
+            state.activeRangeEnd = 10f
+        }
+
+        rule.onAllNodes(isFocusable(), true)[0].assertRangeInfoEquals(
+            ProgressBarRangeInfo(
+                5f,
+                0f..10f,
+                1
+            )
+        )
+
+        rule.onAllNodes(isFocusable(), true)[1].assertRangeInfoEquals(
+            ProgressBarRangeInfo(
+                10f,
+                5f..20f,
+                2,
+            )
+        )
+
+        rule.onAllNodes(isFocusable(), true)[0]
+            .performSemanticsAction(SemanticsActions.SetProgress) { it(10f) }
+
+        rule.onAllNodes(isFocusable(), true)[1]
+            .performSemanticsAction(SemanticsActions.SetProgress) { it(15f) }
+
+        rule.onAllNodes(isFocusable(), true)[0]
+            .assertRangeInfoEquals(ProgressBarRangeInfo(10f, 0f..15f, 2))
+
+        rule.onAllNodes(isFocusable(), true)[1]
+            .assertRangeInfoEquals(ProgressBarRangeInfo(15f, 10f..20f, 1))
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_thumb_recomposition() {
+        val state = RangeSliderState(
+            0f,
+            100f,
+            valueRange = 0f..100f
+        )
+        val startRecompositionCounter = RangeSliderRecompositionCounter()
+        val endRecompositionCounter = RangeSliderRecompositionCounter()
+
+        rule.setContent {
+            RangeSlider(
+                state = state,
+                modifier = Modifier.testTag(tag),
+                startThumb = { rangeSliderState ->
+                    startRecompositionCounter.OuterContent(rangeSliderState)
+                },
+                endThumb = { rangeSliderState ->
+                    endRecompositionCounter.OuterContent(rangeSliderState)
+                }
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(100f, 0f))
+                moveBy(Offset(-100f, 0f))
+                moveBy(Offset(100f, 0f))
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(startRecompositionCounter.outerRecomposition).isEqualTo(1)
+            Truth.assertThat(startRecompositionCounter.innerRecomposition).isEqualTo(3)
+            Truth.assertThat(endRecompositionCounter.outerRecomposition).isEqualTo(1)
+            Truth.assertThat(endRecompositionCounter.innerRecomposition).isEqualTo(3)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_track_recomposition() {
+        val state = RangeSliderState(
+            0f,
+            100f,
+            valueRange = 0f..100f
+        )
+        val recompositionCounter = RangeSliderRecompositionCounter()
+
+        rule.setContent {
+            RangeSlider(
+                state = state,
+                modifier = Modifier.testTag(tag),
+                track = { rangeSliderState ->
+                    recompositionCounter.OuterContent(rangeSliderState)
+                }
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(100f, 0f))
+                moveBy(Offset(-100f, 0f))
+                moveBy(Offset(100f, 0f))
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(recompositionCounter.outerRecomposition).isEqualTo(1)
+            Truth.assertThat(recompositionCounter.innerRecomposition).isEqualTo(4)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_parentWithInfiniteWidth_minWidth() {
+        val state = RangeSliderState(0f, 1f)
+        rule.setMaterialContentForSizeAssertions {
+            Box(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
+                RangeSlider(state)
+            }
+        }.assertWidthIsEqualTo(48.dp)
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_rowWithInfiniteWidth() {
+        val state = RangeSliderState(0f, 1f)
+        expectAssertionError(false) {
+            rule.setContent {
+                Row(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
+                    RangeSlider(
+                        state = state,
+                        modifier = Modifier.weight(1f)
+                    )
+                }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Stable
+class SliderRecompositionCounter {
+    var innerRecomposition = 0
+    var outerRecomposition = 0
+
+    @Composable
+    fun OuterContent(state: SliderState) {
+        SideEffect { ++outerRecomposition }
+        Column {
+            Text("OuterContent")
+            InnerContent(state)
+        }
+    }
+
+    @Composable
+    private fun InnerContent(state: SliderState) {
+        SideEffect { ++innerRecomposition }
+        Text("InnerContent: ${state.value}")
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Stable
+class RangeSliderRecompositionCounter {
+    var innerRecomposition = 0
+    var outerRecomposition = 0
+
+    @Composable
+    fun OuterContent(state: RangeSliderState) {
+        SideEffect {
+            ++outerRecomposition
+        }
+        Column {
+            Text("OuterContent")
+            InnerContent(state)
+        }
+    }
+
+    @Composable
+    private fun InnerContent(state: RangeSliderState) {
+        SideEffect { ++innerRecomposition }
+        Text("InnerContent: ${state.activeRangeStart..state.activeRangeEnd}")
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarHostTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarHostTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarHostTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarHostTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceContentColorTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SurfaceContentColorTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceContentColorTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SurfaceContentColorTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SurfaceTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SurfaceTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwipeToDismissTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwipeToDismissTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwipeToDismissTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwipeToDismissTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwitchTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwitchTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TabTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TabTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextSelectionColorsScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextSelectionColorsScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextSelectionColorsScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextSelectionColorsScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TimeInputScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimeInputScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TimeInputScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimeInputScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TimePickerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TimePickerTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipScreenshotTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipScreenshotTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipScreenshotTest.kt
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt
similarity index 100%
rename from compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt
rename to compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DynamicTonalPalette.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DynamicTonalPalette.kt
index 1fbf2f9..bafbf77 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DynamicTonalPalette.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DynamicTonalPalette.kt
@@ -318,9 +318,9 @@
     onTertiary = tonalPalette.tertiary100,
     tertiaryContainer = tonalPalette.tertiary90,
     onTertiaryContainer = tonalPalette.tertiary10,
-    background = tonalPalette.neutral99,
+    background = tonalPalette.neutral98,
     onBackground = tonalPalette.neutral10,
-    surface = tonalPalette.neutral99,
+    surface = tonalPalette.neutral98,
     onSurface = tonalPalette.neutral10,
     surfaceVariant = tonalPalette.neutralVariant90,
     onSurfaceVariant = tonalPalette.neutralVariant30,
@@ -354,9 +354,9 @@
     onTertiary = tonalPalette.tertiary20,
     tertiaryContainer = tonalPalette.tertiary30,
     onTertiaryContainer = tonalPalette.tertiary90,
-    background = tonalPalette.neutralVariant10,
+    background = tonalPalette.neutralVariant6,
     onBackground = tonalPalette.neutralVariant90,
-    surface = tonalPalette.neutralVariant10,
+    surface = tonalPalette.neutralVariant6,
     onSurface = tonalPalette.neutralVariant90,
     surfaceVariant = tonalPalette.neutralVariant30,
     onSurfaceVariant = tonalPalette.neutralVariant80,
@@ -390,9 +390,9 @@
     onTertiary = tonalPalette.tertiary20,
     tertiaryContainer = tonalPalette.tertiary30,
     onTertiaryContainer = tonalPalette.tertiary90,
-    background = tonalPalette.neutral10,
+    background = tonalPalette.neutral6,
     onBackground = tonalPalette.neutral90,
-    surface = tonalPalette.neutral10,
+    surface = tonalPalette.neutral6,
     onSurface = tonalPalette.neutral90,
     surfaceVariant = tonalPalette.neutralVariant30,
     onSurfaceVariant = tonalPalette.neutralVariant80,
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
index 85e41b3..a053929a 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
@@ -28,6 +28,7 @@
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.draggable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Column
@@ -87,7 +88,6 @@
 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
 import java.util.UUID
 import kotlin.math.max
-import kotlin.math.roundToInt
 import kotlinx.coroutines.launch
 
 /**
@@ -198,10 +198,12 @@
                             )
                         }
                     )
-                    .anchoredDraggable(
-                        state = sheetState.anchoredDraggableState,
+                    .draggable(
+                        state = sheetState.anchoredDraggableState.draggableState,
                         orientation = Orientation.Vertical,
-                        enabled = sheetState.isVisible
+                        enabled = sheetState.isVisible,
+                        startDragImmediately = sheetState.anchoredDraggableState.isAnimationRunning,
+                        onDragStopped = { settleToDismiss(it) }
                     )
                     .modalBottomSheetAnchors(
                         sheetState = sheetState,
@@ -414,10 +416,7 @@
         composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
 
     private val displayWidth: Int
-        get() {
-            val density = context.resources.displayMetrics.density
-            return (context.resources.configuration.screenWidthDp * density).roundToInt()
-        }
+        get() = context.resources.displayMetrics.widthPixels
 
     private val params: WindowManager.LayoutParams =
         WindowManager.LayoutParams().apply {
diff --git a/compose/material3/material3/src/androidTest/AndroidManifest.xml b/compose/material3/material3/src/androidUnitTest/AndroidManifest.xml
similarity index 100%
rename from compose/material3/material3/src/androidTest/AndroidManifest.xml
rename to compose/material3/material3/src/androidUnitTest/AndroidManifest.xml
diff --git a/compose/material3/material3/src/test/kotlin/androidx/compose/material3/ButtonPaparazziTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/ButtonPaparazziTest.kt
similarity index 100%
rename from compose/material3/material3/src/test/kotlin/androidx/compose/material3/ButtonPaparazziTest.kt
rename to compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/ButtonPaparazziTest.kt
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index cd79a42..7d8c9f6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -73,6 +73,7 @@
 import androidx.compose.ui.layout.AlignmentLine
 import androidx.compose.ui.layout.LastBaseline
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.semantics.clearAndSetSemantics
@@ -391,6 +392,7 @@
  * @param contentPadding the padding applied to the content of this BottomAppBar
  * @param windowInsets a window insets that app bar will respect.
  */
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun BottomAppBar(
     actions: @Composable RowScope.() -> Unit,
@@ -402,12 +404,76 @@
     contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
     windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
 ) = BottomAppBar(
+    actions = actions,
+    modifier = modifier,
+    floatingActionButton = floatingActionButton,
+    containerColor = containerColor,
+    contentColor = contentColor,
+    tonalElevation = tonalElevation,
+    contentPadding = contentPadding,
+    windowInsets = windowInsets,
+    scrollBehavior = null
+)
+
+/**
+ * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external" target="_blank">Material Design bottom app bar</a>.
+ *
+ * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ *
+ * ![Bottom app bar image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
+ *
+ * @sample androidx.compose.material3.samples.SimpleBottomAppBar
+ *
+ * It can optionally display a [FloatingActionButton] embedded at the end of the BottomAppBar.
+ *
+ * @sample androidx.compose.material3.samples.BottomAppBarWithFAB
+ *
+ * A bottom app bar that uses a [scrollBehavior] to customize its nested scrolling behavior when
+ * working in conjunction with a scrolling content looks like:
+ *
+ * @sample androidx.compose.material3.samples.ExitAlwaysBottomAppBar
+ *
+ * Also see [NavigationBar].
+ *
+ * @param actions the icon content of this BottomAppBar. The default layout here is a [Row],
+ * so content inside will be placed horizontally.
+ * @param modifier the [Modifier] to be applied to this BottomAppBar
+ * @param floatingActionButton optional floating action button at the end of this BottomAppBar
+ * @param containerColor the color used for the background of this BottomAppBar. Use
+ * [Color.Transparent] to have no color.
+ * @param contentColor the preferred color for content inside this BottomAppBar. Defaults to either
+ * the matching content color for [containerColor], or to the current [LocalContentColor] if
+ * [containerColor] is not a color from the theme.
+ * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
+ * overlay is applied on top of the container. A higher tonal elevation value will result in a
+ * darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param contentPadding the padding applied to the content of this BottomAppBar
+ * @param windowInsets a window insets that app bar will respect.
+ * @param scrollBehavior a [BottomAppBarScrollBehavior] which holds various offset values that will
+ * be applied by this bottom app bar to set up its height. A scroll behavior is designed to
+ * work in conjunction with a scrolled content to change the bottom app bar appearance as the
+ * content scrolls. See [BottomAppBarScrollBehavior.nestedScrollConnection].
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun BottomAppBar(
+    actions: @Composable RowScope.() -> Unit,
+    modifier: Modifier = Modifier,
+    floatingActionButton: @Composable (() -> Unit)? = null,
+    containerColor: Color = BottomAppBarDefaults.containerColor,
+    contentColor: Color = contentColorFor(containerColor),
+    tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation,
+    contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
+    windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
+    scrollBehavior: BottomAppBarScrollBehavior? = null,
+) = BottomAppBar(
     modifier = modifier,
     containerColor = containerColor,
     contentColor = contentColor,
     tonalElevation = tonalElevation,
     windowInsets = windowInsets,
-    contentPadding = contentPadding
+    contentPadding = contentPadding,
+    scrollBehavior = scrollBehavior
 ) {
     Row(
         modifier = Modifier.weight(1f),
@@ -455,6 +521,7 @@
  * @param content the content of this BottomAppBar. The default layout here is a [Row],
  * so content inside will be placed horizontally.
  */
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun BottomAppBar(
     modifier: Modifier = Modifier,
@@ -464,7 +531,81 @@
     contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
     windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
     content: @Composable RowScope.() -> Unit
+) = BottomAppBar(
+    modifier = modifier,
+    containerColor = containerColor,
+    contentColor = contentColor,
+    tonalElevation = tonalElevation,
+    contentPadding = contentPadding,
+    windowInsets = windowInsets,
+    scrollBehavior = null,
+    content = content
+)
+
+/**
+ * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external" target="_blank">Material Design bottom app bar</a>.
+ *
+ * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ *
+ * ![Bottom app bar image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
+ *
+ * If you are interested in displaying a [FloatingActionButton], consider using another overload.
+ *
+ * Also see [NavigationBar].
+ *
+ * @param modifier the [Modifier] to be applied to this BottomAppBar
+ * @param containerColor the color used for the background of this BottomAppBar. Use
+ * [Color.Transparent] to have no color.
+ * @param contentColor the preferred color for content inside this BottomAppBar. Defaults to either
+ * the matching content color for [containerColor], or to the current [LocalContentColor] if
+ * [containerColor] is not a color from the theme.
+ * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
+ * overlay is applied on top of the container. A higher tonal elevation value will result in a
+ * darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param contentPadding the padding applied to the content of this BottomAppBar
+ * @param windowInsets a window insets that app bar will respect.
+ * @param scrollBehavior a [BottomAppBarScrollBehavior] which holds various offset values that will
+ * be applied by this bottom app bar to set up its height. A scroll behavior is designed to
+ * work in conjunction with a scrolled content to change the bottom app bar appearance as the
+ * content scrolls. See [BottomAppBarScrollBehavior.nestedScrollConnection].
+ * @param content the content of this BottomAppBar. The default layout here is a [Row],
+ * so content inside will be placed horizontally.
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun BottomAppBar(
+    modifier: Modifier = Modifier,
+    containerColor: Color = BottomAppBarDefaults.containerColor,
+    contentColor: Color = contentColorFor(containerColor),
+    tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation,
+    contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
+    windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
+    scrollBehavior: BottomAppBarScrollBehavior? = null,
+    content: @Composable RowScope.() -> Unit
 ) {
+    // Set up support for resizing the bottom app bar when vertically dragging the bar itself.
+    val appBarDragModifier = if (scrollBehavior != null && !scrollBehavior.isPinned) {
+        Modifier.draggable(
+            orientation = Orientation.Vertical,
+            state = rememberDraggableState { delta ->
+                scrollBehavior.state.heightOffset -= delta
+            },
+            onDragStopped = { velocity ->
+                settleAppBarBottom(
+                    scrollBehavior.state,
+                    velocity,
+                    scrollBehavior.flingAnimationSpec,
+                    scrollBehavior.snapAnimationSpec
+                )
+            }
+        )
+    } else {
+        Modifier
+    }
+
+    // Compose a Surface with a Row content.
+    // The height of the app bar is determined by subtracting the bar's height offset from the
+    // app bar's defined constant height value (i.e. the ContainerHeight token).
     Surface(
         color = containerColor,
         contentColor = contentColor,
@@ -472,6 +613,19 @@
         // TODO(b/209583788): Consider adding a shape parameter if updated design guidance allows
         shape = BottomAppBarTokens.ContainerShape.value,
         modifier = modifier
+            .layout { measurable, constraints ->
+                // Sets the app bar's height offset to collapse the entire bar's height when content
+                // is scrolled.
+                scrollBehavior?.state?.heightOffsetLimit =
+                    -BottomAppBarTokens.ContainerHeight.toPx()
+
+                val placeable = measurable.measure(constraints)
+                val height = placeable.height + (scrollBehavior?.state?.heightOffset ?: 0f)
+                layout(placeable.width, height.roundToInt()) {
+                    placeable.place(0, 0)
+                }
+            }
+            .then(appBarDragModifier)
     ) {
         Row(
             Modifier
@@ -979,6 +1133,50 @@
     }
 }
 
+/**
+ * A BottomAppBarScrollBehavior defines how a bottom app bar should behave when the content under
+ * it is scrolled.
+ *
+ * @see [BottomAppBarDefaults.exitAlwaysScrollBehavior]
+ */
+@ExperimentalMaterial3Api
+@Stable
+interface BottomAppBarScrollBehavior {
+
+    /**
+     * A [BottomAppBarState] that is attached to this behavior and is read and updated when
+     * scrolling happens.
+     */
+    val state: BottomAppBarState
+
+    /**
+     * Indicates whether the bottom app bar is pinned.
+     *
+     * A pinned app bar will stay fixed in place when content is scrolled and will not react to any
+     * drag gestures.
+     */
+    val isPinned: Boolean
+
+    /**
+     * An optional [AnimationSpec] that defines how the bottom app bar snaps to either fully
+     * collapsed or fully extended state when a fling or a drag scrolled it into an intermediate
+     * position.
+     */
+    val snapAnimationSpec: AnimationSpec<Float>?
+
+    /**
+     * An optional [DecayAnimationSpec] that defined how to fling the bottom app bar when the user
+     * flings the app bar itself, or the content below it.
+     */
+    val flingAnimationSpec: DecayAnimationSpec<Float>?
+
+    /**
+     * A [NestedScrollConnection] that should be attached to a [Modifier.nestedScroll] in order to
+     * keep track of the scroll events.
+     */
+    val nestedScrollConnection: NestedScrollConnection
+}
+
 /** Contains default values used for the bottom app bar implementations. */
 object BottomAppBarDefaults {
 
@@ -1012,6 +1210,287 @@
     val bottomAppBarFabColor: Color
         @Composable get() =
             FabSecondaryTokens.ContainerColor.value
+
+    /**
+     * Returns a [BottomAppBarScrollBehavior]. A bottom app bar that is set up with this
+     * [BottomAppBarScrollBehavior] will immediately collapse when the content is pulled up, and
+     * will immediately appear when the content is pulled down.
+     *
+     * @param state the state object to be used to control or observe the bottom app bar's scroll
+     * state. See [rememberBottomAppBarState] for a state that is remembered across compositions.
+     * @param canScroll a callback used to determine whether scroll events are to be
+     * handled by this [ExitAlwaysScrollBehavior]
+     * @param snapAnimationSpec an optional [AnimationSpec] that defines how the bottom app bar
+     * snaps to either fully collapsed or fully extended state when a fling or a drag scrolled it
+     * into an intermediate position
+     * @param flingAnimationSpec an optional [DecayAnimationSpec] that defined how to fling the
+     * bottom app bar when the user flings the app bar itself, or the content below it
+     */
+    @ExperimentalMaterial3Api
+    @Composable
+    fun exitAlwaysScrollBehavior(
+        state: BottomAppBarState = rememberBottomAppBarState(),
+        canScroll: () -> Boolean = { true },
+        snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
+        flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
+    ): BottomAppBarScrollBehavior =
+        ExitAlwaysScrollBehavior(
+            state = state,
+            snapAnimationSpec = snapAnimationSpec,
+            flingAnimationSpec = flingAnimationSpec,
+            canScroll = canScroll
+        )
+}
+
+/**
+ * Creates a [BottomAppBarState] that is remembered across compositions.
+ *
+ * @param initialHeightOffsetLimit the initial value for [BottomAppBarState.heightOffsetLimit],
+ * which represents the pixel limit that a bottom app bar is allowed to collapse when the scrollable
+ * content is scrolled
+ * @param initialHeightOffset the initial value for [BottomAppBarState.heightOffset]. The initial
+ * offset height offset should be between zero and [initialHeightOffsetLimit].
+ * @param initialContentOffset the initial value for [BottomAppBarState.contentOffset]
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun rememberBottomAppBarState(
+    initialHeightOffsetLimit: Float = -Float.MAX_VALUE,
+    initialHeightOffset: Float = 0f,
+    initialContentOffset: Float = 0f
+): BottomAppBarState {
+    return rememberSaveable(saver = BottomAppBarState.Saver) {
+        BottomAppBarState(
+            initialHeightOffsetLimit,
+            initialHeightOffset,
+            initialContentOffset
+        )
+    }
+}
+
+/**
+ * A state object that can be hoisted to control and observe the bottom app bar state. The state is
+ * read and updated by a [BottomAppBarScrollBehavior] implementation.
+ *
+ * In most cases, this state will be created via [rememberBottomAppBarState].
+ */
+@ExperimentalMaterial3Api
+interface BottomAppBarState {
+
+    /**
+     * The bottom app bar's height offset limit in pixels, which represents the limit that a bottom
+     * app bar is allowed to collapse to.
+     *
+     * Use this limit to coerce the [heightOffset] value when it's updated.
+     */
+    var heightOffsetLimit: Float
+
+    /**
+     * The bottom app bar's current height offset in pixels. This height offset is applied to the
+     * fixed height of the app bar to control the displayed height when content is being scrolled.
+     *
+     * Updates to the [heightOffset] value are coerced between zero and [heightOffsetLimit].
+     */
+    var heightOffset: Float
+
+    /**
+     * The total offset of the content scrolled under the bottom app bar.
+     *
+     * This value is updated by a [BottomAppBarScrollBehavior] whenever a nested scroll connection
+     * consumes scroll events. A common implementation would update the value to be the sum of all
+     * [NestedScrollConnection.onPostScroll] `consumed.y` values.
+     */
+    var contentOffset: Float
+
+    /**
+     * A value that represents the collapsed height percentage of the app bar.
+     *
+     * A `0.0` represents a fully expanded bar, and `1.0` represents a fully collapsed bar (computed
+     * as [heightOffset] / [heightOffsetLimit]).
+     */
+    val collapsedFraction: Float
+
+    companion object {
+        /**
+         * The default [Saver] implementation for [BottomAppBarState].
+         */
+        val Saver: Saver<BottomAppBarState, *> = listSaver(
+            save = { listOf(it.heightOffsetLimit, it.heightOffset, it.contentOffset) },
+            restore = {
+                BottomAppBarState(
+                    initialHeightOffsetLimit = it[0],
+                    initialHeightOffset = it[1],
+                    initialContentOffset = it[2]
+                )
+            }
+        )
+    }
+}
+
+/**
+ * Creates a [BottomAppBarState].
+ *
+ * @param initialHeightOffsetLimit the initial value for [BottomAppBarState.heightOffsetLimit],
+ * which represents the pixel limit that a bottom app bar is allowed to collapse when the scrollable
+ * content is scrolled
+ * @param initialHeightOffset the initial value for [BottomAppBarState.heightOffset]. The initial
+ * offset height offset should be between zero and [initialHeightOffsetLimit].
+ * @param initialContentOffset the initial value for [BottomAppBarState.contentOffset]
+ */
+@ExperimentalMaterial3Api
+fun BottomAppBarState(
+    initialHeightOffsetLimit: Float,
+    initialHeightOffset: Float,
+    initialContentOffset: Float
+): BottomAppBarState = BottomAppBarStateImpl(
+    initialHeightOffsetLimit,
+    initialHeightOffset,
+    initialContentOffset
+)
+
+@ExperimentalMaterial3Api
+@Stable
+private class BottomAppBarStateImpl(
+    initialHeightOffsetLimit: Float,
+    initialHeightOffset: Float,
+    initialContentOffset: Float
+) : BottomAppBarState {
+
+    override var heightOffsetLimit by mutableFloatStateOf(initialHeightOffsetLimit)
+
+    override var heightOffset: Float
+        get() = _heightOffset.floatValue
+        set(newOffset) {
+            _heightOffset.floatValue = newOffset.coerceIn(
+                minimumValue = heightOffsetLimit,
+                maximumValue = 0f
+            )
+        }
+
+    override var contentOffset by mutableFloatStateOf(initialContentOffset)
+
+    override val collapsedFraction: Float
+        get() = if (heightOffsetLimit != 0f) {
+            heightOffset / heightOffsetLimit
+        } else {
+            0f
+        }
+
+    private var _heightOffset = mutableFloatStateOf(initialHeightOffset)
+}
+
+/**
+ * A [BottomAppBarScrollBehavior] that adjusts its properties to affect the colors and height of a
+ * bottom app bar.
+ *
+ * A bottom app bar that is set up with this [BottomAppBarScrollBehavior] will immediately collapse
+ * when the nested content is pulled up, and will immediately appear when the content is pulled
+ * down.
+ *
+ * @param state a [BottomAppBarState]
+ * @param snapAnimationSpec an optional [AnimationSpec] that defines how the bottom app bar snaps to
+ * either fully collapsed or fully extended state when a fling or a drag scrolled it into an
+ * intermediate position
+ * @param flingAnimationSpec an optional [DecayAnimationSpec] that defined how to fling the bottom
+ * app bar when the user flings the app bar itself, or the content below it
+ * @param canScroll a callback used to determine whether scroll events are to be
+ * handled by this [ExitAlwaysScrollBehavior]
+ */
+@ExperimentalMaterial3Api
+private class ExitAlwaysScrollBehavior(
+    override val state: BottomAppBarState,
+    override val snapAnimationSpec: AnimationSpec<Float>?,
+    override val flingAnimationSpec: DecayAnimationSpec<Float>?,
+    val canScroll: () -> Boolean = { true }
+) : BottomAppBarScrollBehavior {
+    override val isPinned: Boolean = false
+    override var nestedScrollConnection =
+        object : NestedScrollConnection {
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                if (!canScroll()) return Offset.Zero
+                state.contentOffset += consumed.y
+                if (state.heightOffset == 0f || state.heightOffset == state.heightOffsetLimit) {
+                    if (consumed.y == 0f && available.y > 0f) {
+                        // Reset the total content offset to zero when scrolling all the way down.
+                        // This will eliminate some float precision inaccuracies.
+                        state.contentOffset = 0f
+                    }
+                }
+                state.heightOffset = state.heightOffset + consumed.y
+                return Offset.Zero
+            }
+
+            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+                val superConsumed = super.onPostFling(consumed, available)
+                return superConsumed + settleAppBarBottom(
+                    state,
+                    available.y,
+                    flingAnimationSpec,
+                    snapAnimationSpec
+                )
+            }
+        }
+}
+
+/**
+ * Settles the app bar by flinging, in case the given velocity is greater than zero, and snapping
+ * after the fling settles.
+ */
+@ExperimentalMaterial3Api
+private suspend fun settleAppBarBottom(
+    state: BottomAppBarState,
+    velocity: Float,
+    flingAnimationSpec: DecayAnimationSpec<Float>?,
+    snapAnimationSpec: AnimationSpec<Float>?
+): Velocity {
+    // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
+    // and just return Zero Velocity.
+    // Note that we don't check for 0f due to float precision with the collapsedFraction
+    // calculation.
+    if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
+        return Velocity.Zero
+    }
+    var remainingVelocity = velocity
+    // In case there is an initial velocity that was left after a previous user fling, animate to
+    // continue the motion to expand or collapse the app bar.
+    if (flingAnimationSpec != null && abs(velocity) > 1f) {
+        var lastValue = 0f
+        AnimationState(
+            initialValue = 0f,
+            initialVelocity = velocity,
+        )
+            .animateDecay(flingAnimationSpec) {
+                val delta = value - lastValue
+                val initialHeightOffset = state.heightOffset
+                state.heightOffset = initialHeightOffset + delta
+                val consumed = abs(initialHeightOffset - state.heightOffset)
+                lastValue = value
+                remainingVelocity = this.velocity
+                // avoid rounding errors and stop if anything is unconsumed
+                if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
+            }
+    }
+    // Snap if animation specs were provided.
+    if (snapAnimationSpec != null) {
+        if (state.heightOffset < 0 &&
+            state.heightOffset > state.heightOffsetLimit
+        ) {
+            AnimationState(initialValue = state.heightOffset).animateTo(
+                if (state.collapsedFraction < 0.5f) {
+                    0f
+                } else {
+                    state.heightOffsetLimit
+                },
+                animationSpec = snapAnimationSpec
+            ) { state.heightOffset = value }
+        }
+    }
+
+    return Velocity(0f, remainingVelocity)
 }
 
 // Padding minus IconButton's min touch target expansion
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
index 88b46ce..259114e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
@@ -117,7 +117,7 @@
     val containerColor = colors.containerColor(enabled).value
     val contentColor = colors.contentColor(enabled).value
     val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp
-    val tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp
+    val tonalElevation = elevation?.tonalElevation(enabled) ?: 0.dp
     Surface(
         onClick = onClick,
         modifier = modifier.semantics { role = Role.Button },
@@ -769,8 +769,7 @@
     private val disabledElevation: Dp,
 ) {
     /**
-     * Represents the tonal elevation used in a button, depending on its [enabled] state and
-     * [interactionSource]. This should typically be the same value as the [shadowElevation].
+     * Represents the tonal elevation used in a button, depending on its [enabled] state.
      *
      * Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
      * When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
@@ -779,16 +778,14 @@
      * See [shadowElevation] which controls the elevation of the shadow drawn around the button.
      *
      * @param enabled whether the button is enabled
-     * @param interactionSource the [InteractionSource] for this button
      */
-    @Composable
-    internal fun tonalElevation(enabled: Boolean, interactionSource: InteractionSource): State<Dp> {
-        return animateElevation(enabled = enabled, interactionSource = interactionSource)
+    internal fun tonalElevation(enabled: Boolean): Dp {
+        return if (enabled) defaultElevation else disabledElevation
     }
 
     /**
      * Represents the shadow elevation used in a button, depending on its [enabled] state and
-     * [interactionSource]. This should typically be the same value as the [tonalElevation].
+     * [interactionSource].
      *
      * Shadow elevation is used to apply a shadow around the button to give it higher emphasis.
      *
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
index 8a55fb0..bbec175 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
@@ -86,7 +86,7 @@
         shape = shape,
         color = colors.containerColor(enabled = true).value,
         contentColor = colors.contentColor(enabled = true).value,
-        tonalElevation = elevation.tonalElevation(enabled = true, interactionSource = null).value,
+        tonalElevation = elevation.tonalElevation(enabled = true),
         shadowElevation = elevation.shadowElevation(enabled = true, interactionSource = null).value,
         border = border,
     ) {
@@ -147,7 +147,7 @@
         shape = shape,
         color = colors.containerColor(enabled).value,
         contentColor = colors.contentColor(enabled).value,
-        tonalElevation = elevation.tonalElevation(enabled, interactionSource).value,
+        tonalElevation = elevation.tonalElevation(enabled),
         shadowElevation = elevation.shadowElevation(enabled, interactionSource).value,
         border = border,
         interactionSource = interactionSource,
@@ -564,8 +564,7 @@
     private val disabledElevation: Dp
 ) {
     /**
-     * Represents the tonal elevation used in a card, depending on its [enabled] state and
-     * [interactionSource]. This should typically be the same value as the [shadowElevation].
+     * Represents the tonal elevation used in a card, depending on its [enabled].
      *
      * Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
      * When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
@@ -574,22 +573,13 @@
      * See [shadowElevation] which controls the elevation of the shadow drawn around the card.
      *
      * @param enabled whether the card is enabled
-     * @param interactionSource the [InteractionSource] for this card
      */
-    @Composable
-    internal fun tonalElevation(
-        enabled: Boolean,
-        interactionSource: InteractionSource?
-    ): State<Dp> {
-        if (interactionSource == null) {
-            return remember { mutableStateOf(defaultElevation) }
-        }
-        return animateElevation(enabled = enabled, interactionSource = interactionSource)
-    }
+    internal fun tonalElevation(enabled: Boolean): Dp =
+        if (enabled) defaultElevation else disabledElevation
 
     /**
      * Represents the shadow elevation used in a card, depending on its [enabled] state and
-     * [interactionSource]. This should typically be the same value as the [tonalElevation].
+     * [interactionSource].
      *
      * Shadow elevation is used to apply a shadow around the card to give it higher emphasis.
      *
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
index 4f00d2c..09096d6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
@@ -1325,7 +1325,7 @@
         enabled = enabled,
         shape = shape,
         color = colors.containerColor(enabled).value,
-        tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp,
+        tonalElevation = elevation?.tonalElevation(enabled) ?: 0.dp,
         shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
         border = border,
         interactionSource = interactionSource,
@@ -1373,7 +1373,7 @@
         enabled = enabled,
         shape = shape,
         color = colors.containerColor(enabled, selected).value,
-        tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp,
+        tonalElevation = elevation?.tonalElevation(enabled) ?: 0.dp,
         shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
         border = border,
         interactionSource = interactionSource,
@@ -1454,8 +1454,7 @@
     private val disabledElevation: Dp
 ) {
     /**
-     * Represents the tonal elevation used in a chip, depending on its [enabled] state and
-     * [interactionSource]. This should typically be the same value as the [shadowElevation].
+     * Represents the tonal elevation used in a chip, depending on its [enabled] state.
      *
      * Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
      * When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
@@ -1464,19 +1463,14 @@
      * See [shadowElevation] which controls the elevation of the shadow drawn around the chip.
      *
      * @param enabled whether the chip is enabled
-     * @param interactionSource the [InteractionSource] for this chip
      */
-    @Composable
-    internal fun tonalElevation(
-        enabled: Boolean,
-        interactionSource: InteractionSource
-    ): State<Dp> {
-        return animateElevation(enabled = enabled, interactionSource = interactionSource)
+    internal fun tonalElevation(enabled: Boolean): Dp {
+        return if (enabled) elevation else disabledElevation
     }
 
     /**
      * Represents the shadow elevation used in a chip, depending on its [enabled] state and
-     * [interactionSource]. This should typically be the same value as the [tonalElevation].
+     * [interactionSource].
      *
      * Shadow elevation is used to apply a shadow around the chip to give it higher emphasis.
      *
@@ -1617,8 +1611,7 @@
     val disabledElevation: Dp
 ) {
     /**
-     * Represents the tonal elevation used in a chip, depending on [enabled] and
-     * [interactionSource]. This should typically be the same value as the [shadowElevation].
+     * Represents the tonal elevation used in a chip, depending on [enabled].
      *
      * Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
      * When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
@@ -1627,19 +1620,14 @@
      * See [shadowElevation] which controls the elevation of the shadow drawn around the Chip.
      *
      * @param enabled whether the chip is enabled
-     * @param interactionSource the [InteractionSource] for this chip
      */
-    @Composable
-    internal fun tonalElevation(
-        enabled: Boolean,
-        interactionSource: InteractionSource
-    ): State<Dp> {
-        return animateElevation(enabled = enabled, interactionSource = interactionSource)
+    internal fun tonalElevation(enabled: Boolean): Dp {
+        return if (enabled) elevation else disabledElevation
     }
 
     /**
      * Represents the shadow elevation used in a chip, depending on [enabled] and
-     * [interactionSource]. This should typically be the same value as the [tonalElevation].
+     * [interactionSource].
      *
      * Shadow elevation is used to apply a shadow around the surface to give it higher emphasis.
      *
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
index ae586c8..dd8ce60 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
@@ -108,7 +108,7 @@
         shape = shape,
         color = containerColor,
         contentColor = contentColor,
-        tonalElevation = elevation.tonalElevation(interactionSource = interactionSource).value,
+        tonalElevation = elevation.tonalElevation(),
         shadowElevation = elevation.shadowElevation(interactionSource = interactionSource).value,
         interactionSource = interactionSource,
     ) {
@@ -496,9 +496,8 @@
         return animateElevation(interactionSource = interactionSource)
     }
 
-    @Composable
-    internal fun tonalElevation(interactionSource: InteractionSource): State<Dp> {
-        return animateElevation(interactionSource = interactionSource)
+    internal fun tonalElevation(): Dp {
+        return defaultElevation
     }
 
     @Composable
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Label.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Label.kt
new file mode 100644
index 0000000..1aedef2
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Label.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.material3
+
+import androidx.compose.foundation.BasicTooltipBox
+import androidx.compose.foundation.BasicTooltipState
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.MutatorMutex
+import androidx.compose.foundation.interaction.DragInteraction
+import androidx.compose.foundation.interaction.HoverInteraction
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.rememberBasicTooltipState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import kotlinx.coroutines.flow.collectLatest
+
+/**
+ * Label component that will append a [label] to [content].
+ * The positioning logic uses [TooltipDefaults.rememberPlainTooltipPositionProvider].
+ *
+ * Label appended to thumbs of Slider:
+ *
+ * @sample androidx.compose.material3.samples.SliderWithCustomThumbSample
+ *
+ * Label appended to thumbs of RangeSlider:
+ *
+ * @sample androidx.compose.material3.samples.RangeSliderWithCustomComponents
+ *
+ * @param label composable that will be appended to [content]
+ * @param modifier [Modifier] that will be applied to [content]
+ * @param interactionSource the [MutableInteractionSource] representing the
+ * stream of [Interaction]s for the [content].
+ * @param isPersistent boolean to determine if the label should be persistent.
+ * If true, then the label will always show and be anchored to [content].
+ * if false, then the label will only show when pressing down or hovering over the [content].
+ * @param content the composable that [label] will anchor to.
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun Label(
+    label: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    isPersistent: Boolean = false,
+    content: @Composable () -> Unit
+) {
+    // Has the same positioning logic as PlainTooltips
+    val positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider()
+    val state = if (isPersistent)
+        remember { LabelStateImpl() }
+    else
+        rememberBasicTooltipState(mutatorMutex = MutatorMutex())
+    BasicTooltipBox(
+        positionProvider = positionProvider,
+        tooltip = label,
+        state = state,
+        modifier = modifier,
+        focusable = false,
+        enableUserInput = false,
+        content = content
+    )
+    HandleInteractions(
+        enabled = !isPersistent,
+        state = state,
+        interactionSource = interactionSource
+    )
+}
+
+@Composable
+private fun HandleInteractions(
+    enabled: Boolean,
+    state: BasicTooltipState,
+    interactionSource: MutableInteractionSource
+) {
+    if (enabled) {
+        LaunchedEffect(interactionSource) {
+            interactionSource.interactions.collectLatest { interaction ->
+                when (interaction) {
+                    is PressInteraction.Press,
+                    is DragInteraction.Start,
+                    is HoverInteraction.Enter -> { state.show(MutatePriority.UserInput) }
+                    is PressInteraction.Release,
+                    is DragInteraction.Stop,
+                    is HoverInteraction.Exit -> { state.dismiss() }
+                }
+            }
+        }
+    }
+}
+
+private class LabelStateImpl(
+    override val isVisible: Boolean = true,
+    override val isPersistent: Boolean = true
+) : BasicTooltipState {
+    override suspend fun show(mutatePriority: MutatePriority) {}
+
+    override fun dismiss() {}
+
+    override fun onDispose() {}
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
index b346d44..4e2d001 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
@@ -27,7 +27,10 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -126,6 +129,7 @@
  * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
  * [content], typically a [NavigationBar].
  */
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun ScaffoldLayout(
     fabPosition: FabPosition,
@@ -135,7 +139,42 @@
     fab: @Composable () -> Unit,
     contentWindowInsets: WindowInsets,
     bottomBar: @Composable () -> Unit
+) {
+    if (ScaffoldSubcomposeInMeasureFix) {
+        ScaffoldLayoutWithMeasureFix(
+            fabPosition = fabPosition,
+            topBar = topBar,
+            content = content,
+            snackbar = snackbar,
+            fab = fab,
+            contentWindowInsets = contentWindowInsets,
+            bottomBar = bottomBar
+        )
+    } else {
+        LegacyScaffoldLayout(
+            fabPosition = fabPosition,
+            topBar = topBar,
+            content = content,
+            snackbar = snackbar,
+            fab = fab,
+            contentWindowInsets = contentWindowInsets,
+            bottomBar = bottomBar
+        )
+    }
+}
 
+/**
+ * Layout for a [Scaffold]'s content, subcomposing and measuring during measurement.
+ */
+@Composable
+private fun ScaffoldLayoutWithMeasureFix(
+    fabPosition: FabPosition,
+    topBar: @Composable () -> Unit,
+    content: @Composable (PaddingValues) -> Unit,
+    snackbar: @Composable () -> Unit,
+    fab: @Composable () -> Unit,
+    contentWindowInsets: WindowInsets,
+    bottomBar: @Composable () -> Unit
 ) {
     SubcomposeLayout { constraints ->
         val layoutWidth = constraints.maxWidth
@@ -197,7 +236,7 @@
                         layoutWidth - FabSpacing.roundToPx() - fabWidth
                     }
                 }
-                FabPosition.End -> {
+                FabPosition.End, FabPosition.EndOverlay -> {
                     if (layoutDirection == LayoutDirection.Ltr) {
                         layoutWidth - FabSpacing.roundToPx() - fabWidth
                     } else {
@@ -225,7 +264,7 @@
 
         val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
         val fabOffsetFromBottom = fabPlacement?.let {
-            if (bottomBarHeight == null) {
+            if (bottomBarHeight == null || fabPosition == FabPosition.EndOverlay) {
                 it.height + FabSpacing.roundToPx() +
                     contentWindowInsets.getBottom(this@SubcomposeLayout)
             } else {
@@ -295,6 +334,175 @@
 }
 
 /**
+ * Layout for a [Scaffold]'s content, subcomposing and measuring during measurement.
+ */
+@Composable
+private fun LegacyScaffoldLayout(
+    fabPosition: FabPosition,
+    topBar: @Composable () -> Unit,
+    content: @Composable (PaddingValues) -> Unit,
+    snackbar: @Composable () -> Unit,
+    fab: @Composable () -> Unit,
+    contentWindowInsets: WindowInsets,
+    bottomBar: @Composable () -> Unit
+) {
+    SubcomposeLayout { constraints ->
+        val layoutWidth = constraints.maxWidth
+        val layoutHeight = constraints.maxHeight
+
+        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+
+        layout(layoutWidth, layoutHeight) {
+            val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
+                it.measure(looseConstraints)
+            }
+
+            val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
+
+            val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
+                // respect only bottom and horizontal for snackbar and fab
+                val leftInset = contentWindowInsets
+                    .getLeft(this@SubcomposeLayout, layoutDirection)
+                val rightInset = contentWindowInsets
+                    .getRight(this@SubcomposeLayout, layoutDirection)
+                val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
+                // offset the snackbar constraints by the insets values
+                it.measure(
+                    looseConstraints.offset(
+                        -leftInset - rightInset,
+                        -bottomInset
+                    )
+                )
+            }
+
+            val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
+            val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 0
+
+            val fabPlaceables =
+                subcompose(ScaffoldLayoutContent.Fab, fab).fastMapNotNull { measurable ->
+                    // respect only bottom and horizontal for snackbar and fab
+                    val leftInset =
+                        contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
+                    val rightInset =
+                        contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
+                    val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
+                    measurable.measure(
+                        looseConstraints.offset(
+                            -leftInset - rightInset,
+                            -bottomInset
+                        )
+                    )
+                        .takeIf { it.height != 0 && it.width != 0 }
+                }
+
+            val fabPlacement = if (fabPlaceables.isNotEmpty()) {
+                val fabWidth = fabPlaceables.fastMaxBy { it.width }!!.width
+                val fabHeight = fabPlaceables.fastMaxBy { it.height }!!.height
+                // FAB distance from the left of the layout, taking into account LTR / RTL
+                val fabLeftOffset = when (fabPosition) {
+                    FabPosition.Start -> {
+                        if (layoutDirection == LayoutDirection.Ltr) {
+                            FabSpacing.roundToPx()
+                        } else {
+                            layoutWidth - FabSpacing.roundToPx() - fabWidth
+                        }
+                    }
+                    FabPosition.End -> {
+                        if (layoutDirection == LayoutDirection.Ltr) {
+                            layoutWidth - FabSpacing.roundToPx() - fabWidth
+                        } else {
+                            FabSpacing.roundToPx()
+                        }
+                    }
+                    else -> (layoutWidth - fabWidth) / 2
+                }
+
+                FabPlacement(
+                    left = fabLeftOffset,
+                    width = fabWidth,
+                    height = fabHeight
+                )
+            } else {
+                null
+            }
+
+            val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) {
+                CompositionLocalProvider(
+                    LocalFabPlacement provides fabPlacement,
+                    content = bottomBar
+                )
+            }.fastMap { it.measure(looseConstraints) }
+
+            val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
+            val fabOffsetFromBottom = fabPlacement?.let {
+                if (bottomBarHeight == null) {
+                    it.height + FabSpacing.roundToPx() +
+                        contentWindowInsets.getBottom(this@SubcomposeLayout)
+                } else {
+                    // Total height is the bottom bar height + the FAB height + the padding
+                    // between the FAB and bottom bar
+                    bottomBarHeight + it.height + FabSpacing.roundToPx()
+                }
+            }
+
+            val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
+                snackbarHeight +
+                    (fabOffsetFromBottom ?: bottomBarHeight
+                    ?: contentWindowInsets.getBottom(this@SubcomposeLayout))
+            } else {
+                0
+            }
+
+            val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
+                val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout)
+                val innerPadding = PaddingValues(
+                    top =
+                    if (topBarPlaceables.isEmpty()) {
+                        insets.calculateTopPadding()
+                    } else {
+                        topBarHeight.toDp()
+                    },
+                    bottom =
+                    if (bottomBarPlaceables.isEmpty() || bottomBarHeight == null) {
+                        insets.calculateBottomPadding()
+                    } else {
+                        bottomBarHeight.toDp()
+                    },
+                    start = insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection),
+                    end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection)
+                )
+                content(innerPadding)
+            }.fastMap { it.measure(looseConstraints) }
+
+            // Placing to control drawing order to match default elevation of each placeable
+            bodyContentPlaceables.fastForEach {
+                it.place(0, 0)
+            }
+            topBarPlaceables.fastForEach {
+                it.place(0, 0)
+            }
+            snackbarPlaceables.fastForEach {
+                it.place(
+                    (layoutWidth - snackbarWidth) / 2 +
+                        contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection),
+                    layoutHeight - snackbarOffsetFromBottom
+                )
+            }
+            // The bottom bar is always at the bottom of the layout
+            bottomBarPlaceables.fastForEach {
+                it.place(0, layoutHeight - (bottomBarHeight ?: 0))
+            }
+            // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
+            fabPlacement?.let { placement ->
+                fabPlaceables.fastForEach {
+                    it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
+                }
+            }
+        }
+    }
+}
+
+/**
  * Object containing various default values for [Scaffold] component.
  */
 object ScaffoldDefaults {
@@ -329,18 +537,41 @@
          * exists)
          */
         val End = FabPosition(2)
+
+        /**
+         * Position FAB at the bottom of the screen at the end, overlaying the [NavigationBar] (if
+         * it exists)
+         */
+        val EndOverlay = FabPosition(3)
     }
 
     override fun toString(): String {
         return when (this) {
             Start -> "FabPosition.Start"
             Center -> "FabPosition.Center"
-            else -> "FabPosition.End"
+            End -> "FabPosition.End"
+            else -> "FabPosition.EndOverlay"
         }
     }
 }
 
 /**
+ * Flag indicating if [Scaffold] should subcompose and measure its children during measurement or
+ * during placement.
+ * Set this flag to false to keep Scaffold's old measurement behavior (measuring in placement).
+ *
+ * <b>This flag will be removed in Compose 1.6.0-beta01.</b> If you encounter any issues with the
+ * new behavior, please file an issue at: issuetracker.google.com/issues/new?component=742043
+ */
+// TODO(b/299621062): Remove flag before beta
+@Suppress("GetterSetterNames", "OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:Suppress("GetterSetterNames")
+@get:ExperimentalMaterial3Api
+@set:ExperimentalMaterial3Api
+@ExperimentalMaterial3Api
+var ScaffoldSubcomposeInMeasureFix by mutableStateOf(true)
+
+/**
  * Placement information for a [FloatingActionButton] inside a [Scaffold].
  *
  * @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SegmentedButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SegmentedButton.kt
index 860b1a8..9f2c7b0 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SegmentedButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SegmentedButton.kt
@@ -32,6 +32,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
@@ -61,13 +62,9 @@
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -75,7 +72,14 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.MultiContentMeasurePolicy
 import androidx.compose.ui.layout.layout
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
@@ -83,6 +87,7 @@
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMap
 import androidx.compose.ui.util.fastMaxBy
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
 /**
@@ -103,11 +108,11 @@
  * @param checked whether this button is checked or not
  * @param onCheckedChange callback to be invoked when the button is clicked.
  * therefore the change of checked state in requested.
+ * @param shape the shape for this button
  * @param modifier the [Modifier] to be applied to this button
  * @param enabled controls the enabled state of this button. When `false`, this component will not
  * respond to user input, and it will appear visually disabled and disabled to accessibility
  * services.
- * @param shape the shape for this button
  * @param colors [SegmentedButtonColors] that will be used to resolve the colors used for this
  * @param border the border for this button, see [SegmentedButtonColors]
  * Button in different states
@@ -117,32 +122,30 @@
  * @param icon the icon slot for this button, you can pass null in unchecked, in which case
  * the content will displace to show the checked icon, or pass different icon lambdas for
  * unchecked and checked in which case the icons will crossfade.
- * @param content content to be rendered inside this button
+ * @param label content to be rendered inside this button
  */
 @Composable
 @ExperimentalMaterial3Api
 fun MultiChoiceSegmentedButtonRowScope.SegmentedButton(
     checked: Boolean,
     onCheckedChange: (Boolean) -> Unit,
+    shape: Shape,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
-    shape: Shape = RectangleShape,
     colors: SegmentedButtonColors = SegmentedButtonDefaults.colors(),
     border: SegmentedButtonBorder = SegmentedButtonDefaults.Border,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    icon: @Composable () -> Unit = { SegmentedButtonDefaults.SegmentedButtonIcon(checked) },
-    content: @Composable () -> Unit,
+    icon: @Composable () -> Unit = { SegmentedButtonDefaults.Icon(checked) },
+    label: @Composable () -> Unit,
 ) {
-
     val containerColor = colors.containerColor(enabled, checked)
     val contentColor = colors.contentColor(enabled, checked)
-    val checkedState by rememberUpdatedState(checked)
-    val interactionCount by interactionSource.interactionCountAsState()
+    val interactionCount = interactionSource.interactionCountAsState()
 
     Surface(
         modifier = modifier
             .weight(1f)
-            .interactionZIndex(checkedState, interactionCount)
+            .interactionZIndex(checked, interactionCount)
             .defaultMinSize(
                 minWidth = ButtonDefaults.MinWidth,
                 minHeight = ButtonDefaults.MinHeight
@@ -156,7 +159,7 @@
         border = border.borderStroke(enabled, checked, colors),
         interactionSource = interactionSource
     ) {
-        SegmentedButtonContent(icon, content)
+        SegmentedButtonContent(icon, label)
     }
 }
 
@@ -178,11 +181,11 @@
  * @param selected whether this button is selected or not
  * @param onClick callback to be invoked when the button is clicked.
  * therefore the change of checked state in requested.
+ * @param shape the shape for this button
  * @param modifier the [Modifier] to be applied to this button
  * @param enabled controls the enabled state of this button. When `false`, this component will not
  * respond to user input, and it will appear visually disabled and disabled to accessibility
  * services.
- * @param shape the shape for this button
  * @param colors [SegmentedButtonColors] that will be used to resolve the colors used for this
  * @param border the border for this button, see [SegmentedButtonColors]
  * Button in different states
@@ -192,35 +195,34 @@
  * @param icon the icon slot for this button, you can pass null in unchecked, in which case
  * the content will displace to show the checked icon, or pass different icon lambdas for
  * unchecked and checked in which case the icons will crossfade.
- * @param content content to be rendered inside this button
+ * @param label content to be rendered inside this button
  */
 @Composable
 @ExperimentalMaterial3Api
 fun SingleChoiceSegmentedButtonRowScope.SegmentedButton(
     selected: Boolean,
     onClick: () -> Unit,
+    shape: Shape,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
-    shape: Shape = RectangleShape,
     colors: SegmentedButtonColors = SegmentedButtonDefaults.colors(),
     border: SegmentedButtonBorder = SegmentedButtonDefaults.Border,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    icon: @Composable () -> Unit = { SegmentedButtonDefaults.SegmentedButtonIcon(selected) },
-    content: @Composable () -> Unit,
+    icon: @Composable () -> Unit = { SegmentedButtonDefaults.Icon(selected) },
+    label: @Composable () -> Unit,
 ) {
     val containerColor = colors.containerColor(enabled, selected)
     val contentColor = colors.contentColor(enabled, selected)
-    val checkedState by rememberUpdatedState(selected)
-    val interactionCount by interactionSource.interactionCountAsState()
+    val interactionCount = interactionSource.interactionCountAsState()
 
     Surface(
         modifier = modifier
             .weight(1f)
-            .interactionZIndex(checkedState, interactionCount)
+            .interactionZIndex(selected, interactionCount)
             .defaultMinSize(
                 minWidth = ButtonDefaults.MinWidth,
                 minHeight = ButtonDefaults.MinHeight
-            ),
+            ).semantics { role = Role.RadioButton },
         selected = selected,
         onClick = onClick,
         enabled = enabled,
@@ -230,7 +232,7 @@
         border = border.borderStroke(enabled, selected, colors),
         interactionSource = interactionSource
     ) {
-        SegmentedButtonContent(icon, content)
+        SegmentedButtonContent(icon, label)
     }
 }
 
@@ -313,57 +315,76 @@
     icon: @Composable () -> Unit,
     content: @Composable () -> Unit,
 ) {
-    Row(
-        modifier = Modifier.padding(ButtonDefaults.TextButtonContentPadding),
-        horizontalArrangement = Arrangement.Center,
-        verticalAlignment = Alignment.CenterVertically
-    ) {
-        ProvideTextStyle(value = MaterialTheme.typography.labelLarge) {
-            var animatable by remember {
-                mutableStateOf<Animatable<Int, AnimationVector1D>?>(null)
+    Box(contentAlignment = Alignment.Center) {
+        val typography =
+            MaterialTheme.typography.fromToken(OutlinedSegmentedButtonTokens.LabelTextFont)
+        ProvideTextStyle(typography) {
+            val scope = rememberCoroutineScope()
+            val measurePolicy = remember { SegmentedButtonContentMeasurePolicy(scope) }
+
+            Layout(
+                modifier = Modifier.padding(ButtonDefaults.TextButtonContentPadding),
+                contents = listOf(icon, content),
+                measurePolicy = measurePolicy
+            )
+        }
+    }
+}
+
+internal class SegmentedButtonContentMeasurePolicy(
+    val scope: CoroutineScope
+) : MultiContentMeasurePolicy {
+    var animatable: Animatable<Int, AnimationVector1D>? = null
+    private var initialOffset: Int? = null
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    override fun MeasureScope.measure(
+        measurables: List<List<Measurable>>,
+        constraints: Constraints
+    ): MeasureResult {
+        val (iconMeasurables, contentMeasurables) = measurables
+        val iconPlaceables = iconMeasurables.fastMap { it.measure(constraints) }
+        val iconDesiredWidth = iconMeasurables.fastFold(0) { acc, it ->
+            maxOf(acc, it.maxIntrinsicWidth(Constraints.Infinity))
+        }
+        val iconWidth = iconPlaceables.fastMaxBy { it.width }?.width ?: 0
+        val contentPlaceables = contentMeasurables.fastMap { it.measure(constraints) }
+        val contentWidth = contentPlaceables.fastMaxBy { it.width }?.width
+        val width = maxOf(SegmentedButtonDefaults.IconSize.roundToPx(), iconDesiredWidth) +
+            IconSpacing.roundToPx() +
+            (contentWidth ?: 0)
+
+        val offsetX = if (iconWidth == 0) {
+            -(SegmentedButtonDefaults.IconSize.roundToPx() + IconSpacing.roundToPx()) / 2
+        } else {
+            iconDesiredWidth - SegmentedButtonDefaults.IconSize.roundToPx()
+        }
+
+        if (initialOffset == null) {
+            initialOffset = offsetX
+        } else {
+            val anim = animatable ?: Animatable(initialOffset!!, Int.VectorConverter)
+                .also { animatable = it }
+            if (anim.targetValue != offsetX) {
+                scope.launch {
+                    anim.animateTo(offsetX, tween(MotionTokens.DurationMedium3.toInt()))
+                }
+            }
+        }
+
+        return layout(width, constraints.maxHeight) {
+            iconPlaceables.fastForEach {
+                it.place(0, (constraints.maxHeight - it.height) / 2)
             }
 
-            val scope = rememberCoroutineScope()
+            val contentOffsetX = SegmentedButtonDefaults.IconSize.roundToPx() +
+                IconSpacing.roundToPx() + (animatable?.value ?: offsetX)
 
-            Layout(listOf(icon, content)) { (iconMeasurables, contentMeasurables), constraints ->
-                val iconPlaceables = iconMeasurables.fastMap { it.measure(constraints) }
-                val iconDesiredWidth = iconMeasurables.fastFold(0) { acc, it ->
-                    maxOf(acc, it.maxIntrinsicWidth(Constraints.Infinity))
-                }
-                val iconWidth = iconPlaceables.fastMaxBy { it.width }?.width ?: 0
-                val contentPlaceables = contentMeasurables.fastMap { it.measure(constraints) }
-                val contentWidth = contentPlaceables.fastMaxBy { it.width }?.width
-                val width = maxOf(SegmentedButtonDefaults.IconSize.roundToPx(), iconDesiredWidth) +
-                    IconSpacing.roundToPx() +
-                    (contentWidth ?: 0)
-
-                val offsetX = if (iconWidth == 0) {
-                    -(SegmentedButtonDefaults.IconSize.roundToPx() + IconSpacing.roundToPx()) / 2
-                } else {
-                    iconDesiredWidth - SegmentedButtonDefaults.IconSize.roundToPx()
-                }
-
-                val anim = animatable ?: Animatable(offsetX, Int.VectorConverter)
-                    .also { animatable = it }
-
-                if (anim.targetValue != offsetX) {
-                    scope.launch {
-                        anim.animateTo(offsetX, tween(MotionTokens.DurationMedium3.toInt()))
-                    }
-                }
-
-                layout(width, constraints.maxHeight) {
-                    iconPlaceables.fastForEach {
-                        it.place(0, (constraints.maxHeight - it.height) / 2)
-                    }
-
-                    val contentOffsetX = SegmentedButtonDefaults.IconSize.roundToPx() +
-                        IconSpacing.roundToPx() + anim.value
-
-                    contentPlaceables.fastForEach {
-                        it.place(contentOffsetX, (constraints.maxHeight - it.height) / 2)
-                    }
-                }
+            contentPlaceables.fastForEach {
+                it.place(
+                    contentOffsetX,
+                    (constraints.maxHeight - it.height) / 2
+                )
             }
         }
     }
@@ -458,26 +479,34 @@
     /** The default [BorderStroke] factory used by [SegmentedButton]. */
     val Border = SegmentedButtonBorder(width = OutlinedSegmentedButtonTokens.OutlineWidth)
 
-    /** The default [Shape] for [SegmentedButton]. */
-    val Shape: CornerBasedShape
+    /**
+     * The shape of the segmented button container, for correct behavior this should or the desired
+     * [CornerBasedShape] should be used with [itemShape] and passed to each segmented button.
+     */
+    val baseShape: CornerBasedShape
         @Composable
         @ReadOnlyComposable
         get() = OutlinedSegmentedButtonTokens.Shape.value as CornerBasedShape
 
     /**
-     * A shape constructor that the button in [position] should have when there are [count] buttons
+     * A shape constructor that the button in [index] should have when there are [count] buttons in
+     * the container.
      *
-     * @param position the position for this button in the row
+     * @param index the index for this button in the row
      * @param count the count of buttons in this row
-     * @param shape the [CornerBasedShape] the base shape that should be used in buttons that are
-     * not in the start or the end.
+     * @param baseShape the [CornerBasedShape] the base shape that should be used in buttons that
+     * are not in the start or the end.
      */
     @Composable
     @ReadOnlyComposable
-    fun shape(position: Int, count: Int, shape: CornerBasedShape = this.Shape): Shape {
-        return when (position) {
-            0 -> shape.start()
-            count - 1 -> shape.end()
+    fun itemShape(index: Int, count: Int, baseShape: CornerBasedShape = this.baseShape): Shape {
+        if (count == 1) {
+            return baseShape
+        }
+
+        return when (index) {
+            0 -> baseShape.start()
+            count - 1 -> baseShape.end()
             else -> RectangleShape
         }
     }
@@ -506,7 +535,7 @@
      * checked.
      */
     @Composable
-    fun SegmentedButtonIcon(
+    fun Icon(
         active: Boolean,
         activeContent: @Composable () -> Unit = { ActiveIcon() },
         inactiveContent: (@Composable () -> Unit)? = null
@@ -676,11 +705,11 @@
     }
 }
 
-private fun Modifier.interactionZIndex(checked: Boolean, interactionCount: Int) =
+private fun Modifier.interactionZIndex(checked: Boolean, interactionCount: State<Int>) =
     this.layout { measurable, constraints ->
         val placeable = measurable.measure(constraints)
         layout(placeable.width, placeable.height) {
-            val zIndex = interactionCount + if (checked) CheckedZIndexFactor else 0f
+            val zIndex = interactionCount.value + if (checked) CheckedZIndexFactor else 0f
             placeable.place(0, 0, zIndex)
         }
     }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index 5aa3895..91e97da 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -51,14 +51,12 @@
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.shadow
@@ -686,8 +684,8 @@
         enabled = enabled,
         interactionSource = interactionSource,
         onDragStopped = { state.gestureEndAction() },
-        startDragImmediately = state.draggableState.isDragging,
-        state = state.draggableState
+        startDragImmediately = state.isDragging,
+        state = state
     )
 
     Layout(
@@ -848,7 +846,7 @@
             .roundToInt()
         // When start thumb and end thumb have different widths,
         // we need to add a correction for the centering of the slider.
-        val endCorrection = (state.startThumbWidth - state.endThumbWidth) / 2
+        val endCorrection = (startThumbPlaceable.width - endThumbPlaceable.width) / 2
         val endThumbOffsetX =
             (trackPlaceable.width * state.coercedActiveRangeEndAsFraction + endCorrection)
                 .roundToInt()
@@ -1004,7 +1002,7 @@
                 )
                 .hoverable(interactionSource = interactionSource)
                 .shadow(if (enabled) elevation else 0.dp, shape, clip = false)
-                .background(colors.thumbColor(enabled).value, shape)
+                .background(colors.thumbColor(enabled), shape)
         )
     }
 
@@ -1020,7 +1018,9 @@
      * not respond to user input, and it will appear visually disabled and disabled to
      * accessibility services.
      */
+    @Suppress("DEPRECATION")
     @Composable
+    @Deprecated("Use version that supports slider state")
     fun Track(
         sliderPositions: SliderPositions,
         modifier: Modifier = Modifier,
@@ -1044,7 +1044,7 @@
             val tickSize = TickSize.toPx()
             val trackStrokeWidth = TrackHeight.toPx()
             drawLine(
-                inactiveTrackColor.value,
+                inactiveTrackColor,
                 sliderStart,
                 sliderEnd,
                 trackStrokeWidth,
@@ -1063,7 +1063,7 @@
             )
 
             drawLine(
-                activeTrackColor.value,
+                activeTrackColor,
                 sliderValueStart,
                 sliderValueEnd,
                 trackStrokeWidth,
@@ -1078,7 +1078,7 @@
                             Offset(lerp(sliderStart, sliderEnd, it).x, center.y)
                         },
                         PointMode.Points,
-                        (if (outsideFraction) inactiveTickColor else activeTickColor).value,
+                        (if (outsideFraction) inactiveTickColor else activeTickColor),
                         tickSize,
                         StrokeCap.Round
                     )
@@ -1105,10 +1105,10 @@
         colors: SliderColors = colors(),
         enabled: Boolean = true
     ) {
-        val inactiveTrackColor by colors.trackColor(enabled, active = false)
-        val activeTrackColor by colors.trackColor(enabled, active = true)
-        val inactiveTickColor by colors.tickColor(enabled, active = false)
-        val activeTickColor by colors.tickColor(enabled, active = true)
+        val inactiveTrackColor = colors.trackColor(enabled, active = false)
+        val activeTrackColor = colors.trackColor(enabled, active = true)
+        val inactiveTickColor = colors.tickColor(enabled, active = false)
+        val activeTickColor = colors.tickColor(enabled, active = true)
         Canvas(
             modifier
                 .fillMaxWidth()
@@ -1145,10 +1145,10 @@
         colors: SliderColors = colors(),
         enabled: Boolean = true
     ) {
-        val inactiveTrackColor by colors.trackColor(enabled, active = false)
-        val activeTrackColor by colors.trackColor(enabled, active = true)
-        val inactiveTickColor by colors.tickColor(enabled, active = false)
-        val activeTickColor by colors.tickColor(enabled, active = true)
+        val inactiveTrackColor = colors.trackColor(enabled, active = false)
+        val activeTrackColor = colors.trackColor(enabled, active = true)
+        val inactiveTickColor = colors.tickColor(enabled, active = false)
+        val activeTickColor = colors.tickColor(enabled, active = true)
         Canvas(
             modifier
                 .fillMaxWidth()
@@ -1208,18 +1208,13 @@
             trackStrokeWidth,
             StrokeCap.Round
         )
-        tickFractions.groupBy {
-            it > activeRangeEnd ||
-                it < activeRangeStart
-        }.forEach { (outsideFraction, list) ->
-            drawPoints(
-                list.fastMap {
-                    Offset(lerp(sliderStart, sliderEnd, it).x, center.y)
-                },
-                PointMode.Points,
-                (if (outsideFraction) inactiveTickColor else activeTickColor),
-                tickSize,
-                StrokeCap.Round
+
+        for (tick in tickFractions) {
+            val outsideFraction = tick > activeRangeEnd || tick < activeRangeStart
+            drawCircle(
+                color = if (outsideFraction) inactiveTickColor else activeTickColor,
+                center = Offset(lerp(sliderStart, sliderEnd, tick).x, center.y),
+                radius = tickSize / 2f
             )
         }
     }
@@ -1322,12 +1317,10 @@
     state: RangeSliderState,
     enabled: Boolean
 ): Modifier {
-    val valueRange = state.valueRange.start..state.coercedEnd
-    val coerced = state.coercedStart.coerceIn(
-        valueRange.start,
-        valueRange.endInclusive
-    )
+    val valueRange = state.valueRange.start..state.activeRangeEnd
+
     return semantics {
+
         if (!enabled) disabled()
         setProgress(
             action = { targetValue ->
@@ -1356,17 +1349,17 @@
 
                 // This is to keep it consistent with AbsSeekbar.java: return false if no
                 // change from current.
-                if (resolvedValue == coerced) {
+                if (resolvedValue == state.activeRangeStart) {
                     false
                 } else {
-                    state.onValueChange(FloatRange(resolvedValue, state.coercedEnd))
+                    state.onValueChange(FloatRange(resolvedValue, state.activeRangeEnd))
                     state.onValueChangeFinished?.invoke()
                     true
                 }
             }
         )
     }.progressSemantics(
-        state.coercedStart,
+        state.activeRangeStart,
         valueRange,
         state.startSteps
     )
@@ -1377,13 +1370,11 @@
     state: RangeSliderState,
     enabled: Boolean
 ): Modifier {
-    val valueRange = state.coercedStart..state.valueRange.endInclusive
-    val coerced = state.coercedEnd.coerceIn(
-        valueRange.start,
-        valueRange.endInclusive
-    )
+    val valueRange = state.activeRangeStart..state.valueRange.endInclusive
+
     return semantics {
         if (!enabled) disabled()
+
         setProgress(
             action = { targetValue ->
                 var newValue = targetValue.coerceIn(valueRange.start, valueRange.endInclusive)
@@ -1408,17 +1399,17 @@
 
                 // This is to keep it consistent with AbsSeekbar.java: return false if no
                 // change from current.
-                if (resolvedValue == coerced) {
+                if (resolvedValue == state.activeRangeEnd) {
                     false
                 } else {
-                    state.onValueChange(FloatRange(state.coercedStart, resolvedValue))
+                    state.onValueChange(FloatRange(state.activeRangeStart, resolvedValue))
                     state.onValueChangeFinished?.invoke()
                     true
                 }
             }
         )
     }.progressSemantics(
-        state.coercedEnd,
+        state.activeRangeEnd,
         valueRange,
         state.endSteps
     )
@@ -1432,9 +1423,9 @@
 ) = if (enabled) {
     pointerInput(state, interactionSource) {
         detectTapGestures(
-            onPress = state.press,
+            onPress = { with(state) { onPress(it) } },
             onTap = {
-                state.draggableState.dispatchRawDelta(0f)
+                state.dispatchRawDelta(0f)
                 state.gestureEndAction()
             }
         )
@@ -1451,13 +1442,7 @@
     enabled: Boolean
 ): Modifier =
     if (enabled) {
-        pointerInput(
-            startInteractionSource,
-            endInteractionSource,
-            state.totalWidth,
-            state.isRtl,
-            state.valueRange
-        ) {
+        pointerInput(startInteractionSource, endInteractionSource, state) {
             val rangeSliderLogic = RangeSliderLogic(
                 state,
                 startInteractionSource,
@@ -1497,7 +1482,7 @@
                     val finishInteraction = try {
                         val success = horizontalDrag(pointerId = event.id) {
                             val deltaX = it.positionChange().x
-                            state.onDrag.invoke(draggingStart, if (state.isRtl) -deltaX else deltaX)
+                            state.onDrag(draggingStart, if (state.isRtl) -deltaX else deltaX)
                         }
                         if (success) {
                             DragInteraction.Stop(interaction)
@@ -1522,7 +1507,7 @@
     }
 
 @OptIn(ExperimentalMaterial3Api::class)
-private class RangeSliderLogic constructor(
+private class RangeSliderLogic(
     val state: RangeSliderState,
     val startInteractionSource: MutableInteractionSource,
     val endInteractionSource: MutableInteractionSource
@@ -1542,7 +1527,7 @@
         interaction: Interaction,
         scope: CoroutineScope
     ) {
-        state.onDrag.invoke(
+        state.onDrag(
             draggingStart,
             posX - if (draggingStart) state.rawOffsetStart else state.rawOffsetEnd
         )
@@ -1591,33 +1576,22 @@
     val disabledInactiveTrackColor: Color,
     val disabledInactiveTickColor: Color
 ) {
+    internal fun thumbColor(enabled: Boolean): Color =
+        if (enabled) thumbColor else disabledThumbColor
 
-    @Composable
-    internal fun thumbColor(enabled: Boolean): State<Color> {
-        return rememberUpdatedState(if (enabled) thumbColor else disabledThumbColor)
-    }
+    internal fun trackColor(enabled: Boolean, active: Boolean): Color =
+        if (enabled) {
+            if (active) activeTrackColor else inactiveTrackColor
+        } else {
+            if (active) disabledActiveTrackColor else disabledInactiveTrackColor
+        }
 
-    @Composable
-    internal fun trackColor(enabled: Boolean, active: Boolean): State<Color> {
-        return rememberUpdatedState(
-            if (enabled) {
-                if (active) activeTrackColor else inactiveTrackColor
-            } else {
-                if (active) disabledActiveTrackColor else disabledInactiveTrackColor
-            }
-        )
-    }
-
-    @Composable
-    internal fun tickColor(enabled: Boolean, active: Boolean): State<Color> {
-        return rememberUpdatedState(
-            if (enabled) {
-                if (active) activeTickColor else inactiveTickColor
-            } else {
-                if (active) disabledActiveTickColor else disabledInactiveTickColor
-            }
-        )
-    }
+    internal fun tickColor(enabled: Boolean, active: Boolean): Color =
+        if (enabled) {
+            if (active) activeTickColor else inactiveTickColor
+        } else {
+            if (active) disabledActiveTickColor else disabledInactiveTickColor
+        }
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
@@ -1663,33 +1637,6 @@
 // Internal to be referred to in tests
 internal val TrackHeight = SliderTokens.InactiveTrackHeight
 
-internal class SliderDraggableState(
-    val onDelta: (Float) -> Unit
-) : DraggableState {
-
-    var isDragging by mutableStateOf(false)
-        private set
-
-    private val dragScope: DragScope = object : DragScope {
-        override fun dragBy(pixels: Float): Unit = onDelta(pixels)
-    }
-
-    private val scrollMutex = MutatorMutex()
-
-    override suspend fun drag(
-        dragPriority: MutatePriority,
-        block: suspend DragScope.() -> Unit
-    ): Unit = coroutineScope {
-        isDragging = true
-        scrollMutex.mutateWith(dragScope, dragPriority, block)
-        isDragging = false
-    }
-
-    override fun dispatchRawDelta(delta: Float) {
-        return onDelta(delta)
-    }
-}
-
 private enum class SliderComponents {
     THUMB,
     TRACK
@@ -1705,6 +1652,8 @@
  * Class that holds information about [Slider]'s and [RangeSlider]'s active track
  * and fractional positions where the discrete ticks should be drawn on the track.
  */
+@Suppress("DEPRECATION")
+@Deprecated("Not necessary with the introduction of Slider state")
 @Stable
 class SliderPositions(
     initialActiveRange: ClosedFloatingPointRange<Float> = 0f..1f,
@@ -1767,7 +1716,8 @@
     val steps: Int = 0,
     val valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
     var onValueChangeFinished: (() -> Unit)? = null
-) {
+) : DraggableState {
+
     private var valueState by mutableFloatStateOf(initialValue)
 
     /**
@@ -1787,6 +1737,24 @@
         }
         get() = valueState
 
+    override suspend fun drag(
+        dragPriority: MutatePriority,
+        block: suspend DragScope.() -> Unit
+    ): Unit = coroutineScope {
+        isDragging = true
+        scrollMutex.mutateWith(dragScope, dragPriority, block)
+        isDragging = false
+    }
+
+    override fun dispatchRawDelta(delta: Float) {
+        val maxPx = max(totalWidth - thumbWidth / 2, 0f)
+        val minPx = min(thumbWidth / 2, maxPx)
+        rawOffset = (rawOffset + delta + pressOffset)
+        pressOffset = 0f
+        val offsetInTrack = snapValueToTick(rawOffset, tickFractions, minPx, maxPx)
+        onValueChange(scaleToUserValue(minPx, maxPx, offsetInTrack))
+    }
+
     /**
      * callback in which value should be updated
      */
@@ -1797,12 +1765,7 @@
     }
 
     internal val tickFractions = stepsToTickFractions(steps)
-
     internal var totalWidth by mutableIntStateOf(0)
-
-    private var rawOffset by mutableFloatStateOf(scaleToOffset(0f, 0f, value))
-    private var pressOffset by mutableFloatStateOf(0f)
-
     internal var isRtl = false
     internal var thumbWidth by mutableFloatStateOf(0f)
 
@@ -1813,24 +1776,25 @@
             value.coerceIn(valueRange.start, valueRange.endInclusive)
         )
 
-    internal val draggableState =
-        SliderDraggableState {
-            val maxPx = max(totalWidth - thumbWidth / 2, 0f)
-            val minPx = min(thumbWidth / 2, maxPx)
-            rawOffset = (rawOffset + it + pressOffset)
-            pressOffset = 0f
-            val offsetInTrack = snapValueToTick(rawOffset, tickFractions, minPx, maxPx)
-            onValueChange(scaleToUserValue(minPx, maxPx, offsetInTrack))
-        }
+    internal var isDragging by mutableStateOf(false)
+        private set
+
+    internal fun updateDimensions(
+        newThumbWidth: Float,
+        newTotalWidth: Int
+    ) {
+        thumbWidth = newThumbWidth
+        totalWidth = newTotalWidth
+    }
 
     internal val gestureEndAction = {
-        if (!draggableState.isDragging) {
+        if (!isDragging) {
             // check isDragging in case the change is still in progress (touch -> drag case)
             onValueChangeFinished?.invoke()
         }
     }
 
-    internal val press: suspend PressGestureScope.(Offset) -> Unit = { pos ->
+    internal suspend fun PressGestureScope.onPress(pos: Offset) {
         val to = if (isRtl) totalWidth - pos.x else pos.x
         pressOffset = to - rawOffset
         try {
@@ -1840,14 +1804,14 @@
         }
     }
 
-    internal fun updateDimensions(
-        newThumbWidth: Float,
-        newTotalWidth: Int
-    ) {
-        thumbWidth = newThumbWidth
-        totalWidth = newTotalWidth
+    private var rawOffset by mutableFloatStateOf(scaleToOffset(0f, 0f, value))
+    private var pressOffset by mutableFloatStateOf(0f)
+    private val dragScope: DragScope = object : DragScope {
+        override fun dragBy(pixels: Float): Unit = dispatchRawDelta(pixels)
     }
 
+    private val scrollMutex = MutatorMutex()
+
     private fun defaultOnValueChange(newVal: Float) { value = newVal }
 
     private fun scaleToUserValue(minPx: Float, maxPx: Float, offset: Float) =
@@ -1907,6 +1871,7 @@
             activeRangeStartState = snappedValue
         }
         get() = activeRangeStartState
+
     var activeRangeEnd: Float
         set(newVal) {
             val coercedValue = newVal.coerceIn(activeRangeStart, valueRange.endInclusive)
@@ -1928,23 +1893,22 @@
 
     internal val tickFractions = stepsToTickFractions(steps)
 
-    internal var startThumbWidth by mutableFloatStateOf(ThumbWidth.value)
-    internal var endThumbWidth by mutableFloatStateOf(ThumbWidth.value)
+    internal var startThumbWidth by mutableFloatStateOf(0f)
+    internal var endThumbWidth by mutableFloatStateOf(0f)
     internal var totalWidth by mutableIntStateOf(0)
-
     internal var rawOffsetStart by mutableFloatStateOf(0f)
     internal var rawOffsetEnd by mutableFloatStateOf(0f)
 
-    internal var isRtl = false
+    internal var isRtl by mutableStateOf(false)
 
     internal val gestureEndAction: (Boolean) -> Unit = {
         onValueChangeFinished?.invoke()
     }
 
-    private var maxPx by mutableFloatStateOf(max(totalWidth - endThumbWidth / 2, 0f))
-    private var minPx by mutableFloatStateOf(min(startThumbWidth / 2, maxPx))
+    private var maxPx by mutableFloatStateOf(0f)
+    private var minPx by mutableFloatStateOf(0f)
 
-    internal val onDrag: (Boolean, Float) -> Unit = { isStart, offset ->
+    internal fun onDrag(isStart: Boolean, offset: Float) {
         val offsetRange = if (isStart) {
             rawOffsetStart = (rawOffsetStart + offset)
             rawOffsetEnd = scaleToOffset(minPx, maxPx, activeRangeEnd)
@@ -1963,24 +1927,18 @@
         onValueChange(scaleToUserValue(minPx, maxPx, offsetRange))
     }
 
-    internal val coercedStart
-        get() = activeRangeStart.coerceIn(valueRange.start, activeRangeEnd)
-
-    internal val coercedEnd
-        get() = activeRangeEnd.coerceIn(activeRangeStart, valueRange.endInclusive)
-
     internal val coercedActiveRangeStartAsFraction
         get() = calcFraction(
             valueRange.start,
             valueRange.endInclusive,
-            coercedStart
+            activeRangeStart
         )
 
     internal val coercedActiveRangeEndAsFraction
         get() = calcFraction(
             valueRange.start,
             valueRange.endInclusive,
-            coercedEnd
+            activeRangeEnd
         )
 
     internal val startSteps
@@ -2007,7 +1965,7 @@
 
     internal fun updateMinMaxPx() {
         val newMaxPx = max(totalWidth - endThumbWidth / 2, 0f)
-        val newMinPx = min(startThumbWidth / 2, maxPx)
+        val newMinPx = min(startThumbWidth / 2, newMaxPx)
         if (minPx != newMinPx || maxPx != newMaxPx) {
             minPx = newMinPx
             maxPx = newMaxPx
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
index 67bdfb5..22cfbbf 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
@@ -43,8 +43,6 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.graphics.Color
@@ -89,7 +87,7 @@
  * relative to the anchor content.
  * @param tooltip the composable that will be used to populate the tooltip's content.
  * @param state handles the state of the tooltip's visibility.
- * @param modifier the [Modifier] to be applied to the tooltip.
+ * @param modifier the [Modifier] to be applied to the TooltipBox.
  * @param focusable [Boolean] that determines if the tooltip is focusable. When true,
  * the tooltip will consume touch events while it's shown and will have accessibility
  * focus move to the first element of the component. When false, the tooltip
@@ -142,16 +140,16 @@
     content: @Composable () -> Unit
 ) {
     Surface(
-        modifier = modifier
+        shape = shape,
+        color = containerColor
+    ) {
+        Box(modifier = modifier
             .sizeIn(
                 minWidth = TooltipMinWidth,
                 maxWidth = PlainTooltipMaxWidth,
                 minHeight = TooltipMinHeight
-            ),
-        shape = shape,
-        color = containerColor
-    ) {
-        Box(modifier = Modifier.padding(PlainTooltipContentPadding)) {
+            ).padding(PlainTooltipContentPadding)
+        ) {
             val textStyle =
                 MaterialTheme.typography.fromToken(PlainTooltipTokens.SupportingTextFont)
             CompositionLocalProvider(
@@ -422,11 +420,10 @@
     initialIsVisible: Boolean = false,
     isPersistent: Boolean = false,
     mutatorMutex: MutatorMutex = BasicTooltipDefaults.GlobalMutatorMutex
-): TooltipState {
-    return rememberSaveable(
+): TooltipState =
+    remember(
         isPersistent,
-        mutatorMutex,
-        saver = TooltipStateImpl.Saver
+        mutatorMutex
     ) {
         TooltipStateImpl(
             initialIsVisible = initialIsVisible,
@@ -434,7 +431,6 @@
             mutatorMutex = mutatorMutex
         )
     }
-}
 
 /**
  * Constructor extension function for [TooltipState]
@@ -528,29 +524,6 @@
     override fun onDispose() {
         job?.cancel()
     }
-
-    companion object {
-        /**
-         * The default [Saver] implementation for [TooltipStateImpl].
-         */
-        val Saver = Saver<TooltipStateImpl, Any>(
-            save = {
-                listOf(
-                    it.isVisible,
-                    it.isPersistent,
-                    it.mutatorMutex
-                )
-            },
-            restore = {
-                val (isVisible, isPersistent, mutatorMutex) = it as List<*>
-                TooltipStateImpl(
-                    initialIsVisible = isVisible as Boolean,
-                    isPersistent = isPersistent as Boolean,
-                    mutatorMutex = mutatorMutex as MutatorMutex,
-                )
-            }
-        )
-    }
 }
 
 /**
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
index 1c78dec..1ead6c8 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
@@ -31,8 +31,7 @@
     override val minApi = CURRENT_API
     override val issues get() = listOf(
         AutoboxingStateValuePropertyDetector.AutoboxingStateValueProperty,
-        // Disabled due to b/298019499.
-        // AutoboxingStateCreationDetector.AutoboxingStateCreation,
+        AutoboxingStateCreationDetector.AutoboxingStateCreation,
         ComposableCoroutineCreationDetector.CoroutineCreationDuringComposition,
         ComposableFlowOperatorDetector.FlowOperatorInvokedInComposition,
         ComposableLambdaParameterDetector.ComposableLambdaParameterNaming,
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index 7f1d7ff..ace0562 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -48,6 +48,7 @@
         }
 
         jvmMain {
+            dependsOn(commonMain)
             dependencies {
             }
         }
@@ -72,7 +73,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation project(':compose:ui:ui')
@@ -96,7 +97,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
diff --git a/compose/runtime/runtime-saveable/src/androidAndroidTest/AndroidManifest.xml b/compose/runtime/runtime-saveable/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/runtime/runtime-saveable/src/androidAndroidTest/AndroidManifest.xml
rename to compose/runtime/runtime-saveable/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/ActivityRecreationTest.kt b/compose/runtime/runtime-saveable/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/saveable/ActivityRecreationTest.kt
similarity index 100%
rename from compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/ActivityRecreationTest.kt
rename to compose/runtime/runtime-saveable/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/saveable/ActivityRecreationTest.kt
diff --git a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/Holder.kt b/compose/runtime/runtime-saveable/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/saveable/Holder.kt
similarity index 100%
rename from compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/Holder.kt
rename to compose/runtime/runtime-saveable/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/saveable/Holder.kt
diff --git a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt b/compose/runtime/runtime-saveable/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
similarity index 100%
rename from compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
rename to compose/runtime/runtime-saveable/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
diff --git a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableWithMutableStateTest.kt b/compose/runtime/runtime-saveable/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableWithMutableStateTest.kt
similarity index 100%
rename from compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableWithMutableStateTest.kt
rename to compose/runtime/runtime-saveable/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableWithMutableStateTest.kt
diff --git a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RestorationInVariousScenariosTest.kt b/compose/runtime/runtime-saveable/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/saveable/RestorationInVariousScenariosTest.kt
similarity index 100%
rename from compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RestorationInVariousScenariosTest.kt
rename to compose/runtime/runtime-saveable/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/saveable/RestorationInVariousScenariosTest.kt
diff --git a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/SaveableStateHolderTest.kt b/compose/runtime/runtime-saveable/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/saveable/SaveableStateHolderTest.kt
similarity index 100%
rename from compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/SaveableStateHolderTest.kt
rename to compose/runtime/runtime-saveable/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/saveable/SaveableStateHolderTest.kt
diff --git a/compose/runtime/runtime-saveable/src/androidAndroidTest/res/values/ids.xml b/compose/runtime/runtime-saveable/src/androidInstrumentedTest/res/values/ids.xml
similarity index 100%
rename from compose/runtime/runtime-saveable/src/androidAndroidTest/res/values/ids.xml
rename to compose/runtime/runtime-saveable/src/androidInstrumentedTest/res/values/ids.xml
diff --git a/compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/AutoSaverTest.kt b/compose/runtime/runtime-saveable/src/androidUnitTest/kotlin/androidx/compose/runtime/saveable/AutoSaverTest.kt
similarity index 100%
rename from compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/AutoSaverTest.kt
rename to compose/runtime/runtime-saveable/src/androidUnitTest/kotlin/androidx/compose/runtime/saveable/AutoSaverTest.kt
diff --git a/compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/ListSaverTest.kt b/compose/runtime/runtime-saveable/src/androidUnitTest/kotlin/androidx/compose/runtime/saveable/ListSaverTest.kt
similarity index 100%
rename from compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/ListSaverTest.kt
rename to compose/runtime/runtime-saveable/src/androidUnitTest/kotlin/androidx/compose/runtime/saveable/ListSaverTest.kt
diff --git a/compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/MapSaverTest.kt b/compose/runtime/runtime-saveable/src/androidUnitTest/kotlin/androidx/compose/runtime/saveable/MapSaverTest.kt
similarity index 100%
rename from compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/MapSaverTest.kt
rename to compose/runtime/runtime-saveable/src/androidUnitTest/kotlin/androidx/compose/runtime/saveable/MapSaverTest.kt
diff --git a/compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/SaveableStateRegistryTest.kt b/compose/runtime/runtime-saveable/src/androidUnitTest/kotlin/androidx/compose/runtime/saveable/SaveableStateRegistryTest.kt
similarity index 100%
rename from compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/SaveableStateRegistryTest.kt
rename to compose/runtime/runtime-saveable/src/androidUnitTest/kotlin/androidx/compose/runtime/saveable/SaveableStateRegistryTest.kt
diff --git a/compose/runtime/runtime/api/current.ignore b/compose/runtime/runtime/api/current.ignore
deleted file mode 100644
index 1a1e78f..0000000
--- a/compose/runtime/runtime/api/current.ignore
+++ /dev/null
@@ -1,3 +0,0 @@
-// Baseline format: 1.0
-RemovedClass: androidx.compose.runtime.PrimitiveSnapshotStateKt:
-    Removed class androidx.compose.runtime.PrimitiveSnapshotStateKt
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 9dfa745..90d3092 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -445,6 +445,12 @@
     property public final boolean isPaused;
   }
 
+  public final class PrimitiveSnapshotStateKt {
+    method public static inline operator float getValue(androidx.compose.runtime.FloatState, Object? thisObj, kotlin.reflect.KProperty<?> property);
+    method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableFloatState mutableFloatStateOf(float value);
+    method public static inline operator void setValue(androidx.compose.runtime.MutableFloatState, Object? thisObj, kotlin.reflect.KProperty<?> property, float value);
+  }
+
   public interface ProduceStateScope<T> extends androidx.compose.runtime.MutableState<T> kotlinx.coroutines.CoroutineScope {
     method public suspend Object? awaitDispose(kotlin.jvm.functions.Function0<kotlin.Unit> onDispose, kotlin.coroutines.Continuation<?>);
   }
@@ -549,12 +555,6 @@
     method public static inline operator void setValue(androidx.compose.runtime.MutableDoubleState, Object? thisObj, kotlin.reflect.KProperty<?> property, double value);
   }
 
-  public final class SnapshotFloatStateKt {
-    method public static inline operator float getValue(androidx.compose.runtime.FloatState, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableFloatState mutableFloatStateOf(float value);
-    method public static inline operator void setValue(androidx.compose.runtime.MutableFloatState, Object? thisObj, kotlin.reflect.KProperty<?> property, float value);
-  }
-
   public final class SnapshotIntStateKt {
     method public static inline operator int getValue(androidx.compose.runtime.IntState, Object? thisObj, kotlin.reflect.KProperty<?> property);
     method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableIntState mutableIntStateOf(int value);
diff --git a/compose/runtime/runtime/api/restricted_current.ignore b/compose/runtime/runtime/api/restricted_current.ignore
deleted file mode 100644
index 1a1e78f..0000000
--- a/compose/runtime/runtime/api/restricted_current.ignore
+++ /dev/null
@@ -1,3 +0,0 @@
-// Baseline format: 1.0
-RemovedClass: androidx.compose.runtime.PrimitiveSnapshotStateKt:
-    Removed class androidx.compose.runtime.PrimitiveSnapshotStateKt
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 7e80ac0..69c5a8e 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -477,6 +477,12 @@
     property public final boolean isPaused;
   }
 
+  public final class PrimitiveSnapshotStateKt {
+    method public static inline operator float getValue(androidx.compose.runtime.FloatState, Object? thisObj, kotlin.reflect.KProperty<?> property);
+    method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableFloatState mutableFloatStateOf(float value);
+    method public static inline operator void setValue(androidx.compose.runtime.MutableFloatState, Object? thisObj, kotlin.reflect.KProperty<?> property, float value);
+  }
+
   public interface ProduceStateScope<T> extends androidx.compose.runtime.MutableState<T> kotlinx.coroutines.CoroutineScope {
     method public suspend Object? awaitDispose(kotlin.jvm.functions.Function0<kotlin.Unit> onDispose, kotlin.coroutines.Continuation<?>);
   }
@@ -585,12 +591,6 @@
     method public static inline operator void setValue(androidx.compose.runtime.MutableDoubleState, Object? thisObj, kotlin.reflect.KProperty<?> property, double value);
   }
 
-  public final class SnapshotFloatStateKt {
-    method public static inline operator float getValue(androidx.compose.runtime.FloatState, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableFloatState mutableFloatStateOf(float value);
-    method public static inline operator void setValue(androidx.compose.runtime.MutableFloatState, Object? thisObj, kotlin.reflect.KProperty<?> property, float value);
-  }
-
   public final class SnapshotIntStateKt {
     method public static inline operator int getValue(androidx.compose.runtime.IntState, Object? thisObj, kotlin.reflect.KProperty<?> property);
     method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableIntState mutableIntStateOf(int value);
diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle
index 8a8b37c..9da1a17 100644
--- a/compose/runtime/runtime/build.gradle
+++ b/compose/runtime/runtime/build.gradle
@@ -93,7 +93,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testExtJunit)
@@ -103,7 +103,7 @@
             }
         }
 
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependsOn(nonEmulatorJvmTest)
         }
diff --git a/compose/runtime/runtime/integration-tests/build.gradle b/compose/runtime/runtime/integration-tests/build.gradle
index f62e6ba..4224740 100644
--- a/compose/runtime/runtime/integration-tests/build.gradle
+++ b/compose/runtime/runtime/integration-tests/build.gradle
@@ -73,11 +73,12 @@
         }
 
         jvmTest {
+            dependsOn(commonTest)
             dependencies {
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(projectOrArtifact(":compose:ui:ui"))
@@ -92,7 +93,7 @@
             }
         }
 
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
         }
 
diff --git a/compose/runtime/runtime/integration-tests/lint-baseline.xml b/compose/runtime/runtime/integration-tests/lint-baseline.xml
index 1826869a..80c0abd 100644
--- a/compose/runtime/runtime/integration-tests/lint-baseline.xml
+++ b/compose/runtime/runtime/integration-tests/lint-baseline.xml
@@ -7,7 +7,7 @@
         errorLine1="                    sleep(1)"
         errorLine2="                    ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidSnapshotTests.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/runtime/AndroidSnapshotTests.kt"/>
     </issue>
 
 </issues>
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/AndroidManifest.xml b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/AndroidManifest.xml
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidCompositionObserverTests.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/AndroidCompositionObserverTests.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidCompositionObserverTests.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/AndroidCompositionObserverTests.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidMovableContentTests.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/AndroidMovableContentTests.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidMovableContentTests.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/AndroidMovableContentTests.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidSnapshotTests.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/AndroidSnapshotTests.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidSnapshotTests.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/AndroidSnapshotTests.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/BaseComposeTest.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/BaseComposeTest.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/BaseComposeTest.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/BaseComposeTest.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/ComposeIntoTests.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/ComposeIntoTests.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/ComposeIntoTests.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/ComposeIntoTests.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/ContextReceiverTests.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/ContextReceiverTests.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/ContextReceiverTests.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/ContextReceiverTests.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/DisposeTests.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/DisposeTests.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/DisposeTests.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/DisposeTests.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/EmittableComposer.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/EmittableComposer.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/EmittableComposer.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/EmittableComposer.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/FlowAdapterTest.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/FlowAdapterTest.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/FlowAdapterTest.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/FlowAdapterTest.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/GroupSizeTests.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/GroupSizeTests.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/GroupSizeTests.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/GroupSizeTests.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/LiveEditApiTests.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/LiveEditApiTests.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/LiveEditApiTests.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/LiveEditApiTests.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/ProduceStateTests.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/ProduceStateTests.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/ProduceStateTests.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/ProduceStateTests.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/RecomposerTests.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/SideEffectTests.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/SideEffectTests.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/SideEffectTests.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/SideEffectTests.kt
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/SuspendingEffectsTests.kt b/compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/SuspendingEffectsTests.kt
similarity index 100%
rename from compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/SuspendingEffectsTests.kt
rename to compose/runtime/runtime/integration-tests/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/SuspendingEffectsTests.kt
diff --git a/compose/runtime/runtime/src/androidAndroidTest/AndroidManifest.xml b/compose/runtime/runtime/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/runtime/runtime/src/androidAndroidTest/AndroidManifest.xml
rename to compose/runtime/runtime/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/runtime/runtime/src/androidAndroidTest/kotlin/androidx/compose/runtime/snapshots/ParcelableMutableStateTests.kt b/compose/runtime/runtime/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/snapshots/ParcelableMutableStateTests.kt
similarity index 100%
rename from compose/runtime/runtime/src/androidAndroidTest/kotlin/androidx/compose/runtime/snapshots/ParcelableMutableStateTests.kt
rename to compose/runtime/runtime/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/snapshots/ParcelableMutableStateTests.kt
diff --git a/compose/runtime/runtime/src/androidAndroidTest/kotlin/androidx/compose/runtime/snapshots/ParcelablePrimitiveMutableStateTests.kt b/compose/runtime/runtime/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/snapshots/ParcelablePrimitiveMutableStateTests.kt
similarity index 100%
rename from compose/runtime/runtime/src/androidAndroidTest/kotlin/androidx/compose/runtime/snapshots/ParcelablePrimitiveMutableStateTests.kt
rename to compose/runtime/runtime/src/androidInstrumentedTest/kotlin/androidx/compose/runtime/snapshots/ParcelablePrimitiveMutableStateTests.kt
diff --git a/compose/runtime/runtime/src/androidAndroidTest/kotlin/kotlinx/test/IgnoreAndroidUnitTestTarget.kt b/compose/runtime/runtime/src/androidInstrumentedTest/kotlin/kotlinx/test/IgnoreAndroidUnitTestTarget.kt
similarity index 100%
rename from compose/runtime/runtime/src/androidAndroidTest/kotlin/kotlinx/test/IgnoreAndroidUnitTestTarget.kt
rename to compose/runtime/runtime/src/androidInstrumentedTest/kotlin/kotlinx/test/IgnoreAndroidUnitTestTarget.kt
diff --git a/compose/runtime/runtime/src/androidMain/baseline-prof.txt b/compose/runtime/runtime/src/androidMain/baseline-prof.txt
index ea25040..d98cdda 100644
--- a/compose/runtime/runtime/src/androidMain/baseline-prof.txt
+++ b/compose/runtime/runtime/src/androidMain/baseline-prof.txt
@@ -42,6 +42,7 @@
 HSPLandroidx/compose/runtime/ParcelableSnapshotMutableState**->**(**)**
 HSPLandroidx/compose/runtime/PausableMonotonicFrameClock;->**(**)**
 HSPLandroidx/compose/runtime/Pending**->**(**)**
+HSPLandroidx/compose/runtime/PrimitiveSnapshotStateKt**->**(**)**
 HSPLandroidx/compose/runtime/ProvidableCompositionLocal;->**(**)**
 HSPLandroidx/compose/runtime/ProvidedValue;->**(**)**
 HSPLandroidx/compose/runtime/RecomposeScopeImpl;->**(**)**
@@ -54,7 +55,6 @@
 HSPLandroidx/compose/runtime/SlotWriter;->**(**)**
 HSPLandroidx/compose/runtime/SnapshotMutableStateImpl**->**(**)**
 HSPLandroidx/compose/runtime/SnapshotDoubleStateKt**->**(**)**
-HSPLandroidx/compose/runtime/SnapshotFloatStateKt**->**(**)**
 HSPLandroidx/compose/runtime/SnapshotIntStateKt**->**(**)**
 HSPLandroidx/compose/runtime/SnapshotLongStateKt**->**(**)**
 HSPLandroidx/compose/runtime/SnapshotStateKt**->**(**)**
diff --git a/compose/runtime/runtime/src/androidTest/kotlin/kotlinx/test/IgnoreAndroidUnitTestTarget.kt b/compose/runtime/runtime/src/androidUnitTest/kotlin/kotlinx/test/IgnoreAndroidUnitTestTarget.kt
similarity index 100%
rename from compose/runtime/runtime/src/androidTest/kotlin/kotlinx/test/IgnoreAndroidUnitTestTarget.kt
rename to compose/runtime/runtime/src/androidUnitTest/kotlin/kotlinx/test/IgnoreAndroidUnitTestTarget.kt
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index a1f94db..4e0c3fe 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -1278,7 +1278,7 @@
     private var reusingGroup = -1
     private var childrenComposing: Int = 0
     private var compositionToken: Int = 0
-    private var sourceInformationEnabled = true
+    private var sourceInformationEnabled = false
     private val derivedStateObserver = object : DerivedStateObserver {
         override fun start(derivedState: DerivedState<*>) {
             childrenComposing++
@@ -1458,6 +1458,12 @@
         if (!forceRecomposeScopes) {
             forceRecomposeScopes = parentContext.collectingParameterInformation
         }
+
+        // Propagate collecting source information
+        if (!sourceInformationEnabled) {
+            sourceInformationEnabled = parentContext.collectingSourceInformation
+        }
+
         parentProvider.read(LocalInspectionTables)?.let {
             it.add(slotTable)
             parentContext.recordInspectionTable(it)
@@ -1544,11 +1550,13 @@
         private set
 
     /**
-     * Start collecting parameter information. This enables the tools API to always be able to
-     * determine the parameter values of composable calls.
+     * Start collecting parameter information and line number information. This enables the tools
+     * API to always be able to determine the parameter values of composable calls as well as the
+     * source location of calls.
      */
     override fun collectParameterInformation() {
         forceRecomposeScopes = true
+        sourceInformationEnabled = true
     }
 
     @OptIn(InternalComposeApi::class)
@@ -2115,6 +2123,7 @@
                 CompositionContextImpl(
                     compoundKeyHash,
                     forceRecomposeScopes,
+                    sourceInformationEnabled,
                     (composition as? CompositionImpl)?.observerHolder
                 )
             )
@@ -3158,20 +3167,22 @@
     @ComposeCompilerApi
     override fun sourceInformation(sourceInformation: String) {
         if (inserting && sourceInformationEnabled) {
-            writer.insertAux(sourceInformation)
+            writer.recordGroupSourceInformation(sourceInformation)
         }
     }
 
     @ComposeCompilerApi
     override fun sourceInformationMarkerStart(key: Int, sourceInformation: String) {
-        if (sourceInformationEnabled)
-            start(key, objectKey = null, kind = GroupKind.Group, data = sourceInformation)
+        if (inserting && sourceInformationEnabled) {
+            writer.recordGrouplessCallSourceInformationStart(key, sourceInformation)
+        }
     }
 
     @ComposeCompilerApi
     override fun sourceInformationMarkerEnd() {
-        if (sourceInformationEnabled)
-            end(isNode = false)
+        if (inserting && sourceInformationEnabled) {
+            writer.recordGrouplessCallSourceInformationEnd()
+        }
     }
 
     override fun disableSourceInformation() {
@@ -3503,6 +3514,7 @@
     private inner class CompositionContextImpl(
         override val compoundHashKey: Int,
         override val collectingParameterInformation: Boolean,
+        override val collectingSourceInformation: Boolean,
         override val observerHolder: CompositionObserverHolder?
     ) : CompositionContext() {
         var inspectionTables: MutableSet<MutableSet<CompositionData>>? = null
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
index 4b64873..ae6583c 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
@@ -38,6 +38,7 @@
 abstract class CompositionContext internal constructor() {
     internal abstract val compoundHashKey: Int
     internal abstract val collectingParameterInformation: Boolean
+    internal abstract val collectingSourceInformation: Boolean
     internal open val observerHolder: CompositionObserverHolder? get() = null
 
     /**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index d75d4bf..6b38d81 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -1303,6 +1303,9 @@
     internal override val collectingParameterInformation: Boolean
         get() = false
 
+    internal override val collectingSourceInformation: Boolean
+        get() = false
+
     internal override fun recordInspectionTable(table: MutableSet<CompositionData>) {
         // TODO: The root recomposer might be a better place to set up inspection
         // than the current configuration with an CompositionLocal
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index 32a9f5c..e619b1c 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -14,10 +14,9 @@
  * limitations under the License.
  */
 
-@file:OptIn(InternalComposeApi::class)
-
 package androidx.compose.runtime
 
+import androidx.compose.runtime.snapshots.fastAny
 import androidx.compose.runtime.snapshots.fastFilterIndexed
 import androidx.compose.runtime.snapshots.fastForEach
 import androidx.compose.runtime.snapshots.fastMap
@@ -133,6 +132,11 @@
     internal var anchors: ArrayList<Anchor> = arrayListOf()
 
     /**
+     * A map of source information to anchor.
+     */
+    internal var sourceInformationMap: HashMap<Anchor, GroupSourceInformation>? = null
+
+    /**
      * Returns true if the slot table is empty
      */
     override val isEmpty get() = groupsSize == 0
@@ -191,7 +195,7 @@
         runtimeCheck(readers <= 0) { "Cannot start a writer when a reader is pending" }
         writer = true
         version++
-        return SlotWriter(this)
+        return SlotWriter(table = this)
     }
 
     /**
@@ -204,7 +208,7 @@
      * at [index] is still in this table.
      */
     fun anchor(index: Int): Anchor {
-        runtimeCheck(!writer) { "use active SlotWriter to create an anchor location instead " }
+        runtimeCheck(!writer) { "use active SlotWriter to create an anchor location instead" }
         require(index in 0 until groupsSize) { "Parameter index is out of range" }
         return anchors.getOrAdd(index, groupsSize) {
             Anchor(index)
@@ -212,6 +216,14 @@
     }
 
     /**
+     * Return an anchor to the given index if there is one already, null otherwise.
+     */
+    fun tryAnchor(index: Int): Anchor? {
+        runtimeCheck(!writer) { "use active SlotWriter to crate an anchor for location instead" }
+        return if (index in 0 until groupsSize) anchors.find(index, groupsSize) else null
+    }
+
+    /**
      * Return the group index for [anchor]. This [SlotTable] is assumed to own [anchor] but that
      * is not validated. If [anchor] is not owned by this [SlotTable] the result is undefined.
      * If a [SlotWriter] is open the [SlotWriter.anchorIndex] must be called instead as [anchor]
@@ -247,9 +259,22 @@
     /**
      * Close [reader].
      */
-    internal fun close(reader: SlotReader) {
+    internal fun close(
+        reader: SlotReader,
+        sourceInformationMap: HashMap<Anchor, GroupSourceInformation>?
+    ) {
         runtimeCheck(reader.table === this && readers > 0) { "Unexpected reader close()" }
         readers--
+        if (sourceInformationMap != null) {
+            synchronized(this) {
+                val thisMap = this.sourceInformationMap
+                if (thisMap != null) {
+                    thisMap.putAll(sourceInformationMap)
+                } else {
+                    this.sourceInformationMap = sourceInformationMap
+                }
+            }
+        }
     }
 
     /**
@@ -263,11 +288,12 @@
         groupsSize: Int,
         slots: Array<Any?>,
         slotsSize: Int,
-        anchors: ArrayList<Anchor>
+        anchors: ArrayList<Anchor>,
+        sourceInformationMap: HashMap<Anchor, GroupSourceInformation>?,
     ) {
         require(writer.table === this && this.writer) { "Unexpected writer close()" }
         this.writer = false
-        setTo(groups, groupsSize, slots, slotsSize, anchors)
+        setTo(groups, groupsSize, slots, slotsSize, anchors, sourceInformationMap)
     }
 
     /**
@@ -279,7 +305,8 @@
         groupsSize: Int,
         slots: Array<Any?>,
         slotsSize: Int,
-        anchors: ArrayList<Anchor>
+        anchors: ArrayList<Anchor>,
+        sourceInformationMap: HashMap<Anchor, GroupSourceInformation>?,
     ) {
         // Adopt the slots from the writer
         this.groups = groups
@@ -287,6 +314,7 @@
         this.slots = slots
         this.slotsSize = slotsSize
         this.anchors = anchors
+        this.sourceInformationMap = sourceInformationMap
     }
 
     /**
@@ -357,6 +385,10 @@
         return groupsSize > 0 && groups.containsMark(0)
     }
 
+    fun sourceInformationOf(group: Int) = sourceInformationMap?.let { map ->
+        tryAnchor(group)?.let { anchor -> map[anchor] }
+    }
+
     /**
      * Find the nearest recompose scope for [group] that, when invalidated, will cause [group]
      * group to be recomposed.
@@ -376,7 +408,7 @@
 
     /**
      * Finds the nearest recompose scope to the provided group and invalidates it. Return
-     * true if the invalidation will cause the scope to reccompose, otherwise false which will
+     * true if the invalidation will cause the scope to recompose, otherwise false which will
      * require forcing recomposition some other way.
      */
     private fun invalidateGroup(group: Int): Boolean {
@@ -487,6 +519,35 @@
             require(lastLocation < location) { "Anchor is out of order" }
             lastLocation = location
         }
+
+        // Verify source information is well-formed
+        fun verifySourceGroup(group: GroupSourceInformation) {
+            group.groups?.fastForEach { item ->
+                when (item) {
+                    is Anchor -> {
+                        require(item.valid) {
+                            "Source map contains invalid anchor"
+                        }
+                        require(ownsAnchor(item)) {
+                            "Source map anchor is not owned by the slot table"
+                        }
+                    }
+                    is GroupSourceInformation -> verifySourceGroup(item)
+                }
+            }
+        }
+
+        sourceInformationMap?.let { sourceInformationMap ->
+            for ((anchor, sourceGroup) in sourceInformationMap) {
+                require(anchor.valid) {
+                    "Source map contains invalid anchor"
+                }
+                require(ownsAnchor(anchor)) {
+                    "Source map anchor is not owned by the slot table"
+                }
+                verifySourceGroup(sourceGroup)
+            }
+        }
     }
 
     /**
@@ -518,7 +579,21 @@
         repeat(level) { append(' ') }
         append("Group(")
         append(index)
-        append(") key=")
+        append(")")
+        tryAnchor(index)?.let { anchor ->
+            sourceInformationMap?.get(anchor)?.let { groupInformation ->
+                groupInformation.sourceInformation?.let {
+                    if (it.startsWith("C(") || it.startsWith("CC(")) {
+                        val start = it.indexOf("(") + 1
+                        val endParen = it.indexOf(')')
+                        append(" ")
+                        append(it.substring(start, endParen))
+                        append("()")
+                    }
+                }
+            }
+        }
+        append(" key=")
         append(groups.key(index))
         fun dataIndex(index: Int) =
             if (index >= groupsSize) slotsSize else groups.dataAnchor(index)
@@ -635,6 +710,105 @@
     val valid get() = location != Int.MIN_VALUE
     fun toIndexFor(slots: SlotTable) = slots.anchorIndex(this)
     fun toIndexFor(writer: SlotWriter) = writer.anchorIndex(this)
+
+    override fun toString(): String {
+        return "${super.toString()}{ location = $location }"
+    }
+}
+
+internal class GroupSourceInformation(val key: Int, var sourceInformation: String?) {
+    var groups: ArrayList<Any /* Anchor | GroupSourceInformation */>? = null
+    var closed = false
+
+    fun startGrouplessCall(key: Int, sourceInformation: String) {
+        openInformation().add(GroupSourceInformation(key, sourceInformation))
+    }
+
+    fun endGrouplessCall() { openInformation().close() }
+
+    fun reportGroup(writer: SlotWriter, group: Int) {
+        openInformation().add(writer.anchor(group))
+    }
+
+    fun reportGroup(table: SlotTable, group: Int) {
+        openInformation().add(table.anchor(group))
+    }
+
+    fun addGroupAfter(writer: SlotWriter, predecessor: Int, group: Int) {
+        val groups = groups ?: ArrayList<Any>().also { groups = it }
+        val index = if (predecessor >= 0) {
+            val anchor = writer.tryAnchor(predecessor)
+            if (anchor != null) {
+                groups.fastIndexOf {
+                    it == anchor ||
+                        (it is GroupSourceInformation && it.hasAnchor(anchor))
+                }
+            } else 0
+        } else 0
+        groups.add(index, writer.anchor(group))
+    }
+
+    fun close() { closed = true }
+
+    // Return the current open nested source information or this.
+    private fun openInformation(): GroupSourceInformation =
+        (groups?.let {
+            groups -> groups.fastLastOrNull { it is GroupSourceInformation && !it.closed }
+        } as? GroupSourceInformation)?.openInformation() ?: this
+
+    private fun add(group: Any /* Anchor | GroupSourceInformation */) {
+        val groups = groups ?: ArrayList()
+        this.groups = groups
+        groups.add(group)
+    }
+
+    private fun hasAnchor(anchor: Anchor): Boolean =
+        groups?.fastAny {
+            it == anchor || (it is GroupSourceInformation && hasAnchor(anchor))
+        } == true
+
+    fun removeAnchor(anchor: Anchor): Boolean {
+        val groups = groups
+        if (groups != null) {
+            var index = groups.size - 1
+            while (index >= 0) {
+                when (val item = groups[index]) {
+                    is Anchor -> if (item == anchor) groups.removeAt(index)
+                    is GroupSourceInformation -> if (!item.removeAnchor(anchor)) {
+                        groups.removeAt(index)
+                    }
+                }
+                index--
+            }
+            if (groups.isEmpty()) {
+                this.groups = null
+                return false
+            }
+            return true
+        }
+        return true
+    }
+}
+
+private inline fun <T> ArrayList<T>.fastLastOrNull(predicate: (T) -> Boolean): T? {
+    var index = size - 1
+    while (index >= 0) {
+        val value = get(index)
+        if (predicate(value)) return value
+        index--
+    }
+    return null
+}
+
+private inline fun <T> ArrayList<T>.fastIndexOf(predicate: (T) -> Boolean): Int {
+    var index = 0
+    val size = size
+    while (index < size) {
+        val value = get(index)
+        if (predicate(value)) return index
+        index++
+    }
+    return -1
 }
 
 /**
@@ -668,6 +842,12 @@
     private val slotsSize: Int = table.slotsSize
 
     /**
+     * A local copy of the [sourceInformationMap] being created to be merged into [table]
+     * when the reader closes.
+     */
+    private var sourceInformationMap: HashMap<Anchor, GroupSourceInformation>? = null
+
+    /**
      * True if the reader has been closed
      */
     var closed: Boolean = false
@@ -927,7 +1107,7 @@
      */
     fun close() {
         closed = true
-        table.close(this)
+        table.close(this, sourceInformationMap)
     }
 
     /**
@@ -935,14 +1115,17 @@
      */
     fun startGroup() {
         if (emptyCount <= 0) {
+            val parent = parent
+            val currentGroup = currentGroup
             require(groups.parentAnchor(currentGroup) == parent) { "Invalid slot table detected" }
-            parent = currentGroup
+            sourceInformationMap?.get(anchor(parent))?.reportGroup(table, currentGroup)
+            this.parent = currentGroup
             currentEnd = currentGroup + groups.groupSize(currentGroup)
-            val current = currentGroup++
-            currentSlot = groups.slotAnchor(current)
-            currentSlotEnd = if (current >= groupsSize - 1)
+            this.currentGroup = currentGroup + 1
+            currentSlot = groups.slotAnchor(currentGroup)
+            currentSlotEnd = if (currentGroup >= groupsSize - 1)
                 slotsSize else
-                groups.dataAnchor(current + 1)
+                groups.dataAnchor(currentGroup + 1)
         }
     }
 
@@ -1057,6 +1240,11 @@
         Anchor(index)
     }
 
+    /**
+     * Return an anchor if one has already been created, null otherwise.
+     */
+    private fun tryAnchor(index: Int) = table.anchors.find(index, groupsSize)
+
     private fun IntArray.node(index: Int) = if (isNode(index)) {
         slots[nodeIndex(index)]
     } else Composer.Empty
@@ -1126,11 +1314,16 @@
     private var slots: Array<Any?> = table.slots
 
     /**
-     * A copy of the [SlotWriter.anchors] to avoid having to index through [table].
+     * A copy of the [SlotTable.anchors] to avoid having to index through [table].
      */
     private var anchors: ArrayList<Anchor> = table.anchors
 
     /**
+     * A copy of [SlotTable.sourceInformationMap] to avoid having to index through [table]
+     */
+    private var sourceInformationMap = table.sourceInformationMap
+
+    /**
      * Group index of the start of the gap in the groups array.
      */
     private var groupGapStart: Int = table.groupsSize
@@ -1340,7 +1533,8 @@
             groupsSize = groupGapStart,
             slots = slots,
             slotsSize = slotsGapStart,
-            anchors = anchors
+            anchors = anchors,
+            sourceInformationMap = sourceInformationMap,
         )
     }
 
@@ -1411,6 +1605,49 @@
         currentSlot++
     }
 
+    fun recordGroupSourceInformation(sourceInformation: String) {
+        if (insertCount > 0) {
+            groupSourceInformationFor(parent, sourceInformation)
+        }
+    }
+
+    fun recordGrouplessCallSourceInformationStart(key: Int, value: String) {
+        if (insertCount > 0) {
+            groupSourceInformationFor(parent, null)?.startGrouplessCall(key, value)
+        }
+    }
+
+    fun recordGrouplessCallSourceInformationEnd() {
+        if (insertCount > 0) {
+            groupSourceInformationFor(parent, null)?.endGrouplessCall()
+        }
+    }
+
+    private fun groupSourceInformationFor(
+        parent: Int,
+        sourceInformation: String?
+    ): GroupSourceInformation? {
+        val map = sourceInformationMap ?: HashMap()
+        this.sourceInformationMap = map
+        return map.getOrPut(anchor(parent)) {
+            val result = GroupSourceInformation(0, sourceInformation)
+
+            // If we called from a groupless call then the groups added before this call
+            // are not reflected in this group information so they need to be added now
+            // if they exist.
+            if (sourceInformation == null) {
+                var child = parent + 1
+                val end = currentGroup
+                while (child < end) {
+                    result.reportGroup(this, child)
+                    child += groups.groupSize(child)
+                }
+            }
+
+            result
+        }
+    }
+
     /**
      * Updates the node for the current node group to [value].
      */
@@ -1601,6 +1838,7 @@
     fun startData(key: Int, aux: Any?) = startGroup(key, Composer.Empty, isNode = false, aux = aux)
 
     private fun startGroup(key: Int, objectKey: Any?, isNode: Boolean, aux: Any?) {
+        val previousParent = parent
         val inserting = insertCount > 0
         nodeCountStack.push(nodeCount)
 
@@ -1637,9 +1875,11 @@
             val newCurrent = current + 1
             this.parent = current
             this.currentGroup = newCurrent
+            if (previousParent >= 0) {
+                sourceInformationOf(previousParent)?.reportGroup(this, current)
+            }
             newCurrent
         } else {
-            val previousParent = parent
             startStack.push(previousParent)
             saveCurrentGroupEnd()
             val currentGroup = currentGroup
@@ -1740,7 +1980,7 @@
     internal fun bashGroup() {
         startGroup()
         while (!isGroupEnd) {
-            insertParentGroup(-3)
+            bashParentGroup()
             skipGroup()
         }
         endGroup()
@@ -1799,6 +2039,13 @@
         val oldSlot = currentSlot
         val count = skipGroup()
 
+        // Remove the group from its parent information
+        sourceInformationOf(parent)?.let { sourceInformation ->
+            tryAnchor(oldGroup)?.let { anchor ->
+                sourceInformation.removeAnchor(anchor)
+            }
+        }
+
         // Remove any recalculate markers ahead of this delete as they are in the group
         // that is being deleted.
         pendingRecalculateMarks?.let {
@@ -2080,6 +2327,42 @@
                 anchors
             } else emptyList()
 
+            // Move any source information from the source table to the destination table
+            if (anchors.isNotEmpty()) {
+                val sourceSourceInformationMap = fromWriter.sourceInformationMap
+                if (sourceSourceInformationMap != null) {
+                    var destinationSourceInformation = toWriter.sourceInformationMap
+                    anchors.fastForEach { anchor ->
+                        val information = sourceSourceInformationMap[anchor]
+                        if (information != null) {
+                            sourceSourceInformationMap.remove(anchor)
+                            val map = destinationSourceInformation ?: run {
+                                val map = HashMap<Anchor, GroupSourceInformation>()
+                                destinationSourceInformation = map
+                                toWriter.sourceInformationMap = destinationSourceInformation
+                                map
+                            }
+                            map[anchor] = information
+                        }
+                    }
+                    if (sourceSourceInformationMap.isEmpty()) {
+                        fromWriter.sourceInformationMap = null
+                    }
+                }
+            }
+
+            // Record the new group in the parent information
+            val toWriterParent = toWriter.parent
+            toWriter.sourceInformationOf(parent)?.let {
+                var predecessor = -1
+                var child = toWriterParent + 1
+                val endGroup = toWriter.currentGroup
+                while (child < endGroup) {
+                    predecessor = child
+                    child += toWriter.groups.groupSize(child)
+                }
+                it.addGroupAfter(toWriter, predecessor, endGroup)
+            }
             val parentGroup = fromWriter.parent(fromIndex)
             val anchorsRemoved = if (!removeSourceGroup) {
                 // e.g.: we can skip groups removal for insertTable of Composer because
@@ -2131,6 +2414,7 @@
             if (hasMarks) {
                 toWriter.updateContainsMark(parent)
             }
+
             return anchors
         }
     }
@@ -2205,10 +2489,12 @@
             val myGroups = groups
             val mySlots = slots
             val myAnchors = anchors
+            val mySourceInformation = sourceInformationMap
             val groups = table.groups
             val groupsSize = table.groupsSize
             val slots = table.slots
             val slotsSize = table.slotsSize
+            val sourceInformation = table.sourceInformationMap
             this.groups = groups
             this.slots = slots
             this.anchors = table.anchors
@@ -2217,8 +2503,9 @@
             this.slotsGapStart = slotsSize
             this.slotsGapLen = slots.size - slotsSize
             this.slotsGapOwner = groupsSize
+            this.sourceInformationMap = sourceInformation
 
-            table.setTo(myGroups, 0, mySlots, 0, myAnchors)
+            table.setTo(myGroups, 0, mySlots, 0, myAnchors, mySourceInformation)
             return this.anchors
         }
 
@@ -2239,7 +2526,8 @@
      * all remaining children of the current group will be parented by a new group and the
      * [currentSlot] will be moved to after the group inserted.
      */
-    fun insertParentGroup(key: Int) {
+    private fun bashParentGroup() {
+        val key = -3
         runtimeCheck(insertCount == 0) { "Writer cannot be inserting" }
         if (isGroupEnd) {
             beginInsert()
@@ -2282,6 +2570,11 @@
             addToGroupSizeAlongSpine(parentAddress, 1)
             fixParentAnchorsFor(parent, currentGroupEnd, currentGroup)
             this.currentGroup = currentGroupEnd
+
+            // Remove any source information for child groups in the bashed group as updating it
+            // will not work as the children are now separated by a group. Just clearing the list
+            // is sufficient as list will be rebuilt when the new content is generated.
+            sourceInformationOf(parent)?.let { group -> group.groups = null }
         }
     }
 
@@ -2693,7 +2986,9 @@
 
             // Move the gap to start of the removal and grow the gap
             moveGroupGapTo(start)
-            if (anchors.isNotEmpty()) anchorsRemoved = removeAnchors(start, len)
+            if (anchors.isNotEmpty()) {
+                anchorsRemoved = removeAnchors(start, len, sourceInformationMap)
+            }
             groupGapStart = start
             val previousGapLen = groupGapLen
             val newGapLen = previousGapLen + len
@@ -2707,14 +3002,25 @@
             }
             if (currentGroupEnd >= groupGapStart) currentGroupEnd -= len
 
+            val parent = parent
             // Update markers if necessary
             if (containsGroupMark(parent)) {
                 updateContainsMark(parent)
             }
+
+            // Remove the group from its parent source information
             anchorsRemoved
         } else false
     }
 
+    private fun sourceInformationOf(group: Int): GroupSourceInformation? =
+        sourceInformationMap?.let { map ->
+            tryAnchor(group)?.let { anchor -> map[anchor] }
+        }
+
+    internal fun tryAnchor(group: Int) =
+        if (group in 0 until size) anchors.find(group, size) else null
+
     /**
      * Remove [len] slots from [start].
      */
@@ -2782,7 +3088,11 @@
     /**
      * A helper function to remove the anchors for groups that are removed.
      */
-    private fun removeAnchors(gapStart: Int, size: Int): Boolean {
+    private fun removeAnchors(
+        gapStart: Int,
+        size: Int,
+        sourceInformationMap: HashMap<Anchor, GroupSourceInformation>?
+    ): Boolean {
         val gapLen = groupGapLen
         val removeEnd = gapStart + size
         val groupsSize = capacity - gapLen
@@ -2797,6 +3107,7 @@
             if (location >= gapStart) {
                 if (location < removeEnd) {
                     anchor.location = Int.MIN_VALUE
+                    sourceInformationMap?.remove(anchor)
                     removeAnchorStart = index
                     if (removeAnchorEnd == 0) removeAnchorEnd = index + 1
                 }
@@ -3037,7 +3348,9 @@
     override val sourceInfo: String?
         get() = if (table.groups.hasAux(group))
             table.slots[table.groups.auxIndex(group)] as? String
-        else null
+        else table.tryAnchor(group)?.let {
+            table.sourceInformationMap?.get(it)?.sourceInformation
+        }
 
     override val node: Any?
         get() = if (table.groups.isNode(group))
@@ -3056,11 +3369,12 @@
 
     override fun iterator(): Iterator<CompositionGroup> {
         validateRead()
-        return GroupIterator(
-            table,
-            group + 1,
-            group + table.groups.groupSize(group)
-        )
+        return table.sourceInformationOf(group)?.let { SourceInformationGroupIterator(table, it) }
+            ?: GroupIterator(
+                table,
+                group + 1,
+                group + table.groups.groupSize(group)
+            )
     }
 
     override val groupSize: Int get() = table.groups.groupSize(group)
@@ -3090,6 +3404,21 @@
         }
 }
 
+private class SourceInformationSlotTableGroup(
+    val table: SlotTable,
+    val sourceInformation: GroupSourceInformation
+) : CompositionGroup, Iterable<CompositionGroup> {
+    override val key: Any = sourceInformation.key
+    override val sourceInfo: String? get() = sourceInformation.sourceInformation
+    override val node: Any? get() = null
+    override val data: Iterable<Any?> = emptyList()
+    override val compositionGroups: Iterable<CompositionGroup> = this
+    override val isEmpty: Boolean
+        get() = sourceInformation.groups?.isEmpty() != false
+    override fun iterator(): Iterator<CompositionGroup> =
+        SourceInformationGroupIterator(table, sourceInformation)
+}
+
 private class GroupIterator(
     val table: SlotTable,
     start: Int,
@@ -3121,7 +3450,7 @@
 
 private class DataIterator(
     val table: SlotTable,
-    val group: Int,
+    group: Int,
 ) : Iterable<Any?>, Iterator<Any?> {
     val start = table.groups.dataAnchor(group)
     val end = if (group + 1 < table.groupsSize)
@@ -3136,6 +3465,22 @@
     ).also { index++ }
 }
 
+private class SourceInformationGroupIterator(
+    val table: SlotTable,
+    val group: GroupSourceInformation,
+) : Iterator<CompositionGroup> {
+    private val version = table.version
+    private var index = 0
+    override fun hasNext(): Boolean = group.groups?.let { index < it.size } ?: false
+    override fun next(): CompositionGroup {
+        return when (val group = group.groups?.get(index++)) {
+            is Anchor -> SlotTableGroup(table, group.location, version)
+            is GroupSourceInformation -> SourceInformationSlotTableGroup(table, group)
+            else -> composeRuntimeError("Unexpected group information structure")
+        }
+    }
+}
+
 // Parent -1 is reserved to be the root parent index so the anchor must pivot on -2.
 private const val parentAnchorPivot = -2
 
@@ -3364,6 +3709,11 @@
     } else get(location)
 }
 
+private fun ArrayList<Anchor>.find(index: Int, effectiveSize: Int): Anchor? {
+    val location = search(index, effectiveSize)
+    return if (location >= 0) get(location) else null
+}
+
 /**
  * This is inlined here instead to avoid allocating a lambda for the compare when this is used.
  */
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SnapshotFloatState.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SnapshotFloatState.kt
index 1490f39..f083f5e 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SnapshotFloatState.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SnapshotFloatState.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:JvmName("SnapshotFloatStateKt")
+@file:JvmName("PrimitiveSnapshotStateKt")
 @file:JvmMultifileClass
 package androidx.compose.runtime
 
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
index 0ed6a87..dd8461b 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
@@ -252,6 +252,8 @@
             }
         }
 
+        verifyConsistent()
+
         expect(useD)
 
         // Modify A
@@ -265,6 +267,7 @@
             use = newUse
             a++
             expectChanges()
+            verifyConsistent()
             revalidate()
             expect(newUse, previous)
         }
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index 8f8615b..d93db8a 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -3424,13 +3424,17 @@
     @Test // regression test for 264467571
     fun test_returnConditionally_fromNodeLambda_local_initial_return() = compositionTest {
         var condition by mutableStateOf(true)
+
         compose {
+            currentComposer.disableSourceInformation()
             Text("Before outer")
             InlineLinear {
                 Text("Before inner")
                 InlineLinear inner@{
                     Text("Before return")
-                    if (condition) return@inner
+                    if (condition) {
+                        return@inner
+                    }
                     Text("After return")
                 }
                 Text("After inner")
@@ -3463,6 +3467,7 @@
     fun test_returnConditionally_fromNodeLambda_local_initial_no_return() = compositionTest {
         var condition by mutableStateOf(true)
         compose {
+            currentComposer.disableSourceInformation()
             Text("Before outer")
             InlineLinear {
                 Text("Before inner")
@@ -3501,6 +3506,7 @@
     fun test_returnConditionally_fromNodeLambda_nonLocal_initial_return() = compositionTest {
         var condition by mutableStateOf(true)
         compose {
+            currentComposer.disableSourceInformation()
             Text("Before outer")
             InlineLinear outer@{
                 Text("Before inner")
@@ -3539,6 +3545,7 @@
     fun test_returnConditionally_fromNodeLambda_nonLocal_initial_no_return() = compositionTest {
         var condition by mutableStateOf(true)
         compose {
+            currentComposer.disableSourceInformation()
             Text("Before outer")
             InlineLinear outer@{
                 Text("Before inner")
@@ -3578,6 +3585,7 @@
         compositionTest {
             var condition by mutableStateOf(true)
             compose {
+                currentComposer.disableSourceInformation()
                 Text("Before outer")
                 InlineLinear outer@{
                     Text("Before inner")
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/SlotTableTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/SlotTableTests.kt
index 333289b..afb306f 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/SlotTableTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/SlotTableTests.kt
@@ -13,10 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-@file:OptIn(InternalComposeApi::class)
 package androidx.compose.runtime
 
+import androidx.compose.runtime.snapshots.fastForEach
+import androidx.compose.runtime.tooling.CompositionData
+import androidx.compose.runtime.tooling.CompositionGroup
 import kotlin.random.Random
 import kotlin.test.Test
 import kotlin.test.assertEquals
@@ -24,7 +25,6 @@
 import kotlin.test.assertSame
 import kotlin.test.assertTrue
 
-@OptIn(InternalComposeApi::class)
 class SlotTableTests {
     @Test
     fun testCanCreate() {
@@ -3910,6 +3910,381 @@
     }
 
     @Test
+    fun canReportNonGroupCallInformationDuringWrite() {
+        val slots = SlotTable()
+        slots.write { writer ->
+            writer.insert {
+                writer.group(100) {
+                    writer.group(200, "C(200)") {
+                        writer.grouplessCall(300, "C(300)") { }
+                        writer.grouplessCall(301, "C(301)") { }
+                        writer.group(302, "C(302)") { }
+                        writer.grouplessCall(303, "C(303)") { }
+                        writer.group(304, "C(304)") { }
+                        writer.grouplessCall(305, "C(305)") {
+                            writer.group(400, "C(400)") { }
+                            writer.group(401, "C(401)") { }
+                        }
+                        writer.grouplessCall(306, "C(306)") {
+                            writer.group(402, "C(402)") { }
+                            writer.grouplessCall(403, "C(403)") {
+                                writer.group(500, "C(500)") { }
+                                writer.group(501, "C(501)") { }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        val expectedRoot = SourceGroup.group(100) {
+            group(200, "C(200)") {
+                group(300, "C(300)") { }
+                group(301, "C(301)") { }
+                group(302, "C(302)") { }
+                group(303, "C(303)") { }
+                group(304, "C(304)") { }
+                group(305, "C(305)") {
+                    group(400, "C(400)") { }
+                    group(401, "C(401)") { }
+                }
+                group(306, "C(306)") {
+                    group(402, "C(402)") { }
+                    group(403, "C(403)") {
+                        group(500, "C(500)") { }
+                        group(501, "C(501)") { }
+                    }
+                }
+            }
+        }
+        val slotsRoot = SourceGroup.group(slots)
+        assertEquals(expectedRoot, slotsRoot)
+    }
+
+    @Test
+    fun canMoveSourceInformationFromAnotherTable() {
+        val sourceTable = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(200, "C(200)") {
+                            grouplessCall(300, "C(300)") {
+                                group(400, "C(400)") { }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        sourceTable.verifyWellFormed()
+
+        val mainTable = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(201, "C(201)") { }
+                        }
+                    }
+                }
+            }
+        }
+        mainTable.verifyWellFormed()
+
+        mainTable.write { writer ->
+            with(writer) {
+                group {
+                    insert {
+                        moveFrom(sourceTable, 0)
+                    }
+                    skipToGroupEnd()
+                }
+            }
+        }
+        mainTable.verifyWellFormed()
+
+        val expected = SourceGroup.group(100) {
+            group(200, "C(200)") {
+                group(300, "C(300)") {
+                    group(400, "C(400)") { }
+                }
+            }
+            group(201, "C(201)") { }
+        }
+        val received = SourceGroup.group(mainTable)
+        assertEquals(expected, received)
+    }
+
+    @Test
+    fun canMoveSourceInformationIntoAGroupWithSourceInformation() {
+        val sourceTable = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(300, "C(300)") {
+                            grouplessCall(400, "C(400)") {
+                                group(500, "C(500)") { }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        sourceTable.verifyWellFormed()
+
+        val mainTable = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(201, "C(201)") { }
+                        }
+                    }
+                }
+            }
+        }
+        mainTable.verifyWellFormed()
+
+        mainTable.write { writer ->
+            with(writer) {
+                group {
+                    group(201) {
+                        insert {
+                            moveFrom(sourceTable, 0)
+                        }
+                        skipToGroupEnd()
+                    }
+                    skipToGroupEnd()
+                }
+            }
+        }
+        mainTable.verifyWellFormed()
+
+        val expected = SourceGroup.group(100) {
+            group(201, "C(201)") {
+                group(300, "C(300)") {
+                    group(400, "C(400)") {
+                        group(500, "C(500)") { }
+                    }
+                }
+            }
+        }
+        val received = SourceGroup.group(mainTable)
+        assertEquals(expected, received)
+    }
+
+    @Test
+    fun canRemoveAGroupBeforeAnEmptyGrouplessCall() {
+        val slots = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(200, "C(2001)") { }
+                            grouplessCall(201, "C(201)") { }
+                            group(202, "C(202)") { }
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        slots.write { writer ->
+            with(writer) {
+                group {
+                    removeGroup()
+                    skipToGroupEnd()
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        val expected = SourceGroup.group(100) {
+            group(201, "C(201)") { }
+            group(202, "C(202)") { }
+        }
+        val received = SourceGroup.group(slots)
+        assertEquals(expected, received)
+    }
+
+    @Test
+    fun canRemoveAGroupAfterAnEmptyGrouplessCall() {
+        val slots = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(200, "C(200)") { }
+                            grouplessCall(201, "C(201)") { }
+                            group(202, "C(202)") { }
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        slots.write { writer ->
+            with(writer) {
+                group {
+                    skipGroup()
+                    removeGroup()
+                    skipToGroupEnd()
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        val expected = SourceGroup.group(100) {
+            group(200, "C(200)") { }
+            group(201, "C(201)") { }
+        }
+        val received = SourceGroup.group(slots)
+        assertEquals(expected, received)
+    }
+
+    @Test
+    fun canRemoveAGroupProducedInAGrouplessCall() {
+        val slots = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(200, "C(200)") { }
+                            grouplessCall(201, "C(201)") {
+                                group(300, "C(300)") { }
+                            }
+                            group(202, "C(202)") { }
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        slots.write { writer ->
+            with(writer) {
+                group {
+                    skipGroup()
+                    removeGroup()
+                    skipToGroupEnd()
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        val expected = SourceGroup.group(100) {
+            group(200, "C(200)") { }
+            group(202, "C(202)") { }
+        }
+        val received = SourceGroup.group(slots)
+        assertEquals(expected, received)
+    }
+
+    @Test
+    fun canRemoveAGroupWithSourceInformation() {
+        val slots = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(200, "C(200)") { }
+                            group(201, "C(201)") { }
+                            group(202, "C(202)") {
+                                grouplessCall(300, "C(300)") {
+                                    group(400, "C(400)") {
+                                        group(500, "C(500)") { }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        slots.write { writer ->
+            with(writer) {
+                group(100) {
+                    skipGroup()
+                    skipGroup()
+                    group {
+                        group {
+                            removeGroup() // Remove group 500
+                            skipToGroupEnd()
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        val expected = SourceGroup.group(100) {
+            group(200, "C(200)") { }
+            group(201, "C(201)") { }
+            group(202, "C(202)") {
+                group(300, "C(300)") {
+                    group(400, "C(400)") { }
+                }
+            }
+        }
+        val received = SourceGroup.group(slots)
+
+        assertEquals(expected, received)
+    }
+
+    @Test
+    fun canAddAGrouplessCallToAGroupWithNoSourceInformation() {
+        val slots = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(200) {
+                                group(300, "C(300)") { }
+                                group(301, "C(301)") { }
+                                grouplessCall(302, "C(302)") {
+                                    group(400, "C(400)") { }
+                                }
+                            }
+                            group(201, "C(201)") {
+                                group(303) {
+                                    group(401, "C(401)") { }
+                                    grouplessCall(402, "C(402)") { }
+                                    group(403, "C(403)") { }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        val expected = SourceGroup.group(100) {
+            group(200) {
+                group(300, "C(300)") { }
+                group(301, "C(301)") { }
+                group(302, "C(302)") {
+                    group(400, "C(400)") { }
+                }
+            }
+            group(201, "C(201)") {
+                group(303) {
+                    group(401, "C(401)") { }
+                    group(402, "C(402)") { }
+                    group(403, "C(403)") { }
+                }
+            }
+        }
+        val received = SourceGroup.group(slots)
+
+        assertEquals(expected, received)
+    }
+
+    @Test
     fun canMoveAGroupFromATableIntoAnotherGroupAndModifyThatGroup() {
         val slots = SlotTable()
         var insertAnchor = Anchor(-1)
@@ -4013,34 +4388,47 @@
 private fun SlotWriter.startNode(key: Any?, node: Any?) =
     startNode(NodeKey, key, node)
 
-@OptIn(InternalComposeApi::class)
 internal inline fun SlotWriter.group(block: () -> Unit) {
     startGroup()
     block()
     endGroup()
 }
 
-@OptIn(InternalComposeApi::class)
 internal inline fun SlotWriter.group(key: Int, block: () -> Unit) {
     startGroup(key)
     block()
     endGroup()
 }
 
-@OptIn(InternalComposeApi::class)
+internal inline fun SlotWriter.group(key: Int, sourceInformation: String, block: () -> Unit) {
+    group(key) {
+        recordGroupSourceInformation(sourceInformation)
+        block()
+    }
+}
+
+internal inline fun SlotWriter.grouplessCall(
+    key: Int,
+    sourceInformation: String,
+    block: () -> Unit
+) {
+    recordGrouplessCallSourceInformationStart(key, sourceInformation)
+    block()
+    recordGrouplessCallSourceInformationEnd()
+}
+
 internal inline fun SlotWriter.nodeGroup(key: Int, node: Any, block: () -> Unit = { }) {
     startNode(NodeKey, key, node)
     block()
     endGroup()
 }
-@OptIn(InternalComposeApi::class)
+
 internal inline fun SlotWriter.insert(block: () -> Unit) {
     beginInsert()
     block()
     endInsert()
 }
 
-@OptIn(InternalComposeApi::class)
 internal inline fun SlotReader.group(key: Int, block: () -> Unit) {
     assertEquals(key, groupKey)
     startGroup()
@@ -4048,14 +4436,12 @@
     endGroup()
 }
 
-@OptIn(InternalComposeApi::class)
 internal inline fun SlotReader.group(block: () -> Unit) {
     startGroup()
     block()
     endGroup()
 }
 
-@OptIn(InternalComposeApi::class)
 private inline fun SlotReader.expectNode(key: Int, node: Any, block: () -> Unit = { }) {
     assertEquals(key, groupObjectKey)
     assertEquals(node, groupNode)
@@ -4067,7 +4453,6 @@
 private const val treeRoot = -1
 private const val elementKey = 100
 
-@OptIn(InternalComposeApi::class)
 private fun testSlotsNumbered(): SlotTable {
     val slotTable = SlotTable()
     slotTable.write { writer ->
@@ -4084,7 +4469,6 @@
 }
 
 // Creates 0 until 10 items each with 10 elements numbered 0...n with 0..n slots
-@OptIn(InternalComposeApi::class)
 private fun testItems(): SlotTable {
     val slots = SlotTable()
     slots.write { writer ->
@@ -4121,7 +4505,6 @@
     return slots
 }
 
-@OptIn(InternalComposeApi::class)
 private fun validateItems(slots: SlotTable) {
     slots.read { reader ->
         check(reader.groupKey == treeRoot) { "Invalid root key" }
@@ -4172,7 +4555,6 @@
     }
 }
 
-@OptIn(InternalComposeApi::class)
 private fun narrowTrees(): Pair<SlotTable, List<Anchor>> {
     val slots = SlotTable()
     val anchors = mutableListOf<Anchor>()
@@ -4221,13 +4603,11 @@
     return slots to anchors
 }
 
-@OptIn(InternalComposeApi::class)
 private fun SlotReader.expectGroup(key: Int): Int {
     assertEquals(key, groupKey)
     return skipGroup()
 }
 
-@OptIn(InternalComposeApi::class)
 private fun SlotReader.expectGroup(
     key: Int,
     block: () -> Unit
@@ -4238,12 +4618,10 @@
     endGroup()
 }
 
-@OptIn(InternalComposeApi::class)
 private fun SlotReader.expectData(value: Any) {
     assertEquals(value, next())
 }
 
-@OptIn(InternalComposeApi::class)
 private fun SlotReader.expectGroup(
     key: Int,
     objectKey: Any?,
@@ -4280,3 +4658,43 @@
         "Expected test to throw an exception containing \"$message\""
     )
 }
+
+data class SourceGroup(val key: Any, val source: String?, val children: List<SourceGroup>) {
+
+    override fun toString(): String = buildString { toStringBuilder(this, 0) }
+
+    private fun toStringBuilder(builder: StringBuilder, indent: Int) {
+        repeat(indent) { builder.append(' ') }
+        builder.append("Group(")
+        builder.append(key)
+        builder.append(")")
+        if (source != null) {
+            builder.append(' ')
+            builder.append(source)
+        }
+        builder.appendLine()
+        children.fastForEach { it.toStringBuilder(builder, indent + 2) }
+    }
+
+    data class BuilderScope(private val children: ArrayList<SourceGroup> = ArrayList()) {
+        fun group(key: Int, source: String? = null, block: BuilderScope.() -> Unit) {
+            val scope = BuilderScope()
+            scope.block()
+            this.children.add(SourceGroup(key, source, scope.children))
+        }
+    }
+
+    companion object {
+        fun group(key: Int, block: BuilderScope.() -> Unit): SourceGroup {
+            val children = ArrayList<SourceGroup>()
+            val scope = BuilderScope(children)
+            scope.block()
+            return SourceGroup(key, null, children)
+        }
+
+        fun group(compositionData: CompositionData): SourceGroup =
+            groupOf(compositionData.compositionGroups.first())
+        private fun groupOf(group: CompositionGroup): SourceGroup =
+            SourceGroup(group.key, group.sourceInfo, group.compositionGroups.map(::groupOf))
+    }
+}
diff --git a/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/LiveEditTests.kt b/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/LiveEditTests.kt
index dcb954f..7fc505e 100644
--- a/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/LiveEditTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/LiveEditTests.kt
@@ -102,7 +102,9 @@
     }
 
     @Test
-    fun testNonRestartableFunctionPreservesParentAndSiblingState() = liveEditTest {
+    fun testNonRestartableFunctionPreservesParentAndSiblingState() = liveEditTest(
+        collectSourceInformation = SourceInfo.None
+    ) {
         EnsureStatePreservedButRecomposed("a")
         RestartGroup {
             Text("Hello World")
@@ -112,7 +114,9 @@
     }
 
     @Test
-    fun testMultipleNonRestartableFunctionPreservesParentAndSiblingState() = liveEditTest {
+    fun testMultipleNonRestartableFunctionPreservesParentAndSiblingState() = liveEditTest(
+        collectSourceInformation = SourceInfo.None
+    ) {
         RestartGroup {
             EnsureStatePreservedButRecomposed("a")
             Target("b", restartable = false)
@@ -136,7 +140,9 @@
     }
 
     @Test
-    fun testInlineComposableLambda() = liveEditTest {
+    fun testInlineComposableLambda() = liveEditTest(
+        collectSourceInformation = SourceInfo.None
+    ) {
         RestartGroup {
             InlineTarget("a")
             EnsureStatePreservedButRecomposed("b")
@@ -165,7 +171,10 @@
     @Test
     fun testThrowing_recomposition() {
         var recomposeCount = 0
-        liveEditTest(reloadCount = 2) {
+        liveEditTest(
+            reloadCount = 2,
+            collectSourceInformation = SourceInfo.None,
+        ) {
             RestartGroup {
                 MarkAsTarget()
 
@@ -216,7 +225,9 @@
     @Test
     fun testThrowing_recomposition_sideEffect() {
         var recomposeCount = 0
-        liveEditTest {
+        liveEditTest(
+            collectSourceInformation = SourceInfo.None
+        ) {
             RestartGroup {
                 MarkAsTarget()
 
@@ -286,7 +297,9 @@
     @Test
     fun testThrowing_recomposition_remembered() {
         var recomposeCount = 0
-        liveEditTest {
+        liveEditTest(
+            collectSourceInformation = SourceInfo.None,
+        ) {
             RestartGroup {
                 MarkAsTarget()
 
@@ -333,7 +346,10 @@
     fun testThrowing_invalidationsCarriedAfterCrash() {
         var recomposeCount = 0
         val state = mutableStateOf(0)
-        liveEditTest(reloadCount = 2) {
+        liveEditTest(
+            reloadCount = 2,
+            collectSourceInformation = SourceInfo.None,
+        ) {
             RestartGroup {
                 RestartGroup {
                     MarkAsTarget()
@@ -391,7 +407,10 @@
     @Test
     fun testThrowing_movableContent_recomposition() {
         var recomposeCount = 0
-        liveEditTest(reloadCount = 2) {
+        liveEditTest(
+            reloadCount = 2,
+            collectSourceInformation = SourceInfo.None,
+        ) {
             RestartGroup {
                 MarkAsTarget()
 
@@ -423,7 +442,10 @@
     @Test
     fun testThrowing_movableContent_throwAfterMove() {
         var recomposeCount = 0
-        liveEditTest(reloadCount = 2) {
+        liveEditTest(
+            reloadCount = 2,
+            collectSourceInformation = SourceInfo.None,
+        ) {
             expectError("throwInMovableContent", 1)
 
             val content = remember {
@@ -567,28 +589,71 @@
     addTargetKey((currentComposer as ComposerImpl).parentKey())
 }
 
+enum class SourceInfo {
+    None,
+    Collect,
+    Both,
+}
+
 @OptIn(InternalComposeApi::class)
 fun liveEditTest(
     reloadCount: Int = 1,
+    collectSourceInformation: SourceInfo = SourceInfo.Both,
     fn: @Composable LiveEditTestScope.() -> Unit,
-) = compositionTest {
-    with(LiveEditTestScope()) {
-        addCheck {
-            (composition as? ControlledComposition)?.verifyConsistent()
-        }
+) {
+    if (
+        collectSourceInformation == SourceInfo.Both ||
+        collectSourceInformation == SourceInfo.Collect
+    ) {
+        compositionTest {
+            with(LiveEditTestScope()) {
+                addCheck {
+                    (composition as? ControlledComposition)?.verifyConsistent()
+                }
 
-        recordErrors {
-            compose { fn(this) }
-        }
+                recordErrors {
+                    compose {
+                        currentComposer.collectParameterInformation()
+                        fn(this)
+                    }
+                }
 
-        repeat(reloadCount) {
-            invalidateTargets()
-            recordErrors {
-                advance()
+                repeat(reloadCount) {
+                    invalidateTargets()
+                    recordErrors {
+                        advance()
+                    }
+                }
+
+                runChecks()
             }
         }
+    }
 
-        runChecks()
+    if (
+        collectSourceInformation == SourceInfo.Both ||
+        collectSourceInformation == SourceInfo.None
+    ) {
+        compositionTest {
+            with(LiveEditTestScope()) {
+                addCheck {
+                    (composition as? ControlledComposition)?.verifyConsistent()
+                }
+
+                recordErrors {
+                    compose { fn(this) }
+                }
+
+                repeat(reloadCount) {
+                    invalidateTargets()
+                    recordErrors {
+                        advance()
+                    }
+                }
+
+                runChecks()
+            }
+        }
     }
 }
 
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 4331b6f..b33c4c9 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -85,7 +85,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.truth)
@@ -97,7 +97,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.truth)
diff --git a/compose/test-utils/src/androidAndroidTest/AndroidManifest.xml b/compose/test-utils/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/test-utils/src/androidAndroidTest/AndroidManifest.xml
rename to compose/test-utils/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/test-utils/src/androidAndroidTest/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunnerTest.kt b/compose/test-utils/src/androidInstrumentedTest/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunnerTest.kt
similarity index 100%
rename from compose/test-utils/src/androidAndroidTest/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunnerTest.kt
rename to compose/test-utils/src/androidInstrumentedTest/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunnerTest.kt
diff --git a/compose/test-utils/src/androidAndroidTest/kotlin/androidx/compose/testutils/ImageAssertionsTest.kt b/compose/test-utils/src/androidInstrumentedTest/kotlin/androidx/compose/testutils/ImageAssertionsTest.kt
similarity index 100%
rename from compose/test-utils/src/androidAndroidTest/kotlin/androidx/compose/testutils/ImageAssertionsTest.kt
rename to compose/test-utils/src/androidInstrumentedTest/kotlin/androidx/compose/testutils/ImageAssertionsTest.kt
diff --git a/compose/test-utils/src/test/kotlin/androidx/compose/testutils/DpAssertionsTest.kt b/compose/test-utils/src/androidUnitTest/kotlin/androidx/compose/testutils/DpAssertionsTest.kt
similarity index 100%
rename from compose/test-utils/src/test/kotlin/androidx/compose/testutils/DpAssertionsTest.kt
rename to compose/test-utils/src/androidUnitTest/kotlin/androidx/compose/testutils/DpAssertionsTest.kt
diff --git a/compose/test-utils/src/test/kotlin/androidx/compose/testutils/ExpectTest.kt b/compose/test-utils/src/androidUnitTest/kotlin/androidx/compose/testutils/ExpectTest.kt
similarity index 100%
rename from compose/test-utils/src/test/kotlin/androidx/compose/testutils/ExpectTest.kt
rename to compose/test-utils/src/androidUnitTest/kotlin/androidx/compose/testutils/ExpectTest.kt
diff --git a/compose/ui/ui-geometry/build.gradle b/compose/ui/ui-geometry/build.gradle
index 951f1d1..13aaf88 100644
--- a/compose/ui/ui-geometry/build.gradle
+++ b/compose/ui/ui-geometry/build.gradle
@@ -76,13 +76,13 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
             }
         }
 
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
         }
 
diff --git a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/CornerRadiusTest.kt b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/CornerRadiusTest.kt
similarity index 100%
rename from compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/CornerRadiusTest.kt
rename to compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/CornerRadiusTest.kt
diff --git a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/GeometryUtilsTest.kt b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/GeometryUtilsTest.kt
similarity index 100%
rename from compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/GeometryUtilsTest.kt
rename to compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/GeometryUtilsTest.kt
diff --git a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/MutableRectTest.kt b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/MutableRectTest.kt
similarity index 100%
rename from compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/MutableRectTest.kt
rename to compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/MutableRectTest.kt
diff --git a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/OffsetTest.kt b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/OffsetTest.kt
similarity index 100%
rename from compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/OffsetTest.kt
rename to compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/OffsetTest.kt
diff --git a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/RectTest.kt b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/RectTest.kt
similarity index 100%
rename from compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/RectTest.kt
rename to compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/RectTest.kt
diff --git a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/RoundRectTest.kt b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/RoundRectTest.kt
similarity index 100%
rename from compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/RoundRectTest.kt
rename to compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/RoundRectTest.kt
diff --git a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/SizeTest.kt b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/SizeTest.kt
similarity index 100%
rename from compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/SizeTest.kt
rename to compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/SizeTest.kt
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 66bd67f..b4e7d7e 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -1367,6 +1367,7 @@
     method public androidx.compose.ui.graphics.vector.PathBuilder reflectiveQuadToRelative(float dx1, float dy1);
     method public androidx.compose.ui.graphics.vector.PathBuilder verticalLineTo(float y);
     method public androidx.compose.ui.graphics.vector.PathBuilder verticalLineToRelative(float dy);
+    property public final java.util.List<androidx.compose.ui.graphics.vector.PathNode> nodes;
   }
 
   @androidx.compose.runtime.Immutable public abstract sealed class PathNode {
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index ecfd67e..c8d7b1d 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -1426,6 +1426,7 @@
     method public androidx.compose.ui.graphics.vector.PathBuilder reflectiveQuadToRelative(float dx1, float dy1);
     method public androidx.compose.ui.graphics.vector.PathBuilder verticalLineTo(float y);
     method public androidx.compose.ui.graphics.vector.PathBuilder verticalLineToRelative(float dy);
+    property public final java.util.List<androidx.compose.ui.graphics.vector.PathNode> nodes;
   }
 
   @androidx.compose.runtime.Immutable public abstract sealed class PathNode {
diff --git a/compose/ui/ui-graphics/build.gradle b/compose/ui/ui-graphics/build.gradle
index 760efaf..897e9f3 100644
--- a/compose/ui/ui-graphics/build.gradle
+++ b/compose/ui/ui-graphics/build.gradle
@@ -36,7 +36,7 @@
         commonMain {
             dependencies {
                 implementation(libs.kotlinStdlibCommon)
-                implementation(project(":annotation:annotation"))
+                api(libs.androidx.annotation)
 
                 api(project(":compose:ui:ui-unit"))
                 implementation(project(":compose:runtime:runtime"))
@@ -70,7 +70,7 @@
         androidMain {
             dependsOn(jvmMain)
             dependencies {
-                api("androidx.annotation:annotation:1.2.0")
+                api(libs.androidx.annotation)
             }
         }
 
@@ -90,7 +90,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:ui:ui-graphics:ui-graphics-samples"))
@@ -100,6 +100,7 @@
                 implementation(libs.testRunner)
                 implementation(libs.espressoCore)
                 implementation(libs.junit)
+                implementation(libs.truth)
             }
         }
 
@@ -107,7 +108,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/AndroidManifest.xml b/compose/ui/ui-graphics/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/AndroidManifest.xml
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidBlendModeTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidBlendModeTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidBlendModeTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidBlendModeTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidCanvasTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidCanvasTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidCanvasTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidCanvasTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidColorFilterTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidColorFilterTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidColorFilterTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidColorFilterTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidColorSpaceTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidColorSpaceTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidColorSpaceTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidColorSpaceTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidMatrixTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidMatrixTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidMatrixTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidMatrixTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidRenderEffectTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidRenderEffectTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidRenderEffectTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidRenderEffectTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidTileModeTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidTileModeTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidTileModeTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/AndroidTileModeTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/GradientTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/GradientTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/GradientTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/GradientTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ImageBitmapTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/ImageBitmapTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ImageBitmapTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/ImageBitmapTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/OutlineTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/OutlineTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/OutlineTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/OutlineTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PaintTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/PaintTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PaintTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/PaintTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/PathTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/PathTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PixelMapTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/PixelMapTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PixelMapTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/PixelMapTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/RectHelperTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/RectHelperTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/RectHelperTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/RectHelperTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/RectangleShapeTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/RectangleShapeTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/RectangleShapeTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/RectangleShapeTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/TestActivity.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/TestActivity.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/TestActivity.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/TestActivity.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/painter/BitmapPainterTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/painter/BitmapPainterTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/painter/BitmapPainterTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/painter/BitmapPainterTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/painter/BrushPainterTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/painter/BrushPainterTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/painter/BrushPainterTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/painter/BrushPainterTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/painter/PainterTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/painter/PainterTest.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/painter/PainterTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/painter/PainterTest.kt
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/painter/PainterTestHelper.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/painter/PainterTestHelper.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/painter/PainterTestHelper.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/painter/PainterTestHelper.kt
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathBuilder.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathBuilder.kt
index 8754a23..49d5949 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathBuilder.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathBuilder.kt
@@ -17,28 +17,56 @@
 package androidx.compose.ui.graphics.vector
 
 class PathBuilder {
+    // 88% of Material icons use 32 or fewer path nodes
+    private val _nodes = ArrayList<PathNode>(32)
 
-    private val nodes = mutableListOf<PathNode>()
+    val nodes: List<PathNode>
+        get() = _nodes
 
-    fun getNodes(): List<PathNode> = nodes
+    fun close(): PathBuilder {
+        _nodes.add(PathNode.Close)
+        return this
+    }
 
-    fun close(): PathBuilder = addNode(PathNode.Close)
+    fun moveTo(x: Float, y: Float): PathBuilder {
+        _nodes.add(PathNode.MoveTo(x, y))
+        return this
+    }
 
-    fun moveTo(x: Float, y: Float) = addNode(PathNode.MoveTo(x, y))
+    fun moveToRelative(dx: Float, dy: Float): PathBuilder {
+        _nodes.add(PathNode.RelativeMoveTo(dx, dy))
+        return this
+    }
 
-    fun moveToRelative(dx: Float, dy: Float) = addNode(PathNode.RelativeMoveTo(dx, dy))
+    fun lineTo(x: Float, y: Float): PathBuilder {
+        _nodes.add(PathNode.LineTo(x, y))
+        return this
+    }
 
-    fun lineTo(x: Float, y: Float) = addNode(PathNode.LineTo(x, y))
+    fun lineToRelative(dx: Float, dy: Float): PathBuilder {
+        _nodes.add(PathNode.RelativeLineTo(dx, dy))
+        return this
+    }
 
-    fun lineToRelative(dx: Float, dy: Float) = addNode(PathNode.RelativeLineTo(dx, dy))
+    fun horizontalLineTo(x: Float): PathBuilder {
+        _nodes.add(PathNode.HorizontalTo(x))
+        return this
+    }
 
-    fun horizontalLineTo(x: Float) = addNode(PathNode.HorizontalTo(x))
+    fun horizontalLineToRelative(dx: Float): PathBuilder {
+        _nodes.add(PathNode.RelativeHorizontalTo(dx))
+        return this
+    }
 
-    fun horizontalLineToRelative(dx: Float) = addNode(PathNode.RelativeHorizontalTo(dx))
+    fun verticalLineTo(y: Float): PathBuilder {
+        _nodes.add(PathNode.VerticalTo(y))
+        return this
+    }
 
-    fun verticalLineTo(y: Float) = addNode(PathNode.VerticalTo(y))
-
-    fun verticalLineToRelative(dy: Float) = addNode(PathNode.RelativeVerticalTo(dy))
+    fun verticalLineToRelative(dy: Float): PathBuilder {
+        _nodes.add(PathNode.RelativeVerticalTo(dy))
+        return this
+    }
 
     fun curveTo(
         x1: Float,
@@ -47,7 +75,10 @@
         y2: Float,
         x3: Float,
         y3: Float
-    ) = addNode(PathNode.CurveTo(x1, y1, x2, y2, x3, y3))
+    ): PathBuilder {
+        _nodes.add(PathNode.CurveTo(x1, y1, x2, y2, x3, y3))
+        return this
+    }
 
     fun curveToRelative(
         dx1: Float,
@@ -56,25 +87,40 @@
         dy2: Float,
         dx3: Float,
         dy3: Float
-    ) = addNode(PathNode.RelativeCurveTo(dx1, dy1, dx2, dy2, dx3, dy3))
+    ): PathBuilder {
+        _nodes.add(PathNode.RelativeCurveTo(dx1, dy1, dx2, dy2, dx3, dy3))
+        return this
+    }
 
-    fun reflectiveCurveTo(x1: Float, y1: Float, x2: Float, y2: Float) =
-        addNode(PathNode.ReflectiveCurveTo(x1, y1, x2, y2))
+    fun reflectiveCurveTo(x1: Float, y1: Float, x2: Float, y2: Float): PathBuilder {
+        _nodes.add(PathNode.ReflectiveCurveTo(x1, y1, x2, y2))
+        return this
+    }
 
-    fun reflectiveCurveToRelative(dx1: Float, dy1: Float, dx2: Float, dy2: Float) =
-        addNode(PathNode.RelativeReflectiveCurveTo(dx1, dy1, dx2, dy2))
+    fun reflectiveCurveToRelative(dx1: Float, dy1: Float, dx2: Float, dy2: Float): PathBuilder {
+        _nodes.add(PathNode.RelativeReflectiveCurveTo(dx1, dy1, dx2, dy2))
+        return this
+    }
 
-    fun quadTo(x1: Float, y1: Float, x2: Float, y2: Float) =
-        addNode(PathNode.QuadTo(x1, y1, x2, y2))
+    fun quadTo(x1: Float, y1: Float, x2: Float, y2: Float): PathBuilder {
+        _nodes.add(PathNode.QuadTo(x1, y1, x2, y2))
+        return this
+    }
 
-    fun quadToRelative(dx1: Float, dy1: Float, dx2: Float, dy2: Float) =
-        addNode(PathNode.RelativeQuadTo(dx1, dy1, dx2, dy2))
+    fun quadToRelative(dx1: Float, dy1: Float, dx2: Float, dy2: Float): PathBuilder {
+        _nodes.add(PathNode.RelativeQuadTo(dx1, dy1, dx2, dy2))
+        return this
+    }
 
-    fun reflectiveQuadTo(x1: Float, y1: Float) =
-        addNode(PathNode.ReflectiveQuadTo(x1, y1))
+    fun reflectiveQuadTo(x1: Float, y1: Float): PathBuilder {
+        _nodes.add(PathNode.ReflectiveQuadTo(x1, y1))
+        return this
+    }
 
-    fun reflectiveQuadToRelative(dx1: Float, dy1: Float) =
-        addNode(PathNode.RelativeReflectiveQuadTo(dx1, dy1))
+    fun reflectiveQuadToRelative(dx1: Float, dy1: Float): PathBuilder {
+        _nodes.add(PathNode.RelativeReflectiveQuadTo(dx1, dy1))
+        return this
+    }
 
     fun arcTo(
         horizontalEllipseRadius: Float,
@@ -84,17 +130,20 @@
         isPositiveArc: Boolean,
         x1: Float,
         y1: Float
-    ) = addNode(
-        PathNode.ArcTo(
-            horizontalEllipseRadius,
-            verticalEllipseRadius,
-            theta,
-            isMoreThanHalf,
-            isPositiveArc,
-            x1,
-            y1
+    ): PathBuilder {
+        _nodes.add(
+            PathNode.ArcTo(
+                horizontalEllipseRadius,
+                verticalEllipseRadius,
+                theta,
+                isMoreThanHalf,
+                isPositiveArc,
+                x1,
+                y1
+            )
         )
-    )
+        return this
+    }
 
     fun arcToRelative(
         a: Float,
@@ -104,10 +153,8 @@
         isPositiveArc: Boolean,
         dx1: Float,
         dy1: Float
-    ) = addNode(PathNode.RelativeArcTo(a, b, theta, isMoreThanHalf, isPositiveArc, dx1, dy1))
-
-    private fun addNode(node: PathNode): PathBuilder {
-        nodes.add(node)
+    ): PathBuilder {
+        _nodes.add(PathNode.RelativeArcTo(a, b, theta, isMoreThanHalf, isPositiveArc, dx1, dy1))
         return this
     }
 }
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt
index 536eb5b..41f582a 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt
@@ -147,7 +147,7 @@
  * If the key is unknown then [IllegalArgumentException] is thrown
  * @throws IllegalArgumentException
  */
-internal fun Char.addPathNodes(nodes: MutableList<PathNode>, args: FloatArray, count: Int) {
+internal fun Char.addPathNodes(nodes: ArrayList<PathNode>, args: FloatArray, count: Int) {
     when (this) {
         RelativeCloseKey, CloseKey -> nodes.add(PathNode.Close)
 
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
index b4e0ed9..405910e 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
@@ -49,7 +49,7 @@
 internal val EmptyArray = FloatArray(0)
 
 class PathParser {
-    private val nodes = mutableListOf<PathNode>()
+    private val nodes = ArrayList<PathNode>()
 
     private val floatResult = FloatResult()
     private var nodeData = FloatArray(64)
@@ -139,7 +139,7 @@
         return this
     }
 
-    fun toNodes() = nodes
+    fun toNodes(): List<PathNode> = nodes
 
     fun toPath(target: Path = Path()) = nodes.toPath(target)
 
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 8ba40fa..a36e3d6 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -502,23 +502,26 @@
         val box = context.bounds
         val size = box.size.toSize()
         val coordinates = layoutInfo.coordinates
-        val topLeft = toIntOffset(coordinates.localToWindow(Offset.Zero))
-        val topRight = toIntOffset(coordinates.localToWindow(Offset(size.width, 0f)))
-        val bottomRight = toIntOffset(coordinates.localToWindow(Offset(size.width, size.height)))
-        val bottomLeft = toIntOffset(coordinates.localToWindow(Offset(0f, size.height)))
         var bounds: QuadBounds? = null
+        if (layoutInfo.isAttached) {
+            val topLeft = toIntOffset(coordinates.localToWindow(Offset.Zero))
+            val topRight = toIntOffset(coordinates.localToWindow(Offset(size.width, 0f)))
+            val bottomRight =
+                toIntOffset(coordinates.localToWindow(Offset(size.width, size.height)))
+            val bottomLeft = toIntOffset(coordinates.localToWindow(Offset(0f, size.height)))
 
-        if (topLeft.x != box.left || topLeft.y != box.top ||
-            topRight.x != box.right || topRight.y != box.top ||
-            bottomRight.x != box.right || bottomRight.y != box.bottom ||
-            bottomLeft.x != box.left || bottomLeft.y != box.bottom
-        ) {
-            bounds = QuadBounds(
-                topLeft.x, topLeft.y,
-                topRight.x, topRight.y,
-                bottomRight.x, bottomRight.y,
-                bottomLeft.x, bottomLeft.y,
-            )
+            if (topLeft.x != box.left || topLeft.y != box.top ||
+                topRight.x != box.right || topRight.y != box.top ||
+                bottomRight.x != box.right || bottomRight.y != box.bottom ||
+                bottomLeft.x != box.left || bottomLeft.y != box.bottom
+            ) {
+                bounds = QuadBounds(
+                    topLeft.x, topLeft.y,
+                    topRight.x, topRight.y,
+                    bottomRight.x, bottomRight.y,
+                    bottomLeft.x, bottomLeft.y,
+                )
+            }
         }
         if (!includeNodesOutsizeOfWindow) {
             // Ignore this node if the bounds are completely outside the window
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index 8bb1fb3..a65f822 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -92,7 +92,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:animation:animation"))
@@ -112,7 +112,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:animation:animation-core"))
@@ -147,7 +147,7 @@
 }
 
 dependencies {
-    // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
+    // Can't declare this in kotlin { sourceSets { androidUnitTest.dependencies { .. } } } as that
     // leaks into instrumented tests (b/214407011)
     testImplementation(libs.robolectric)
 }
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/AndroidManifest.xml b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/AndroidManifest.xml
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ApplySnapshotImmediatelyTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ApplySnapshotImmediatelyTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ApplySnapshotImmediatelyTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ApplySnapshotImmediatelyTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeInFragmentTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ComposeInFragmentTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeInFragmentTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ComposeInFragmentTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeRootRegistryTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ComposeRootRegistryTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeRootRegistryTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ComposeRootRegistryTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeTestRuleWaitUntilTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ComposeTestRuleWaitUntilTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeTestRuleWaitUntilTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ComposeTestRuleWaitUntilTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/CustomActivityTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/CustomActivityTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/CustomActivityTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/CustomActivityTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/EspressoLinkTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/EspressoLinkTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/EspressoLinkTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/EspressoLinkTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/FirstDrawTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/FirstDrawTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/FirstDrawTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/FirstDrawTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/LateActivityLaunchTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/LateActivityLaunchTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/LateActivityLaunchTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/LateActivityLaunchTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/LateSetContentTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/LateSetContentTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/LateSetContentTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/LateSetContentTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/LaunchActivityTooEarlyTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/LaunchActivityTooEarlyTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/LaunchActivityTooEarlyTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/LaunchActivityTooEarlyTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesClickTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesClickTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesClickTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesClickTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesFindTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesFindTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesFindTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesFindTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesFirstDrawTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesFirstDrawTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesFirstDrawTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesFirstDrawTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesWithoutComposeTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesWithoutComposeTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesWithoutComposeTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesWithoutComposeTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleComposeRootsTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/MultipleComposeRootsTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleComposeRootsTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/MultipleComposeRootsTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TimeOutTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/TimeOutTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TimeOutTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/TimeOutTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/UncaughtExceptionsInCoroutinesTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/UncaughtExceptionsInCoroutinesTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/UncaughtExceptionsInCoroutinesTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/UncaughtExceptionsInCoroutinesTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ViewVisibilityTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ViewVisibilityTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ViewVisibilityTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ViewVisibilityTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitingForOnCommitCallbackTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitingForOnCommitCallbackTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitingForOnCommitCallbackTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitingForOnCommitCallbackTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitingForPopupTest.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitingForPopupTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitingForPopupTest.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitingForPopupTest.kt
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/util/BoundaryNodes.kt b/compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/util/BoundaryNodes.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/util/BoundaryNodes.kt
rename to compose/ui/ui-test-junit4/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/util/BoundaryNodes.kt
diff --git a/compose/ui/ui-test-junit4/src/test/kotlin/androidx/compose/ui/test/junit4/InfiniteAnimationPolicyTest.kt b/compose/ui/ui-test-junit4/src/androidUnitTest/kotlin/androidx/compose/ui/test/junit4/InfiniteAnimationPolicyTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/test/kotlin/androidx/compose/ui/test/junit4/InfiniteAnimationPolicyTest.kt
rename to compose/ui/ui-test-junit4/src/androidUnitTest/kotlin/androidx/compose/ui/test/junit4/InfiniteAnimationPolicyTest.kt
diff --git a/compose/ui/ui-test-junit4/src/test/kotlin/androidx/compose/ui/test/junit4/RobolectricComposeTest.kt b/compose/ui/ui-test-junit4/src/androidUnitTest/kotlin/androidx/compose/ui/test/junit4/RobolectricComposeTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/test/kotlin/androidx/compose/ui/test/junit4/RobolectricComposeTest.kt
rename to compose/ui/ui-test-junit4/src/androidUnitTest/kotlin/androidx/compose/ui/test/junit4/RobolectricComposeTest.kt
diff --git a/compose/ui/ui-test-junit4/src/test/kotlin/androidx/compose/ui/test/junit4/ViewVisibilityRobolectricTest.kt b/compose/ui/ui-test-junit4/src/androidUnitTest/kotlin/androidx/compose/ui/test/junit4/ViewVisibilityRobolectricTest.kt
similarity index 100%
rename from compose/ui/ui-test-junit4/src/test/kotlin/androidx/compose/ui/test/junit4/ViewVisibilityRobolectricTest.kt
rename to compose/ui/ui-test-junit4/src/androidUnitTest/kotlin/androidx/compose/ui/test/junit4/ViewVisibilityRobolectricTest.kt
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index fa5ac5f..56b3e50 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -96,7 +96,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependsOn(androidCommonTest)
             dependencies {
@@ -114,7 +114,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependsOn(androidCommonTest)
             dependencies {
@@ -144,7 +144,7 @@
 }
 
 dependencies {
-    // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
+    // Can't declare this in kotlin { sourceSets { androidUnitTest.dependencies { .. } } } as that
     // leaks into instrumented tests (b/214407011)
     testImplementation(libs.robolectric)
 }
diff --git a/compose/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml b/compose/ui/ui-test/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml
rename to compose/ui/ui-test/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ActivityWithActionBar.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/ActivityWithActionBar.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ActivityWithActionBar.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/ActivityWithActionBar.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/AssertExistsTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AssertExistsTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/AssertExistsTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AssertExistsTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/AssertsTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AssertsTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/AssertsTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AssertsTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/BitmapCapturingTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/BitmapCapturingTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/BitmapCapturingTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/BitmapCapturingTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/CallSemanticsActionTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/CallSemanticsActionTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/CallSemanticsActionTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/CallSemanticsActionTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ClickTestRuleTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/ClickTestRuleTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ClickTestRuleTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/ClickTestRuleTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ErrorMessagesTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/ErrorMessagesTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ErrorMessagesTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/ErrorMessagesTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/FindAllTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/FindAllTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/FindAllTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/FindAllTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/FindInPopupTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/FindInPopupTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/FindInPopupTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/FindInPopupTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/FindersTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/FindersTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/FindersTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/FindersTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/FocusActionsTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/FocusActionsTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/FocusActionsTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/FocusActionsTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/GlobalAssertionsTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/GlobalAssertionsTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/GlobalAssertionsTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/GlobalAssertionsTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/InfiniteAnimationTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/InfiniteAnimationTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/InfiniteAnimationTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/InfiniteAnimationTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/IsDisplayedTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/IsDisplayedTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/IsDisplayedTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/IsDisplayedTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PhaseOrderingTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/PhaseOrderingTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PhaseOrderingTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/PhaseOrderingTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/RootExistenceAssertTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/RootExistenceAssertTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/RootExistenceAssertTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/RootExistenceAssertTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestMonotonicFrameClockTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/TestMonotonicFrameClockTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestMonotonicFrameClockTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/TestMonotonicFrameClockTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TextActionsTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/TextActionsTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TextActionsTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/TextActionsTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/actions/ScrollToIndexTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/ScrollToIndexTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/actions/ScrollToIndexTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/ScrollToIndexTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/actions/ScrollToKeyTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/ScrollToKeyTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/actions/ScrollToKeyTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/ScrollToKeyTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/actions/ScrollToNodeTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/ScrollToNodeTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/actions/ScrollToNodeTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/ScrollToNodeTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/actions/ScrollToTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/ScrollToTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/actions/ScrollToTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/ScrollToTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/assertions/AssertAllTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/assertions/AssertAllTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/assertions/AssertAllTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/assertions/AssertAllTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/assertions/AssertAnyTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/assertions/AssertAnyTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/assertions/AssertAnyTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/assertions/AssertAnyTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/assertions/AssertContentDescription.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/assertions/AssertContentDescription.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/assertions/AssertContentDescription.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/assertions/AssertContentDescription.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/assertions/AssertText.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/assertions/AssertText.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/assertions/AssertText.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/assertions/AssertText.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/assertions/BoundsAssertionsTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/assertions/BoundsAssertionsTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/assertions/BoundsAssertionsTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/assertions/BoundsAssertionsTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/LocalToRootTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/LocalToRootTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/LocalToRootTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/LocalToRootTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/PositionsTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/PositionsTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/PositionsTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/PositionsTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendClickTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/SendClickTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendClickTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/SendClickTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendDoubleClickTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/SendDoubleClickTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendDoubleClickTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/SendDoubleClickTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendLongClickTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/SendLongClickTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendLongClickTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/SendLongClickTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendPinchTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/SendPinchTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendPinchTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/SendPinchTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendSwipeTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/SendSwipeTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendSwipeTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/SendSwipeTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendSwipeVelocityTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/SendSwipeVelocityTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendSwipeVelocityTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/gesturescope/SendSwipeVelocityTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/PositionsTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/PositionsTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/PositionsTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/PositionsTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/key/Common.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/Common.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/key/Common.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/Common.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyDownTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyDownTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyDownTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyDownTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyPressTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyPressTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyPressTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyPressTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyUpTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyUpTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyUpTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyUpTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/key/LockKeysTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/LockKeysTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/key/LockKeysTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/LockKeysTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/key/MetaKeysTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/MetaKeysTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/key/MetaKeysTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/MetaKeysTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/CancelTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/CancelTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/CancelTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/CancelTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/ClickTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/ClickTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/ClickTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/ClickTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/Common.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/Common.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/Common.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/Common.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/MoveTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/MoveTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/MoveTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/MoveTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/ScrollTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/ScrollTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/ScrollTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/ScrollTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/CancelTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/CancelTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/CancelTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/CancelTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/ClickTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/ClickTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/ClickTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/ClickTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/Common.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/Common.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/Common.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/Common.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/CurrentPositionTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/CurrentPositionTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/CurrentPositionTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/CurrentPositionTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/DoubleClickTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/DoubleClickTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/DoubleClickTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/DoubleClickTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/DownTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/DownTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/DownTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/DownTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/LongClickTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/LongClickTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/LongClickTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/LongClickTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveByTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveByTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveByTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveByTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveToTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveToTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveToTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveToTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveWithHistoryTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveWithHistoryTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveWithHistoryTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/MoveWithHistoryTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/PinchTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/PinchTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/PinchTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/PinchTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeCurveTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeCurveTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeCurveTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeCurveTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeCurveWithKeyTimesTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeCurveWithKeyTimesTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeCurveWithKeyTimesTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeCurveWithKeyTimesTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeDirectionTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeDirectionTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeDirectionTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeDirectionTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeStartEndTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeStartEndTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeStartEndTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeStartEndTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeWithTouchSlopTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeWithTouchSlopTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeWithTouchSlopTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeWithTouchSlopTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeWithVelocityTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeWithVelocityTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeWithVelocityTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeWithVelocityTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SynchronizedWithMainClockTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SynchronizedWithMainClockTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SynchronizedWithMainClockTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SynchronizedWithMainClockTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/UpTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/UpTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/UpTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/UpTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/VelocityPathFinderCalculateDurationTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/VelocityPathFinderCalculateDurationTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/VelocityPathFinderCalculateDurationTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/VelocityPathFinderCalculateDurationTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/VelocityPathFinderTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/VelocityPathFinderTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/injectionscope/touch/VelocityPathFinderTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/VelocityPathFinderTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/Common.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/Common.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/Common.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/Common.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendCancelTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendCancelTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendCancelTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendCancelTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendDownTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendDownTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendDownTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendDownTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMoveByTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMoveByTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMoveByTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMoveByTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMoveTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMoveTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMoveTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMoveTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMoveToTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMoveToTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMoveToTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMoveToTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMultipleGesturesTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMultipleGesturesTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMultipleGesturesTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendMultipleGesturesTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendUpTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendUpTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendUpTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/partialgesturescope/SendUpTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/predicates/HasAnyAncestorTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/predicates/HasAnyAncestorTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/predicates/HasAnyAncestorTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/predicates/HasAnyAncestorTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/predicates/HasAnyChildTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/predicates/HasAnyChildTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/predicates/HasAnyChildTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/predicates/HasAnyChildTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/predicates/HasAnyDescendantTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/predicates/HasAnyDescendantTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/predicates/HasAnyDescendantTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/predicates/HasAnyDescendantTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/predicates/HasAnySiblingTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/predicates/HasAnySiblingTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/predicates/HasAnySiblingTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/predicates/HasAnySiblingTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/predicates/HasParentTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/predicates/HasParentTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/predicates/HasParentTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/predicates/HasParentTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/AddIndexSelectorTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/AddIndexSelectorTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/AddIndexSelectorTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/AddIndexSelectorTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/AncestorsSelectorTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/AncestorsSelectorTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/AncestorsSelectorTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/AncestorsSelectorTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/ChildSelectorTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/ChildSelectorTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/ChildSelectorTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/ChildSelectorTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/ChildrenSelectorTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/ChildrenSelectorTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/ChildrenSelectorTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/ChildrenSelectorTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/FilterSelectorTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/FilterSelectorTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/FilterSelectorTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/FilterSelectorTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/FilterToOneSelectorTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/FilterToOneSelectorTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/FilterToOneSelectorTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/FilterToOneSelectorTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/LastNodeSelectorTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/LastNodeSelectorTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/LastNodeSelectorTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/LastNodeSelectorTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/ParentSelectorTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/ParentSelectorTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/ParentSelectorTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/ParentSelectorTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/SiblingSelectorTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/SiblingSelectorTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/SiblingSelectorTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/SiblingSelectorTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/SiblingsSelectorTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/SiblingsSelectorTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/selectors/SiblingsSelectorTest.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/selectors/SiblingsSelectorTest.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/BoundaryNodes.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/util/BoundaryNodes.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/BoundaryNodes.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/util/BoundaryNodes.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/ClickableTestBox.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/util/ClickableTestBox.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/ClickableTestBox.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/util/ClickableTestBox.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/Output.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/util/Output.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/Output.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/util/Output.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/PointerInputs.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/util/PointerInputs.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/PointerInputs.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/util/PointerInputs.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/TestCounter.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/util/TestCounter.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/TestCounter.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/util/TestCounter.kt
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/TestTextField.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/util/TestTextField.kt
similarity index 100%
rename from compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/TestTextField.kt
rename to compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/util/TestTextField.kt
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/Constants.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/Constants.kt
similarity index 100%
rename from compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/Constants.kt
rename to compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/Constants.kt
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/AdvanceEventTimeTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/AdvanceEventTimeTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/AdvanceEventTimeTest.kt
rename to compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/AdvanceEventTimeTest.kt
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/BatchingTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/BatchingTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/BatchingTest.kt
rename to compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/BatchingTest.kt
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/InputDispatcherTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/InputDispatcherTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/InputDispatcherTest.kt
rename to compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/InputDispatcherTest.kt
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/IsGestureInProgressTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/IsGestureInProgressTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/IsGestureInProgressTest.kt
rename to compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/IsGestureInProgressTest.kt
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/KeyEventsTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/KeyEventsTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/KeyEventsTest.kt
rename to compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/KeyEventsTest.kt
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/MouseEventsTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/MouseEventsTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/MouseEventsTest.kt
rename to compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/MouseEventsTest.kt
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/TouchEventsTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/TouchEventsTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/TouchEventsTest.kt
rename to compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/TouchEventsTest.kt
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/multimodal/KeyAndMouseEventsTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/multimodal/KeyAndMouseEventsTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/multimodal/KeyAndMouseEventsTest.kt
rename to compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/multimodal/KeyAndMouseEventsTest.kt
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/util/IsMonotonicBetweenTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/util/IsMonotonicBetweenTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/util/IsMonotonicBetweenTest.kt
rename to compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/util/IsMonotonicBetweenTest.kt
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/util/SplitsDurationEquallyIntoTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/util/SplitsDurationEquallyIntoTest.kt
similarity index 100%
rename from compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/util/SplitsDurationEquallyIntoTest.kt
rename to compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/util/SplitsDurationEquallyIntoTest.kt
diff --git a/compose/ui/ui-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/compose/ui/ui-test/src/androidUnitTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
similarity index 100%
rename from compose/ui/ui-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
rename to compose/ui/ui-test/src/androidUnitTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt
deleted file mode 100644
index 72f0dbc..0000000
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.text.benchmark
-
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.text.LineBreakConfig
-import android.graphics.text.LineBreaker
-import android.os.Build
-import android.text.Layout
-import android.text.TextPaint
-import androidx.benchmark.junit4.BenchmarkRule
-import androidx.benchmark.junit4.measureRepeated
-import androidx.compose.ui.text.android.InternalPlatformTextApi
-import androidx.compose.ui.text.android.StaticLayoutFactory
-import androidx.compose.ui.text.style.Hyphens
-import androidx.compose.ui.text.style.LineBreak
-import androidx.test.filters.LargeTest
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-@OptIn(InternalPlatformTextApi::class)
-class HyphensLineBreakBenchmark(
-    private val textLength: Int,
-    private val hyphensString: String,
-    private val lineBreakString: String
-) {
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "length={0} hyphens={1} lineBreak={2}")
-        fun initParameters(): List<Array<Any?>> {
-            return cartesian(
-                arrayOf(32, 128, 512),
-                arrayOf(
-                    Hyphens.None.toTestString(),
-                    Hyphens.Auto.toTestString()
-                ),
-                arrayOf(
-                    LineBreak.Paragraph.toTestString(),
-                    LineBreak.Simple.toTestString(),
-                    LineBreak.Heading.toTestString()
-                )
-            )
-        }
-    }
-
-    @get:Rule
-    val benchmarkRule = BenchmarkRule()
-
-    @get:Rule
-    val textBenchmarkRule = TextBenchmarkTestRule()
-
-    private val width = 100
-    private val textSize: Float = 10F
-    private val hyphenationFrequency = toLayoutHyphenationFrequency(hyphensString.toHyphens())
-    private val lineBreakStyle = toLayoutLineBreakStyle(lineBreakString.toLineBreak().strictness)
-    private val breakStrategy = toLayoutBreakStrategy(lineBreakString.toLineBreak().strategy)
-    private val lineBreakWordStyle =
-        toLayoutLineBreakWordStyle(lineBreakString.toLineBreak().wordBreak)
-
-    @Test
-    fun constructLayout() {
-        textBenchmarkRule.generator { textGenerator ->
-            val text = textGenerator.nextParagraph(textLength)
-            val textPaint = TextPaint()
-            textPaint.textSize = textSize
-            benchmarkRule.measureRepeated {
-                StaticLayoutFactory.create(
-                    text = text,
-                    width = width,
-                    paint = textPaint,
-                    hyphenationFrequency = hyphenationFrequency,
-                    lineBreakStyle = lineBreakStyle,
-                    breakStrategy = breakStrategy,
-                    lineBreakWordStyle = lineBreakWordStyle
-                )
-            }
-        }
-    }
-
-    @Test
-    fun constructLayoutDraw() {
-        textBenchmarkRule.generator { textGenerator ->
-            val text = textGenerator.nextParagraph(textLength)
-            val textPaint = TextPaint()
-            textPaint.textSize = textSize
-            val canvas = Canvas(Bitmap.createBitmap(width, 1000, Bitmap.Config.ARGB_8888))
-            benchmarkRule.measureRepeated {
-                val layout = StaticLayoutFactory.create(
-                    text = text,
-                    width = width,
-                    paint = textPaint,
-                    hyphenationFrequency = hyphenationFrequency,
-                    lineBreakStyle = lineBreakStyle,
-                    breakStrategy = breakStrategy,
-                    lineBreakWordStyle = lineBreakWordStyle
-                )
-                layout.draw(canvas)
-            }
-        }
-    }
-
-    private fun toLayoutHyphenationFrequency(hyphens: Hyphens?): Int = when (hyphens) {
-        Hyphens.Auto -> if (Build.VERSION.SDK_INT <= 32) {
-            Layout.HYPHENATION_FREQUENCY_NORMAL
-        } else {
-            Layout.HYPHENATION_FREQUENCY_NORMAL_FAST
-        }
-        Hyphens.None -> Layout.HYPHENATION_FREQUENCY_NONE
-        else -> Layout.HYPHENATION_FREQUENCY_NONE
-    }
-
-    private fun toLayoutBreakStrategy(breakStrategy: LineBreak.Strategy?): Int =
-        when (breakStrategy) {
-            LineBreak.Strategy.Simple -> LineBreaker.BREAK_STRATEGY_SIMPLE
-            LineBreak.Strategy.HighQuality -> LineBreaker.BREAK_STRATEGY_HIGH_QUALITY
-            LineBreak.Strategy.Balanced -> LineBreaker.BREAK_STRATEGY_BALANCED
-            else -> LineBreaker.BREAK_STRATEGY_SIMPLE
-        }
-
-    private fun toLayoutLineBreakStyle(lineBreakStrictness: LineBreak.Strictness?): Int =
-        when (lineBreakStrictness) {
-            LineBreak.Strictness.Default -> LineBreakConfig.LINE_BREAK_STYLE_NONE
-            LineBreak.Strictness.Loose -> LineBreakConfig.LINE_BREAK_STYLE_LOOSE
-            LineBreak.Strictness.Normal -> LineBreakConfig.LINE_BREAK_STYLE_NORMAL
-            LineBreak.Strictness.Strict -> LineBreakConfig.LINE_BREAK_STYLE_STRICT
-            else -> LineBreakConfig.LINE_BREAK_STYLE_NONE
-        }
-
-    private fun toLayoutLineBreakWordStyle(lineBreakWordStyle: LineBreak.WordBreak?): Int =
-        when (lineBreakWordStyle) {
-            LineBreak.WordBreak.Default -> LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE
-            LineBreak.WordBreak.Phrase -> LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE
-            else -> LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE
-        }
-}
-
-/**
- * Required to make this test work due to a bug with value classes and Parameterized JUnit tests.
- * https://youtrack.jetbrains.com/issue/KT-35523
- *
- * However, it's not enough to use a wrapper because wrapper makes the test name unnecessarily
- * long which causes Perfetto to be unable to create output files with a very long name in some
- * file systems.
- *
- * Using a String instead of an Integer gives us a better test naming.
- */
-private fun String.toLineBreak(): LineBreak = when (this) {
-    "Simple" -> LineBreak.Simple
-    "Heading" -> LineBreak.Heading
-    "Paragraph" -> LineBreak.Paragraph
-    else -> throw IllegalArgumentException("Unrecognized LineBreak value for this test")
-}
-
-private fun LineBreak.toTestString(): String = when (this) {
-    LineBreak.Simple -> "Simple"
-    LineBreak.Heading -> "Heading"
-    LineBreak.Paragraph -> "Paragraph"
-    else -> throw IllegalArgumentException("Unrecognized LineBreak value for this test")
-}
-
-private fun String.toHyphens(): Hyphens = when (this) {
-    "None" -> Hyphens.None
-    "Auto" -> Hyphens.Auto
-    else -> throw IllegalArgumentException("Unrecognized Hyphens value for this test")
-}
-
-private fun Hyphens.toTestString(): String = when (this) {
-    Hyphens.None -> "None"
-    Hyphens.Auto -> "Auto"
-    else -> throw IllegalArgumentException("Unrecognized Hyphens value for this test")
-}
diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle
index 4486d14..d4383a4 100644
--- a/compose/ui/ui-text/build.gradle
+++ b/compose/ui/ui-text/build.gradle
@@ -96,7 +96,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:ui:ui-test-junit4"))
@@ -117,7 +117,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":internal-testutils-fonts"))
@@ -150,7 +150,7 @@
 }
 
 dependencies {
-    // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
+    // Can't declare this in kotlin { sourceSets { androidUnitTest.dependencies { .. } } } as that
     // leaks into instrumented tests (b/214407011)
     testImplementation(libs.mockitoCore4)
     testImplementation(libs.mockitoKotlin4)
diff --git a/compose/ui/ui-text/lint-baseline.xml b/compose/ui/ui-text/lint-baseline.xml
index f49a0ce..a0b00ff 100644
--- a/compose/ui/ui-text/lint-baseline.xml
+++ b/compose/ui/ui-text/lint-baseline.xml
@@ -1,31 +1,85 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
-        id="BanInlineOptIn"
-        message="Inline functions cannot opt into experimental APIs."
-        errorLine1="internal inline fun &lt;T> List&lt;T>.fastForEach(action: (T) -> Unit) {"
-        errorLine2="                                ~~~~~~~~~~~">
+        id="NewApi"
+        message="Call requires API level 29 (current min is 21): `android.graphics.Paint#getBlendMode`"
+        errorLine1="        assertThat(paragraph.textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
+        errorLine2="                                       ~~~~~~~~~">
         <location
-            file="../../../text/text/src/main/java/androidx/compose/ui/text/android/TempListUtils.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt"/>
     </issue>
 
     <issue
-        id="BanInlineOptIn"
-        message="Inline functions cannot opt into experimental APIs."
-        errorLine1="internal inline fun &lt;T, R, C : MutableCollection&lt;in R>> List&lt;T>.fastMapTo("
-        errorLine2="                                                                ~~~~~~~~~">
+        id="NewApi"
+        message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+        errorLine1="        assertThat(paragraph.textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
-            file="../../../text/text/src/main/java/androidx/compose/ui/text/android/TempListUtils.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt"/>
     </issue>
 
     <issue
-        id="BanInlineOptIn"
-        message="Inline functions cannot opt into experimental APIs."
-        errorLine1="internal inline fun &lt;T, R> List&lt;T>.fastZipWithNext(transform: (T, T) -> R): List&lt;R> {"
-        errorLine2="                                   ~~~~~~~~~~~~~~~">
+        id="NewApi"
+        message="Call requires API level 29 (current min is 21): `android.graphics.Paint#getBlendMode`"
+        errorLine1="        assertThat(paragraph.textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
+        errorLine2="                                       ~~~~~~~~~">
         <location
-            file="../../../text/text/src/main/java/androidx/compose/ui/text/android/TempListUtils.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+        errorLine1="        assertThat(paragraph.textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 29 (current min is 21): `android.graphics.Paint#getBlendMode`"
+        errorLine1="        assertThat(textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
+        errorLine2="                             ~~~~~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+        errorLine1="        assertThat(textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 29 (current min is 21): `android.graphics.Paint#setBlendMode`"
+        errorLine1="        textPaint.blendMode = BlendMode.DstOver"
+        errorLine2="                  ~~~~~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 29 (current min is 21): `android.graphics.Paint#getBlendMode`"
+        errorLine1="        assertThat(textPaint.blendMode).isEqualTo(BlendMode.DstOver)"
+        errorLine2="                             ~~~~~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+        errorLine1="        assertThat(textPaint.blendMode).isEqualTo(BlendMode.DstOver)"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt"/>
     </issue>
 
     <issue
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphExt.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphExt.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphExt.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphExt.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTextDirectionTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTextDirectionTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTextDirectionTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTextDirectionTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/CacheTextLayoutInputTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/CacheTextLayoutInputTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/CacheTextLayoutInputTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/CacheTextLayoutInputTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/FontTestData.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/FontTestData.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/FontTestData.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/FontTestData.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphFillBoundingBoxesTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphFillBoundingBoxesTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphFillBoundingBoxesTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphFillBoundingBoxesTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationBoundingBoxTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationBoundingBoxTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationBoundingBoxTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationBoundingBoxTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTextDirectionTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTextDirectionTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTextDirectionTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTextDirectionTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicIntegrationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicIntegrationTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicIntegrationTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicIntegrationTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicsAsyncTypefaceTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicsAsyncTypefaceTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicsAsyncTypefaceTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicsAsyncTypefaceTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/PlatformParagraphStyleTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/PlatformParagraphStyleTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/PlatformParagraphStyleTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/PlatformParagraphStyleTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/PlatformSpanStyleTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/PlatformSpanStyleTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/PlatformSpanStyleTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/PlatformSpanStyleTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/PlatformTextStyleTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/PlatformTextStyleTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/PlatformTextStyleTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/PlatformTextStyleTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/StringTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/StringTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/StringTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/StringTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextLayoutCacheTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextLayoutCacheTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextLayoutCacheTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextLayoutCacheTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextPainterTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextPainterTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextPainterTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextPainterTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextTestExtensions.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextTestExtensions.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextTestExtensions.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextTestExtensions.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TtsAnnotationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TtsAnnotationTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TtsAnnotationTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TtsAnnotationTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/AndroidFontTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/AndroidFontTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/AndroidFontTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/AndroidFontTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/AndroidFontUtilsTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/AndroidFontUtilsTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/AndroidFontUtilsTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/AndroidFontUtilsTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/AndroidVariableFontTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/AndroidVariableFontTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/AndroidVariableFontTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/AndroidVariableFontTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsageTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsageTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsageTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsageTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/DeviceFontFamilyNameFontTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/DeviceFontFamilyNameFontTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/DeviceFontFamilyNameFontTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/DeviceFontFamilyNameFontTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverFileTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverFileTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverFileTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverFileTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplCancellationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplCancellationTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplCancellationTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplCancellationTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplPreloadTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplPreloadTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplPreloadTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplPreloadTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolver_androidKtTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolver_androidKtTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolver_androidKtTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolver_androidKtTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterPreloadTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterPreloadTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterPreloadTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterPreloadTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontSynthesisTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontSynthesisTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontSynthesisTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/FontSynthesisTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapterTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapterTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapterTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapterTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/PlatformTypefacesTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/PlatformTypefacesTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/PlatformTypefacesTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/PlatformTypefacesTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/testutils/AsyncTestFonts.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/testutils/AsyncTestFonts.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/testutils/AsyncTestFonts.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/testutils/AsyncTestFonts.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/testutils/FontFamilyResolverUtils.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/testutils/FontFamilyResolverUtils.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/testutils/FontFamilyResolverUtils.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/font/testutils/FontFamilyResolverUtils.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/BackspaceCommandTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/BackspaceCommandTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/BackspaceCommandTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/BackspaceCommandTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/MoveCursorCommandTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/MoveCursorCommandTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/MoveCursorCommandTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/MoveCursorCommandTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/intl/LocaleListTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/intl/LocaleListTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/intl/LocaleListTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/intl/LocaleListTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/intl/LocaleTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/intl/LocaleTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/intl/LocaleTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/intl/LocaleTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/BitmapSubject.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/BitmapSubject.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/BitmapSubject.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/BitmapSubject.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/CharSequenceSubject.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/CharSequenceSubject.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/CharSequenceSubject.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/CharSequenceSubject.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/ComposeMatchers.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/ComposeMatchers.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/ComposeMatchers.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/ComposeMatchers.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/RectSubject.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/RectSubject.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/RectSubject.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/RectSubject.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/TypefaceResultSubject.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/TypefaceResultSubject.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/TypefaceResultSubject.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/TypefaceResultSubject.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/TypefaceSubject.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/TypefaceSubject.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/TypefaceSubject.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/TypefaceSubject.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsicsTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsicsTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsicsTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsicsTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceCacheTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceCacheTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceCacheTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceCacheTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceSubsetTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceSubsetTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceSubsetTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceSubsetTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatusTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatusTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatusTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatusTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/GenericFontFamilyCacheTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/GenericFontFamilyCacheTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/GenericFontFamilyCacheTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/GenericFontFamilyCacheTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/SpannableExtensionsTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/SpannableExtensionsTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/SpannableExtensionsTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/SpannableExtensionsTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/TextPaintExtensionsTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/TextPaintExtensionsTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/TextPaintExtensionsTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/TextPaintExtensionsTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/TextTestExtensions.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/TextTestExtensions.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/TextTestExtensions.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/TextTestExtensions.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/style/HyphensTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/HyphensTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/style/HyphensTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/HyphensTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/style/LineBreakTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/LineBreakTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/style/LineBreakTest.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/LineBreakTest.kt
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/style/TextLineBreaker.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/TextLineBreaker.kt
similarity index 100%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/style/TextLineBreaker.kt
rename to compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/TextLineBreaker.kt
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
index 1d92b39..c7d3794 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
@@ -162,12 +162,27 @@
     density: Density
 ): Float {
     return when (lineHeight.type) {
-        TextUnitType.Sp -> with(density) { lineHeight.toPx() }
+        TextUnitType.Sp -> {
+            if (!isNonLinearFontScalingActive(density)) {
+                // Non-linear font scaling is not being used, this SP is safe to use directly.
+                with(density) { lineHeight.toPx() }
+            } else {
+                // Determine the intended line height multiplier and use that, since non-linear font
+                // scaling may compress the line height if it is much larger than the font size.
+                // i.e. preserve the original proportions rather than the absolute converted value.
+                val fontSizeSp = with(density) { contextFontSize.toSp() }
+                val lineHeightMultiplier = lineHeight.value / fontSizeSp.value
+                lineHeightMultiplier * contextFontSize
+            }
+        }
         TextUnitType.Em -> lineHeight.value * contextFontSize
         else -> Float.NaN
     }
 }
 
+// TODO(b/294384826): replace this with the actual platform method once available in core
+private fun isNonLinearFontScalingActive(density: Density) = density.fontScale > 1.05
+
 internal fun Spannable.setSpanStyles(
     contextTextStyle: TextStyle,
     spanStyles: List<AnnotatedString.Range<SpanStyle>>,
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/AnnotatedStringTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/AnnotatedStringTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTransformTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/AnnotatedStringTransformTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTransformTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/AnnotatedStringTransformTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/MultiParagraphTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/MultiParagraphTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/MultiParagraphTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/MultiParagraphTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/ParagraphStyleTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/ParagraphStyleTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/PlaceholderTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/PlaceholderTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/PlaceholderTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/PlaceholderTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/SaversTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/SaversTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/SaversTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/SaversTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/SpanStyleTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/SpanStyleTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/SpanStyleTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/SpanStyleTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextInputServiceTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextInputServiceTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextRangeTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextRangeTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextRangeTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextRangeTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextSpanParagraphStyleTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextSpanParagraphStyleTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextSpanParagraphStyleTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextSpanParagraphStyleTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleLayoutAttributesTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextStyleLayoutAttributesTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleLayoutAttributesTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextStyleLayoutAttributesTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleResolveDefaultsTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextStyleResolveDefaultsTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleResolveDefaultsTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextStyleResolveDefaultsTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextStyleTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextStyleTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/AndroidFontResolverInterceptorTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/AndroidFontResolverInterceptorTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/AndroidFontResolverInterceptorTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/AndroidFontResolverInterceptorTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/AsyncFontListLoaderTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/AsyncFontListLoaderTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/AsyncFontListLoaderTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/AsyncFontListLoaderTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/FontFamilyTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/FontFamilyTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/FontFamilyTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/FontFamilyTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/FontMatcherTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/FontMatcherTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/FontMatcherTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/FontMatcherTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/FontTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/FontTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/FontTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/FontTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/FontTestData.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/FontTestData.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/FontTestData.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/FontTestData.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/FontVariationTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/FontVariationTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/FontVariationTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/FontVariationTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/FontWeightTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/FontWeightTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/FontWeightTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/FontWeightTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/LoadedFontFamilyTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/LoadedFontFamilyTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/LoadedFontFamilyTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/LoadedFontFamilyTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/ResourceFontVariationSettingsTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/ResourceFontVariationSettingsTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/font/ResourceFontVariationSettingsTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/font/ResourceFontVariationSettingsTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/CommitTextCommandTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/CommitTextCommandTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/CommitTextCommandTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/CommitTextCommandTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/DeleteSurroundingTextCommandTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/DeleteSurroundingTextCommandTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/DeleteSurroundingTextCommandTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/DeleteSurroundingTextCommandTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/DeleteSurroundingTextInCodePointsCommandTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/DeleteSurroundingTextInCodePointsCommandTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/DeleteSurroundingTextInCodePointsCommandTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/DeleteSurroundingTextInCodePointsCommandTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/EditProcessorTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/EditProcessorTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/EditProcessorTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/EditProcessorTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/EditingBufferDeleteRangeTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/EditingBufferDeleteRangeTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/EditingBufferDeleteRangeTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/EditingBufferDeleteRangeTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/EditingBufferTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/EditingBufferTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/EditingBufferTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/EditingBufferTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/FinishComposingTextCommandTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/FinishComposingTextCommandTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/FinishComposingTextCommandTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/FinishComposingTextCommandTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/GapBufferTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/GapBufferTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/GapBufferTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/GapBufferTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/PasswordVisualTransformationTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/PasswordVisualTransformationTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/PasswordVisualTransformationTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/PasswordVisualTransformationTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/SetComposingRegionCommandTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/SetComposingRegionCommandTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/SetComposingRegionCommandTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/SetComposingRegionCommandTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/SetComposingTextCommandTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/SetComposingTextCommandTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/SetComposingTextCommandTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/SetComposingTextCommandTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/SetSelectionCommandTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/SetSelectionCommandTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/SetSelectionCommandTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/SetSelectionCommandTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/TextFieldValueTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/TextFieldValueTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/TextFieldValueTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/TextFieldValueTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/matchers/EditBufferSubject.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/matchers/EditBufferSubject.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/matchers/EditBufferSubject.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/matchers/EditBufferSubject.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/BaselineShiftTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/style/BaselineShiftTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/BaselineShiftTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/style/BaselineShiftTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/HyphensTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/style/HyphensTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/HyphensTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/style/HyphensTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/LineHeightStyleTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/style/LineHeightStyleTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/LineHeightStyleTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/style/LineHeightStyleTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/TextDecorationTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/style/TextDecorationTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/TextDecorationTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/style/TextDecorationTest.kt
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/TextForegroundStyleTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/style/TextForegroundStyleTest.kt
similarity index 100%
rename from compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/TextForegroundStyleTest.kt
rename to compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/style/TextForegroundStyleTest.kt
diff --git a/compose/ui/ui-text/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/compose/ui/ui-text/src/androidUnitTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
similarity index 100%
rename from compose/ui/ui-text/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
rename to compose/ui/ui-text/src/androidUnitTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
diff --git a/compose/ui/ui-tooling-data/build.gradle b/compose/ui/ui-tooling-data/build.gradle
index 0faefb4..4b88d71 100644
--- a/compose/ui/ui-tooling-data/build.gradle
+++ b/compose/ui/ui-tooling-data/build.gradle
@@ -88,7 +88,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:ui:ui-test-junit4"))
@@ -106,7 +106,7 @@
             }
         }
 
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.truth)
diff --git a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
deleted file mode 100644
index a0802ab..0000000
--- a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
+++ /dev/null
@@ -1,478 +0,0 @@
-/*
- * Copyright 2019 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.tooling.data
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.Button
-import androidx.compose.material.ModalDrawer
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.InternalComposeApi
-import androidx.compose.runtime.ReusableContent
-import androidx.compose.runtime.tooling.CompositionData
-import androidx.compose.runtime.tooling.CompositionGroup
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNotEquals
-import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@UiToolingDataApi
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class InspectableTests : ToolingTest() {
-    @Test
-    fun simpleInspection() {
-        val slotTableRecord = CompositionDataRecord.create()
-        show {
-            Inspectable(slotTableRecord) {
-                Column {
-                    Box(
-                        Modifier.size(100.dp).drawBehind {
-                            drawRect(Color.Black)
-                        }
-                    )
-                }
-            }
-        }
-
-        // Should be able to find the group for this test
-        val tree = slotTableRecord.store.first().asTree()
-        val group = tree.firstOrNull {
-            it.location?.sourceFile?.equals("InspectableTests.kt") == true && it.box.right > 0
-        } ?: error("Expected a group from this file")
-        assertNotNull(group)
-
-        // The group should have a non-empty bounding box
-        assertNotEquals(0, group.box.width)
-        assertNotEquals(0, group.box.height)
-    }
-
-    @Test
-    fun parametersTest() {
-        val slotTableRecord = CompositionDataRecord.create()
-        fun unknown(i: Int) = i
-
-        show {
-            Inspectable(slotTableRecord) {
-                OneParameter(1)
-                OneParameter(2)
-
-                OneDefaultParameter()
-                OneDefaultParameter(2)
-
-                ThreeParameters(1, 2, 3)
-
-                ThreeDefaultParameters()
-                ThreeDefaultParameters(a = 1)
-                ThreeDefaultParameters(b = 2)
-                ThreeDefaultParameters(a = 1, b = 2)
-                ThreeDefaultParameters(c = 3)
-                ThreeDefaultParameters(a = 1, c = 3)
-                ThreeDefaultParameters(b = 2, c = 3)
-                ThreeDefaultParameters(a = 1, b = 2, c = 3)
-
-                val ua = unknown(1)
-                val ub = unknown(2)
-                val uc = unknown(3)
-
-                ThreeDefaultParameters()
-                ThreeDefaultParameters(a = ua)
-                ThreeDefaultParameters(b = ub)
-                ThreeDefaultParameters(a = ua, b = ub)
-                ThreeDefaultParameters(c = uc)
-                ThreeDefaultParameters(a = ua, c = uc)
-                ThreeDefaultParameters(b = ub, c = uc)
-                ThreeDefaultParameters(a = ua, b = ub, c = uc)
-            }
-        }
-
-        val tree = slotTableRecord.store.first().asTree()
-        val list = tree.asList()
-        val parameters = list.filter { group ->
-            group.parameters.isNotEmpty() && group.location.let {
-                it != null && it.sourceFile == "InspectableTests.kt"
-            }
-        }
-
-        val callCursor = parameters.listIterator()
-        class ParameterValidationReceiver(val parameterCursor: Iterator<ParameterInformation>) {
-            fun parameter(
-                name: String,
-                value: Any,
-                fromDefault: Boolean,
-                static: Boolean,
-                compared: Boolean
-            ) {
-                assertTrue(parameterCursor.hasNext())
-                val parameter = parameterCursor.next()
-                assertEquals(name, parameter.name)
-                assertEquals(value, parameter.value)
-                assertEquals(fromDefault, parameter.fromDefault)
-                assertEquals(static, parameter.static)
-                assertEquals(compared, parameter.compared)
-            }
-        }
-
-        fun validate(block: ParameterValidationReceiver.() -> Unit) {
-            assertTrue(callCursor.hasNext())
-            val call = callCursor.next()
-            val receiver = ParameterValidationReceiver(call.parameters.listIterator())
-            receiver.block()
-            assertFalse(receiver.parameterCursor.hasNext())
-        }
-
-        // Skip Inspectable
-        callCursor.next()
-
-        // OneParameter(1)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
-        }
-
-        // OneParameter(2)
-        validate {
-            parameter(name = "a", value = 2, fromDefault = false, static = true, compared = false)
-        }
-
-        // OneDefaultParameter()
-        validate {
-            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
-        }
-
-        // OneDefaultParameter(2)
-        validate {
-            parameter(name = "a", value = 2, fromDefault = false, static = true, compared = false)
-        }
-
-        // ThreeParameters(1, 2, 3)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
-            parameter(name = "b", value = 2, fromDefault = false, static = true, compared = false)
-            parameter(name = "c", value = 3, fromDefault = false, static = true, compared = false)
-        }
-
-        // ThreeDefaultParameters()
-        validate {
-            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
-            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
-            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
-        }
-
-        // ThreeDefaultParameters(a = 1)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
-            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
-            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
-        }
-
-        // ThreeDefaultParameters(b = 2)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
-            parameter(name = "b", value = 2, fromDefault = false, static = true, compared = false)
-            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
-        }
-
-        // ThreeDefaultParameters(a = 1, b = 2)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
-            parameter(name = "b", value = 2, fromDefault = false, static = true, compared = false)
-            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
-        }
-
-        // ThreeDefaultParameters(c = 3)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
-            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
-            parameter(name = "c", value = 3, fromDefault = false, static = true, compared = false)
-        }
-
-        // ThreeDefaultParameters(a = 1, c = 3)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
-            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
-            parameter(name = "c", value = 3, fromDefault = false, static = true, compared = false)
-        }
-
-        // ThreeDefaultParameters(b = 2, c = 3)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
-            parameter(name = "b", value = 2, fromDefault = false, static = true, compared = false)
-            parameter(name = "c", value = 3, fromDefault = false, static = true, compared = false)
-        }
-
-        // ThreeDefaultParameters(a = 1, b = 2, c = 3)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
-            parameter(name = "b", value = 2, fromDefault = false, static = true, compared = false)
-            parameter(name = "c", value = 3, fromDefault = false, static = true, compared = false)
-        }
-
-        // ThreeDefaultParameters()
-        validate {
-            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
-            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
-            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
-        }
-
-        // ThreeDefaultParameters(a = ua)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = false, static = false, compared = true)
-            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
-            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
-        }
-
-        // ThreeDefaultParameters(b = ub)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
-            parameter(name = "b", value = 2, fromDefault = false, static = false, compared = true)
-            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
-        }
-
-        // ThreeDefaultParameters(a = ua, b = ub)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = false, static = false, compared = true)
-            parameter(name = "b", value = 2, fromDefault = false, static = false, compared = true)
-            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
-        }
-
-        // ThreeDefaultParameters(c = uc)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
-            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
-            parameter(name = "c", value = 3, fromDefault = false, static = false, compared = true)
-        }
-
-        // ThreeDefaultParameters(a = ua, c = uc)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = false, static = false, compared = true)
-            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
-            parameter(name = "c", value = 3, fromDefault = false, static = false, compared = true)
-        }
-
-        // ThreeDefaultParameters(b = ub, c = uc)
-        validate {
-            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
-            parameter(name = "b", value = 2, fromDefault = false, static = false, compared = true)
-            parameter(name = "c", value = 3, fromDefault = false, static = false, compared = true)
-        }
-
-        // ThreeDefaultParameters(a = ua, b = ub, c = uc)\
-        validate {
-            parameter(name = "a", value = 1, fromDefault = false, static = false, compared = true)
-            parameter(name = "b", value = 2, fromDefault = false, static = false, compared = true)
-            parameter(name = "c", value = 3, fromDefault = false, static = false, compared = true)
-        }
-
-        assertFalse(callCursor.hasNext())
-    }
-
-    @Test
-    fun inInspectionMode() {
-        var displayed = false
-        show {
-            Inspectable(CompositionDataRecord.create()) {
-                Column {
-                    InInspectionModeOnly {
-                        Box(Modifier.size(100.dp).background(color = Color.Black))
-                        displayed = true
-                    }
-                }
-            }
-        }
-
-        assertTrue(displayed)
-    }
-
-    @Test
-    fun notInInspectionMode() {
-        var displayed = false
-        show {
-            Column {
-                InInspectionModeOnly {
-                    Box(Modifier.size(100.dp).background(color = Color.Black))
-                    displayed = true
-                }
-            }
-        }
-
-        assertFalse(displayed)
-    }
-
-    @InternalComposeApi
-    @Test // regression test for b/161839910
-    fun textParametersAreCorrect() {
-        val slotTableRecord = CompositionDataRecord.create()
-        show {
-            Inspectable(slotTableRecord) {
-                Text("Test")
-            }
-        }
-        val tree = slotTableRecord.store.first().asTree()
-        val list = tree.asList()
-        val parameters = list.filter { group ->
-            group.parameters.isNotEmpty() && group.location.let {
-                it != null && it.sourceFile == "InspectableTests.kt"
-            }
-        }
-        val names = parameters.drop(1).first().parameters.map { it.name }
-        assertEquals(
-            "text, modifier, color, fontSize, fontStyle, fontWeight, fontFamily, " +
-                "letterSpacing, textDecoration, textAlign, lineHeight, overflow, softWrap, " +
-                "maxLines, minLines, onTextLayout, style",
-            names.joinToString()
-        )
-    }
-
-    @OptIn(InternalComposeApi::class)
-    @Test // regression test for b/162092315
-    fun inspectingModalDrawer() {
-        val positioned = CountDownLatch(1)
-        val tables = showAndRecord {
-            ModalDrawer(
-                drawerContent = { Text("Something") },
-                content = {
-                    Column(
-                        Modifier.onGloballyPositioned {
-                            positioned.countDown()
-                        }
-                    ) {
-                        Text(text = "Hello World", color = Color.Green)
-                        Button(onClick = {}) { Text(text = "OK") }
-                    }
-                }
-            )
-        }
-
-        assertTrue(positioned.await(2, TimeUnit.SECONDS))
-
-        // Wait for composition to complete
-        activity.runOnUiThread { }
-
-        assertFalse(tables.isNullOrEmpty())
-        assertTrue(tables.size > 1)
-
-        val calls = activity.uiThread {
-            tables.flatMap { table ->
-                if (!table.isEmpty) table.asTree().asList() else emptyList()
-            }.filter {
-                val location = it.location
-                location != null && location.sourceFile == "InspectableTests.kt"
-            }.map {
-                it.name
-            }
-        }
-
-        assertTrue(calls.contains("Column"))
-        assertTrue(calls.contains("Text"))
-        assertTrue(calls.contains("Button"))
-    }
-
-    @Test
-    fun allowKeysThatLookLikeInvalidSourceInformation() {
-        val slotTableRecord = CompositionDataRecord.create()
-        show {
-            Inspectable(slotTableRecord) {
-                ReusableContent("1234123412341234") {
-                    Text("Test")
-                }
-            }
-        }
-        slotTableRecord.store.first().asTree()
-    }
-
-    @Test
-    fun emptyCompostionDataShouldProduceEmptyTree() {
-        val emptyCompositionData = object : CompositionData {
-            override val compositionGroups: Iterable<CompositionGroup> =
-                emptyList()
-            override val isEmpty = true
-        }
-
-        val emptyTree = emptyCompositionData.asTree()
-        assertTrue(emptyTree.children.isEmpty())
-    }
-}
-
-private fun <T> TestActivity.uiThread(block: () -> T): T {
-    val latch = CountDownLatch(1)
-    var result: T? = null
-    runOnUiThread {
-        result = block()
-        latch.countDown()
-    }
-    latch.await(1, TimeUnit.SECONDS)
-    return result!!
-}
-
-@Suppress("UNUSED_PARAMETER")
-@Composable
-fun OneParameter(a: Int) {
-}
-
-@Suppress("UNUSED_PARAMETER")
-@Composable
-fun OneDefaultParameter(a: Int = 1) {
-}
-
-@Suppress("UNUSED_PARAMETER")
-@Composable
-fun ThreeParameters(a: Int, b: Int, c: Int) {
-}
-
-@Suppress("UNUSED_PARAMETER")
-@Composable
-fun ThreeDefaultParameters(a: Int = 1, b: Int = 2, c: Int = 3) {
-}
-
-// BFS
-@UiToolingDataApi
-internal fun Group.firstOrNull(predicate: (Group) -> Boolean): Group? {
-    val stack = mutableListOf(this)
-    while (stack.isNotEmpty()) {
-        val next = stack.removeAt(0)
-        if (predicate(next)) return next
-        stack.addAll(next.children)
-    }
-    return null
-}
-
-@UiToolingDataApi
-internal fun Group.asList(): List<Group> {
-    val result = mutableListOf<Group>()
-    val stack = mutableListOf(this)
-    while (stack.isNotEmpty()) {
-        val next = stack.removeAt(stack.size - 1)
-        result.add(next)
-        stack.addAll(next.children.reversed())
-    }
-    return result
-}
diff --git a/compose/ui/ui-tooling-data/src/androidAndroidTest/AndroidManifest.xml b/compose/ui/ui-tooling-data/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidAndroidTest/AndroidManifest.xml
rename to compose/ui/ui-tooling-data/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/BoundsTest.kt b/compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/BoundsTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/BoundsTest.kt
rename to compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/BoundsTest.kt
diff --git a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/Inspectable.kt b/compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/Inspectable.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/Inspectable.kt
rename to compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/Inspectable.kt
diff --git a/compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt b/compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
new file mode 100644
index 0000000..196eb55
--- /dev/null
+++ b/compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
@@ -0,0 +1,475 @@
+/*
+ * Copyright 2019 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.tooling.data
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Button
+import androidx.compose.material.ModalDrawer
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.InternalComposeApi
+import androidx.compose.runtime.ReusableContent
+import androidx.compose.runtime.tooling.CompositionData
+import androidx.compose.runtime.tooling.CompositionGroup
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@UiToolingDataApi
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class InspectableTests : ToolingTest() {
+    @Test
+    fun simpleInspection() {
+        val slotTableRecord = CompositionDataRecord.create()
+        show {
+            Inspectable(slotTableRecord) {
+                Column {
+                    Box(
+                        Modifier.size(100.dp).drawBehind {
+                            drawRect(Color.Black)
+                        }
+                    )
+                }
+            }
+        }
+
+        // Should be able to find the group for this test
+        val tree = slotTableRecord.store.first().asTree()
+        val group = tree.firstOrNull {
+            it.location?.sourceFile?.equals("InspectableTests.kt") == true && it.box.right > 0
+        } ?: error("Expected a group from this file")
+        assertNotNull(group)
+
+        // The group should have a non-empty bounding box
+        assertNotEquals(0, group.box.width)
+        assertNotEquals(0, group.box.height)
+    }
+
+    @Test
+    fun parametersTest() {
+        val slotTableRecord = CompositionDataRecord.create()
+        fun unknown(i: Int) = i
+
+        show {
+            Inspectable(slotTableRecord) {
+                OneParameter(1)
+                OneParameter(2)
+
+                OneDefaultParameter()
+                OneDefaultParameter(2)
+
+                ThreeParameters(1, 2, 3)
+
+                ThreeDefaultParameters()
+                ThreeDefaultParameters(a = 1)
+                ThreeDefaultParameters(b = 2)
+                ThreeDefaultParameters(a = 1, b = 2)
+                ThreeDefaultParameters(c = 3)
+                ThreeDefaultParameters(a = 1, c = 3)
+                ThreeDefaultParameters(b = 2, c = 3)
+                ThreeDefaultParameters(a = 1, b = 2, c = 3)
+
+                val ua = unknown(1)
+                val ub = unknown(2)
+                val uc = unknown(3)
+
+                ThreeDefaultParameters()
+                ThreeDefaultParameters(a = ua)
+                ThreeDefaultParameters(b = ub)
+                ThreeDefaultParameters(a = ua, b = ub)
+                ThreeDefaultParameters(c = uc)
+                ThreeDefaultParameters(a = ua, c = uc)
+                ThreeDefaultParameters(b = ub, c = uc)
+                ThreeDefaultParameters(a = ua, b = ub, c = uc)
+            }
+        }
+
+        val tree = slotTableRecord.store.first().asTree()
+        val list = tree.asList()
+        val parameters = list.filter { group ->
+            group.parameters.isNotEmpty() && group.location.let {
+                it != null && it.sourceFile == "InspectableTests.kt"
+            }
+        }
+
+        val callCursor = parameters.listIterator()
+        class ParameterValidationReceiver(val parameterCursor: Iterator<ParameterInformation>) {
+            fun parameter(
+                name: String,
+                value: Any,
+                fromDefault: Boolean,
+                static: Boolean,
+                compared: Boolean
+            ) {
+                assertTrue(parameterCursor.hasNext())
+                val parameter = parameterCursor.next()
+                assertEquals(name, parameter.name)
+                assertEquals(value, parameter.value)
+                assertEquals(fromDefault, parameter.fromDefault)
+                assertEquals(static, parameter.static)
+                assertEquals(compared, parameter.compared)
+            }
+        }
+
+        fun validate(block: ParameterValidationReceiver.() -> Unit) {
+            assertTrue(callCursor.hasNext())
+            val call = callCursor.next()
+            val receiver = ParameterValidationReceiver(call.parameters.listIterator())
+            receiver.block()
+            assertFalse(receiver.parameterCursor.hasNext())
+        }
+
+        // OneParameter(1)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
+        }
+
+        // OneParameter(2)
+        validate {
+            parameter(name = "a", value = 2, fromDefault = false, static = true, compared = false)
+        }
+
+        // OneDefaultParameter()
+        validate {
+            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
+        }
+
+        // OneDefaultParameter(2)
+        validate {
+            parameter(name = "a", value = 2, fromDefault = false, static = true, compared = false)
+        }
+
+        // ThreeParameters(1, 2, 3)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
+            parameter(name = "b", value = 2, fromDefault = false, static = true, compared = false)
+            parameter(name = "c", value = 3, fromDefault = false, static = true, compared = false)
+        }
+
+        // ThreeDefaultParameters()
+        validate {
+            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
+            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
+            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
+        }
+
+        // ThreeDefaultParameters(a = 1)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
+            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
+            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
+        }
+
+        // ThreeDefaultParameters(b = 2)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
+            parameter(name = "b", value = 2, fromDefault = false, static = true, compared = false)
+            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
+        }
+
+        // ThreeDefaultParameters(a = 1, b = 2)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
+            parameter(name = "b", value = 2, fromDefault = false, static = true, compared = false)
+            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
+        }
+
+        // ThreeDefaultParameters(c = 3)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
+            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
+            parameter(name = "c", value = 3, fromDefault = false, static = true, compared = false)
+        }
+
+        // ThreeDefaultParameters(a = 1, c = 3)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
+            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
+            parameter(name = "c", value = 3, fromDefault = false, static = true, compared = false)
+        }
+
+        // ThreeDefaultParameters(b = 2, c = 3)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
+            parameter(name = "b", value = 2, fromDefault = false, static = true, compared = false)
+            parameter(name = "c", value = 3, fromDefault = false, static = true, compared = false)
+        }
+
+        // ThreeDefaultParameters(a = 1, b = 2, c = 3)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
+            parameter(name = "b", value = 2, fromDefault = false, static = true, compared = false)
+            parameter(name = "c", value = 3, fromDefault = false, static = true, compared = false)
+        }
+
+        // ThreeDefaultParameters()
+        validate {
+            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
+            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
+            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
+        }
+
+        // ThreeDefaultParameters(a = ua)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = false, static = false, compared = true)
+            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
+            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
+        }
+
+        // ThreeDefaultParameters(b = ub)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
+            parameter(name = "b", value = 2, fromDefault = false, static = false, compared = true)
+            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
+        }
+
+        // ThreeDefaultParameters(a = ua, b = ub)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = false, static = false, compared = true)
+            parameter(name = "b", value = 2, fromDefault = false, static = false, compared = true)
+            parameter(name = "c", value = 3, fromDefault = true, static = false, compared = false)
+        }
+
+        // ThreeDefaultParameters(c = uc)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
+            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
+            parameter(name = "c", value = 3, fromDefault = false, static = false, compared = true)
+        }
+
+        // ThreeDefaultParameters(a = ua, c = uc)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = false, static = false, compared = true)
+            parameter(name = "b", value = 2, fromDefault = true, static = false, compared = false)
+            parameter(name = "c", value = 3, fromDefault = false, static = false, compared = true)
+        }
+
+        // ThreeDefaultParameters(b = ub, c = uc)
+        validate {
+            parameter(name = "a", value = 1, fromDefault = true, static = false, compared = false)
+            parameter(name = "b", value = 2, fromDefault = false, static = false, compared = true)
+            parameter(name = "c", value = 3, fromDefault = false, static = false, compared = true)
+        }
+
+        // ThreeDefaultParameters(a = ua, b = ub, c = uc)\
+        validate {
+            parameter(name = "a", value = 1, fromDefault = false, static = false, compared = true)
+            parameter(name = "b", value = 2, fromDefault = false, static = false, compared = true)
+            parameter(name = "c", value = 3, fromDefault = false, static = false, compared = true)
+        }
+
+        assertFalse(callCursor.hasNext())
+    }
+
+    @Test
+    fun inInspectionMode() {
+        var displayed = false
+        show {
+            Inspectable(CompositionDataRecord.create()) {
+                Column {
+                    InInspectionModeOnly {
+                        Box(Modifier.size(100.dp).background(color = Color.Black))
+                        displayed = true
+                    }
+                }
+            }
+        }
+
+        assertTrue(displayed)
+    }
+
+    @Test
+    fun notInInspectionMode() {
+        var displayed = false
+        show {
+            Column {
+                InInspectionModeOnly {
+                    Box(Modifier.size(100.dp).background(color = Color.Black))
+                    displayed = true
+                }
+            }
+        }
+
+        assertFalse(displayed)
+    }
+
+    @InternalComposeApi
+    @Test // regression test for b/161839910
+    fun textParametersAreCorrect() {
+        val slotTableRecord = CompositionDataRecord.create()
+        show {
+            Inspectable(slotTableRecord) {
+                Text("Test")
+            }
+        }
+        val tree = slotTableRecord.store.first().asTree()
+        val list = tree.asList()
+        val parameters = list.filter { group ->
+            group.parameters.isNotEmpty() && group.name == "Text" && group.location.let {
+                it != null && it.sourceFile == "InspectableTests.kt"
+            }
+        }
+        val names = parameters.first().parameters.map { it.name }
+        assertEquals(
+            "text, modifier, color, fontSize, fontStyle, fontWeight, fontFamily, " +
+                "letterSpacing, textDecoration, textAlign, lineHeight, overflow, softWrap, " +
+                "maxLines, minLines, onTextLayout, style",
+            names.joinToString()
+        )
+    }
+
+    @OptIn(InternalComposeApi::class)
+    @Test // regression test for b/162092315
+    fun inspectingModalDrawer() {
+        val positioned = CountDownLatch(1)
+        val tables = showAndRecord {
+            ModalDrawer(
+                drawerContent = { Text("Something") },
+                content = {
+                    Column(
+                        Modifier.onGloballyPositioned {
+                            positioned.countDown()
+                        }
+                    ) {
+                        Text(text = "Hello World", color = Color.Green)
+                        Button(onClick = {}) { Text(text = "OK") }
+                    }
+                }
+            )
+        }
+
+        assertTrue(positioned.await(2, TimeUnit.SECONDS))
+
+        // Wait for composition to complete
+        activity.runOnUiThread { }
+
+        assertFalse(tables.isNullOrEmpty())
+        assertTrue(tables.size > 1)
+
+        val calls = activity.uiThread {
+            tables.flatMap { table ->
+                if (!table.isEmpty) table.asTree().asList() else emptyList()
+            }.filter {
+                val location = it.location
+                location != null && location.sourceFile == "InspectableTests.kt"
+            }.map {
+                it.name
+            }
+        }
+
+        assertTrue(calls.contains("Column"))
+        assertTrue(calls.contains("Text"))
+        assertTrue(calls.contains("Button"))
+    }
+
+    @Test
+    fun allowKeysThatLookLikeInvalidSourceInformation() {
+        val slotTableRecord = CompositionDataRecord.create()
+        show {
+            Inspectable(slotTableRecord) {
+                ReusableContent("1234123412341234") {
+                    Text("Test")
+                }
+            }
+        }
+        slotTableRecord.store.first().asTree()
+    }
+
+    @Test
+    fun emptyCompostionDataShouldProduceEmptyTree() {
+        val emptyCompositionData = object : CompositionData {
+            override val compositionGroups: Iterable<CompositionGroup> =
+                emptyList()
+            override val isEmpty = true
+        }
+
+        val emptyTree = emptyCompositionData.asTree()
+        assertTrue(emptyTree.children.isEmpty())
+    }
+}
+
+private fun <T> TestActivity.uiThread(block: () -> T): T {
+    val latch = CountDownLatch(1)
+    var result: T? = null
+    runOnUiThread {
+        result = block()
+        latch.countDown()
+    }
+    latch.await(1, TimeUnit.SECONDS)
+    return result!!
+}
+
+@Suppress("UNUSED_PARAMETER")
+@Composable
+fun OneParameter(a: Int) {
+}
+
+@Suppress("UNUSED_PARAMETER")
+@Composable
+fun OneDefaultParameter(a: Int = 1) {
+}
+
+@Suppress("UNUSED_PARAMETER")
+@Composable
+fun ThreeParameters(a: Int, b: Int, c: Int) {
+}
+
+@Suppress("UNUSED_PARAMETER")
+@Composable
+fun ThreeDefaultParameters(a: Int = 1, b: Int = 2, c: Int = 3) {
+}
+
+// BFS
+@UiToolingDataApi
+internal fun Group.firstOrNull(predicate: (Group) -> Boolean): Group? {
+    val stack = mutableListOf(this)
+    while (stack.isNotEmpty()) {
+        val next = stack.removeAt(0)
+        if (predicate(next)) return next
+        stack.addAll(next.children)
+    }
+    return null
+}
+
+@UiToolingDataApi
+internal fun Group.asList(): List<Group> {
+    val result = mutableListOf<Group>()
+    val stack = mutableListOf(this)
+    while (stack.isNotEmpty()) {
+        val next = stack.removeAt(stack.size - 1)
+        result.add(next)
+        stack.addAll(next.children.reversed())
+    }
+    return result
+}
diff --git a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ModifierInfoTest.kt b/compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/ModifierInfoTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ModifierInfoTest.kt
rename to compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/ModifierInfoTest.kt
diff --git a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetData.kt b/compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/OffsetData.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetData.kt
rename to compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/OffsetData.kt
diff --git a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetInformationTest.kt b/compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/OffsetInformationTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetInformationTest.kt
rename to compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/OffsetInformationTest.kt
diff --git a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/TestActivity.kt b/compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/TestActivity.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/TestActivity.kt
rename to compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/TestActivity.kt
diff --git a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ToolingTest.kt b/compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/ToolingTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ToolingTest.kt
rename to compose/ui/ui-tooling-data/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/data/ToolingTest.kt
diff --git a/compose/ui/ui-tooling-preview/api/current.txt b/compose/ui/ui-tooling-preview/api/current.txt
index 4d4cc08..e43cd20 100644
--- a/compose/ui/ui-tooling-preview/api/current.txt
+++ b/compose/ui/ui-tooling-preview/api/current.txt
@@ -102,7 +102,7 @@
     property public abstract kotlin.sequences.Sequence<T> values;
   }
 
-  @androidx.compose.ui.tooling.preview.Preview(name="Phone", device=androidx.compose.ui.tooling.preview.Devices.PHONE) @androidx.compose.ui.tooling.preview.Preview(name="Phone - Landscape", device="spec:width = 411dp, height = 891dp, orientation = landscape, dpi = 420") @androidx.compose.ui.tooling.preview.Preview(name="Unfolded Foldable", device=androidx.compose.ui.tooling.preview.Devices.FOLDABLE) @androidx.compose.ui.tooling.preview.Preview(name="Tablet", device=androidx.compose.ui.tooling.preview.Devices.TABLET) @androidx.compose.ui.tooling.preview.Preview(name="Desktop", device=androidx.compose.ui.tooling.preview.Devices.DESKTOP) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface PreviewScreenSizes {
+  @androidx.compose.ui.tooling.preview.Preview(name="Phone", device=androidx.compose.ui.tooling.preview.Devices.PHONE, showSystemUi=true) @androidx.compose.ui.tooling.preview.Preview(name="Phone - Landscape", device="spec:width = 411dp, height = 891dp, orientation = landscape, dpi = 420", showSystemUi=true) @androidx.compose.ui.tooling.preview.Preview(name="Unfolded Foldable", device=androidx.compose.ui.tooling.preview.Devices.FOLDABLE, showSystemUi=true) @androidx.compose.ui.tooling.preview.Preview(name="Tablet", device=androidx.compose.ui.tooling.preview.Devices.TABLET, showSystemUi=true) @androidx.compose.ui.tooling.preview.Preview(name="Desktop", device=androidx.compose.ui.tooling.preview.Devices.DESKTOP, showSystemUi=true) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface PreviewScreenSizes {
   }
 
   public final class Wallpapers {
diff --git a/compose/ui/ui-tooling-preview/api/restricted_current.txt b/compose/ui/ui-tooling-preview/api/restricted_current.txt
index 4d4cc08..e43cd20 100644
--- a/compose/ui/ui-tooling-preview/api/restricted_current.txt
+++ b/compose/ui/ui-tooling-preview/api/restricted_current.txt
@@ -102,7 +102,7 @@
     property public abstract kotlin.sequences.Sequence<T> values;
   }
 
-  @androidx.compose.ui.tooling.preview.Preview(name="Phone", device=androidx.compose.ui.tooling.preview.Devices.PHONE) @androidx.compose.ui.tooling.preview.Preview(name="Phone - Landscape", device="spec:width = 411dp, height = 891dp, orientation = landscape, dpi = 420") @androidx.compose.ui.tooling.preview.Preview(name="Unfolded Foldable", device=androidx.compose.ui.tooling.preview.Devices.FOLDABLE) @androidx.compose.ui.tooling.preview.Preview(name="Tablet", device=androidx.compose.ui.tooling.preview.Devices.TABLET) @androidx.compose.ui.tooling.preview.Preview(name="Desktop", device=androidx.compose.ui.tooling.preview.Devices.DESKTOP) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface PreviewScreenSizes {
+  @androidx.compose.ui.tooling.preview.Preview(name="Phone", device=androidx.compose.ui.tooling.preview.Devices.PHONE, showSystemUi=true) @androidx.compose.ui.tooling.preview.Preview(name="Phone - Landscape", device="spec:width = 411dp, height = 891dp, orientation = landscape, dpi = 420", showSystemUi=true) @androidx.compose.ui.tooling.preview.Preview(name="Unfolded Foldable", device=androidx.compose.ui.tooling.preview.Devices.FOLDABLE, showSystemUi=true) @androidx.compose.ui.tooling.preview.Preview(name="Tablet", device=androidx.compose.ui.tooling.preview.Devices.TABLET, showSystemUi=true) @androidx.compose.ui.tooling.preview.Preview(name="Desktop", device=androidx.compose.ui.tooling.preview.Devices.DESKTOP, showSystemUi=true) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface PreviewScreenSizes {
   }
 
   public final class Wallpapers {
diff --git a/compose/ui/ui-tooling-preview/build.gradle b/compose/ui/ui-tooling-preview/build.gradle
index c32950f..d5f1884 100644
--- a/compose/ui/ui-tooling-preview/build.gradle
+++ b/compose/ui/ui-tooling-preview/build.gradle
@@ -85,13 +85,13 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
             }
         }
 
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.junit)
diff --git a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.kt
index 866d472..284dd31 100644
--- a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.kt
+++ b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.kt
@@ -35,12 +35,13 @@
         AnnotationTarget.ANNOTATION_CLASS,
         AnnotationTarget.FUNCTION
 )
-@Preview(name = "Phone", device = PHONE)
+@Preview(name = "Phone", device = PHONE, showSystemUi = true)
 @Preview(name = "Phone - Landscape",
-         device = "spec:width = 411dp, height = 891dp, orientation = landscape, dpi = 420")
-@Preview(name = "Unfolded Foldable", device = FOLDABLE)
-@Preview(name = "Tablet", device = TABLET)
-@Preview(name = "Desktop", device = DESKTOP)
+         device = "spec:width = 411dp, height = 891dp, orientation = landscape, dpi = 420",
+         showSystemUi = true)
+@Preview(name = "Unfolded Foldable", device = FOLDABLE, showSystemUi = true)
+@Preview(name = "Tablet", device = TABLET, showSystemUi = true)
+@Preview(name = "Desktop", device = DESKTOP, showSystemUi = true)
 annotation class PreviewScreenSizes
 
 /**
diff --git a/compose/ui/ui-tooling-preview/src/test/java/androidx/compose/ui/tooling/preview/datasource/LoremIpsumTest.kt b/compose/ui/ui-tooling-preview/src/androidUnitTest/kotlin/androidx/compose/ui/tooling/preview/datasource/LoremIpsumTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-preview/src/test/java/androidx/compose/ui/tooling/preview/datasource/LoremIpsumTest.kt
rename to compose/ui/ui-tooling-preview/src/androidUnitTest/kotlin/androidx/compose/ui/tooling/preview/datasource/LoremIpsumTest.kt
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index ac62e0d..9e399a7 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -98,7 +98,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:ui:ui-test-junit4"))
@@ -117,7 +117,7 @@
             }
         }
 
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
             }
diff --git a/compose/ui/ui-tooling/lint-baseline.xml b/compose/ui/ui-tooling/lint-baseline.xml
index 7b89361..2191869 100644
--- a/compose/ui/ui-tooling/lint-baseline.xml
+++ b/compose/ui/ui-tooling/lint-baseline.xml
@@ -7,7 +7,7 @@
         errorLine1="            Thread.sleep(250)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt"/>
     </issue>
 
     <issue
@@ -16,7 +16,7 @@
         errorLine1="            Thread.sleep(200)"
         errorLine2="                   ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt"/>
     </issue>
 
     <issue
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/AndroidManifest.xml b/compose/ui/ui-tooling/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/AndroidManifest.xml
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeInvokerTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/ComposeInvokerTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeInvokerTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/ComposeInvokerTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/DesignInfoProviderComposable.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/DesignInfoProviderComposable.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/DesignInfoProviderComposable.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/DesignInfoProviderComposable.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/HotReloaderTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/HotReloaderTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/HotReloaderTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/HotReloaderTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/LazyColumnPreview.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/LazyColumnPreview.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/LazyColumnPreview.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/LazyColumnPreview.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/LineNumberPreview.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/LineNumberPreview.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/LineNumberPreview.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/LineNumberPreview.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ParameterProviderComposable.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/ParameterProviderComposable.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ParameterProviderComposable.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/ParameterProviderComposable.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/PreviewActivityTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/PreviewActivityTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/PreviewActivityTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/PreviewActivityTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/PreviewParameterTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/PreviewParameterTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/PreviewParameterTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/PreviewParameterTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestActivity.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/TestActivity.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestActivity.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/TestActivity.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestInvalidationPreview.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/TestInvalidationPreview.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestInvalidationPreview.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/TestInvalidationPreview.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestViewModel.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/TestViewModel.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestViewModel.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/TestViewModel.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ToolingTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/ToolingTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ToolingTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/ToolingTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimationTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimationTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimationTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimationTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimatedContentComposeAnimationTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/AnimatedContentComposeAnimationTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimatedContentComposeAnimationTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/AnimatedContentComposeAnimationTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimatedVisibilityComposeAnimationTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/AnimatedVisibilityComposeAnimationTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimatedVisibilityComposeAnimationTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/AnimatedVisibilityComposeAnimationTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/InfiniteTransitionComposeAnimationTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/InfiniteTransitionComposeAnimationTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/InfiniteTransitionComposeAnimationTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/InfiniteTransitionComposeAnimationTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/TransitionComposeAnimationTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/TransitionComposeAnimationTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/TransitionComposeAnimationTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/TransitionComposeAnimationTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimatedVisibilityClockTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimatedVisibilityClockTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimatedVisibilityClockTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimatedVisibilityClockTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/InfiniteTransitionClockTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/InfiniteTransitionClockTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/InfiniteTransitionClockTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/InfiniteTransitionClockTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/UtilsTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/UtilsTest.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/UtilsTest.kt
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/UtilsTest.kt
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/res/layout/compose_adapter_test.xml b/compose/ui/ui-tooling/src/androidInstrumentedTest/res/layout/compose_adapter_test.xml
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/res/layout/compose_adapter_test.xml
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/res/layout/compose_adapter_test.xml
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/res/values/style.xml b/compose/ui/ui-tooling/src/androidInstrumentedTest/res/values/style.xml
similarity index 100%
rename from compose/ui/ui-tooling/src/androidAndroidTest/res/values/style.xml
rename to compose/ui/ui-tooling/src/androidInstrumentedTest/res/values/style.xml
diff --git a/compose/ui/ui-unit/build.gradle b/compose/ui/ui-unit/build.gradle
index 8bf9c195..d87b7b3 100644
--- a/compose/ui/ui-unit/build.gradle
+++ b/compose/ui/ui-unit/build.gradle
@@ -51,6 +51,7 @@
         }
 
         jvmMain {
+            dependsOn(commonMain)
             dependencies {
                 implementation(libs.kotlinStdlib)
             }
@@ -79,13 +80,14 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
                 implementation(libs.testExtJunit)
                 implementation(libs.espressoCore)
+                implementation(libs.truth)
                 implementation('androidx.collection:collection-ktx:1.2.0')
             }
         }
@@ -94,7 +96,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.truth)
diff --git a/compose/ui/ui-unit/src/androidAndroidTest/AndroidManifest.xml b/compose/ui/ui-unit/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-unit/src/androidAndroidTest/AndroidManifest.xml
rename to compose/ui/ui-unit/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/ui/ui-unit/src/androidAndroidTest/kotlin/androidx/compose/ui/unit/AndroidDensityTest.kt b/compose/ui/ui-unit/src/androidInstrumentedTest/kotlin/androidx/compose/ui/unit/AndroidDensityTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/androidAndroidTest/kotlin/androidx/compose/ui/unit/AndroidDensityTest.kt
rename to compose/ui/ui-unit/src/androidInstrumentedTest/kotlin/androidx/compose/ui/unit/AndroidDensityTest.kt
diff --git a/compose/ui/ui-unit/src/androidAndroidTest/kotlin/androidx/compose/ui/unit/DpDeviceTest.kt b/compose/ui/ui-unit/src/androidInstrumentedTest/kotlin/androidx/compose/ui/unit/DpDeviceTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/androidAndroidTest/kotlin/androidx/compose/ui/unit/DpDeviceTest.kt
rename to compose/ui/ui-unit/src/androidInstrumentedTest/kotlin/androidx/compose/ui/unit/DpDeviceTest.kt
diff --git a/compose/ui/ui-unit/src/androidAndroidTest/kotlin/androidx/compose/ui/unit/SpDeviceTest.kt b/compose/ui/ui-unit/src/androidInstrumentedTest/kotlin/androidx/compose/ui/unit/SpDeviceTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/androidAndroidTest/kotlin/androidx/compose/ui/unit/SpDeviceTest.kt
rename to compose/ui/ui-unit/src/androidInstrumentedTest/kotlin/androidx/compose/ui/unit/SpDeviceTest.kt
diff --git a/compose/ui/ui-unit/src/androidAndroidTest/kotlin/androidx/compose/ui/unit/fontscaling/FontScaleConverterFactoryTest.kt b/compose/ui/ui-unit/src/androidInstrumentedTest/kotlin/androidx/compose/ui/unit/fontscaling/FontScaleConverterFactoryTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/androidAndroidTest/kotlin/androidx/compose/ui/unit/fontscaling/FontScaleConverterFactoryTest.kt
rename to compose/ui/ui-unit/src/androidInstrumentedTest/kotlin/androidx/compose/ui/unit/fontscaling/FontScaleConverterFactoryTest.kt
diff --git a/compose/ui/ui-unit/src/androidAndroidTest/kotlin/androidx/compose/ui/unit/fontscaling/FontScaleConverterTableTest.kt b/compose/ui/ui-unit/src/androidInstrumentedTest/kotlin/androidx/compose/ui/unit/fontscaling/FontScaleConverterTableTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/androidAndroidTest/kotlin/androidx/compose/ui/unit/fontscaling/FontScaleConverterTableTest.kt
rename to compose/ui/ui-unit/src/androidInstrumentedTest/kotlin/androidx/compose/ui/unit/fontscaling/FontScaleConverterTableTest.kt
diff --git a/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/DensityTest.kt b/compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/DensityTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/DensityTest.kt
rename to compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/DensityTest.kt
diff --git a/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/DpOffsetTest.kt b/compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/DpOffsetTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/DpOffsetTest.kt
rename to compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/DpOffsetTest.kt
diff --git a/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/DpSizeTest.kt b/compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/DpSizeTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/DpSizeTest.kt
rename to compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/DpSizeTest.kt
diff --git a/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/DpTest.kt b/compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/DpTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/DpTest.kt
rename to compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/DpTest.kt
diff --git a/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/IntOffsetTest.kt b/compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/IntOffsetTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/IntOffsetTest.kt
rename to compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/IntOffsetTest.kt
diff --git a/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/IntRectTest.kt b/compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/IntRectTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/IntRectTest.kt
rename to compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/IntRectTest.kt
diff --git a/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/IntSizeTest.kt b/compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/IntSizeTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/IntSizeTest.kt
rename to compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/IntSizeTest.kt
diff --git a/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/TextUnitTest.kt b/compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/TextUnitTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/TextUnitTest.kt
rename to compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/TextUnitTest.kt
diff --git a/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/VelocityTest.kt b/compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/VelocityTest.kt
similarity index 100%
rename from compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/VelocityTest.kt
rename to compose/ui/ui-unit/src/androidUnitTest/kotlin/androidx/compose/ui/unit/VelocityTest.kt
diff --git a/compose/ui/ui-util/build.gradle b/compose/ui/ui-util/build.gradle
index a041871..1f53f9b 100644
--- a/compose/ui/ui-util/build.gradle
+++ b/compose/ui/ui-util/build.gradle
@@ -70,7 +70,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
             }
@@ -80,7 +80,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.truth)
diff --git a/compose/ui/ui-util/src/test/kotlin/androidx/compose/ui/util/InlineClassHelperTest.kt b/compose/ui/ui-util/src/androidUnitTest/kotlin/androidx/compose/ui/util/InlineClassHelperTest.kt
similarity index 100%
rename from compose/ui/ui-util/src/test/kotlin/androidx/compose/ui/util/InlineClassHelperTest.kt
rename to compose/ui/ui-util/src/androidUnitTest/kotlin/androidx/compose/ui/util/InlineClassHelperTest.kt
diff --git a/compose/ui/ui-util/src/test/kotlin/androidx/compose/ui/util/ListUtilsTest.kt b/compose/ui/ui-util/src/androidUnitTest/kotlin/androidx/compose/ui/util/ListUtilsTest.kt
similarity index 100%
rename from compose/ui/ui-util/src/test/kotlin/androidx/compose/ui/util/ListUtilsTest.kt
rename to compose/ui/ui-util/src/androidUnitTest/kotlin/androidx/compose/ui/util/ListUtilsTest.kt
diff --git a/compose/ui/ui-util/src/test/kotlin/androidx/compose/ui/util/MathHelpersTest.kt b/compose/ui/ui-util/src/androidUnitTest/kotlin/androidx/compose/ui/util/MathHelpersTest.kt
similarity index 100%
rename from compose/ui/ui-util/src/test/kotlin/androidx/compose/ui/util/MathHelpersTest.kt
rename to compose/ui/ui-util/src/androidUnitTest/kotlin/androidx/compose/ui/util/MathHelpersTest.kt
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index e2ac70a..e5c14c2 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -609,7 +609,7 @@
   }
 
   public final class FocusRestorerKt {
-    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier focusRestorer(androidx.compose.ui.Modifier);
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier focusRestorer(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function0<androidx.compose.ui.focus.FocusRequester>? onRestoreFailed);
   }
 
   public interface FocusState {
@@ -1872,12 +1872,12 @@
     ctor @Deprecated public PointerInputChange(long id, long uptimeMillis, long position, boolean pressed, long previousUptimeMillis, long previousPosition, boolean previousPressed, androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type);
     ctor public PointerInputChange(long id, long uptimeMillis, long position, boolean pressed, long previousUptimeMillis, long previousPosition, boolean previousPressed, boolean isInitiallyConsumed, optional int type, optional long scrollDelta);
     method public void consume();
-    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional float pressure, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, optional java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical, optional long scrollDelta);
     method public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional float pressure, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, optional long scrollDelta);
     method @Deprecated public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type);
     method @Deprecated public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type, optional long scrollDelta);
     method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical, optional long scrollDelta);
     method public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, optional long scrollDelta);
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional long originalEventPosition, optional boolean currentPressed, optional float pressure, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, optional java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical, optional long scrollDelta);
     method @Deprecated public androidx.compose.ui.input.pointer.ConsumedData getConsumed();
     method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> getHistorical();
     method public long getId();
@@ -2653,6 +2653,36 @@
     method public static void invalidateSemantics(androidx.compose.ui.node.SemanticsModifierNode);
   }
 
+  public interface TraversableNode extends androidx.compose.ui.node.DelegatableNode {
+    method public Object getTraverseKey();
+    property public abstract Object traverseKey;
+    field public static final androidx.compose.ui.node.TraversableNode.Companion Companion;
+  }
+
+  public static final class TraversableNode.Companion {
+  }
+
+  public enum TraversableNode.Companion.VisitSubtreeIfAction {
+    method public static androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+    method public static androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction[] values();
+    enum_constant public static final androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction CancelTraversal;
+    enum_constant public static final androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction SkipSubtree;
+    enum_constant public static final androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction VisitSubtree;
+  }
+
+  public final class TraversableNodeKt {
+    method public static <T extends androidx.compose.ui.node.TraversableNode> T? nearestTraversableAncestor(T);
+    method public static androidx.compose.ui.node.TraversableNode? nearestTraversableAncestorWithKey(androidx.compose.ui.node.DelegatableNode, Object? key);
+    method public static <T extends androidx.compose.ui.node.TraversableNode> void traverseAncestors(T, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> block);
+    method public static void traverseAncestorsWithKey(androidx.compose.ui.node.DelegatableNode, Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.TraversableNode,java.lang.Boolean> block);
+    method public static <T extends androidx.compose.ui.node.TraversableNode> void traverseChildren(T, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> block);
+    method public static void traverseChildrenWithKey(androidx.compose.ui.node.DelegatableNode, Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.TraversableNode,java.lang.Boolean> block);
+    method public static <T extends androidx.compose.ui.node.TraversableNode> void traverseSubtree(T, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> block);
+    method public static <T extends androidx.compose.ui.node.TraversableNode> void traverseSubtreeIf(T, kotlin.jvm.functions.Function1<? super T,? extends androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction> block);
+    method public static void traverseSubtreeIfWithKey(androidx.compose.ui.node.DelegatableNode, Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.TraversableNode,? extends androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction> block);
+    method public static void traverseSubtreeWithKey(androidx.compose.ui.node.DelegatableNode, Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.TraversableNode,java.lang.Boolean> block);
+  }
+
 }
 
 package androidx.compose.ui.platform {
@@ -2681,6 +2711,12 @@
     method public long calculateRecommendedTimeoutMillis(long originalTimeoutMillis, optional boolean containsIcons, optional boolean containsText, optional boolean containsControls);
   }
 
+  public final class AndroidComposeViewAccessibilityDelegateCompat_androidKt {
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static boolean getDisableContentCapture();
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static void setDisableContentCapture(boolean);
+    property @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static final boolean DisableContentCapture;
+  }
+
   public final class AndroidCompositionLocals_androidKt {
     method public static androidx.compose.runtime.ProvidableCompositionLocal<android.content.res.Configuration> getLocalConfiguration();
     method public static androidx.compose.runtime.ProvidableCompositionLocal<android.content.Context> getLocalContext();
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 9866e4c..71da5eb 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -609,7 +609,7 @@
   }
 
   public final class FocusRestorerKt {
-    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier focusRestorer(androidx.compose.ui.Modifier);
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier focusRestorer(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function0<androidx.compose.ui.focus.FocusRequester>? onRestoreFailed);
   }
 
   public interface FocusState {
@@ -1872,12 +1872,12 @@
     ctor @Deprecated public PointerInputChange(long id, long uptimeMillis, long position, boolean pressed, long previousUptimeMillis, long previousPosition, boolean previousPressed, androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type);
     ctor public PointerInputChange(long id, long uptimeMillis, long position, boolean pressed, long previousUptimeMillis, long previousPosition, boolean previousPressed, boolean isInitiallyConsumed, optional int type, optional long scrollDelta);
     method public void consume();
-    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional float pressure, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, optional java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical, optional long scrollDelta);
     method public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional float pressure, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, optional long scrollDelta);
     method @Deprecated public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type);
     method @Deprecated public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type, optional long scrollDelta);
     method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical, optional long scrollDelta);
     method public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, optional long scrollDelta);
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional long originalEventPosition, optional boolean currentPressed, optional float pressure, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, optional java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical, optional long scrollDelta);
     method @Deprecated public androidx.compose.ui.input.pointer.ConsumedData getConsumed();
     method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> getHistorical();
     method public long getId();
@@ -2706,6 +2706,36 @@
     method public static void invalidateSemantics(androidx.compose.ui.node.SemanticsModifierNode);
   }
 
+  public interface TraversableNode extends androidx.compose.ui.node.DelegatableNode {
+    method public Object getTraverseKey();
+    property public abstract Object traverseKey;
+    field public static final androidx.compose.ui.node.TraversableNode.Companion Companion;
+  }
+
+  public static final class TraversableNode.Companion {
+  }
+
+  public enum TraversableNode.Companion.VisitSubtreeIfAction {
+    method public static androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+    method public static androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction[] values();
+    enum_constant public static final androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction CancelTraversal;
+    enum_constant public static final androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction SkipSubtree;
+    enum_constant public static final androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction VisitSubtree;
+  }
+
+  public final class TraversableNodeKt {
+    method public static <T extends androidx.compose.ui.node.TraversableNode> T? nearestTraversableAncestor(T);
+    method public static androidx.compose.ui.node.TraversableNode? nearestTraversableAncestorWithKey(androidx.compose.ui.node.DelegatableNode, Object? key);
+    method public static <T extends androidx.compose.ui.node.TraversableNode> void traverseAncestors(T, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> block);
+    method public static void traverseAncestorsWithKey(androidx.compose.ui.node.DelegatableNode, Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.TraversableNode,java.lang.Boolean> block);
+    method public static <T extends androidx.compose.ui.node.TraversableNode> void traverseChildren(T, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> block);
+    method public static void traverseChildrenWithKey(androidx.compose.ui.node.DelegatableNode, Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.TraversableNode,java.lang.Boolean> block);
+    method public static <T extends androidx.compose.ui.node.TraversableNode> void traverseSubtree(T, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> block);
+    method public static <T extends androidx.compose.ui.node.TraversableNode> void traverseSubtreeIf(T, kotlin.jvm.functions.Function1<? super T,? extends androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction> block);
+    method public static void traverseSubtreeIfWithKey(androidx.compose.ui.node.DelegatableNode, Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.TraversableNode,? extends androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction> block);
+    method public static void traverseSubtreeWithKey(androidx.compose.ui.node.DelegatableNode, Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.TraversableNode,java.lang.Boolean> block);
+  }
+
 }
 
 package androidx.compose.ui.platform {
@@ -2734,6 +2764,12 @@
     method public long calculateRecommendedTimeoutMillis(long originalTimeoutMillis, optional boolean containsIcons, optional boolean containsText, optional boolean containsControls);
   }
 
+  public final class AndroidComposeViewAccessibilityDelegateCompat_androidKt {
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static boolean getDisableContentCapture();
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static void setDisableContentCapture(boolean);
+    property @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static final boolean DisableContentCapture;
+  }
+
   public final class AndroidCompositionLocals_androidKt {
     method public static androidx.compose.runtime.ProvidableCompositionLocal<android.content.res.Configuration> getLocalConfiguration();
     method public static androidx.compose.runtime.ProvidableCompositionLocal<android.content.Context> getLocalContext();
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/graphics/vector/CreateVectorPainterBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/graphics/vector/CreateVectorPainterBenchmark.kt
new file mode 100644
index 0000000..00182f8
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/graphics/vector/CreateVectorPainterBenchmark.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.ui.benchmark.graphics.vector
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.ComposeTestCase
+import androidx.compose.testutils.ToggleableTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.toggleStateBenchmarkDraw
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.benchmark.R
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class CreateVectorPainterBenchmark {
+
+    @get:Rule
+    val benchmarkRule = ComposeBenchmarkRule()
+
+    @Test
+    fun recreateContent() {
+        benchmarkRule.toggleStateBenchmarkDraw({
+            RecreateVectorPainterTestCase()
+        }, assertOneRecomposition = false)
+    }
+
+    @Test
+    fun renderVectorWithDifferentSizes() {
+        benchmarkRule.toggleStateBenchmarkDraw({
+            ResizeVectorPainter()
+        }, assertOneRecomposition = false)
+    }
+}
+
+private class RecreateVectorPainterTestCase : ComposeTestCase, ToggleableTestCase {
+
+    private var alpha by mutableStateOf(1f)
+
+    @Composable
+    override fun Content() {
+        Column {
+            Box(modifier = Modifier.wrapContentSize()) {
+                Image(
+                    painter = painterResource(R.drawable.ic_hourglass),
+                    contentDescription = null,
+                    modifier = Modifier.size(200.dp),
+                    alpha = alpha
+                )
+            }
+        }
+    }
+
+    override fun toggleState() {
+        if (alpha == 1.0f) {
+            alpha = 0.5f
+        } else {
+            alpha = 1.0f
+        }
+    }
+}
+
+private class ResizeVectorPainter : ComposeTestCase, ToggleableTestCase {
+
+    private var alpha by mutableStateOf(1f)
+
+    @Composable
+    override fun Content() {
+        Column {
+            Box(modifier = Modifier.wrapContentSize()) {
+                Image(
+                    painter = painterResource(R.drawable.ic_hourglass),
+                    contentDescription = null,
+                    modifier = Modifier.size(100.dp),
+                    alpha = alpha
+                )
+            }
+
+            Box(modifier = Modifier.wrapContentSize()) {
+                Image(
+                    painter = painterResource(R.drawable.ic_hourglass),
+                    contentDescription = null,
+                    modifier = Modifier.size(200.dp),
+                    alpha = alpha
+                )
+            }
+        }
+    }
+
+    override fun toggleState() {
+        if (alpha == 1.0f) {
+            alpha = 0.5f
+        } else {
+            alpha = 1.0f
+        }
+    }
+}
diff --git a/compose/ui/ui/benchmark/src/main/res/drawable/ic_hourglass.xml b/compose/ui/ui/benchmark/src/main/res/drawable/ic_hourglass.xml
new file mode 100644
index 0000000..1666c76
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/main/res/drawable/ic_hourglass.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24" >
+    <group
+        android:name="hourglass_frame"
+        android:translateX="12"
+        android:translateY="12"
+        android:scaleX="0.75"
+        android:scaleY="0.75" >
+        <group
+            android:name="hourglass_frame_pivot"
+            android:translateX="-12"
+            android:translateY="-12" >
+            <group
+                android:name="group_2_2"
+                android:translateX="12"
+                android:translateY="6.5" >
+                <path
+                    android:name="path_2_2"
+                    android:pathData="M 6.52099609375 -3.89300537109 c 0.0 0.0 -6.52099609375 6.87901306152 -6.52099609375 6.87901306152 c 0 0.0 -6.52099609375 -6.87901306152 -6.52099609375 -6.87901306152 c 0.0 0.0 13.0419921875 0.0 13.0419921875 0.0 Z M 9.99800109863 -6.5 c 0.0 0.0 -19.9960021973 0.0 -19.9960021973 0.0 c -0.890991210938 0.0 -1.33700561523 1.07699584961 -0.707000732422 1.70700073242 c 0.0 0.0 10.7050018311 11.2929992676 10.7050018311 11.2929992676 c 0 0.0 10.7050018311 -11.2929992676 10.7050018311 -11.2929992676 c 0.630004882812 -0.630004882812 0.183990478516 -1.70700073242 -0.707000732422 -1.70700073242 Z"
+                    android:fillColor="#FF777777" />
+            </group>
+            <group
+                android:name="group_1_2"
+                android:translateX="12"
+                android:translateY="17.5" >
+                <path
+                    android:name="path_2_1"
+                    android:pathData="M 0 -2.98600769043 c 0 0.0 6.52099609375 6.87901306152 6.52099609375 6.87901306152 c 0.0 0.0 -13.0419921875 0.0 -13.0419921875 0.0 c 0.0 0.0 6.52099609375 -6.87901306152 6.52099609375 -6.87901306152 Z M 0 -6.5 c 0 0.0 -10.7050018311 11.2929992676 -10.7050018311 11.2929992676 c -0.630004882812 0.630004882812 -0.184005737305 1.70700073242 0.707000732422 1.70700073242 c 0.0 0.0 19.9960021973 0.0 19.9960021973 0.0 c 0.890991210938 0.0 1.33699035645 -1.07699584961 0.707000732422 -1.70700073242 c 0.0 0.0 -10.7050018311 -11.2929992676 -10.7050018311 -11.2929992676 Z"
+                    android:fillColor="#FF777777" />
+            </group>
+        </group>
+    </group>
+    <group
+        android:name="fill_outlines"
+        android:translateX="12"
+        android:translateY="12"
+        android:scaleX="0.75"
+        android:scaleY="0.75" >
+        <group
+            android:name="fill_outlines_pivot"
+            android:translateX="-12"
+            android:translateY="-12" >
+            <clip-path
+                android:name="mask_1"
+                android:pathData="M 24 13.3999938965 c 0 0.0 -24 0.0 -24 0.0 c 0 0.0 0 10.6000061035 0 10.6000061035 c 0 0 24 0 24 0 c 0 0 0 -10.6000061035 0 -10.6000061035 Z" />
+            <group
+                android:name="group_1_3"
+                android:translateX="12"
+                android:translateY="12" >
+                <path
+                    android:name="path_1_6"
+                    android:pathData="M 10.7100067139 10.2900085449 c 0.629989624023 0.629989624023 0.179992675781 1.70999145508 -0.710006713867 1.70999145508 c 0 0 -20 0 -20 0 c -0.889999389648 0 -1.33999633789 -1.08000183105 -0.710006713867 -1.70999145508 c 0.0 0.0 9.76000976562 -10.2900085449 9.76000976563 -10.2900085449 c 0.0 0 -9.76000976562 -10.2899932861 -9.76000976563 -10.2899932861 c -0.629989624023 -0.630004882812 -0.179992675781 -1.71000671387 0.710006713867 -1.71000671387 c 0 0 20 0 20 0 c 0.889999389648 0 1.33999633789 1.08000183105 0.710006713867 1.71000671387 c 0.0 0.0 -9.76000976562 10.2899932861 -9.76000976563 10.2899932861 c 0.0 0 9.76000976562 10.2900085449 9.76000976563 10.2900085449 Z"
+                    android:fillColor="#FF777777" />
+            </group>
+        </group>
+    </group>
+</vector>
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index f88d2c1..f78c397 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -116,7 +116,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation("androidx.fragment:fragment:1.3.0")
@@ -155,7 +155,7 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
@@ -163,6 +163,7 @@
                 implementation(libs.kotlinCoroutinesTest)
                 implementation(libs.junit)
                 implementation(libs.truth)
+                implementation(libs.kotlinTest)
                 implementation(project(":compose:ui:ui-test-junit4"))
                 implementation(project(":internal-testutils-fonts"))
                 implementation(project(":compose:test-utils"))
@@ -210,7 +211,7 @@
     lintChecks(project(":compose:ui:ui-lint"))
     lintPublish(project(":compose:ui:ui-lint"))
 
-    // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
+    // Can't declare this in kotlin { sourceSets { androidUnitTest.dependencies { .. } } } as that
     // leaks into instrumented tests (b/214407011)
     testImplementation(libs.robolectric)
     testImplementation(libs.mockitoCore4)
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/viewinterop/ViewInterop.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/viewinterop/ViewInterop.kt
index 2ea6de9..22effa7 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/viewinterop/ViewInterop.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/viewinterop/ViewInterop.kt
@@ -16,16 +16,27 @@
 
 package androidx.compose.ui.demos.viewinterop
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Paint
 import android.graphics.Rect
+import android.os.Build
 import android.view.View
 import android.view.ViewGroup
+import android.widget.LinearLayout
 import android.widget.TextView
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.Button
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
@@ -38,9 +49,11 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.node.Ref
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.compose.ui.viewinterop.AndroidViewBinding
 
+@SuppressLint("ResourceType")
 @Composable
 fun ViewInteropDemo() {
     Column {
@@ -85,6 +98,62 @@
         ) {
             Text("Change color of Android view")
         }
+
+        Column(modifier =
+            Modifier.fillMaxWidth().height(100.dp).verticalScroll(rememberScrollState())
+        ) {
+            AndroidView({ c ->
+                LinearLayout(c).apply {
+                    val text1 = TextView(c).apply { text = "LinearLayout child 1"; id = 11 }
+                    val text2 = TextView(c).apply { text = "LinearLayout child 2"; id = 22 }
+                    val text3 = TextView(c).apply { text = "LinearLayout child 3"; id = 33 }
+                    if (Build.VERSION.SDK_INT >= 26) {
+                        Api26Impl.setAccessibilityTraversalAfter(text3, text2.getId());
+                        Api26Impl.setAccessibilityTraversalAfter(text2, text1.getId());
+                    }
+                    addView(text3)
+                    addView(text2)
+                    addView(text1)
+                }
+            })
+        }
+
+        Spacer(Modifier.height(20.dp))
+
+        LazyColumn(
+            modifier = Modifier.fillMaxWidth().height(50.dp)
+        ) {
+            item {
+                AndroidView(::TextView) { it.text = "TextView in LazyColumn 1A" }
+                AndroidView(::TextView) { it.text = "TextView in LazyColumn 1B" }
+            }
+            item {
+                AndroidView(::TextView) { it.text = "TextView in LazyColumn 2A" }
+                AndroidView(::TextView) { it.text = "TextView in LazyColumn 2B" }
+            }
+            item {
+                AndroidView(::TextView) { it.text = "TextView in LazyColumn 3A" }
+                AndroidView(::TextView) { it.text = "TextView in LazyColumn 3B" }
+            }
+            item {
+                AndroidView(::TextView) { it.text = "TextView in LazyColumn 4A" }
+                AndroidView(::TextView) { it.text = "TextView in LazyColumn 4B" }
+            }
+        }
+        Spacer(Modifier.height(20.dp))
+        Column(
+            modifier = Modifier.fillMaxWidth().height(50.dp).verticalScroll(rememberScrollState())
+        ) {
+            AndroidView(::TextView) { it.text = "TextView in verticalScroll 1" }
+            AndroidView(::TextView) { it.text = "TextView in verticalScroll 2" }
+            AndroidView(::TextView) { it.text = "TextView in verticalScroll 3" }
+            AndroidView(::TextView) { it.text = "TextView in verticalScroll 4" }
+            AndroidView(::TextView) { it.text = "TextView in verticalScroll 5" }
+            AndroidView(::TextView) { it.text = "TextView in verticalScroll 6" }
+            AndroidView(::TextView) { it.text = "TextView in verticalScroll 7" }
+            AndroidView(::TextView) { it.text = "TextView in verticalScroll 8" }
+            AndroidView(::TextView) { it.text = "TextView in verticalScroll 9" }
+        }
     }
 }
 
@@ -120,3 +189,15 @@
         canvas.drawRect(rect, paint)
     }
 }
+
+@RequiresApi(Build.VERSION_CODES.O)
+private object Api26Impl {
+    @DoNotInline
+    @JvmStatic
+    fun setAccessibilityTraversalAfter(
+        view: View,
+        id: Int
+    ) {
+        view.setAccessibilityTraversalAfter(id);
+    }
+}
diff --git a/compose/ui/ui/lint-baseline.xml b/compose/ui/ui/lint-baseline.xml
index cfa3fd2..8dadbf5 100644
--- a/compose/ui/ui/lint-baseline.xml
+++ b/compose/ui/ui/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="cli" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="cli" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="BanHideTag"
@@ -16,7 +16,7 @@
         errorLine1="                Thread.sleep(sleepTime)"
         errorLine2="                       ~~~~~">
         <location
-            file="src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt"/>
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt"/>
     </issue>
 
     <issue
@@ -130,15 +130,6 @@
     <issue
         id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in variable &apos;scrollAction&apos; with type AccessibilityAction&lt;Function2&lt;? super Float, ? super Float, ? extends Boolean>>."
-        errorLine1="                val scrollAction ="
-        errorLine2="                ^">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInLambda"
-        message="Use a functional interface instead of lambda syntax for lambdas with primitive values in variable &apos;scrollAction&apos; with type AccessibilityAction&lt;Function2&lt;? super Float, ? super Float, ? extends Boolean>>."
         errorLine1="                var scrollAction = scrollableAncestor?.config?.getOrNull(SemanticsActions.ScrollBy)"
         errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt
index 69035e5..4041e63 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt
@@ -134,6 +134,27 @@
     }
 }
 
+@OptIn(ExperimentalComposeUiApi::class)
+@Sampled
+@Composable
+fun FocusRestorerCustomFallbackSample() {
+    val focusRequester = remember { FocusRequester() }
+    LazyRow(
+        // If restoration fails, focus would fallback to the item associated with focusRequester.
+        Modifier.focusRestorer { focusRequester }
+    ) {
+        item {
+            Button(
+                modifier = Modifier.focusRequester(focusRequester),
+                onClick = {}
+            ) { Text("1") }
+        }
+        item { Button(onClick = {}) { Text("2") } }
+        item { Button(onClick = {}) { Text("3") } }
+        item { Button(onClick = {}) { Text("4") } }
+    }
+}
+
 @Sampled
 @Composable
 fun RequestFocusSample() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
deleted file mode 100644
index 0a41454..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
+++ /dev/null
@@ -1,826 +0,0 @@
-/*
- * Copyright 2020 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.draw
-
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.testutils.assertPixels
-import androidx.compose.ui.AtLeastSize
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.BlendMode
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Path
-import androidx.compose.ui.graphics.asAndroidBitmap
-import androidx.compose.ui.graphics.drawscope.ContentDrawScope
-import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.LayoutModifier
-import androidx.compose.ui.layout.LayoutModifierImpl
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.node.DelegatingNode
-import androidx.compose.ui.platform.InspectableValue
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.elementFor
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Assert.assertEquals
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class DrawModifierTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Before
-    fun before() {
-        isDebugInspectorInfoEnabled = true
-    }
-
-    @After
-    fun after() {
-        isDebugInspectorInfoEnabled = false
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testCacheHitWithStateChange() {
-        // Verify that a state change outside of the cache block does not
-        // require the cache block to be invalidated
-        val testTag = "testTag"
-        var cacheBuildCount = 0
-        val size = 200
-        rule.setContent {
-            var rectColor by remember { mutableStateOf(Color.Blue) }
-            AtLeastSize(
-                size = size,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .drawWithCache {
-                        val drawSize = this.size
-                        val path = Path().apply {
-                            lineTo(drawSize.width / 2f, 0f)
-                            lineTo(drawSize.width / 2f, drawSize.height)
-                            lineTo(0f, drawSize.height)
-                            close()
-                        }
-                        cacheBuildCount++
-                        onDrawBehind {
-                            drawRect(rectColor)
-                            drawPath(path, Color.Red)
-                        }
-                    }
-                    .clickable {
-                        if (rectColor == Color.Blue) {
-                            rectColor = Color.Green
-                        } else {
-                            rectColor = Color.Blue
-                        }
-                    }
-            ) { }
-        }
-
-        rule.onNodeWithTag(testTag).apply {
-            // Verify that the path was created only once
-            assertEquals(1, cacheBuildCount)
-            captureToBitmap().apply {
-                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
-                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 2, 1))
-                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 2, height / 2 - 2))
-                assertEquals(Color.Red.toArgb(), getPixel(1, height / 2 - 2))
-
-                assertEquals(Color.Blue.toArgb(), getPixel(width / 2 + 1, 1))
-                assertEquals(Color.Blue.toArgb(), getPixel(width - 2, 1))
-                assertEquals(Color.Blue.toArgb(), getPixel(width / 2 + 1, height - 2))
-                assertEquals(Color.Blue.toArgb(), getPixel(width - 2, height - 2))
-            }
-            performClick()
-        }
-
-        rule.waitForIdle()
-
-        rule.onNodeWithTag(testTag).apply {
-            // Verify that the path was re-used and only built once
-            assertEquals(1, cacheBuildCount)
-            captureToBitmap().apply {
-                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
-                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 2, 1))
-                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 2, height / 2 - 1))
-                assertEquals(Color.Red.toArgb(), getPixel(1, height / 2 - 2))
-
-                assertEquals(Color.Green.toArgb(), getPixel(width / 2 + 1, 1))
-                assertEquals(Color.Green.toArgb(), getPixel(width - 2, 1))
-                assertEquals(Color.Green.toArgb(), getPixel(width / 2 + 1, height - 2))
-                assertEquals(Color.Green.toArgb(), getPixel(width - 2, height - 2))
-            }
-        }
-    }
-
-    @Test
-    fun invalidationForDrawWithCache() {
-        var size by mutableStateOf(10f)
-        var drawCount = 0
-        rule.setContent {
-            Box(Modifier.fillMaxSize()) {
-                Box(Modifier
-                    .graphicsLayer { }
-                    .size(50.dp)
-                    .drawWithCache {
-                        val rectSize = Size(size, size)
-                        onDrawBehind {
-                            drawRect(Color.Blue, Offset.Zero, rectSize)
-                            drawCount++
-                        }
-                    }
-                    .graphicsLayer { }
-                )
-            }
-        }
-        rule.waitForIdle()
-        assertThat(drawCount).isEqualTo(1)
-
-        size = 15f
-        rule.waitForIdle()
-        assertThat(drawCount).isEqualTo(2)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testCacheInvalidatedAfterStateChange() {
-        // Verify that a state change within the cache block does
-        // require the cache block to be invalidated
-        val testTag = "testTag"
-        var cacheBuildCount = 0
-        val size = 200
-
-        rule.setContent {
-            var pathFillBounds by remember { mutableStateOf(false) }
-            AtLeastSize(
-                size = size,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .drawWithCache {
-                        val pathSize = if (pathFillBounds) this.size else this.size / 2f
-                        val path = Path().apply {
-                            lineTo(pathSize.width, 0f)
-                            lineTo(pathSize.width, pathSize.height)
-                            lineTo(0f, pathSize.height)
-                            close()
-                        }
-                        cacheBuildCount++
-                        onDrawBehind {
-                            drawRect(Color.Red)
-                            drawPath(path, Color.Blue)
-                        }
-                    }
-                    .clickable {
-                        pathFillBounds = !pathFillBounds
-                    }
-            ) { }
-        }
-
-        rule.onNodeWithTag(testTag).apply {
-            // Verify that the path was created only once
-            assertEquals(1, cacheBuildCount)
-            captureToBitmap().apply {
-                assertEquals(Color.Blue.toArgb(), getPixel(1, 1))
-                assertEquals(Color.Blue.toArgb(), getPixel(width / 2 - 2, 1))
-                assertEquals(Color.Blue.toArgb(), getPixel(width / 2 - 2, height / 2 - 2))
-                assertEquals(Color.Blue.toArgb(), getPixel(1, height / 2 - 1))
-
-                assertEquals(Color.Red.toArgb(), getPixel(width / 2 + 1, 1))
-                assertEquals(Color.Red.toArgb(), getPixel(width / 2 + 1, height / 2 - 1))
-                assertEquals(Color.Red.toArgb(), getPixel(width / 2 + 1, height / 2 - 2))
-                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 2, height / 2 + 1))
-                assertEquals(Color.Red.toArgb(), getPixel(1, height / 2 + 1))
-
-                assertEquals(Color.Red.toArgb(), getPixel(1, height - 2))
-                assertEquals(Color.Red.toArgb(), getPixel(width - 2, 1))
-                assertEquals(Color.Red.toArgb(), getPixel(width - 2, height - 2))
-            }
-            performClick()
-        }
-
-        rule.waitForIdle()
-
-        rule.onNodeWithTag(testTag).apply {
-            assertEquals(2, cacheBuildCount)
-            captureToBitmap().apply {
-                assertEquals(Color.Blue.toArgb(), getPixel(1, 1))
-                assertEquals(Color.Blue.toArgb(), getPixel(size - 2, 1))
-                assertEquals(Color.Blue.toArgb(), getPixel(size - 2, size - 2))
-                assertEquals(Color.Blue.toArgb(), getPixel(1, size - 2))
-            }
-        }
-    }
-
-    @Test
-    fun combinedModifiers_drawingSizesAreUsingTheSizeDefinedByLayoutModifier() {
-        var drawingSize: Size = Size.Unspecified
-        var drawingCacheSize: Size = Size.Unspecified
-        val modifier = object : LayoutModifier, DrawCacheModifier {
-            override fun onBuildCache(params: BuildDrawCacheParams) {
-                drawingCacheSize = params.size
-            }
-
-            override fun ContentDrawScope.draw() {
-                drawingSize = size
-            }
-
-            override fun MeasureScope.measure(
-                measurable: Measurable,
-                constraints: Constraints
-            ): MeasureResult {
-                val placeable = measurable.measure(Constraints.fixed(10, 10))
-                return layout(20, 20) {
-                    placeable.place(0, 0)
-                }
-            }
-        }
-        rule.setContent {
-            Box(modifier)
-        }
-
-        rule.runOnIdle {
-            val expectedSize = Size(10f, 10f)
-            assertThat(drawingSize).isEqualTo(expectedSize)
-            assertThat(drawingCacheSize).isEqualTo(expectedSize)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testCacheInvalidatedAfterSizeChange() {
-        // Verify that a size change does cause the cache block to be invalidated
-        val testTag = "testTag"
-        var cacheBuildCount = 0
-        val startSize = 200
-        val endSize = 400
-        rule.setContent {
-            var size by remember { mutableStateOf(startSize) }
-            AtLeastSize(
-                size = size,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .drawWithCache {
-                        val drawSize = this.size
-                        val path = Path().apply {
-                            lineTo(drawSize.width, 0f)
-                            lineTo(drawSize.height, drawSize.height)
-                            lineTo(0f, drawSize.height)
-                            close()
-                        }
-                        cacheBuildCount++
-                        onDrawBehind {
-                            drawPath(path, Color.Red)
-                        }
-                    }
-                    .clickable {
-                        if (size == startSize) {
-                            size = endSize
-                        } else {
-                            size = startSize
-                        }
-                    }
-            ) { }
-        }
-
-        rule.onNodeWithTag(testTag).apply {
-            // Verify that the path was created only once
-            assertEquals(1, cacheBuildCount)
-            captureToBitmap().apply {
-                assertEquals(startSize, this.width)
-                assertEquals(startSize, this.height)
-                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
-                assertEquals(Color.Red.toArgb(), getPixel(width - 2, height - 2))
-            }
-            performClick()
-        }
-
-        rule.waitForIdle()
-
-        rule.onNodeWithTag(testTag).apply {
-            // Verify that the path was re-used and only built once
-            assertEquals(2, cacheBuildCount)
-            captureToBitmap().apply {
-                assertEquals(endSize, this.width)
-                assertEquals(endSize, this.height)
-                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
-                assertEquals(Color.Red.toArgb(), getPixel(width - 2, height - 2))
-            }
-        }
-    }
-
-    @Test
-    fun testCacheInvalidatedAfterLayoutDirectionChange() {
-        var layoutDirection by mutableStateOf(LayoutDirection.Ltr)
-        var realLayoutDirection: LayoutDirection? = null
-        rule.setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
-                AtLeastSize(
-                    size = 10,
-                    modifier = Modifier.drawWithCache {
-                        realLayoutDirection = layoutDirection
-                        onDrawBehind {}
-                    }
-                ) { }
-            }
-        }
-
-        rule.runOnIdle {
-            assertEquals(LayoutDirection.Ltr, realLayoutDirection)
-            layoutDirection = LayoutDirection.Rtl
-        }
-
-        rule.runOnIdle {
-            assertEquals(LayoutDirection.Rtl, realLayoutDirection)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testCacheInvalidatedWithHelperModifier() {
-        // If Modifier.drawWithCache is used as part of the implementation for another modifier
-        // defined in a helper function, make sure that an change in state parameter ends up calling
-        // ModifiedDrawNode.onModifierChanged and updates the internal cache for
-        // Modifier.drawWithCache
-        val testTag = "testTag"
-        val startSize = 200
-        rule.setContent {
-            val color = remember { mutableStateOf(Color.Red) }
-            AtLeastSize(
-                size = startSize,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .drawPathHelperModifier(color.value)
-                    .clickable {
-                        if (color.value == Color.Red) {
-                            color.value = Color.Blue
-                        } else {
-                            color.value = Color.Red
-                        }
-                    }
-            ) { }
-        }
-
-        rule.onNodeWithTag(testTag).apply {
-            // Verify that the path was created only once
-            captureToBitmap().apply {
-                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
-                assertEquals(Color.Red.toArgb(), getPixel(width - 2, height - 2))
-            }
-            performClick()
-        }
-
-        rule.waitForIdle()
-
-        rule.onNodeWithTag(testTag).apply {
-            // Verify that the path was re-used and only built once
-            captureToBitmap().apply {
-                assertEquals(Color.Blue.toArgb(), getPixel(1, 1))
-                assertEquals(Color.Blue.toArgb(), getPixel(width - 2, height - 2))
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testGraphicsLayerCacheInvalidatedAfterStateChange() {
-        // Verify that a state change within the cache block does
-        // require the cache block to be invalidated if a graphicsLayer is also
-        // configured on the composable and the state parameter is configured elsewhere
-        val boxTag = "boxTag"
-        val clickTag = "clickTag"
-
-        var cacheBuildCount = 0
-
-        rule.setContent {
-            val flag = remember { mutableStateOf(false) }
-            Column {
-                AtLeastSize(
-                    size = 50,
-                    modifier = Modifier
-                        .testTag(boxTag)
-                        .graphicsLayer()
-                        .drawWithCache {
-                            // State read of flag
-                            val color = if (flag.value) Color.Red else Color.Blue
-                            cacheBuildCount++
-
-                            onDrawBehind {
-                                drawRect(color)
-                            }
-                        }
-                )
-
-                Box(
-                    Modifier
-                        .testTag(clickTag)
-                        .size(20.dp)
-                        .clickable {
-                            flag.value = !flag.value
-                        }
-                )
-            }
-        }
-
-        rule.onNodeWithTag(boxTag).apply {
-            // Verify that the cache lambda was invoked once
-            assertEquals(1, cacheBuildCount)
-            captureToImage().assertPixels { Color.Blue }
-        }
-
-        rule.onNodeWithTag(clickTag).performClick()
-
-        rule.waitForIdle()
-
-        rule.onNodeWithTag(boxTag).apply {
-            // Verify the cache lambda was invoked again and the
-            // rect is drawn with the updated color
-            assertEquals(2, cacheBuildCount)
-            captureToImage().assertPixels { Color.Red }
-        }
-    }
-
-    // Helper Modifier that uses Modifier.drawWithCache internally. If the color
-    // parameter
-    private fun Modifier.drawPathHelperModifier(color: Color) =
-        this.then(
-            Modifier.drawWithCache {
-                val drawSize = this.size
-                val path = Path().apply {
-                    lineTo(drawSize.width, 0f)
-                    lineTo(drawSize.height, drawSize.height)
-                    lineTo(0f, drawSize.height)
-                    close()
-                }
-                onDrawBehind {
-                    drawPath(path, color)
-                }
-            }
-        )
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testDrawWithCacheContentDrawnImplicitly() {
-        // Verify that drawContent is invoked even if it is not explicitly called within
-        // the implementation of the callback provided in the onDraw method
-        // in Modifier.drawWithCache
-        val testTag = "testTag"
-        val testSize = 200
-        rule.setContent {
-            AtLeastSize(
-                size = testSize,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .drawWithCache {
-                        onDrawBehind {
-                            drawRect(Color.Red, size = Size(size.width / 2, size.height))
-                        }
-                    }
-                    .background(Color.Blue)
-            )
-        }
-
-        rule.onNodeWithTag(testTag).apply {
-            captureToBitmap().apply {
-                assertEquals(Color.Blue.toArgb(), getPixel(0, 0))
-                assertEquals(Color.Blue.toArgb(), getPixel(width - 1, 0))
-                assertEquals(Color.Blue.toArgb(), getPixel(width - 1, height - 1))
-                assertEquals(Color.Blue.toArgb(), getPixel(0, height - 1))
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testDrawWithCacheOverContent() {
-        // Verify that drawContent is not invoked implicitly if it is explicitly called within
-        // the implementation of the callback provided in the onDraw method
-        // in Modifier.drawWithCache. That is the red rectangle is drawn above the contents
-        val testTag = "testTag"
-        val testSize = 200
-        rule.setContent {
-            AtLeastSize(
-                size = testSize,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .drawWithCache {
-                        onDrawWithContent {
-                            drawContent()
-                            drawRect(Color.Red, size = Size(size.width / 2, size.height))
-                        }
-                    }
-                    .background(Color.Blue)
-            )
-        }
-
-        rule.onNodeWithTag(testTag).apply {
-            captureToBitmap().apply {
-                assertEquals(Color.Blue.toArgb(), getPixel(width / 2 + 1, 0))
-                assertEquals(Color.Blue.toArgb(), getPixel(width - 1, 0))
-                assertEquals(Color.Blue.toArgb(), getPixel(width - 1, height - 1))
-                assertEquals(Color.Blue.toArgb(), getPixel(width / 2 + 1, height - 1))
-
-                assertEquals(Color.Red.toArgb(), getPixel(0, 0))
-                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 1, 0))
-                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 1, height - 1))
-                assertEquals(Color.Red.toArgb(), getPixel(0, height - 1))
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testDrawWithCacheBlendsContent() {
-        // Verify that the drawing commands of drawContent are blended against the green
-        // rectangle with the specified BlendMode
-        val testTag = "testTag"
-        val testSize = 200
-        rule.setContent {
-            AtLeastSize(
-                size = testSize,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .drawWithCache {
-                        onDrawWithContent {
-                            drawContent()
-                            drawRect(Color.Green, blendMode = BlendMode.Plus)
-                        }
-                    }
-                    .background(Color.Blue)
-            )
-        }
-
-        rule.onNodeWithTag(testTag).apply {
-            captureToBitmap().apply {
-                assertEquals(Color.Cyan.toArgb(), getPixel(0, 0))
-                assertEquals(Color.Cyan.toArgb(), getPixel(width - 1, 0))
-                assertEquals(Color.Cyan.toArgb(), getPixel(width - 1, height - 1))
-                assertEquals(Color.Cyan.toArgb(), getPixel(0, height - 1))
-            }
-        }
-    }
-
-    @Test
-    fun testInspectorValueForDrawBehind() {
-        val onDraw: DrawScope.() -> Unit = {}
-        rule.setContent {
-            val modifier = Modifier.drawBehind(onDraw) as InspectableValue
-            assertThat(modifier.nameFallback).isEqualTo("drawBehind")
-            assertThat(modifier.valueOverride).isNull()
-            assertThat(modifier.inspectableElements.map { it.name }.asIterable())
-                .containsExactly("onDraw")
-        }
-    }
-
-    @Test
-    fun testInspectorValueForDrawWithCache() {
-        val onBuildDrawCache: CacheDrawScope.() -> DrawResult = { DrawResult {} }
-        rule.setContent {
-            val modifier = Modifier.drawWithCache(onBuildDrawCache) as InspectableValue
-            assertThat(modifier.nameFallback).isEqualTo("drawWithCache")
-            assertThat(modifier.valueOverride).isNull()
-            assertThat(modifier.inspectableElements.map { it.name }.asIterable())
-                .containsExactly("onBuildDrawCache")
-        }
-    }
-
-    @Test
-    fun testInspectorValueForDrawWithContent() {
-        val onDraw: DrawScope.() -> Unit = {}
-        rule.setContent {
-            val modifier = Modifier.drawWithContent(onDraw) as InspectableValue
-            assertThat(modifier.nameFallback).isEqualTo("drawWithContent")
-            assertThat(modifier.valueOverride).isNull()
-            assertThat(modifier.inspectableElements.map { it.name }.asIterable())
-                .containsExactly("onDraw")
-        }
-    }
-
-    @Test
-    fun recompositionWithTheSameDrawBehindLambdaIsNotTriggeringRedraw() {
-        val recompositionCounter = mutableStateOf(0)
-        var redrawCounter = 0
-        val drawBlock: DrawScope.() -> Unit = {
-            redrawCounter++
-        }
-        rule.setContent {
-            recompositionCounter.value
-            Layout({}, modifier = Modifier.drawBehind(drawBlock)) { _, _ ->
-                layout(100, 100) {}
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(redrawCounter).isEqualTo(1)
-            recompositionCounter.value = 1
-        }
-
-        rule.runOnIdle {
-            assertThat(redrawCounter).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun recompositionWithTheSameDrawWithContentLambdaIsNotTriggeringRedraw() {
-        val recompositionCounter = mutableStateOf(0)
-        var redrawCounter = 0
-        val drawBlock: ContentDrawScope.() -> Unit = {
-            redrawCounter++
-        }
-        rule.setContent {
-            recompositionCounter.value
-            Layout({}, modifier = Modifier.drawWithContent(drawBlock)) { _, _ ->
-                layout(100, 100) {}
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(redrawCounter).isEqualTo(1)
-            recompositionCounter.value = 1
-        }
-
-        rule.runOnIdle {
-            assertThat(redrawCounter).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun recompositionWithTheSameDrawWithCacheLambdaIsNotTriggeringRedraw() {
-        val recompositionCounter = mutableStateOf(0)
-        var cacheRebuildCounter = 0
-        var redrawCounter = 0
-        val drawBlock: CacheDrawScope.() -> DrawResult = {
-            cacheRebuildCounter++
-            onDrawBehind {
-                redrawCounter++
-            }
-        }
-        rule.setContent {
-            recompositionCounter.value
-            Layout({}, modifier = Modifier.drawWithCache(drawBlock)) { _, _ ->
-                layout(100, 100) {}
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(cacheRebuildCounter).isEqualTo(1)
-            assertThat(redrawCounter).isEqualTo(1)
-            recompositionCounter.value = 1
-        }
-
-        rule.runOnIdle {
-            assertThat(cacheRebuildCounter).isEqualTo(1)
-            assertThat(redrawCounter).isEqualTo(1)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testDelegatedDrawNodesDraw() {
-        val testTag = "testTag"
-        val size = 200
-
-        val node = object : DelegatingNode() {
-            val draw = delegate(DrawBackgroundModifier {
-                drawRect(Color.Red)
-            })
-        }
-
-        rule.setContent {
-            AtLeastSize(
-                size = size,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .elementFor(node)
-            ) { }
-        }
-
-        rule.onNodeWithTag(testTag).apply {
-            captureToBitmap().apply {
-                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testMultipleDelegatedDrawNodes() {
-        val testTag = "testTag"
-
-        val node = object : DelegatingNode() {
-            val a = delegate(DrawBackgroundModifier {
-                drawRect(
-                    Color.Red,
-                    size = Size(10f, 10f)
-                )
-            })
-
-            val b = delegate(DrawBackgroundModifier {
-                drawRect(
-                    Color.Blue,
-                    topLeft = Offset(10f, 0f),
-                    size = Size(10f, 10f))
-            })
-        }
-
-        rule.setContent {
-            AtLeastSize(
-                size = 200,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .elementFor(node)
-            ) { }
-        }
-
-        rule.onNodeWithTag(testTag).apply {
-            captureToBitmap().apply {
-                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
-                assertEquals(Color.Blue.toArgb(), getPixel(11, 1))
-            }
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testDelegatedLayoutModifierNode() {
-        val testTag = "testTag"
-
-        val node = object : DelegatingNode() {
-            val a = delegate(LayoutModifierImpl { measurable, constraints ->
-                val p = measurable.measure(constraints)
-                layout(10.dp.roundToPx(), 10.dp.roundToPx()) {
-                    p.place(0, 0)
-                }
-            })
-        }
-
-        rule.setContent {
-            Box(
-                modifier = Modifier
-                    .testTag(testTag)
-                    .elementFor(node)
-            )
-        }
-
-        rule
-            .onNodeWithTag(testTag)
-            .assertWidthIsEqualTo(10.dp)
-            .assertHeightIsEqualTo(10.dp)
-    }
-
-    // captureToImage() requires API level 26
-    @RequiresApi(Build.VERSION_CODES.O)
-    private fun SemanticsNodeInteraction.captureToBitmap() = captureToImage().asAndroidBitmap()
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt
deleted file mode 100644
index dbe8b98..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt
+++ /dev/null
@@ -1,180 +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.compose.ui.focus
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.focusGroup
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.key
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@ExperimentalFoundationApi
-@OptIn(ExperimentalComposeUiApi::class)
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class FocusRestorerTest {
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Test
-    fun restoresSavedChild() {
-        // Arrange.
-        val (parent, child2) = FocusRequester.createRefs()
-        lateinit var focusManager: FocusManager
-        lateinit var child1State: FocusState
-        lateinit var child2State: FocusState
-        rule.setFocusableContent {
-            focusManager = LocalFocusManager.current
-            Box(
-                Modifier
-                    .size(10.dp)
-                    .focusRequester(parent)
-                    .focusRestorer()
-                    .focusGroup()
-            ) {
-                key(1) {
-                    Box(
-                        Modifier
-                            .size(10.dp)
-                            .onFocusChanged { child1State = it }
-                            .focusTarget()
-                    )
-                }
-                key(2) {
-                    Box(
-                        Modifier
-                            .size(10.dp)
-                            .focusRequester(child2)
-                            .onFocusChanged { child2State = it }
-                            .focusTarget()
-                    )
-                }
-            }
-        }
-        rule.runOnIdle { child2.requestFocus() }
-
-        // Act.
-        rule.runOnIdle { focusManager.clearFocus() }
-        rule.runOnIdle { parent.requestFocus() }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(child1State.isFocused).isFalse()
-            assertThat(child2State.isFocused).isTrue()
-        }
-    }
-
-    @Test
-    fun withoutUniqueKeysRestoresFirstMatchingChild() {
-        // Arrange.
-        val (parent, child2) = FocusRequester.createRefs()
-        lateinit var focusManager: FocusManager
-        lateinit var child1State: FocusState
-        lateinit var child2State: FocusState
-        rule.setFocusableContent {
-            focusManager = LocalFocusManager.current
-            Box(
-                Modifier
-                    .size(10.dp)
-                    .focusRequester(parent)
-                    .focusRestorer()
-                    .focusGroup()
-            ) {
-                Box(
-                    Modifier
-                        .size(10.dp)
-                        .onFocusChanged { child1State = it }
-                        .focusTarget()
-                )
-                Box(
-                    Modifier
-                        .size(10.dp)
-                        .focusRequester(child2)
-                        .onFocusChanged { child2State = it }
-                        .focusTarget()
-                )
-            }
-        }
-        rule.runOnIdle { child2.requestFocus() }
-
-        // Act.
-        rule.runOnIdle { focusManager.clearFocus() }
-        rule.runOnIdle { parent.requestFocus() }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(child1State.isFocused).isTrue()
-            assertThat(child2State.isFocused).isFalse()
-        }
-    }
-
-    @Test
-    fun doesNotRestoreGrandChild_butFocusesOnChildInstead() {
-        // Arrange.
-        val (parent, grandChild) = FocusRequester.createRefs()
-        lateinit var focusManager: FocusManager
-        lateinit var childState: FocusState
-        lateinit var grandChildState: FocusState
-        rule.setFocusableContent {
-            focusManager = LocalFocusManager.current
-            Box(
-                Modifier
-                    .size(10.dp)
-                    .focusRequester(parent)
-                    .focusRestorer()
-                    .focusGroup()
-            ) {
-                Box(
-                    Modifier
-                        .size(10.dp)
-                        .onFocusChanged { childState = it }
-                        .focusTarget()
-                ) {
-                    Box(
-                        Modifier
-                            .size(10.dp)
-                            .focusRequester(grandChild)
-                            .onFocusChanged { grandChildState = it }
-                            .focusTarget()
-                    )
-                }
-            }
-        }
-        rule.runOnIdle { grandChild.requestFocus() }
-
-        // Act.
-        rule.runOnIdle { focusManager.clearFocus() }
-        rule.runOnIdle { parent.requestFocus() }
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(childState.isFocused).isTrue()
-            assertThat(grandChildState.isFocused).isFalse()
-        }
-    }
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/KeyEventToFocusDirectionTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/KeyEventToFocusDirectionTest.kt
deleted file mode 100644
index 94dce03..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/KeyEventToFocusDirectionTest.kt
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright 2021 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.focus
-
-import android.view.KeyEvent as AndroidKeyEvent
-import android.view.KeyEvent.ACTION_DOWN as KeyDown
-import android.view.KeyEvent.META_SHIFT_ON as Shift
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.focus.FocusDirection.Companion.Down
-import androidx.compose.ui.focus.FocusDirection.Companion.Enter
-import androidx.compose.ui.focus.FocusDirection.Companion.Exit
-import androidx.compose.ui.focus.FocusDirection.Companion.Left
-import androidx.compose.ui.focus.FocusDirection.Companion.Next
-import androidx.compose.ui.focus.FocusDirection.Companion.Previous
-import androidx.compose.ui.focus.FocusDirection.Companion.Right
-import androidx.compose.ui.focus.FocusDirection.Companion.Up
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.nativeKeyCode
-import androidx.compose.ui.node.Owner
-import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalComposeUiApi::class)
-class KeyEventToFocusDirectionTest {
-    @get:Rule
-    val rule = createComposeRule()
-
-    private lateinit var owner: Owner
-
-    @Before
-    fun setup() {
-        rule.setContent {
-            owner = LocalView.current as Owner
-        }
-    }
-
-    @Test
-    fun left() {
-        // Arrange.
-        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionLeft.nativeKeyCode))
-
-        // Act.
-        val focusDirection = owner.getFocusDirection(keyEvent)
-
-        // Assert.
-        assertThat(focusDirection).isEqualTo(Left)
-    }
-
-    @Test
-    fun right() {
-        // Arrange.
-        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionRight.nativeKeyCode))
-
-        // Act.
-        val focusDirection = owner.getFocusDirection(keyEvent)
-
-        // Assert.
-        assertThat(focusDirection).isEqualTo(Right)
-    }
-
-    @Test
-    fun up() {
-        // Arrange.
-        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionUp.nativeKeyCode))
-
-        // Act.
-        val focusDirection = owner.getFocusDirection(keyEvent)
-
-        // Assert.
-        assertThat(focusDirection).isEqualTo(Up)
-    }
-
-    @Test
-    fun down() {
-        // Arrange.
-        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionDown.nativeKeyCode))
-
-        // Act.
-        val focusDirection = owner.getFocusDirection(keyEvent)
-
-        // Assert.
-        assertThat(focusDirection).isEqualTo(Down)
-    }
-
-    @Test
-    fun tab_next() {
-        // Arrange.
-        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.Tab.nativeKeyCode))
-
-        // Act.
-        val focusDirection = owner.getFocusDirection(keyEvent)
-
-        // Assert.
-        assertThat(focusDirection).isEqualTo(Next)
-    }
-
-    @Test
-    fun shiftTab_previous() {
-        // Arrange.
-        val keyEvent = KeyEvent(AndroidKeyEvent(0L, 0L, KeyDown, Key.Tab.nativeKeyCode, 0, Shift))
-
-        // Act.
-        val focusDirection = owner.getFocusDirection(keyEvent)
-
-        // Assert.
-        assertThat(focusDirection).isEqualTo(Previous)
-    }
-
-    @Test
-    fun dpadCenter_enter() {
-        // Arrange.
-        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionCenter.nativeKeyCode))
-
-        // Act.
-        val focusDirection = owner.getFocusDirection(keyEvent)
-
-        // Assert.
-        @OptIn(ExperimentalComposeUiApi::class)
-        assertThat(focusDirection).isEqualTo(Enter)
-    }
-
-    @Test
-    fun enter_enter() {
-        // Arrange.
-        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.Enter.nativeKeyCode))
-
-        // Act.
-        val focusDirection = owner.getFocusDirection(keyEvent)
-
-        // Assert.
-        @OptIn(ExperimentalComposeUiApi::class)
-        assertThat(focusDirection).isEqualTo(Enter)
-    }
-
-    @Test
-    fun numPadEnter_enter() {
-        // Arrange.
-        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.NumPadEnter.nativeKeyCode))
-
-        // Act.
-        val focusDirection = owner.getFocusDirection(keyEvent)
-
-        // Assert.
-        @OptIn(ExperimentalComposeUiApi::class)
-        assertThat(focusDirection).isEqualTo(Enter)
-    }
-
-    @Test
-    fun back_exit() {
-        // Arrange.
-        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.Back.nativeKeyCode))
-
-        // Act.
-        val focusDirection = owner.getFocusDirection(keyEvent)
-
-        // Assert.
-        @OptIn(ExperimentalComposeUiApi::class)
-        assertThat(focusDirection).isEqualTo(Exit)
-    }
-
-    @Test
-    fun esc_exit() {
-        // Arrange.
-        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.Escape.nativeKeyCode))
-
-        // Act.
-        val focusDirection = owner.getFocusDirection(keyEvent)
-
-        // Assert.
-        @OptIn(ExperimentalComposeUiApi::class)
-        assertThat(focusDirection).isEqualTo(Exit)
-    }
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
deleted file mode 100644
index 0e0a75b..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
+++ /dev/null
@@ -1,1416 +0,0 @@
-/*
- * Copyright 2019 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.graphics.vector
-
-import android.app.Application
-import android.content.ComponentCallbacks2
-import android.content.pm.ActivityInfo
-import android.content.res.Configuration
-import android.content.res.Resources
-import android.graphics.Bitmap
-import android.os.Build
-import androidx.activity.ComponentActivity
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.testutils.assertPixels
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.AtLeastSize
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.background
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.draw.paint
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.BlendMode
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Canvas
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.graphics.CompositingStrategy
-import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.graphics.ImageBitmapConfig
-import androidx.compose.ui.graphics.SolidColor
-import androidx.compose.ui.graphics.asAndroidBitmap
-import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.graphics.painter.BitmapPainter
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.graphics.toPixelMap
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalImageVectorCache
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.ImageVectorCache
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.tests.R
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import org.junit.Assert
-import org.junit.Assert.assertArrayEquals
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class VectorTest {
-
-    @get:Rule
-    val rule = createAndroidComposeRule<ComponentActivity>()
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testVectorTint() {
-        rule.setContent {
-            VectorTint()
-        }
-
-        takeScreenShot(200).apply {
-            assertEquals(getPixel(100, 100), Color.Cyan.toArgb())
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testVectorIntrinsicTint() {
-        rule.setContent {
-            val background = Modifier.paint(
-                createTestVectorPainter(200, Color.Magenta),
-                alignment = Alignment.Center
-            )
-            AtLeastSize(size = 200, modifier = background) {
-            }
-        }
-        takeScreenShot(200).apply {
-            assertEquals(getPixel(100, 100), Color.Magenta.toArgb())
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testVectorIntrinsicTintFirstFrame() {
-        var vector: VectorPainter? = null
-        rule.setContent {
-            vector = createTestVectorPainter(200, Color.Magenta)
-
-            val bitmap = remember {
-                val bitmap = ImageBitmap(200, 200)
-                val canvas = Canvas(bitmap)
-                val bitmapSize = Size(200f, 200f)
-                CanvasDrawScope().draw(
-                    Density(1f),
-                    LayoutDirection.Ltr,
-                    canvas,
-                    bitmapSize
-                ) {
-                    with(vector!!) {
-                        draw(bitmapSize)
-                    }
-                }
-                bitmap
-            }
-
-            val background = Modifier.paint(BitmapPainter(bitmap))
-
-            AtLeastSize(size = 200, modifier = background) {
-            }
-        }
-        takeScreenShot(200).apply {
-            assertEquals(getPixel(100, 100), Color.Magenta.toArgb())
-        }
-        assertEquals(ImageBitmapConfig.Alpha8, vector!!.bitmapConfig)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testVectorAlignment() {
-        rule.setContent {
-            VectorTint(minimumSize = 450, alignment = Alignment.BottomEnd)
-        }
-
-        takeScreenShot(450).apply {
-            assertEquals(getPixel(430, 430), Color.Cyan.toArgb())
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testVectorInvalidation() {
-        val testCase = VectorInvalidationTestCase()
-        rule.setContent {
-            testCase.TestVector()
-        }
-
-        rule.waitUntil { testCase.measured }
-        val size = testCase.vectorSize
-        takeScreenShot(size).apply {
-            assertEquals(Color.Blue.toArgb(), getPixel(5, size - 5))
-            assertEquals(Color.White.toArgb(), getPixel(size - 5, 5))
-        }
-
-        testCase.measured = false
-        rule.runOnUiThread {
-            testCase.toggle()
-        }
-
-        rule.waitUntil { testCase.measured }
-
-        takeScreenShot(size).apply {
-            assertEquals(Color.White.toArgb(), getPixel(5, size - 5))
-            assertEquals(Color.Red.toArgb(), getPixel(size - 5, 5))
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testVectorRendersOnceOnFirstFrame() {
-        var drawCount = 0
-        val testTag = "TestTag"
-        rule.setContent {
-            Box(modifier = Modifier
-                .wrapContentSize()
-                .drawBehind {
-                    drawCount++
-                }
-                .paint(painterResource(R.drawable.ic_triangle2))
-                .testTag(testTag))
-        }
-
-        rule.onNodeWithTag(testTag).captureToImage().toPixelMap().apply {
-            assertEquals(1, drawCount)
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testVectorClipPath() {
-        rule.setContent {
-            VectorClip()
-        }
-
-        takeScreenShot(200).apply {
-            assertEquals(getPixel(100, 50), Color.Cyan.toArgb())
-            assertEquals(getPixel(100, 150), Color.Black.toArgb())
-        }
-    }
-
-    @Test
-    fun testVectorZeroSizeDoesNotCrash() {
-        // Make sure that if we are given the size of zero we should not crash and instead
-        // act as a no-op
-        rule.setContent {
-            Box(modifier = Modifier.size(0.dp).paint(createTestVectorPainter()))
-        }
-    }
-
-    @Test
-    fun testVectorZeroWidthDoesNotCrash() {
-        rule.setContent {
-            Box(
-                modifier = Modifier.width(0.dp).height(100.dp).paint
-                (createTestVectorPainter())
-            )
-        }
-    }
-
-    @Test
-    fun testVectorZeroHeightDoesNotCrash() {
-        rule.setContent {
-            Box(
-                modifier = Modifier.width(50.dp).height(0.dp).paint(
-                    createTestVectorPainter()
-                )
-            )
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testVectorTrimPath() {
-        rule.setContent {
-            VectorTrim()
-        }
-
-        takeScreenShot(200).apply {
-            assertEquals(Color.Yellow.toArgb(), getPixel(25, 100))
-            assertEquals(Color.Blue.toArgb(), getPixel(100, 100))
-            assertEquals(Color.Yellow.toArgb(), getPixel(175, 100))
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testImageVectorChangeOnStateChange() {
-        val defaultWidth = 48.dp
-        val defaultHeight = 48.dp
-        val viewportWidth = 24f
-        val viewportHeight = 24f
-
-        val icon1 = ImageVector.Builder(
-            defaultWidth = defaultWidth,
-            defaultHeight = defaultHeight,
-            viewportWidth = viewportWidth,
-            viewportHeight = viewportHeight
-        )
-            .addPath(
-                fill = SolidColor(Color.Black),
-                pathData = PathData {
-                    lineTo(viewportWidth, 0f)
-                    lineTo(viewportWidth, viewportHeight)
-                    lineTo(0f, 0f)
-                    close()
-                }
-            ).build()
-
-        val icon2 = ImageVector.Builder(
-            defaultWidth = defaultWidth,
-            defaultHeight = defaultHeight,
-            viewportWidth = viewportWidth,
-            viewportHeight = viewportHeight
-        )
-            .addPath(
-                fill = SolidColor(Color.Black),
-                pathData = PathData {
-                    lineTo(0f, viewportHeight)
-                    lineTo(viewportWidth, viewportHeight)
-                    lineTo(0f, 0f)
-                    close()
-                }
-            ).build()
-
-        val testTag = "iconClick"
-        rule.setContent {
-            val clickState = remember { mutableStateOf(false) }
-            Image(
-                imageVector = if (clickState.value) icon1 else icon2,
-                contentDescription = null,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .size(icon1.defaultWidth, icon1.defaultHeight)
-                    .background(Color.Red)
-                    .clickable { clickState.value = !clickState.value },
-                alignment = Alignment.TopStart,
-                contentScale = ContentScale.FillHeight
-            )
-        }
-
-        rule.onNodeWithTag(testTag).apply {
-            captureToImage().asAndroidBitmap().apply {
-                assertEquals(Color.Red.toArgb(), getPixel(width - 2, 0))
-                assertEquals(Color.Red.toArgb(), getPixel(2, 0))
-                assertEquals(Color.Red.toArgb(), getPixel(width - 1, height - 4))
-
-                assertEquals(Color.Black.toArgb(), getPixel(0, 2))
-                assertEquals(Color.Black.toArgb(), getPixel(0, height - 2))
-                assertEquals(Color.Black.toArgb(), getPixel(width - 4, height - 2))
-            }
-            performClick()
-        }
-
-        rule.waitForIdle()
-
-        rule.onNodeWithTag(testTag).captureToImage().asAndroidBitmap().apply {
-            assertEquals(Color.Black.toArgb(), getPixel(width - 2, 0))
-            assertEquals(Color.Black.toArgb(), getPixel(2, 0))
-            assertEquals(Color.Black.toArgb(), getPixel(width - 1, height - 4))
-
-            assertEquals(Color.Red.toArgb(), getPixel(0, 2))
-            assertEquals(Color.Red.toArgb(), getPixel(0, height - 2))
-            assertEquals(Color.Red.toArgb(), getPixel(width - 4, height - 2))
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testDrawWithoutColorFilterAfterPreviouslyConfigured() {
-        val defaultWidth = 24.dp
-        val defaultHeight = 24.dp
-        val testTag = "testTag"
-        var vectorPainter: VectorPainter? = null
-
-        var tint: ColorFilter? by mutableStateOf(ColorFilter.tint(Color.Green))
-        rule.setContent {
-            vectorPainter = rememberVectorPainter(
-                defaultWidth = defaultWidth,
-                defaultHeight = defaultHeight,
-                autoMirror = false
-            ) { viewportWidth, viewportHeight ->
-                Path(
-                    fill = SolidColor(Color.Blue),
-                    pathData = PathData {
-                        lineTo(viewportWidth, 0f)
-                        lineTo(viewportWidth, viewportHeight)
-                        lineTo(0f, viewportHeight)
-                        close()
-                    }
-                )
-            }
-            Image(
-                painter = vectorPainter!!,
-                contentDescription = null,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .background(Color.Red),
-                contentScale = ContentScale.FillBounds,
-                colorFilter = tint
-            )
-        }
-
-        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Green }
-
-        tint = null
-        rule.waitForIdle()
-        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
-        assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testDrawWithColorFilterAfterNotPreviouslyConfigured() {
-        val defaultWidth = 24.dp
-        val defaultHeight = 24.dp
-        val testTag = "testTag"
-        var vectorPainter: VectorPainter? = null
-
-        var tint: ColorFilter? by mutableStateOf(null)
-        rule.setContent {
-            vectorPainter = rememberVectorPainter(
-                defaultWidth = defaultWidth,
-                defaultHeight = defaultHeight,
-                autoMirror = false
-            ) { viewportWidth, viewportHeight ->
-                Path(
-                    fill = SolidColor(Color.Blue),
-                    pathData = PathData {
-                        lineTo(viewportWidth, 0f)
-                        lineTo(viewportWidth, viewportHeight)
-                        lineTo(0f, viewportHeight)
-                        close()
-                    }
-                )
-            }
-            Image(
-                painter = vectorPainter!!,
-                contentDescription = null,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .background(Color.Red),
-                contentScale = ContentScale.FillBounds,
-                colorFilter = tint
-            )
-        }
-
-        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
-
-        tint = ColorFilter.tint(Color.Green)
-        rule.waitForIdle()
-        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Green }
-        assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicClearBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Clear)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicSrcBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Src)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicDstBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Dst)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicSrcOverBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.SrcOver, expectedConfig = ImageBitmapConfig.Alpha8)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicDstOverBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.DstOver)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicSrcInBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.SrcIn, expectedConfig = ImageBitmapConfig.Alpha8)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicDstInBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.DstIn)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicSrcOutBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.SrcOut)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicDstOutBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.DstOut)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicSrcAtopBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.SrcAtop)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicDstAtopBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.DstAtop)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicXorBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Xor)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicPlusBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Plus)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicModulateBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Modulate)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicScreenBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Screen)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicOverlayBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Overlay)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicDarkenBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Darken)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicLightenBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Lighten)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicColorDodgeBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.ColorDodge)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicColorBurnBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.ColorBurn)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicHardlightBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Hardlight)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicSoftLightBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Softlight)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicDifferenceBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Difference)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicExclusionBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Exclusion)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicMultiplyBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Multiply)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicHueBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Hue)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicSaturationBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Saturation)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicColorBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Color)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithIntrinsicLuminosityBlendMode() {
-        verifyAlphaMaskWithBlendModes(BlendMode.Luminosity)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawClearBlendMode() {
-        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Clear))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawSrcBlendMode() {
-        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Src))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawDstBlendMode() {
-        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Dst))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawSrcOverBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.SrcOver),
-            expectedConfig = ImageBitmapConfig.Alpha8
-        )
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawDstOverBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.DstOver))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawSrcInBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.SrcIn),
-            expectedConfig = ImageBitmapConfig.Alpha8
-        )
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawDstInBlendMode() {
-        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.DstIn))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawSrcOutBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.SrcOut))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawDstOutBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.DstOut))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawSrcAtopBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.SrcAtop))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawDstAtopBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.DstAtop))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawXorBlendMode() {
-        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Xor))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawPlusBlendMode() {
-        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Plus))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawModulateBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Modulate))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawScreenBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Screen))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawOverlayBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Overlay))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawDarkenBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Darken))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawLightenBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Lighten))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawColorDodgeBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.ColorDodge))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawColorBurnBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.ColorBurn))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawHardlightBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Hardlight))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawSoftLightBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Softlight))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawDifferenceBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Difference))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawExclusionBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Exclusion))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawMultiplyBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Multiply))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawHueBlendMode() {
-        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Hue))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawSaturationBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Saturation))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawColorBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Color))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testAlphaMaskWithDrawLuminosityBlendMode() {
-        verifyAlphaMaskWithBlendModes(
-            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Luminosity))
-    }
-
-    @RequiresApi(Build.VERSION_CODES.O)
-    private fun verifyAlphaMaskWithBlendModes(
-        intrinsicBlendMode: BlendMode = BlendMode.SrcIn,
-        colorFilter: ColorFilter? = null,
-        expectedConfig: ImageBitmapConfig? = null,
-    ) {
-        val defaultWidth = 24.dp
-        val defaultHeight = 24.dp
-        val testTag = "testTag"
-        var vectorPainter: VectorPainter? = null
-
-        // Create a gradient of the same color as a solid in order to verify behavior
-        // of intrinsic color filter usage both with and without the optimization to tint
-        // use a tinted alpha channel bitmap instead of a ARGB8888
-        val solidBlueGradient = Brush.horizontalGradient(listOf(Color.Blue, Color.Blue))
-        val solidBlueColor = SolidColor(Color.Blue)
-        var targetBrush: Brush by mutableStateOf(solidBlueColor)
-        rule.setContent {
-            vectorPainter = rememberVectorPainter(
-                defaultWidth = defaultWidth,
-                defaultHeight = defaultHeight,
-                tintColor = Color.Cyan,
-                tintBlendMode = intrinsicBlendMode,
-                autoMirror = false
-            ) { viewportWidth, viewportHeight ->
-                Path(
-                    fill = targetBrush,
-                    pathData = PathData {
-                        lineTo(viewportWidth, 0f)
-                        lineTo(viewportWidth, viewportHeight)
-                        lineTo(0f, viewportHeight)
-                        close()
-                    }
-                )
-            }
-            Image(
-                painter = vectorPainter!!,
-                contentDescription = null,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .background(
-                        Brush.horizontalGradient(
-                            listOf(Color.Transparent, Color.Yellow, Color.Transparent)
-                        )
-                    )
-                    .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen },
-                contentScale = ContentScale.FillBounds,
-                colorFilter = colorFilter
-            )
-        }
-
-        rule.waitForIdle()
-
-        val solidBrushImage = rule.onNodeWithTag(testTag).captureToImage()
-        if (expectedConfig != null) {
-            assertEquals(expectedConfig, vectorPainter!!.bitmapConfig)
-        }
-
-        targetBrush = solidBlueGradient
-        rule.waitForIdle()
-
-        val gradientBrushImage = rule.onNodeWithTag(testTag).captureToImage()
-
-        assertArrayEquals(
-            "Optimized vector does not match expected for $intrinsicBlendMode",
-            gradientBrushImage.toPixelMap().buffer,
-            solidBrushImage.toPixelMap().buffer
-        )
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testPathColorChangeUpdatesBitmapConfig() {
-        val defaultWidth = 24.dp
-        val defaultHeight = 24.dp
-        val testTag = "testTag"
-        var vectorPainter: VectorPainter? = null
-        var brush: Brush by mutableStateOf(SolidColor(Color.Blue))
-        rule.setContent {
-            vectorPainter = rememberVectorPainter(
-                defaultWidth = defaultWidth,
-                defaultHeight = defaultHeight,
-                autoMirror = false
-            ) { viewportWidth, viewportHeight ->
-                Path(
-                    fill = brush,
-                    pathData = PathData {
-                        lineTo(viewportWidth, 0f)
-                        lineTo(viewportWidth, viewportHeight)
-                        lineTo(0f, viewportHeight)
-                        close()
-                    }
-                )
-            }
-            Image(
-                painter = vectorPainter!!,
-                contentDescription = null,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .size(defaultWidth * 8, defaultHeight * 2)
-                    .background(Color.Red),
-                contentScale = ContentScale.FillBounds
-            )
-        }
-
-        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
-        assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig)
-
-        brush = Brush.horizontalGradient(listOf(Color.Blue, Color.Blue))
-        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
-        assertEquals(ImageBitmapConfig.Argb8888, vectorPainter!!.bitmapConfig)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testGroupPathColorChangeUpdatesBitmapConfig() {
-        val defaultWidth = 24.dp
-        val defaultHeight = 24.dp
-        val testTag = "testTag"
-        var vectorPainter: VectorPainter? = null
-        var brush: Brush by mutableStateOf(SolidColor(Color.Blue))
-        rule.setContent {
-            vectorPainter = rememberVectorPainter(
-                defaultWidth = defaultWidth,
-                defaultHeight = defaultHeight,
-                autoMirror = false
-            ) { viewportWidth, viewportHeight ->
-                Group {
-                    Path(
-                        fill = brush,
-                        pathData = PathData {
-                            lineTo(viewportWidth, 0f)
-                            lineTo(viewportWidth, viewportHeight)
-                            lineTo(0f, viewportHeight)
-                            close()
-                        }
-                    )
-                }
-            }
-            Image(
-                painter = vectorPainter!!,
-                contentDescription = null,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .size(defaultWidth * 8, defaultHeight * 2)
-                    .background(Color.Red),
-                contentScale = ContentScale.FillBounds
-            )
-        }
-
-        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
-        assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig)
-
-        brush = Brush.horizontalGradient(listOf(Color.Blue, Color.Blue))
-        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
-        assertEquals(ImageBitmapConfig.Argb8888, vectorPainter!!.bitmapConfig)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testVectorScaleNonUniformly() {
-        val defaultWidth = 24.dp
-        val defaultHeight = 24.dp
-        val testTag = "testTag"
-        var vectorPainter: VectorPainter? = null
-        rule.setContent {
-            vectorPainter = rememberVectorPainter(
-                defaultWidth = defaultWidth,
-                defaultHeight = defaultHeight,
-                autoMirror = false
-            ) { viewportWidth, viewportHeight ->
-                Path(
-                    fill = SolidColor(Color.Blue),
-                    pathData = PathData {
-                        lineTo(viewportWidth, 0f)
-                        lineTo(viewportWidth, viewportHeight)
-                        lineTo(0f, viewportHeight)
-                        close()
-                    }
-                )
-            }
-            Image(
-                painter = vectorPainter!!,
-                contentDescription = null,
-                modifier = Modifier
-                    .testTag(testTag)
-                    .size(defaultWidth * 8, defaultHeight * 2)
-                    .background(Color.Red),
-                contentScale = ContentScale.FillBounds
-            )
-        }
-
-        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
-        assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testVectorChangeSize() {
-        val size = mutableStateOf(200)
-        val color = mutableStateOf(Color.Magenta)
-
-        rule.setContent {
-            val background = Modifier.background(Color.Red).paint(
-                createTestVectorPainter(size.value, color.value),
-                alignment = Alignment.TopStart
-            )
-            AtLeastSize(size = 400, modifier = background) {
-            }
-        }
-
-        takeScreenShot(400).apply {
-            assertEquals(getPixel(100, 100), Color.Magenta.toArgb())
-            assertEquals(getPixel(300, 300), Color.Red.toArgb())
-        }
-
-        size.value = 400
-        color.value = Color.Cyan
-
-        takeScreenShot(400).apply {
-            assertEquals(getPixel(100, 100), Color.Cyan.toArgb())
-            assertEquals(getPixel(300, 300), Color.Cyan.toArgb())
-        }
-
-        size.value = 50
-        color.value = Color.Yellow
-
-        takeScreenShot(400).apply {
-            assertEquals(getPixel(10, 10), Color.Yellow.toArgb())
-            assertEquals(getPixel(100, 100), Color.Red.toArgb())
-            assertEquals(getPixel(300, 300), Color.Red.toArgb())
-        }
-    }
-
-    @Test
-    fun testImageVectorCacheHit() {
-        var vectorInCache = false
-        rule.setContent {
-            val theme = LocalContext.current.theme
-            val imageVectorCache = LocalImageVectorCache.current
-            imageVectorCache.clear()
-            Image(
-                painterResource(R.drawable.ic_triangle),
-                contentDescription = null
-            )
-
-            vectorInCache =
-                imageVectorCache[ImageVectorCache.Key(theme, R.drawable.ic_triangle)] != null
-        }
-
-        assertTrue(vectorInCache)
-    }
-
-    @Test
-    fun testImageVectorCacheCleared() {
-        var vectorInCache = false
-        var application: Application? = null
-        var theme: Resources.Theme? = null
-        var vectorCache: ImageVectorCache? = null
-        rule.setContent {
-            application = LocalContext.current.applicationContext as Application
-            theme = LocalContext.current.theme
-            val imageVectorCache = LocalImageVectorCache.current
-            imageVectorCache.clear()
-            Image(
-                painterResource(R.drawable.ic_triangle),
-                contentDescription = null
-            )
-
-            vectorInCache =
-                imageVectorCache[ImageVectorCache.Key(theme!!, R.drawable.ic_triangle)] != null
-
-            vectorCache = imageVectorCache
-        }
-
-        application?.onTrimMemory(0)
-
-        val cacheCleared = vectorCache?.let {
-            it[ImageVectorCache.Key(theme!!, R.drawable.ic_triangle)] == null
-        } ?: false
-
-        assertTrue("Vector was not inserted in cache after initial creation", vectorInCache)
-        assertTrue("Cache was not cleared after trim memory call", cacheCleared)
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testImageVectorConfigChange() {
-        val tag = "testTag"
-        rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
-
-        val latch = CountDownLatch(1)
-
-        rule.activity.application.registerComponentCallbacks(object : ComponentCallbacks2 {
-            override fun onConfigurationChanged(p0: Configuration) {
-                latch.countDown()
-            }
-
-            override fun onLowMemory() {
-                // NO-OP
-            }
-
-            override fun onTrimMemory(p0: Int) {
-                // NO-OP
-            }
-        })
-
-        try {
-            latch.await(1500, TimeUnit.MILLISECONDS)
-            rule.setContent {
-                Image(
-                    painterResource(R.drawable.ic_triangle_config),
-                    contentDescription = null,
-                    modifier = Modifier.testTag(tag)
-                )
-            }
-            rule.onNodeWithTag(tag).captureToImage().apply {
-                assertEquals(Color.Blue, toPixelMap()[width - 5, 5])
-            }
-        } catch (e: InterruptedException) {
-            fail("Unable to verify vector asset in landscape orientation")
-        } finally {
-            rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testVectorMirror() {
-        val tag = "mirroredVector"
-        rule.setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                Image(
-                    painter = VectorMirror(20),
-                    contentDescription = null,
-                    modifier = Modifier.testTag(tag)
-                )
-            }
-        }
-        rule.onNodeWithTag(tag).captureToImage().toPixelMap().apply {
-            assertEquals(Color.Blue, this[2, 2])
-            assertEquals(Color.Blue, this[2, height - 3])
-            assertEquals(Color.Blue, this[width / 2 - 3, 2])
-            assertEquals(Color.Blue, this[width / 2 - 3, height - 3])
-
-            assertEquals(Color.Red, this[width - 3, 2])
-            assertEquals(Color.Red, this[width - 3, height - 3])
-            assertEquals(Color.Red, this[width / 2 + 3, 2])
-            assertEquals(Color.Red, this[width / 2 + 3, height - 3])
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testVectorStrokeWidth() {
-        val strokeWidth = mutableStateOf(100)
-        rule.setContent {
-            VectorStroke(strokeWidth = strokeWidth.value)
-        }
-        takeScreenShot(200).apply {
-            assertEquals(Color.Yellow.toArgb(), getPixel(100, 25))
-            assertEquals(Color.Blue.toArgb(), getPixel(100, 75))
-        }
-        rule.runOnUiThread { strokeWidth.value = 200 }
-        rule.waitForIdle()
-        takeScreenShot(200).apply {
-            assertEquals(Color.Yellow.toArgb(), getPixel(100, 25))
-            assertEquals(Color.Yellow.toArgb(), getPixel(100, 75))
-        }
-    }
-
-    @Composable
-    private fun VectorTint(
-        size: Int = 200,
-        minimumSize: Int = size,
-        alignment: Alignment = Alignment.Center
-    ) {
-        val background = Modifier.paint(
-            createTestVectorPainter(size),
-            colorFilter = ColorFilter.tint(Color.Cyan),
-            alignment = alignment
-        )
-        AtLeastSize(size = minimumSize, modifier = background) {
-        }
-    }
-
-    @Composable
-    private fun createTestVectorPainter(
-        size: Int = 200,
-        tintColor: Color = Color.Unspecified
-    ): VectorPainter {
-        val sizePx = size.toFloat()
-        val sizeDp = (size / LocalDensity.current.density).dp
-        return rememberVectorPainter(
-            defaultWidth = sizeDp,
-            defaultHeight = sizeDp,
-            autoMirror = false,
-            content = { _, _ ->
-                Path(
-                    pathData = PathData {
-                        lineTo(sizePx, 0.0f)
-                        lineTo(sizePx, sizePx)
-                        lineTo(0.0f, sizePx)
-                        close()
-                    },
-                    fill = SolidColor(Color.Black)
-                )
-            },
-            tintColor = tintColor
-        )
-    }
-
-    @Composable
-    private fun VectorClip(
-        size: Int = 200,
-        minimumSize: Int = size,
-        alignment: Alignment = Alignment.Center
-    ) {
-        val sizePx = size.toFloat()
-        val sizeDp = (size / LocalDensity.current.density).dp
-        val background = Modifier.paint(
-            rememberVectorPainter(
-                defaultWidth = sizeDp,
-                defaultHeight = sizeDp,
-                autoMirror = false
-            ) { _, _ ->
-                Path(
-                    // Cyan background.
-                    pathData = PathData {
-                        lineTo(sizePx, 0.0f)
-                        lineTo(sizePx, sizePx)
-                        lineTo(0.0f, sizePx)
-                        close()
-                    },
-                    fill = SolidColor(Color.Cyan)
-                )
-                Group(
-                    // Only show the top half...
-                    clipPathData = PathData {
-                        lineTo(sizePx, 0.0f)
-                        lineTo(sizePx, sizePx / 2)
-                        lineTo(0.0f, sizePx / 2)
-                        close()
-                    },
-                    // And rotate it, resulting in the bottom half being black.
-                    pivotX = sizePx / 2,
-                    pivotY = sizePx / 2,
-                    rotation = 180f
-                ) {
-                    Path(
-                        pathData = PathData {
-                            lineTo(sizePx, 0.0f)
-                            lineTo(sizePx, sizePx)
-                            lineTo(0.0f, sizePx)
-                            close()
-                        },
-                        fill = SolidColor(Color.Black)
-                    )
-                }
-            },
-            alignment = alignment
-        )
-        AtLeastSize(size = minimumSize, modifier = background) {
-        }
-    }
-
-    @Composable
-    private fun VectorTrim(
-        size: Int = 200,
-        minimumSize: Int = size,
-        alignment: Alignment = Alignment.Center
-    ) {
-        val sizePx = size.toFloat()
-        val sizeDp = (size / LocalDensity.current.density).dp
-        val background = Modifier.paint(
-            rememberVectorPainter(
-                defaultWidth = sizeDp,
-                defaultHeight = sizeDp,
-                autoMirror = false
-            ) { _, _ ->
-                Path(
-                    pathData = PathData {
-                        lineTo(sizePx, 0.0f)
-                        lineTo(sizePx, sizePx)
-                        lineTo(0.0f, sizePx)
-                        close()
-                    },
-                    fill = SolidColor(Color.Blue)
-                )
-                // A thick stroke
-                Path(
-                    pathData = PathData {
-                        moveTo(0.0f, sizePx / 2)
-                        lineTo(sizePx, sizePx / 2)
-                    },
-                    stroke = SolidColor(Color.Yellow),
-                    strokeLineWidth = sizePx / 2,
-                    trimPathStart = 0.25f,
-                    trimPathEnd = 0.75f,
-                    trimPathOffset = 0.5f
-                )
-            },
-            alignment = alignment
-        )
-        AtLeastSize(size = minimumSize, modifier = background) {
-        }
-    }
-
-    @Composable
-    private fun VectorStroke(
-        size: Int = 200,
-        strokeWidth: Int = 100,
-        minimumSize: Int = size,
-        alignment: Alignment = Alignment.Center
-    ) {
-        val sizePx = size.toFloat()
-        val sizeDp = (size / LocalDensity.current.density).dp
-        val strokeWidthPx = strokeWidth.toFloat()
-        val background = Modifier.paint(
-            rememberVectorPainter(
-                defaultWidth = sizeDp,
-                defaultHeight = sizeDp,
-                autoMirror = false
-            ) { _, _ ->
-                Path(
-                    pathData = PathData {
-                        lineTo(sizePx, 0.0f)
-                        lineTo(sizePx, sizePx)
-                        lineTo(0.0f, sizePx)
-                        close()
-                    },
-                    fill = SolidColor(Color.Blue)
-                )
-                // A thick stroke
-                Path(
-                    pathData = PathData {
-                        moveTo(0.0f, 0.0f)
-                        lineTo(sizePx, 0.0f)
-                    },
-                    stroke = SolidColor(Color.Yellow),
-                    strokeLineWidth = strokeWidthPx,
-                )
-            },
-            alignment = alignment
-        )
-        AtLeastSize(size = minimumSize, modifier = background) {
-        }
-    }
-
-    @Composable
-    private fun VectorMirror(size: Int): VectorPainter {
-        val sizePx = size.toFloat()
-        val sizeDp = (size / LocalDensity.current.density).dp
-        return rememberVectorPainter(
-                defaultWidth = sizeDp,
-                defaultHeight = sizeDp,
-                autoMirror = true
-            ) { _, _ ->
-                Path(
-                    pathData = PathData {
-                        lineTo(sizePx / 2, 0f)
-                        lineTo(sizePx / 2, sizePx)
-                        lineTo(0f, sizePx)
-                        close()
-                    },
-                    fill = SolidColor(Color.Red)
-                )
-
-                Path(
-                    pathData = PathData {
-                        moveTo(sizePx / 2, 0f)
-                        lineTo(sizePx, 0f)
-                        lineTo(sizePx, sizePx)
-                        lineTo(sizePx / 2, sizePx)
-                        close()
-                    },
-                    fill = SolidColor(Color.Blue)
-                )
-            }
-    }
-
-    // captureToImage() requires API level 26
-    @RequiresApi(Build.VERSION_CODES.O)
-    private fun takeScreenShot(width: Int, height: Int = width): Bitmap {
-        val bitmap = rule.onRoot().captureToImage().asAndroidBitmap()
-        Assert.assertEquals(width, bitmap.width)
-        Assert.assertEquals(height, bitmap.height)
-        return bitmap
-    }
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapterTest.kt
deleted file mode 100644
index c06be12..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapterTest.kt
+++ /dev/null
@@ -1,1801 +0,0 @@
-/*
- * Copyright 2019 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.input.pointer
-
-import android.util.SparseLongArray
-import android.view.InputDevice
-import android.view.MotionEvent
-import android.view.MotionEvent.ACTION_CANCEL
-import android.view.MotionEvent.ACTION_DOWN
-import android.view.MotionEvent.ACTION_HOVER_ENTER
-import android.view.MotionEvent.ACTION_HOVER_EXIT
-import android.view.MotionEvent.ACTION_MOVE
-import android.view.MotionEvent.ACTION_POINTER_DOWN
-import android.view.MotionEvent.ACTION_POINTER_UP
-import android.view.MotionEvent.ACTION_SCROLL
-import android.view.MotionEvent.ACTION_UP
-import android.view.MotionEvent.AXIS_HSCROLL
-import android.view.MotionEvent.AXIS_VSCROLL
-import android.view.MotionEvent.TOOL_TYPE_FINGER
-import android.view.MotionEvent.TOOL_TYPE_MOUSE
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Matrix
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class MotionEventAdapterTest {
-
-    private lateinit var motionEventAdapter: MotionEventAdapter
-    private val positionCalculator = object : PositionCalculator {
-        override fun screenToLocal(positionOnScreen: Offset): Offset = positionOnScreen
-
-        override fun localToScreen(localPosition: Offset): Offset = localPosition
-
-        override fun localToScreen(localTransform: Matrix) {}
-    }
-
-    @Before
-    fun setup() {
-        motionEventAdapter = MotionEventAdapter()
-    }
-
-    @Test
-    fun convertToolType() {
-        val types = mapOf(
-            MotionEvent.TOOL_TYPE_FINGER to PointerType.Touch,
-            MotionEvent.TOOL_TYPE_UNKNOWN to PointerType.Unknown,
-            MotionEvent.TOOL_TYPE_ERASER to PointerType.Eraser,
-            MotionEvent.TOOL_TYPE_STYLUS to PointerType.Stylus,
-            MotionEvent.TOOL_TYPE_MOUSE to PointerType.Mouse,
-        )
-        types.entries.forEach { (toolType, pointerType) ->
-            motionEventAdapter = MotionEventAdapter()
-            val motionEvent = MotionEvent(
-                2894,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(
-                    PointerProperties(1000, toolType),
-                ),
-                arrayOf(
-                    PointerCoords(2967f, 5928f),
-                )
-            )
-            val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)!!
-            assertPointerInputEventData(
-                pointerInputEvent.pointers[0],
-                PointerId(0),
-                true,
-                2967f,
-                5928f,
-                pointerType
-            )
-        }
-    }
-
-    @Test
-    fun hoverEventsStay() {
-        // When a hover event happens, the pointer ID should stick around until it is removed.
-        val hoverEnter = MotionEvent(
-            0,
-            ACTION_HOVER_ENTER,
-            1,
-            0,
-            arrayOf(PointerProperties(1, TOOL_TYPE_MOUSE)),
-            arrayOf(PointerCoords(10f, 10f))
-        )
-        val hoverEnterEvent = motionEventAdapter.convertToPointerInputEvent(hoverEnter)!!
-        assertThat(hoverEnterEvent.pointers).hasSize(1)
-        val hoverEnterId = hoverEnterEvent.pointers[0].id
-
-        val hoverExit = MotionEvent(
-            1,
-            ACTION_HOVER_EXIT,
-            1,
-            0,
-            arrayOf(PointerProperties(1, TOOL_TYPE_MOUSE)),
-            arrayOf(PointerCoords(10f, 10f))
-        )
-
-        val hoverExitEvent = motionEventAdapter.convertToPointerInputEvent(hoverExit)!!
-        assertThat(hoverExitEvent.pointers).hasSize(1)
-        assertThat(hoverExitEvent.pointers[0].id).isEqualTo(hoverEnterId)
-
-        val down = MotionEvent(
-            1,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(1, TOOL_TYPE_MOUSE)),
-            arrayOf(PointerCoords(10f, 10f))
-        )
-
-        val downEvent = motionEventAdapter.convertToPointerInputEvent(down)!!
-        assertThat(downEvent.pointers).hasSize(1)
-        assertThat(downEvent.pointers[0].id).isEqualTo(hoverEnterId)
-
-        val up = MotionEvent(
-            2,
-            ACTION_UP,
-            1,
-            0,
-            arrayOf(PointerProperties(1, TOOL_TYPE_MOUSE)),
-            arrayOf(PointerCoords(10f, 10f))
-        )
-
-        val upEvent = motionEventAdapter.convertToPointerInputEvent(up)!!
-        assertThat(upEvent.pointers).hasSize(1)
-        assertThat(upEvent.pointers[0].id).isEqualTo(hoverEnterId)
-
-        val hoverEnterEvent2 = motionEventAdapter.convertToPointerInputEvent(hoverEnter)!!
-        assertThat(hoverEnterEvent2.pointers).hasSize(1)
-        assertThat(hoverEnterEvent2.pointers[0].id).isEqualTo(hoverEnterId)
-        motionEventAdapter.convertToPointerInputEvent(hoverExit)!!
-
-        val touchDown = MotionEvent(
-            3,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(1, TOOL_TYPE_FINGER)),
-            arrayOf(PointerCoords(10f, 10f))
-        )
-        val touchDownEvent = motionEventAdapter.convertToPointerInputEvent(touchDown)!!
-        assertThat(touchDownEvent.pointers).hasSize(1)
-        assertThat(touchDownEvent.pointers[0].id).isNotEqualTo(hoverEnterId)
-        val touchDownId = touchDownEvent.pointers[0].id
-
-        val touchUp = MotionEvent(
-            4,
-            ACTION_UP,
-            1,
-            0,
-            arrayOf(PointerProperties(1, TOOL_TYPE_FINGER)),
-            arrayOf(PointerCoords(10f, 10f))
-        )
-        val touchUpEvent = motionEventAdapter.convertToPointerInputEvent(touchUp)!!
-        assertThat(touchUpEvent.pointers).hasSize(1)
-        assertThat(touchUpEvent.pointers[0].id).isEqualTo(touchDownEvent.pointers[0].id)
-
-        val hoverEnterEvent3 = motionEventAdapter.convertToPointerInputEvent(hoverEnter)!!
-        assertThat(hoverEnterEvent3.pointers).hasSize(1)
-        assertThat(hoverEnterEvent3.pointers[0].id).isNotEqualTo(touchDownId)
-        assertThat(hoverEnterEvent3.pointers[0].id).isNotEqualTo(hoverEnterId)
-    }
-
-    @Test
-    fun robustIdConversion() {
-        // When an ID shows up unexpectedly, it shouldn't crash
-        val hoverExit = MotionEvent(
-            3,
-            ACTION_HOVER_EXIT,
-            1,
-            0,
-            arrayOf(PointerProperties(1, TOOL_TYPE_MOUSE)),
-            arrayOf(PointerCoords(10f, 10f))
-        )
-        val event = motionEventAdapter.convertToPointerInputEvent(hoverExit)!!
-        assertThat(event.pointers).hasSize(1)
-    }
-
-    @Test
-    fun convertToPointerInputEvent_1pointerActionDown_convertsCorrectly() {
-        val motionEvent = MotionEvent(
-            2894,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(8290)),
-            arrayOf(PointerCoords(2967f, 5928f))
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        val platformEvent = pointerInputEvent.motionEvent
-        assertThat(uptime).isEqualTo(2_894L)
-        assertThat(pointers).hasSize(1)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(0),
-            true,
-            2967f,
-            5928f
-        )
-        assertThat(platformEvent).isSameInstanceAs(motionEvent)
-    }
-
-    @Test
-    fun convertToPointerInputEvent_1pointerActionMove_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        val motionEvent = MotionEvent(
-            5,
-            ACTION_MOVE,
-            1,
-            0,
-            arrayOf(PointerProperties(2)),
-            arrayOf(PointerCoords(6f, 7f))
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(5L)
-        assertThat(pointers).hasSize(1)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(0),
-            true,
-            6f,
-            7f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_1pointerActionUp_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                10,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(46)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        val motionEvent = MotionEvent(
-            34,
-            ACTION_UP,
-            1,
-            0,
-            arrayOf(PointerProperties(46)),
-            arrayOf(PointerCoords(3f, 4f))
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(34L)
-        assertThat(uptime).isEqualTo(34L)
-        assertThat(pointers).hasSize(1)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(0),
-            false,
-            3f,
-            4f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_2pointers1stPointerActionPointerDown_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        val motionEvent = MotionEvent(
-            4,
-            ACTION_POINTER_DOWN,
-            2,
-            0,
-            arrayOf(
-                PointerProperties(5),
-                PointerProperties(2)
-            ),
-            arrayOf(
-                PointerCoords(7f, 8f),
-                PointerCoords(3f, 4f)
-            )
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(4L)
-        assertThat(pointers).hasSize(2)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(1),
-            true,
-            7f,
-            8f
-        )
-        assertPointerInputEventData(
-            pointers[1],
-            PointerId(0),
-            true,
-            3f,
-            4f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_2pointers2ndPointerActionPointerDown_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        val motionEvent = MotionEvent(
-            4,
-            ACTION_POINTER_DOWN,
-            2,
-            1,
-            arrayOf(
-                PointerProperties(2),
-                PointerProperties(5)
-            ),
-            arrayOf(
-                PointerCoords(3f, 4f),
-                PointerCoords(7f, 8f)
-            )
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(4L)
-        assertThat(pointers).hasSize(2)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(0),
-            true,
-            3f,
-            4f
-        )
-        assertPointerInputEventData(
-            pointers[1],
-            PointerId(1),
-            true,
-            7f,
-            8f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_3pointers1stPointerActionPointerDown_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                4,
-                ACTION_POINTER_DOWN,
-                2,
-                1,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f)
-                )
-            )
-        )
-        val motionEvent =
-            MotionEvent(
-                12,
-                ACTION_POINTER_DOWN,
-                3,
-                0,
-                arrayOf(
-                    PointerProperties(9),
-                    PointerProperties(2),
-                    PointerProperties(5)
-                ),
-                arrayOf(
-                    PointerCoords(10f, 11f),
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f)
-                )
-            )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(12L)
-        assertThat(pointers).hasSize(3)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(2),
-            true,
-            10f,
-            11f
-        )
-        assertPointerInputEventData(
-            pointers[1],
-            PointerId(0),
-            true,
-            3f,
-            4f
-        )
-        assertPointerInputEventData(
-            pointers[2],
-            PointerId(1),
-            true,
-            7f,
-            8f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_3pointers2ndPointerActionPointerDown_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                4,
-                ACTION_POINTER_DOWN,
-                2,
-                1,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f)
-                )
-            )
-        )
-        val motionEvent =
-            MotionEvent(
-                12,
-                ACTION_POINTER_DOWN,
-                3,
-                1,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(9),
-                    PointerProperties(5)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(10f, 11f),
-                    PointerCoords(7f, 8f)
-                )
-            )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(12L)
-        assertThat(pointers).hasSize(3)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(0),
-            true,
-            3f,
-            4f
-        )
-        assertPointerInputEventData(
-            pointers[1],
-            PointerId(2),
-            true,
-            10f,
-            11f
-        )
-        assertPointerInputEventData(
-            pointers[2],
-            PointerId(1),
-            true,
-            7f,
-            8f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_3pointers3rdPointerActionPointerDown_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                4,
-                ACTION_POINTER_DOWN,
-                2,
-                1,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f)
-                )
-            )
-        )
-        val motionEvent =
-            MotionEvent(
-                12,
-                ACTION_POINTER_DOWN,
-                3,
-                2,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5),
-                    PointerProperties(9)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f),
-                    PointerCoords(10f, 11f)
-                )
-            )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(12L)
-        assertThat(pointers).hasSize(3)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(0),
-            true,
-            3f,
-            4f
-        )
-        assertPointerInputEventData(
-            pointers[1],
-            PointerId(1),
-            true,
-            7f,
-            8f
-        )
-        assertPointerInputEventData(
-            pointers[2],
-            PointerId(2),
-            true,
-            10f,
-            11f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_2pointersActionMove_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                4,
-                ACTION_POINTER_DOWN,
-                2,
-                1,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f)
-                )
-            )
-        )
-        val motionEvent = MotionEvent(
-            10,
-            ACTION_MOVE,
-            2,
-            0,
-            arrayOf(
-                PointerProperties(2),
-                PointerProperties(5)
-            ),
-            arrayOf(
-                PointerCoords(11f, 12f),
-                PointerCoords(13f, 15f)
-            )
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(10L)
-        assertThat(pointers).hasSize(2)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(0),
-            true,
-            11f,
-            12f
-        )
-        assertPointerInputEventData(
-            pointers[1],
-            PointerId(1),
-            true,
-            13f,
-            15f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_2pointers1stPointerActionPointerUP_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                4,
-                ACTION_POINTER_DOWN,
-                2,
-                1,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f)
-                )
-            )
-        )
-
-        val motionEvent = MotionEvent(
-            10,
-            ACTION_POINTER_UP,
-            2,
-            0,
-            arrayOf(
-                PointerProperties(2),
-                PointerProperties(5)
-            ),
-            arrayOf(
-                PointerCoords(3f, 4f),
-                PointerCoords(7f, 8f)
-            )
-        )
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(10L)
-        assertThat(pointers).hasSize(2)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(0),
-            false,
-            3f,
-            4f
-        )
-        assertPointerInputEventData(
-            pointers[1],
-            PointerId(1),
-            true,
-            7f,
-            8f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_2pointers2ndPointerActionPointerUp_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                4,
-                ACTION_POINTER_DOWN,
-                2,
-                1,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f)
-                )
-            )
-        )
-
-        val motionEvent = MotionEvent(
-            10,
-            ACTION_POINTER_UP,
-            2,
-            1,
-            arrayOf(
-                PointerProperties(2),
-                PointerProperties(5)
-            ),
-            arrayOf(
-                PointerCoords(3f, 4f),
-                PointerCoords(7f, 8f)
-            )
-        )
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(10L)
-        assertThat(pointers).hasSize(2)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(0),
-            true,
-            3f,
-            4f
-        )
-        assertPointerInputEventData(
-            pointers[1],
-            PointerId(1),
-            false,
-            7f,
-            8f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_3pointers1stPointerActionPointerUp_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                4,
-                ACTION_POINTER_DOWN,
-                2,
-                1,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f)
-                )
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                12,
-                ACTION_POINTER_DOWN,
-                3,
-                2,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5),
-                    PointerProperties(9)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f),
-                    PointerCoords(10f, 11f)
-                )
-            )
-        )
-
-        val motionEvent = MotionEvent(
-            20,
-            ACTION_POINTER_UP,
-            3,
-            0,
-            arrayOf(
-                PointerProperties(2),
-                PointerProperties(5),
-                PointerProperties(9)
-            ),
-            arrayOf(
-                PointerCoords(3f, 4f),
-                PointerCoords(7f, 8f),
-                PointerCoords(10f, 11f)
-            )
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(20L)
-        assertThat(pointers).hasSize(3)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(0),
-            false,
-            3f,
-            4f
-        )
-        assertPointerInputEventData(
-            pointers[1],
-            PointerId(1),
-            true,
-            7f,
-            8f
-        )
-        assertPointerInputEventData(
-            pointers[2],
-            PointerId(2),
-            true,
-            10f,
-            11f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_3pointers2ndPointerActionPointerUp_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                4,
-                ACTION_POINTER_DOWN,
-                2,
-                1,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f)
-                )
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                12,
-                ACTION_POINTER_DOWN,
-                3,
-                2,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5),
-                    PointerProperties(9)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f),
-                    PointerCoords(10f, 11f)
-                )
-            )
-        )
-
-        val motionEvent = MotionEvent(
-            20,
-            ACTION_POINTER_UP,
-            3,
-            1,
-            arrayOf(
-                PointerProperties(2),
-                PointerProperties(5),
-                PointerProperties(9)
-            ),
-            arrayOf(
-                PointerCoords(3f, 4f),
-                PointerCoords(7f, 8f),
-                PointerCoords(10f, 11f)
-            )
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(20L)
-        assertThat(pointers).hasSize(3)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(0),
-            true,
-            3f,
-            4f
-        )
-        assertPointerInputEventData(
-            pointers[1],
-            PointerId(1),
-            false,
-            7f,
-            8f
-        )
-        assertPointerInputEventData(
-            pointers[2],
-            PointerId(2),
-            true,
-            10f,
-            11f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_3pointers3rdPointerActionPointerUp_convertsCorrectly() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                4,
-                ACTION_POINTER_DOWN,
-                2,
-                1,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f)
-                )
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                12,
-                ACTION_POINTER_DOWN,
-                3,
-                2,
-                arrayOf(
-                    PointerProperties(2),
-                    PointerProperties(5),
-                    PointerProperties(9)
-                ),
-                arrayOf(
-                    PointerCoords(3f, 4f),
-                    PointerCoords(7f, 8f),
-                    PointerCoords(10f, 11f)
-                )
-            )
-        )
-
-        val motionEvent = MotionEvent(
-            20,
-            ACTION_POINTER_UP,
-            3,
-            2,
-            arrayOf(
-                PointerProperties(2),
-                PointerProperties(5),
-                PointerProperties(9)
-            ),
-            arrayOf(
-                PointerCoords(3f, 4f),
-                PointerCoords(7f, 8f),
-                PointerCoords(10f, 11f)
-            )
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(20L)
-        assertThat(pointers).hasSize(3)
-        assertPointerInputEventData(
-            pointers[0],
-            PointerId(0),
-            true,
-            3f,
-            4f
-        )
-        assertPointerInputEventData(
-            pointers[1],
-            PointerId(1),
-            true,
-            7f,
-            8f
-        )
-        assertPointerInputEventData(
-            pointers[2],
-            PointerId(2),
-            false,
-            10f,
-            11f
-        )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_downUpDownUpDownUpSameMotionEventId_pointerIdsAreUnique() {
-        val down1 = MotionEvent(
-            100,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(0)),
-            arrayOf(PointerCoords(10f, 11f))
-        )
-
-        val up1 = MotionEvent(
-            200,
-            ACTION_UP,
-            1,
-            0,
-            arrayOf(PointerProperties(0)),
-            arrayOf(PointerCoords(10f, 11f))
-        )
-
-        val down2 = MotionEvent(
-            300,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(0)),
-            arrayOf(PointerCoords(20f, 21f))
-        )
-
-        val up2 = MotionEvent(
-            400,
-            ACTION_UP,
-            1,
-            0,
-            arrayOf(PointerProperties(0)),
-            arrayOf(PointerCoords(20f, 21f))
-        )
-
-        val down3 = MotionEvent(
-            500,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(0)),
-            arrayOf(PointerCoords(30f, 31f))
-        )
-
-        val up3 = MotionEvent(
-            600,
-            ACTION_UP,
-            1,
-            0,
-            arrayOf(PointerProperties(0)),
-            arrayOf(PointerCoords(30f, 31f))
-        )
-
-        // Test the different events sequentially, since the returned event contains a list that
-        // will be reused by convertToPointerInputEvent for performance, so it shouldn't be held
-        // for longer than needed during the sequential dispatch.
-
-        val pointerInputEventDown1 = motionEventAdapter.convertToPointerInputEvent(down1)
-        assertThat(pointerInputEventDown1).isNotNull()
-        assertThat(pointerInputEventDown1!!.pointers[0].id).isEqualTo(PointerId(0))
-
-        val pointerInputEventUp1 = motionEventAdapter.convertToPointerInputEvent(up1)
-        assertThat(pointerInputEventUp1).isNotNull()
-        assertThat(pointerInputEventUp1!!.pointers[0].id).isEqualTo(PointerId(0))
-
-        val pointerInputEventDown2 = motionEventAdapter.convertToPointerInputEvent(down2)
-        assertThat(pointerInputEventDown2).isNotNull()
-        assertThat(pointerInputEventDown2!!.pointers[0].id).isEqualTo(PointerId(1))
-
-        val pointerInputEventUp2 = motionEventAdapter.convertToPointerInputEvent(up2)
-        assertThat(pointerInputEventUp2).isNotNull()
-        assertThat(pointerInputEventUp2!!.pointers[0].id).isEqualTo(PointerId(1))
-
-        val pointerInputEventDown3 = motionEventAdapter.convertToPointerInputEvent(down3)
-        assertThat(pointerInputEventDown3).isNotNull()
-        assertThat(pointerInputEventDown3!!.pointers[0].id).isEqualTo(PointerId(2))
-
-        val pointerInputEventUp3 = motionEventAdapter.convertToPointerInputEvent(up3)
-        assertThat(pointerInputEventUp3).isNotNull()
-        assertThat(pointerInputEventUp3!!.pointers[0].id).isEqualTo(PointerId(2))
-    }
-
-    @Test
-    fun convertToPointerInputEvent_downDownDownRandomMotionEventIds_pointerIdsAreUnique() {
-        val down1 = MotionEvent(
-            100,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(
-                PointerProperties(9276)
-            ),
-            arrayOf(
-                PointerCoords(10f, 11f)
-            )
-        )
-
-        val down2 = MotionEvent(
-            200,
-            ACTION_POINTER_DOWN,
-            2,
-            1,
-            arrayOf(
-                PointerProperties(9276),
-                PointerProperties(1759)
-            ),
-            arrayOf(
-                PointerCoords(10f, 11f),
-                PointerCoords(20f, 21f)
-            )
-        )
-
-        val down3 = MotionEvent(
-            300,
-            ACTION_POINTER_DOWN,
-            3,
-            2,
-            arrayOf(
-                PointerProperties(9276),
-                PointerProperties(1759),
-                PointerProperties(5043)
-            ),
-            arrayOf(
-                PointerCoords(10f, 11f),
-                PointerCoords(20f, 21f),
-                PointerCoords(30f, 31f)
-            )
-        )
-
-        // Test the different events sequentially, since the returned event contains a list that
-        // will be reused by convertToPointerInputEvent for performance, so it shouldn't be held
-        // for longer than needed during the sequential dispatch.
-
-        val pointerInputEventDown1 = motionEventAdapter.convertToPointerInputEvent(down1)
-
-        assertThat(pointerInputEventDown1).isNotNull()
-        assertThat(pointerInputEventDown1!!.pointers).hasSize(1)
-        assertThat(pointerInputEventDown1.pointers[0].id).isEqualTo(PointerId(0))
-
-        val pointerInputEventDown2 = motionEventAdapter.convertToPointerInputEvent(down2)
-
-        assertThat(pointerInputEventDown2).isNotNull()
-        assertThat(pointerInputEventDown2!!.pointers).hasSize(2)
-        assertThat(pointerInputEventDown2.pointers[0].id).isEqualTo(PointerId(0))
-        assertThat(pointerInputEventDown2.pointers[1].id).isEqualTo(PointerId(1))
-
-        val pointerInputEventDown3 = motionEventAdapter.convertToPointerInputEvent(down3)
-
-        assertThat(pointerInputEventDown3).isNotNull()
-        assertThat(pointerInputEventDown3!!.pointers).hasSize(3)
-        assertThat(pointerInputEventDown2.pointers[0].id).isEqualTo(PointerId(0))
-        assertThat(pointerInputEventDown2.pointers[1].id).isEqualTo(PointerId(1))
-        assertThat(pointerInputEventDown3.pointers[2].id).isEqualTo(PointerId(2))
-    }
-
-    @Test
-    fun convertToPointerInputEvent_motionEventOffset_usesRawCoordinatesInsteadOfOffset() {
-        val motionEvent = MotionEvent(
-            0,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(0)),
-            arrayOf(PointerCoords(1f, 2f))
-        )
-
-        motionEvent.offsetLocation(10f, 20f)
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        val uptime = pointerInputEvent!!.uptime
-        val pointers = pointerInputEvent.pointers
-        assertThat(uptime).isEqualTo(0L)
-        assertThat(pointers).hasSize(1)
-        assertPointerInputEventData(pointers[0], PointerId(0), true, 1f, 2f)
-    }
-
-    @Test
-    fun convertToPointerInputEvent_actionCancel_returnsNull() {
-        val motionEvent = MotionEvent(
-            0,
-            ACTION_CANCEL,
-            1,
-            0,
-            arrayOf(PointerProperties(0)),
-            arrayOf(PointerCoords(1f, 2f))
-        )
-
-        motionEvent.offsetLocation(10f, 20f)
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNull()
-    }
-
-    @Test
-    fun convertToPointerInputEvent_downUp_noPointersTracked() {
-        val motionEvent1 = MotionEvent(
-            2894,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(8290)),
-            arrayOf(PointerCoords(2967f, 5928f))
-        )
-        val motionEvent2 = MotionEvent(
-            2894,
-            ACTION_UP,
-            1,
-            0,
-            arrayOf(PointerProperties(8290)),
-            arrayOf(PointerCoords(2967f, 5928f))
-        )
-
-        motionEventAdapter.convertToPointerInputEvent(motionEvent1)
-        motionEventAdapter.convertToPointerInputEvent(motionEvent2)
-
-        assertThat(motionEventAdapter.motionEventToComposePointerIdMap.size()).isEqualTo(0)
-    }
-
-    @Test
-    fun convertToPointerInputEvent_downDown_correctPointersTracked() {
-        val motionEvent1 = MotionEvent(
-            1,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(2)),
-            arrayOf(PointerCoords(3f, 4f))
-        )
-        val motionEvent2 = MotionEvent(
-            4,
-            ACTION_POINTER_DOWN,
-            2,
-            0,
-            arrayOf(
-                PointerProperties(5),
-                PointerProperties(2)
-            ),
-            arrayOf(
-                PointerCoords(7f, 8f),
-                PointerCoords(3f, 4f)
-            )
-        )
-
-        motionEventAdapter.convertToPointerInputEvent(motionEvent1)
-        motionEventAdapter.convertToPointerInputEvent(motionEvent2)
-
-        assertThat(motionEventAdapter.motionEventToComposePointerIdMap.toMap())
-            .containsExactlyEntriesIn(
-                mapOf(
-                    2 to PointerId(0),
-                    5 to PointerId(1)
-                )
-            )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_downDownFirstUp_correctPointerTracked() {
-        val motionEvent1 = MotionEvent(
-            1,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(2)),
-            arrayOf(PointerCoords(3f, 4f))
-        )
-        val motionEvent2 = MotionEvent(
-            4,
-            ACTION_POINTER_DOWN,
-            2,
-            0,
-            arrayOf(
-                PointerProperties(5),
-                PointerProperties(2)
-            ),
-            arrayOf(
-                PointerCoords(7f, 8f),
-                PointerCoords(3f, 4f)
-            )
-        )
-        val motionEvent3 = MotionEvent(
-            10,
-            ACTION_POINTER_UP,
-            2,
-            0,
-            arrayOf(
-                PointerProperties(5),
-                PointerProperties(2)
-            ),
-            arrayOf(
-                PointerCoords(7f, 8f),
-                PointerCoords(3f, 4f)
-            )
-        )
-
-        motionEventAdapter.convertToPointerInputEvent(motionEvent1)
-        motionEventAdapter.convertToPointerInputEvent(motionEvent2)
-        motionEventAdapter.convertToPointerInputEvent(motionEvent3)
-
-        assertThat(motionEventAdapter.motionEventToComposePointerIdMap.toMap())
-            .containsExactlyEntriesIn(
-                mapOf(2 to PointerId(0))
-            )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_downDownSecondUp_correctPointerTracked() {
-        val motionEvent1 = MotionEvent(
-            1,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(2)),
-            arrayOf(PointerCoords(3f, 4f))
-        )
-        val motionEvent2 = MotionEvent(
-            4,
-            ACTION_POINTER_DOWN,
-            2,
-            0,
-            arrayOf(
-                PointerProperties(5),
-                PointerProperties(2)
-            ),
-            arrayOf(
-                PointerCoords(7f, 8f),
-                PointerCoords(3f, 4f)
-            )
-        )
-        val motionEvent3 = MotionEvent(
-            10,
-            ACTION_POINTER_UP,
-            2,
-            1,
-            arrayOf(
-                PointerProperties(5),
-                PointerProperties(2)
-            ),
-            arrayOf(
-                PointerCoords(7f, 8f),
-                PointerCoords(3f, 4f)
-            )
-        )
-
-        motionEventAdapter.convertToPointerInputEvent(motionEvent1)
-        motionEventAdapter.convertToPointerInputEvent(motionEvent2)
-        motionEventAdapter.convertToPointerInputEvent(motionEvent3)
-
-        assertThat(motionEventAdapter.motionEventToComposePointerIdMap.toMap())
-            .containsExactlyEntriesIn(
-                mapOf(5 to PointerId(1))
-            )
-    }
-
-    @Test
-    fun convertToPointerInputEvent_downDownUpUp_noPointersTracked() {
-        val motionEvent1 = MotionEvent(
-            1,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(2)),
-            arrayOf(PointerCoords(3f, 4f))
-        )
-        val motionEvent2 = MotionEvent(
-            4,
-            ACTION_POINTER_DOWN,
-            2,
-            0,
-            arrayOf(
-                PointerProperties(5),
-                PointerProperties(2)
-            ),
-            arrayOf(
-                PointerCoords(7f, 8f),
-                PointerCoords(3f, 4f)
-            )
-        )
-        val motionEvent3 = MotionEvent(
-            10,
-            ACTION_POINTER_UP,
-            2,
-            0,
-            arrayOf(
-                PointerProperties(5),
-                PointerProperties(2)
-            ),
-            arrayOf(
-                PointerCoords(7f, 8f),
-                PointerCoords(3f, 4f)
-            )
-        )
-        val motionEvent4 = MotionEvent(
-            20,
-            ACTION_UP,
-            1,
-            0,
-            arrayOf(PointerProperties(2)),
-            arrayOf(PointerCoords(3f, 4f))
-        )
-
-        motionEventAdapter.convertToPointerInputEvent(motionEvent1)
-        motionEventAdapter.convertToPointerInputEvent(motionEvent2)
-        motionEventAdapter.convertToPointerInputEvent(motionEvent3)
-        motionEventAdapter.convertToPointerInputEvent(motionEvent4)
-
-        assertThat(motionEventAdapter.motionEventToComposePointerIdMap.toMap()).isEmpty()
-    }
-
-    @Test
-    fun convertToPointerInputEvent_downCancel_noPointersTracked() {
-        val motionEvent1 = MotionEvent(
-            1,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(2)),
-            arrayOf(PointerCoords(3f, 4f))
-        )
-        val motionEvent2 = MotionEvent(
-            4,
-            ACTION_POINTER_DOWN,
-            2,
-            0,
-            arrayOf(
-                PointerProperties(5),
-                PointerProperties(2)
-            ),
-            arrayOf(
-                PointerCoords(7f, 8f),
-                PointerCoords(3f, 4f)
-            )
-        )
-        val motionEvent3 = MotionEvent(
-            10,
-            ACTION_CANCEL,
-            2,
-            0,
-            arrayOf(
-                PointerProperties(5),
-                PointerProperties(2)
-            ),
-            arrayOf(
-                PointerCoords(7f, 8f),
-                PointerCoords(3f, 4f)
-            )
-        )
-        motionEventAdapter.convertToPointerInputEvent(motionEvent1)
-        motionEventAdapter.convertToPointerInputEvent(motionEvent2)
-        motionEventAdapter.convertToPointerInputEvent(motionEvent3)
-
-        assertThat(motionEventAdapter.motionEventToComposePointerIdMap.toMap()).isEmpty()
-    }
-
-    @Test
-    fun convertToPointerInputEvent_doesNotSynchronouslyMutateMotionEvent() {
-        val motionEvent = MotionEvent(
-            1,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(2)),
-            arrayOf(PointerCoords(3f, 4f))
-        )
-        motionEvent.offsetLocation(10f, 100f)
-
-        motionEventAdapter.convertToPointerInputEvent(motionEvent)
-
-        assertThat(motionEvent.x).isEqualTo(13f)
-        assertThat(motionEvent.y).isEqualTo(104f)
-    }
-
-    @Test
-    fun convertToPointerInputEvent_1PointerActionDown_includesMotionEvent() {
-        val motionEvent = MotionEvent(
-            2894,
-            ACTION_DOWN,
-            1,
-            0,
-            arrayOf(PointerProperties(8290)),
-            arrayOf(PointerCoords(2967f, 5928f))
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        assertThat(pointerInputEvent!!.motionEvent).isSameInstanceAs(motionEvent)
-    }
-
-    @Test
-    fun convertToPointerInputEvent_1pointerActionMove_includesMotionEvent() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                1,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(2)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        val motionEvent = MotionEvent(
-            5,
-            ACTION_MOVE,
-            1,
-            0,
-            arrayOf(PointerProperties(2)),
-            arrayOf(PointerCoords(6f, 7f))
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        assertThat(pointerInputEvent!!.motionEvent).isSameInstanceAs(motionEvent)
-    }
-
-    @Test
-    fun convertToPointerInputEvent_1pointerActionUp_includesMotionEvent() {
-        motionEventAdapter.convertToPointerInputEvent(
-            MotionEvent(
-                10,
-                ACTION_DOWN,
-                1,
-                0,
-                arrayOf(PointerProperties(46)),
-                arrayOf(PointerCoords(3f, 4f))
-            )
-        )
-        val motionEvent = MotionEvent(
-            34,
-            ACTION_UP,
-            1,
-            0,
-            arrayOf(PointerProperties(46)),
-            arrayOf(PointerCoords(3f, 4f))
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-
-        assertThat(pointerInputEvent!!.motionEvent).isSameInstanceAs(motionEvent)
-    }
-
-    @Test
-    fun convertScrollEvent_horizontalPositive() {
-        val motionEvent = MotionEvent(
-            eventTime = 1,
-            action = ACTION_SCROLL,
-            numPointers = 1,
-            actionIndex = 0,
-            pointerProperties = arrayOf(PointerProperties(2)),
-            pointerCoords = arrayOf(
-                PointerCoords(3f, 4f).apply {
-                    setAxisValue(AXIS_HSCROLL, 5f)
-                }
-            )
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-        assertThat(pointerInputEvent!!.pointers[0].scrollDelta).isEqualTo(Offset(5f, 0f))
-        assertThat(pointerInputEvent.motionEvent).isSameInstanceAs(motionEvent)
-    }
-
-    @Test
-    fun convertScrollEvent_horizontalNegative() {
-        val motionEvent = MotionEvent(
-            eventTime = 1,
-            action = ACTION_SCROLL,
-            numPointers = 1,
-            actionIndex = 0,
-            pointerProperties = arrayOf(PointerProperties(2)),
-            pointerCoords = arrayOf(
-                PointerCoords(3f, 4f).apply {
-                    setAxisValue(AXIS_HSCROLL, -5f)
-                }
-            )
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-        assertThat(pointerInputEvent!!.pointers[0].scrollDelta).isEqualTo(Offset(-5f, 0f))
-        assertThat(pointerInputEvent.motionEvent).isSameInstanceAs(motionEvent)
-    }
-
-    @Test
-    fun convertScrollEvent_verticalPositive() {
-        val motionEvent = MotionEvent(
-            eventTime = 1,
-            action = ACTION_SCROLL,
-            numPointers = 1,
-            actionIndex = 0,
-            pointerProperties = arrayOf(PointerProperties(2)),
-            pointerCoords = arrayOf(
-                PointerCoords(3f, 4f).apply {
-                    setAxisValue(AXIS_VSCROLL, 5f)
-                }
-            )
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-        // Note: y is inverted, per https://r.android.com/2071209
-        assertThat(pointerInputEvent!!.pointers[0].scrollDelta).isEqualTo(Offset(0f, -5f))
-        assertThat(pointerInputEvent.motionEvent).isSameInstanceAs(motionEvent)
-    }
-
-    @Test
-    fun convertScrollEvent_verticalNegative() {
-        val motionEvent = MotionEvent(
-            eventTime = 1,
-            action = ACTION_SCROLL,
-            numPointers = 1,
-            actionIndex = 0,
-            pointerProperties = arrayOf(PointerProperties(2)),
-            pointerCoords = arrayOf(
-                PointerCoords(3f, 4f).apply {
-                    setAxisValue(AXIS_VSCROLL, -5f)
-                }
-            )
-        )
-
-        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
-        assertThat(pointerInputEvent).isNotNull()
-        // Note: y is inverted, per https://r.android.com/2071209
-        assertThat(pointerInputEvent!!.pointers[0].scrollDelta).isEqualTo(Offset(0f, 5f))
-        assertThat(pointerInputEvent.motionEvent).isSameInstanceAs(motionEvent)
-    }
-
-    private fun MotionEventAdapter.convertToPointerInputEvent(motionEvent: MotionEvent) =
-        convertToPointerInputEvent(motionEvent, positionCalculator)
-
-    private fun SparseLongArray.toMap(): Map<Int, PointerId> {
-        val map = mutableMapOf<Int, PointerId>()
-        for (i in 0 until size()) {
-            val key = keyAt(i)
-            val value = valueAt(i)
-            map[key] = PointerId(value)
-        }
-        return map
-    }
-}
-
-// Private helper functions
-
-private fun MotionEvent(
-    eventTime: Int,
-    action: Int,
-    numPointers: Int,
-    actionIndex: Int,
-    pointerProperties: Array<MotionEvent.PointerProperties>,
-    pointerCoords: Array<MotionEvent.PointerCoords>,
-    downTime: Long = 0
-) = MotionEvent.obtain(
-    downTime,
-    eventTime.toLong(),
-    action + (actionIndex shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
-    numPointers,
-    pointerProperties,
-    pointerCoords,
-    0,
-    0,
-    0f,
-    0f,
-    0,
-    0,
-    InputDevice.SOURCE_TOUCHSCREEN,
-    0
-)
-
-private fun assertPointerInputEventData(
-    actual: PointerInputEventData,
-    id: PointerId,
-    isDown: Boolean,
-    x: Float,
-    y: Float,
-    type: PointerType = PointerType.Touch
-) {
-    assertThat(actual.id).isEqualTo(id)
-    assertThat(actual.down).isEqualTo(isDown)
-    assertThat(actual.positionOnScreen.x).isEqualTo(x)
-    assertThat(actual.positionOnScreen.y).isEqualTo(y)
-    assertThat(actual.type).isEqualTo(type)
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt
deleted file mode 100644
index 26e13a8..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt
+++ /dev/null
@@ -1,4101 +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.compose.ui.input.pointer
-
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.movableContentOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.SolidColor
-import androidx.compose.ui.platform.InspectableValue
-import androidx.compose.ui.platform.LocalPointerIconService
-import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performMouseInput
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@OptIn(ExperimentalTestApi::class)
-@RunWith(AndroidJUnit4::class)
-class PointerIconTest {
-    @get:Rule
-    val rule = createComposeRule()
-    private val parentIconTag = "myParentIcon"
-    private val childIconTag = "myChildIcon"
-    private val grandchildIconTag = "myGrandchildIcon"
-    private val desiredParentIcon = PointerIcon.Crosshair
-    private val desiredChildIcon = PointerIcon.Text
-    private val desiredGrandchildIcon = PointerIcon.Hand
-    private val desiredDefaultIcon = PointerIcon.Default
-    private lateinit var iconService: PointerIconService
-
-    @Before
-    fun setup() {
-        iconService = object : PointerIconService {
-            private var currentIcon: PointerIcon = PointerIcon.Default
-            override fun getIcon(): PointerIcon {
-                return currentIcon
-            }
-
-            override fun setIcon(value: PointerIcon?) {
-                currentIcon = value ?: PointerIcon.Default
-            }
-        }
-    }
-
-    @Test
-    fun testInspectorValue() {
-        isDebugInspectorInfoEnabled = true
-        rule.setContent {
-            val modifier = Modifier.pointerHoverIcon(
-                PointerIcon.Hand,
-                overrideDescendants = false
-            ) as InspectableValue
-            assertThat(modifier.nameFallback).isEqualTo("pointerHoverIcon")
-            assertThat(modifier.valueOverride).isNull()
-            assertThat(modifier.inspectableElements.map { it.name }.asIterable()).containsExactly(
-                "icon",
-                "overrideDescendants",
-            )
-        }
-        isDebugInspectorInfoEnabled = false
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Child Box’s [PointerIcon.Text] wins for the entire Box area because it’s lower in
-     *  the hierarchy than Parent Box. If the Parent Box's overrideDescendants = false, the Child
-     *  Box takes priority.
-     */
-    @Test
-    fun parentChildFullOverlap_noOverrideDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Box(
-                        Modifier
-                            .requiredSize(200.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    )
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify Parent Box is respecting Child Box's icon
-        verifyIconOnHover(parentIconTag, desiredChildIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire Box area because it’s higher in
-     *  the hierarchy than Child Box. Also the Parent Box's overrideDescendants value is TRUE, so
-     *  as the topmost parent in the hierarchy with overrideDescendants = true, all its children
-     *  must respect it.
-     */
-    @Test
-    fun parentChildFullOverlap_parentOverridesDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
-                ) {
-                    Box(
-                        Modifier
-                            .requiredSize(200.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    )
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-        // Verify Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Child Box’s [PointerIcon.Text] wins for the entire Box area because its lower in priority
-     *  than Parent Box. If the Parent Box's overrideDescendants = false, the Child Box takes
-     *  priority.
-     */
-    @Test
-    fun parentChildFullOverlap_childOverridesDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Box(
-                        Modifier
-                            .requiredSize(200.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    )
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify Parent Box is respecting Child Box's icon
-        verifyIconOnHover(parentIconTag, desiredChildIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire Box area because its
-     *  overrideDescendants = true. The Parent Box takes precedence because it is the topmost parent
-     *  in the hierarchy with overrideDescendants = true.
-     */
-    @Test
-    fun parentChildFullOverlap_bothOverrideDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
-                ) {
-                    Box(
-                        Modifier
-                            .requiredSize(200.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    )
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-        // Verify Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Child Box’s [PointerIcon.Hand] wins for the entire Child Box surface area because there's
-     *  no parent in its hierarchy that has overrideDescendants = true. Parent Box's
-     *  [PointerIcon.Crosshair] wins for all remaining surface area of its Box that doesn't overlap
-     *  with Child Box.
-     */
-    @Test
-    fun parentChildPartialOverlap_noOverrideDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(100.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    )
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify remaining Parent Box's area is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Parent Box’s [PointerIcon.Hand] wins for the entire Box area because its
-     *  overrideDescendants = true, so every child underneath it in the hierarchy must respect its
-     *  pointer icon since it's the topmost parent in the hierarchy with overrideDescendants = true.
-     */
-    @Test
-    fun parentChildPartialOverlap_parentOverridesDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(100.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    )
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-        // Verify Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Child Box’s [PointerIcon.Hand] wins for the entire Child Box surface area because it’s lower
-     *  in the hierarchy than Parent Box. If Parent Box's overrideDescendants = false, the Child
-     *  Box takes priority.
-     */
-    @Test
-    fun parentChildPartialOverlap_childOverridesDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(100.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    )
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify remaining Parent Box's area is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire Box area because its
-     *  overrideDescendants = true. If multiple locations in the hierarchy set
-     *  overrideDescendants = true, the highest parent in the hierarchy takes precedence (in this
-     *  example, it was Parent Box).
-     */
-    @Test
-    fun parentChildPartialOverlap_bothOverrideDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(100.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    )
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-        // Verify Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Parent Box (no custom icon)
-     *      ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Initially, the Child Box's [PointerIcon.Text] should win for its entire surface area
-     *  because it has no competition in the hierarchy for any other custom icons. After the Parent
-     *  Box dynamically has the pointerHoverIcon Modifier added to it, the Parent Box's
-     *  [PointerIcon.Crosshair] should win for the entire surface area of the Parent Box and Child
-     *  Box because the Parent Box has overrideDescendants = true.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
-     */
-    @Ignore("b/267170292 - not yet implemented")
-    @Test
-    fun parentChildPartialOverlap_parentModifierDynamicallyAdded() {
-        val isVisible = mutableStateOf(false)
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .then(
-                            if (isVisible.value) Modifier.pointerHoverIcon(
-                                desiredParentIcon,
-                                overrideDescendants = true
-                            ) else Modifier
-                        )
-
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    )
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Child Box is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify Parent Box's icon is the desired default icon
-        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
-        // Dynamically add the pointerHoverIcon Modifier to the Parent Box
-        rule.runOnIdle {
-            isVisible.value = true
-        }
-        // Verify Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-        // Verify Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Parent Box (no custom icon)
-     *      ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Initially, the Child Box's [PointerIcon.Text] should win for its entire surface area
-     *  because it has no competition in the hierarchy for any other custom icons. After the Parent
-     *  Box dynamically has the pointerHoverIcon Modifier added to it, the Parent Box's
-     *  [PointerIcon.Crosshair] should win for the entire surface area of the Parent Box and Child
-     *  Box because the Parent Box has overrideDescendants = true.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
-     */
-    @Ignore("b/267170292 - not yet implemented")
-    @Test
-    fun parentChildPartialOverlap_parentModifierDynamicallyAddedWithMoveEvents() {
-        val isVisible = mutableStateOf(false)
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .then(
-                            if (isVisible.value) Modifier.pointerHoverIcon(
-                                desiredParentIcon,
-                                overrideDescendants = true
-                            ) else Modifier
-                        )
-
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    )
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over Child Box and verify it has the desired child icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            enter(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move to Parent Box and verify its icon is the desired default icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Move back to the Child Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Dynamically add the pointerHoverIcon Modifier to the Parent Box
-        rule.runOnIdle {
-            isVisible.value = true
-        }
-        // Verify the Child Box has updated to respect the desired parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move within the Child Box and verify it is still respecting the desired parent icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        // Move to the Parent Box and verify it also has the desired parent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Exit hovering over Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     *  The hierarchy for the initial setup of this test is:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *      ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Initially, Child Box’s [PointerIcon.Hand] wins for the entire Child Box surface area because
-     *  there's no parent in its hierarchy that has overrideDescendants = true. Additionally, Parent
-     *  Box's [PointerIcon.Crosshair] would initially win for all remaining surface area of its Box
-     *  that doesn't overlap with Child Box. Once Parent Box's overrideDescendants parameter is
-     *  dynamically updated to true, the Parent Box's icon should win for its entire surface area,
-     *  including within Child Box.
-     */
-    @Ignore("b/266976920 - not yet implemented")
-    @Test
-    fun parentChildPartialOverlap_parentOverrideDescendantsDynamicallyUpdated() {
-        val parentOverrideState = mutableStateOf(false)
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(
-                            desiredParentIcon,
-                            overrideDescendants = parentOverrideState.value
-                        )
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    )
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify remaining Parent Box's area is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-        rule.runOnIdle {
-            parentOverrideState.value = true
-        }
-        // Verify Child Box's icon is the desired parent icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-        // Verify Parent Box also has the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  ChildA Box’s [PointerIcon.Text] wins for the entire surface area of ChildA's Box. ChildB
-     *  Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
-     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that's not
-     *  covered by ChildA Box or ChildB Box. In this example, there's no competition for pointer
-     *  icons because the parent has no icon set and neither ChildA or ChildB Boxes overlap.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *    ⤷ Child Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun NonOverlappingSiblings_noOverrideDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Column {
-                        Box(
-                            Modifier
-                                .padding(20.dp)
-                                .requiredSize(50.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                        )
-                        // Referencing grandchild tag/icon for ChildB in this test
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(50.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify ChildA Box's icon is the desired ChildA icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify ChildB Box's icon is the desired ChildB icon
-        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
-        // Verify remaining Parent Box's icon is the default icon
-        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  ChildA Box’s [PointerIcon.Text] wins for the entire surface area of ChildA's Box. ChildB
-     *  Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
-     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that's not
-     *  covered by ChildA Box or ChildB Box. In this example, it doesn't matter whether ChildA Box's
-     *  overrideDescendants = true or false because there's no competition for pointer icons in
-     *  this example.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *    ⤷ Child Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun NonOverlappingSiblings_firstChildOverridesDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Column {
-                        Box(
-                            Modifier
-                                .padding(20.dp)
-                                .requiredSize(50.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                        )
-                        // Referencing grandchild tag/icon for ChildB in this test
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(50.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify ChildA Box's icon is the desired ChildA icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify ChildB Box's icon is the desired ChildB icon
-        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
-        // Verify remaining Parent Box's icon is the default icon
-        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  ChildA Box’s [PointerIcon.Text] wins for the entire surface area of ChildA's Box. ChildB
-     *  Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
-     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that's not
-     *  covered by ChildA Box or ChildB Box. In this example, it doesn't matter whether ChildB Box's
-     *  overrideDescendants = true or false because there's no competition for pointer icons in
-     *  this example.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *    ⤷ Child Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun NonOverlappingSiblings_secondChildOverridesDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Column {
-                        Box(
-                            Modifier
-                                .padding(20.dp)
-                                .requiredSize(50.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                        )
-                        // Referencing grandchild tag/icon for ChildB in this test
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(50.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = true)
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify ChildA Box's icon is the desired ChildA icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify ChildB Box's icon is the desired ChildB icon
-        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
-        // Verify remaining Parent Box's icon is the default icon
-        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  ChildA Box’s [PointerIcon.Text] wins for the entire surface area of ChildA's Box. ChildB
-     *  Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
-     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that's not
-     *  covered by ChildA Box or ChildB Box. In this example, it doesn't matter whether ChildA Box
-     *  and ChildB Box's overrideDescendants = true or false because there's no competition for
-     *  pointer icons in this example.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ ChildA Box (output icon = [PointerIcon.Text])
-     *    ⤷ ChildB Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun NonOverlappingSiblings_bothOverrideDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Column {
-                        Box(
-                            Modifier
-                                .padding(20.dp)
-                                .requiredSize(50.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                        )
-                        // Referencing grandchild tag/icon for ChildB in this test
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(50.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = true)
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify ChildA Box's icon is the desired ChildA icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify ChildB Box's icon is the desired ChildB icon
-        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
-        // Verify remaining Parent Box's icon is the default icon
-        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE) where
-     *        ChildB Box's surface area overlaps with its sibling, ChildA, within the Parent Box
-     *
-     *  Expected Output:
-     *  ChildB Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
-     *  ChildA Box's [PointerIcon.Text] wins for the remaining surface area of ChildA Box not
-     *  covered by ChildB Box. [PointerIcon.Default] wins for the remainder of the surface area of
-     *  Parent Box that's not covered by ChildA Box or ChildB Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ ChildA Box (output icon = [PointerIcon.Text])
-     *    ⤷ ChildB Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun OverlappingSiblings_noOverrideDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(120.dp, 60.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    )
-                    // Referencing grandchild tag/icon for ChildB in this test
-                    Box(
-                        Modifier
-                            .padding(horizontal = 100.dp, vertical = 40.dp)
-                            .requiredSize(120.dp, 20.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                            .testTag(grandchildIconTag)
-                            .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = false)
-                    )
-                }
-            }
-        }
-
-        verifyOverlappingSiblings()
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE) where
-     *        ChildB Box's surface area overlaps with its sibling, ChildA, within the Parent Box
-     *
-     *  Expected Output:
-     *  ChildB Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
-     *  ChildA Box's [PointerIcon.Text] wins for the remaining surface area of ChildA Box not
-     *  covered by ChildB Box. [PointerIcon.Default] wins for the remainder of the surface area of
-     *  Parent Box that's not covered by ChildA Box or ChildB Box. The overrideDescendants param
-     *  only affects that element's children. So in this example, it doesn't matter whether ChildA
-     *  Box's overrideDescendants = true because ChildB is its sibling and is therefore unaffected
-     *  by this param.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ ChildA Box (output icon = [PointerIcon.Text])
-     *    ⤷ ChildB Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun OverlappingSiblings_childAOverridesDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(120.dp, 60.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    )
-                    // Referencing grandchild tag/icon for ChildB in this test
-                    Box(
-                        Modifier
-                            .padding(horizontal = 100.dp, vertical = 40.dp)
-                            .requiredSize(120.dp, 20.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                            .testTag(grandchildIconTag)
-                            .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = false)
-                    )
-                }
-            }
-        }
-
-        verifyOverlappingSiblings()
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE) where
-     *        ChildB Box's surface area overlaps with its sibling, ChildA, within the Parent Box
-     *
-     *  Expected Output:
-     *  ChildB Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
-     *  ChildA Box's [PointerIcon.Text] wins for the remaining surface area of ChildA Box not
-     *  covered by ChildB Box. [PointerIcon.Default] wins for the remainder of the surface area of
-     *  Parent Box that's not covered by ChildA Box or ChildB Box. The overrideDescendants param
-     *  only affects that element's children. So in this example, it doesn't matter whether ChildB
-     *  Box's overrideDescendants = true because ChildA is its sibling and is therefore unaffected
-     *  by this param.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ ChildA Box (output icon = [PointerIcon.Text])
-     *    ⤷ ChildB Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun OverlappingSiblings_childBOverridesDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(120.dp, 60.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    )
-                    // Referencing grandchild tag/icon for ChildB in this test
-                    Box(
-                        Modifier
-                            .padding(horizontal = 100.dp, vertical = 40.dp)
-                            .requiredSize(120.dp, 20.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                            .testTag(grandchildIconTag)
-                            .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = true)
-                    )
-                }
-            }
-        }
-
-        verifyOverlappingSiblings()
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE) where
-     *        ChildB Box's surface area overlaps with its sibling, ChildA, within the Parent Box
-     *
-     *  Expected Output:
-     *  ChildB Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
-     *  ChildA Box's [PointerIcon.Text] wins for the remaining surface area of ChildA Box not
-     *  covered by ChildB Box. [PointerIcon.Default] wins for the remainder of the surface area of
-     *  Parent Box that's not covered by ChildA Box or ChildB Box. The overrideDescendants param
-     *  only affects that element's children. So in this example, it doesn't matter whether ChildA
-     *  Box or ChildB Box's overrideDescendants = true because ChildA and ChildB Boxes are siblings
-     *  and are unaffected by each other's overrideDescendants param.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ ChildA Box (output icon = [PointerIcon.Text])
-     *    ⤷ ChildB Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun OverlappingSiblings_bothOverrideDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(120.dp, 60.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    )
-                    // Referencing grandchild tag/icon for ChildB in this test
-                    Box(
-                        Modifier
-                            .padding(horizontal = 100.dp, vertical = 40.dp)
-                            .requiredSize(120.dp, 20.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                            .testTag(grandchildIconTag)
-                            .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = true)
-                    )
-                }
-            }
-        }
-
-        verifyOverlappingSiblings()
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE) where
-     *        ChildB Box's surface area overlaps with its sibling, ChildA, within the Parent Box
-     *
-     *  Expected Output:
-     *  Parent Box's [PointerIcon.Crosshair] wins for the entire surface area of its box, including
-     *  the surface area within ChildA Box and ChildB Box. Parent Box has overrideDescendants =
-     *  true, which takes priority over any custom icon set by its children.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ ChildA Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ ChildB Box (output icon = [PointerIcon.Crosshair])
-     */
-    @Test
-    fun OverlappingSiblings_parentOverridesDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(120.dp, 60.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    )
-                    // Referencing grandchild tag/icon for ChildB in this test
-                    Box(
-                        Modifier
-                            .padding(horizontal = 100.dp, vertical = 40.dp)
-                            .requiredSize(120.dp, 20.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                            .testTag(grandchildIconTag)
-                            .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = true)
-                    )
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over ChildB (bottom right corner) and verify desired Parent icon
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            enter(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Then hover to parent (bottom right corner) and icon hasn't changed
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Then hover to ChildA (bottom left corner) and verify icon hasn't changed
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(bottomLeft)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Exit hovering
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ Child Box (no custom icon set)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Grandchild Box’s [PointerIcon.Hand] wins for the entire surface area of Grandchild's Box.
-     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that isn't
-     *  covered by Grandchild Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ Child Box (output icon = [PointerIcon.Default])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun multiLayeredNesting_grandchildCustomIconNoOverride() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Grandchild Box's icon is the desired grandchild icon
-        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
-        // Verify remaining Child Box's area is the default arrow icon
-        verifyIconOnHover(childIconTag, desiredDefaultIcon)
-        // Verify remaining Parent Box's area is the default arrow icon
-        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ Child Box (no custom icon set)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Grandchild Box’s [PointerIcon.Hand] wins for the entire surface area of Grandchild's Box.
-     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that isn't
-     *  covered by Grandchild Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ Child Box (output icon = [PointerIcon.Default])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun multiLayeredNesting_grandchildCustomIconHasOverride() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = true
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Grandchild Box's icon is the desired grandchild icon
-        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
-        // Verify remaining Child Box's area is the default arrow icon
-        verifyIconOnHover(childIconTag, desiredDefaultIcon)
-        // Verify remaining Parent Box's area is the default arrow icon
-        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of the Grandchild Box.
-     *  Child Box’s [PointerIcon.Text] wins for remaining surface area of its Box not covered by the
-     *  Grandchild Box. [PointerIcon.Default] wins for the remainder of the surface area of Parent
-     *  Box that isn't covered by Child Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun multiLayeredNesting_childAndGrandchildCustomIconsNoOverrides() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Grandchild Box's icon is the desired grandchild icon
-        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
-        // Verify remaining Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify remaining Parent Box's area is the default arrow icon
-        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of the Grandchild Box.
-     *  Child Box’s [PointerIcon.Text] wins for the remainder of the Child Box's surface area
-     *  that's not covered by the Grandchild box. [PointerIcon.Default] wins for the remainder of
-     *  the surface area of Parent Box that isn't covered by Child Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun multiLayeredNesting_childCustomIconGrandchildHasOverride() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = true
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Grandchild Box's icon is the desired grandchild icon
-        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
-        // Verify remaining Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify remaining Parent Box's area is the default arrow icon
-        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Child Box’s [PointerIcon.Text] wins for the entire surface area of its Box (including all
-     *  of the Grandchild Box since it is contained within Child Box's surface area).
-     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that isn't
-     *  covered by Child Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Text])
-     */
-    @Test
-    fun multiLayeredNesting_grandchildCustomIconChildHasOverride() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify Grandchild Box is respecting Child Box's icon
-        verifyIconOnHover(grandchildIconTag, desiredChildIcon)
-        // Verify remaining Parent Box's area is the default arrow icon
-        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (no custom icon set)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Child Box’s [PointerIcon.Text] wins for the entire surface area of its Box (including all
-     *  of the Grandchild Box since it is contained within Child Box's surface area).
-     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that isn't
-     *  covered by Child Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Default])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Text])
-     */
-    @Test
-    fun multiLayeredNesting_childAndGrandchildOverrideDescendants() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = true
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify Grandchild Box is respecting Child Box's icon
-        verifyIconOnHover(grandchildIconTag, desiredChildIcon)
-        // Verify remaining Parent Box's area is the default arrow icon
-        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (no icon set)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of the Grandchild Box.
-     *  Parent Box’s [PointerIcon.Crosshair] wins for the remaining surface area of the Pare Box
-     *  that's not covered by the Grandchild Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun multiLayeredNesting_parentAndGrandchildCustomIconNoOverrides() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Grandchild Box's icon is the desired grandchild icon
-        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
-        // Verify remaining Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-        // Verify remaining Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (no icon set)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of its Box. Parent
-     *  Box’s [PointerIcon.Crosshair] wins for the remaining surface area of the Pare Box that's
-     *  not covered by the Grandchild Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun multiLayeredNesting_parentCustomIconGrandchildOverrides() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = true
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Grandchild Box's icon is the desired grandchild icon
-        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
-        // Verify remaining Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-        // Verify remaining Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of the Grandchild Box.
-     *  Child Box's [PointerIcon.Text] wins for the remaining surface area of the Child Box not
-     *  covered by the Grandchild Box. Parent Box’s [PointerIcon.Crosshair] wins for the remaining
-     *  surface area not covered by the Child Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun multiLayeredNesting_allCustomIconsNoOverrides() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Grandchild Box's icon is the desired grandchild icon
-        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
-        // Verify remaining Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify remaining Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of the Grandchild Box.
-     *  Child Box's [PointerIcon.Text] wins for the remaining surface area of the Child Box not
-     *  covered by the Grandchild Box. Parent Box’s [PointerIcon.Crosshair] wins for the remaining
-     *  surface area not covered by the Child Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun multiLayeredNesting_allCustomIconsGrandchildOverrides() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = true
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Grandchild Box's icon is the desired grandchild icon
-        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
-        // Verify remaining Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify remaining Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Child Box's [PointerIcon.Hand] wins for the entire surface area of its Box. Parent
-     *  Box’s [PointerIcon.Crosshair] wins for the remaining surface area of its Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Text])
-     */
-    @Test
-    fun multiLayeredNesting_allCustomIconsChildOverrides() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify Grandchild Box is respecting Child Box's icon
-        verifyIconOnHover(grandchildIconTag, desiredChildIcon)
-        // Verify remaining Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Child Box's [PointerIcon.Hand] wins for the entire surface area of its Box. Parent
-     *  Box’s [PointerIcon.Crosshair] wins for the remaining surface area of its Box. The addition
-     *  of Grandchild Box’s overrideDescendants = true in this test doesn’t impact the outcome; this
-     *  is because Child Box is Grandchild Box's parent in the hierarchy and it already has
-     *  overrideDescendants = true, which takes priority over anything Grandchild Box sets.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Text])
-     */
-    @Test
-    fun multiLayeredNesting_allCustomIconsChildAndGrandchildOverrides() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = true
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Child Box's icon is the desired child icon
-        verifyIconOnHover(childIconTag, desiredChildIcon)
-        // Verify Grandchild Box is respecting Child Box's icon
-        verifyIconOnHover(grandchildIconTag, desiredChildIcon)
-        // Verify remaining Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (no icon set)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire surface area of its Box. Even
-     *  though the Grandchild Box’s icon was set, the Parent Box will always take priority because
-     *  it's the highestmost level in the hierarchy where overrideDescendants = true.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Crosshair])
-     */
-    @Test
-    fun multiLayeredNesting_parentGrandChildCustomIconsParentOverrides() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-        // Verify Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-        // Verify Grandchild Box is respecting Parent Box's icon
-        verifyIconOnHover(grandchildIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (no icon set)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire surface area of its Box. Even
-     *  though the Grandchild Box’s icon was set, the Parent Box will always take priority because
-     *  it's the highestmost level in the hierarchy where overrideDescendants = true.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Crosshair])
-     */
-    @Test
-    fun multiLayeredNesting_parentGrandChildCustomIconsBothOverride() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = true
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-        // Verify Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-        // Verify Grandchild Box is respecting Parent Box's icon
-        verifyIconOnHover(grandchildIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire surface area of its Box. Even
-     *  though the Child and Grandchild Box’s icons were set, the Parent Box will always take
-     *  priority because it's the highestmost level in the hierarchy where overrideDescendants =
-     *  true.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Crosshair])
-     */
-    @Test
-    fun multiLayeredNesting_allCustomIconsParentOverrides() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-        // Verify Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-        // Verify Grandchild Box is respecting Parent Box's icon
-        verifyIconOnHover(grandchildIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire surface area of its Box. Even
-     *  though the Child and Grandchild Box’s icons were set, the Parent Box will always take
-     *  priority because it's the highestmost level in the hierarchy where overrideDescendants =
-     *  true.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Crosshair])
-     */
-    @Test
-    fun multiLayeredNesting_allCustomIconsParentAndGrandchildOverride() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = true
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-        // Verify Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-        // Verify Grandchild Box is respecting Parent Box's icon
-        verifyIconOnHover(grandchildIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire surface area of its Box. Even
-     *  though the Child and Grandchild Box’s icons were set, the Parent Box will always take
-     *  priority because it's the highestmost level in the hierarchy where overrideDescendants =
-     *  true.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Crosshair])
-     */
-    @Test
-    fun multiLayeredNesting_allCustomIconsParentAndChildOverride() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-        // Verify Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-        // Verify Grandchild Box is respecting Parent Box's icon
-        verifyIconOnHover(grandchildIconTag, desiredParentIcon)
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire surface area of its Box. Even
-     *  though the Child and Grandchild Box’s icons were set, the Parent Box will always take
-     *  priority because it's the highestmost level in the hierarchy where overrideDescendants =
-     *  true.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Crosshair])
-     */
-    @Test
-    fun multiLayeredNesting_allIconsOverride() {
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
-                    ) {
-                        Box(
-                            Modifier
-                                .padding(40.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(grandchildIconTag)
-                                .pointerHoverIcon(
-                                    desiredGrandchildIcon,
-                                    overrideDescendants = true
-                                )
-                        )
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Verify Parent Box's icon is the desired parent icon
-        verifyIconOnHover(parentIconTag, desiredParentIcon)
-        // Verify Child Box is respecting Parent Box's icon
-        verifyIconOnHover(childIconTag, desiredParentIcon)
-        // Verify Grandchild Box is respecting Parent Box's icon
-        verifyIconOnHover(grandchildIconTag, desiredParentIcon)
-    }
-
-    /**
-     * This test takes an existing Box with a custom icon and changes the custom icon to a different
-     * custom icon while the cursor is hovered over the box.
-     */
-    @Test
-    fun dynamicallyUpdatedIcon() {
-        val icon = mutableStateOf(desiredChildIcon)
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Box(
-                        Modifier
-                            .padding(20.dp)
-                            .requiredSize(100.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(icon.value, overrideDescendants = false)
-                    )
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over Child Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            enter(bottomRight)
-        }
-        // Verify Child Box has the desired child icon and dynamically update the icon assigned to
-        // the Child Box while hovering over Child Box
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-            icon.value = desiredGrandchildIcon
-        }
-        // Verify the icon has been updated to the desired grandchild icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move cursor within Child Box and verify it still has the updated icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Exit hovering over Child Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *
-     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Initially, the Parent Box's [PointerIcon.Crosshair] should win for its entire surface area
-     *  because it has no competition in the hierarchy for any other custom icons. After the Child
-     *  Box is dynamically added under the cursor, the Child Box's [PointerIcon.Text] should win
-     *  for the entire surface area of the Child Box. This also requires updating the user facing
-     *  cursor icon to reflect the Child Box that was added under the cursor.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     */
-    @Test
-    fun dynamicallyAddAndRemoveChild_noOverrideDescendants() {
-        val isChildVisible = mutableStateOf(false)
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
-                    contentAlignment = Alignment.Center
-                ) {
-                    if (isChildVisible.value) {
-                        Box(
-                            modifier = Modifier
-                                .padding(20.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                        )
-                    }
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over center of Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            enter(center)
-        }
-        // Verify Parent Box has the desired parent icon and dynamically add the Child Box under the
-        // cursor
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-            isChildVisible.value = true
-        }
-        // Verify the icon has been updated to the desired child icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor within Child Box and verify it still has the updated icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomCenter)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor back to the center of the Child Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Dynamically remove the Child Box
-        rule.runOnIdle {
-            isChildVisible.value = false
-        }
-        // Verify the icon has been updated to the desired parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Exit hovering over Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *
-     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  The Parent Box's [PointerIcon.Crosshair] should win for its entire surface area regardless
-     *  of whether the Child Box is visible or not. This is because the Parent Box's
-     *  overrideDescendants = true, so its children should always respect Parent Box's custom icon.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
-     */
-    @Test
-    fun dynamicallyAddAndRemoveChild_parentOverridesDescendants() {
-        val isChildVisible = mutableStateOf(false)
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true),
-                    contentAlignment = Alignment.Center
-                ) {
-                    if (isChildVisible.value) {
-                        Box(
-                            modifier = Modifier
-                                .padding(20.dp)
-                                .requiredSize(100.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                        )
-                    }
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over center of Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            enter(center)
-        }
-        // Verify Parent Box has the desired parent icon and dynamically add the Child Box under the
-        // cursor
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-            isChildVisible.value = true
-        }
-        // Verify the icon stays as the parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor within Child Box and verify it still is the parent icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomCenter)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor back to the center of the Child Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Dynamically remove the Child Box
-        rule.runOnIdle {
-            isChildVisible.value = false
-        }
-        // Verify the icon still the desired parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Exit hovering over Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *
-     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Initially, the Parent Box's [PointerIcon.Crosshair] should win for its entire surface area
-     *  because it has no competition in the hierarchy for any other custom icons. After the Child
-     *  Box and the Grandchild Box are dynamically added under the cursor, the Grandchild Box's
-     *  [PointerIcon.Hand] should win for the entire surface area of the Grandchild Box. The Child
-     *  Box's [PointerIcon.Text] should win for the remaining surface area of the Child Box not
-     *  covered by the Grandchild Box. This also requires updating the user facing cursor icon to
-     *  reflect the Child Box and Grandchild Box that were added under the cursor.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *      ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun dynamicallyAddAndRemoveChildAndGrandchild_noOverrideDescendants() {
-        val areDescendantsVisible = mutableStateOf(false)
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
-                    contentAlignment = Alignment.Center
-                ) {
-                    if (areDescendantsVisible.value) {
-                        Box(
-                            modifier = Modifier
-                                .padding(20.dp)
-                                .requiredSize(150.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false),
-                            contentAlignment = Alignment.Center
-                        ) {
-                            Box(
-                                modifier = Modifier
-                                    .padding(40.dp)
-                                    .requiredSize(100.dp)
-                                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                    .testTag(grandchildIconTag)
-                                    .pointerHoverIcon(
-                                        desiredGrandchildIcon,
-                                        overrideDescendants = false
-                                    )
-                            )
-                        }
-                    }
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over center of Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            enter(center)
-        }
-        // Verify Parent Box has the desired parent icon and dynamically add the Child Box and
-        // Grandchild Box under the cursor
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-            areDescendantsVisible.value = true
-        }
-        // Verify the icon has been updated to the desired grandchild icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move cursor within Grandchild Box and verify it still has the grandchild icon
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move cursor outside Grandchild Box within Child Box and verify it has the child icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomCenter)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor back to the center of the Grandchild Box
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Dynamically remove the Child Box and Grandchild Box
-        rule.runOnIdle {
-            areDescendantsVisible.value = false
-        }
-        // Verify the icon has been updated to the desired parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Exit hovering over Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *
-     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
-     *
-     *  Expected Output:
-     *  Initially, the Parent Box's [PointerIcon.Crosshair] should win for its entire surface area
-     *  because it has no competition in the hierarchy for any other custom icons. After the Child
-     *  Box and the Grandchild Box are dynamically added under the cursor, the Grandchild Box's
-     *  [PointerIcon.Hand] should win for the entire surface area of the Grandchild Box. Because the
-     *  Grandchild Box is the lowest level in the hierarchy, the outcome doesn't change whether it
-     *  has overrideDescendants = true or not. The Child Box's [PointerIcon.Text] should win for the
-     *  remaining surface area of the Child Box not covered by the Grandchild Box. This also
-     *  requires updating the user facing cursor icon to reflect the Child Box and Grandchild Box
-     *  that were added under the cursor.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *      ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun dynamicallyAddAndRemoveChildAndGrandchild_grandchildOverridesDescendants() {
-        val areDescendantsVisible = mutableStateOf(false)
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
-                    contentAlignment = Alignment.Center
-                ) {
-                    if (areDescendantsVisible.value) {
-                        Box(
-                            modifier = Modifier
-                                .padding(20.dp)
-                                .requiredSize(150.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false),
-                            contentAlignment = Alignment.Center
-                        ) {
-                            Box(
-                                modifier = Modifier
-                                    .padding(40.dp)
-                                    .requiredSize(100.dp)
-                                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                    .testTag(grandchildIconTag)
-                                    .pointerHoverIcon(
-                                        desiredGrandchildIcon,
-                                        overrideDescendants = true
-                                    )
-                            )
-                        }
-                    }
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over center of Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            enter(center)
-        }
-        // Verify Parent Box has the desired parent icon and dynamically add the Child Box and
-        // Grandchild Box under the cursor
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-            areDescendantsVisible.value = true
-        }
-        // Verify the icon has been updated to the desired grandchild icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move cursor within Grandchild Box and verify it still has the grandchild icon
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move cursor outside Grandchild Box within Child Box and verify it has the child icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomCenter)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor back to the center of the Grandchild Box
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Dynamically remove the Child Box and Grandchild Box
-        rule.runOnIdle {
-            areDescendantsVisible.value = false
-        }
-        // Verify the icon has been updated to the desired parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Exit hovering over Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *
-     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Initially, the Parent Box's [PointerIcon.Crosshair] should win for its entire surface area
-     *  because it has no competition in the hierarchy for any other custom icons. After the Child
-     *  Box and the Grandchild Box are dynamically added under the cursor, the Child Box's
-     *  [PointerIcon.Text] should win for the entire surface area of the Child Box. This includes
-     *  the Grandchild Box's [PointerIcon.Text] should win for the remaining surface area of the
-     *  Child Box not covered by the Grandchild Box. This also requires updating the user facing
-     *  cursor icon to reflect the Child Box and Grandchild Box that were added under the cursor.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *      ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun dynamicallyAddAndRemoveChildAndGrandchild_childOverridesDescendants() {
-        val areDescendantsVisible = mutableStateOf(false)
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
-                    contentAlignment = Alignment.Center
-                ) {
-                    if (areDescendantsVisible.value) {
-                        Box(
-                            modifier = Modifier
-                                .padding(20.dp)
-                                .requiredSize(150.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = true),
-                            contentAlignment = Alignment.Center
-                        ) {
-                            Box(
-                                modifier = Modifier
-                                    .padding(40.dp)
-                                    .requiredSize(100.dp)
-                                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                    .testTag(grandchildIconTag)
-                                    .pointerHoverIcon(
-                                        desiredGrandchildIcon,
-                                        overrideDescendants = false
-                                    )
-                            )
-                        }
-                    }
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over center of Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            enter(center)
-        }
-        // Verify Parent Box has the desired parent icon, then dynamically add the Child Box and
-        // Grandchild Box under the cursor
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-            areDescendantsVisible.value = true
-        }
-        // Verify the icon has been updated to the desired child icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor within Grandchild Box and verify it still has the child icon
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor outside Grandchild Box within Child Box to verify it still has the child icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomCenter)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor back to the center of the Grandchild Box
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Dynamically remove the Child Box and Grandchild Box
-        rule.runOnIdle {
-            areDescendantsVisible.value = false
-        }
-        // Verify the icon has been updated to the desired parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Exit hovering over Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  The Child Box's [PointerIcon.Text] should win for its entire surface area regardless of
-     *  whether there's a Parent Box present or not. This is because the Parent Box has
-     *  overrideDescendants = false and should therefore not have its custom icon take priority over
-     *  the Child Box's custom icon. The Parent Box's [PointerIcon.Crosshair] should win for its
-     *  remaining surface area not covered by the Child Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     */
-    @Test
-    fun dynamicallyAddAndRemoveParent_noOverrideDescendants() {
-        val isParentVisible = mutableStateOf(false)
-        val child = movableContentOf {
-            Box(
-                modifier = Modifier
-                    .requiredSize(150.dp)
-                    .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                    .testTag(childIconTag)
-                    .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-            )
-        }
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                if (isParentVisible.value) {
-                    Box(
-                        modifier = Modifier
-                            .requiredSize(200.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                            .testTag(parentIconTag)
-                            .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
-                        contentAlignment = Alignment.Center
-                    ) {
-                        child()
-                    }
-                } else {
-                    child()
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over center of Child Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            enter(center)
-        }
-        // Verify Child Box has the desired child icon and dynamically add the Parent Box under the
-        // cursor
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-            isParentVisible.value = true
-        }
-        // Verify the icon stays as the desired child icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor within Child Box and verify it still has the child icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomCenter)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor back to the center of the Child Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Dynamically remove the Parent Box
-        rule.runOnIdle {
-            isParentVisible.value = false
-        }
-        // Verify the icon stays as the desired child icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Exit hovering over Child Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  The Child Box's [PointerIcon.Text] should win for its entire surface area when the Parent
-     *  Box isn't present. Once the Parent Box becomes visible, the Parent Box's
-     *  [PointerIcon.Crosshair] should win for its entire surface area. This is because the Parent
-     *  Box's overrideDescendants = true, so its children should always respect Parent Box's custom
-     *  icon. This also requires updating the user facing cursor icon to reflect the Parent Box that
-     *  was added under the cursor.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
-     */
-    @Test
-    fun dynamicallyAddAndRemoveParent_parentOverridesDescendants() {
-        val isParentVisible = mutableStateOf(false)
-        val child = movableContentOf {
-            Box(
-                modifier = Modifier
-                    .requiredSize(150.dp)
-                    .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                    .testTag(childIconTag)
-                    .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-            )
-        }
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                if (isParentVisible.value) {
-                    Box(
-                        modifier = Modifier
-                            .requiredSize(200.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                            .testTag(parentIconTag)
-                            .pointerHoverIcon(desiredParentIcon, overrideDescendants = true),
-                        contentAlignment = Alignment.Center
-                    ) {
-                        child()
-                    }
-                } else {
-                    child()
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over center of Child Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            enter(center)
-        }
-        // Verify Child Box has the desired child icon and dynamically add the Parent Box under the
-        // cursor
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-            isParentVisible.value = true
-        }
-        // Verify the icon has been updated to the desired parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor within Child Box and verify it still has the parent icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor outside Child Box and verify the icon is still the parent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomCenter)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor back to the center of the Child Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Dynamically remove the Parent Box
-        rule.runOnIdle {
-            isParentVisible.value = false
-        }
-        // Verify the icon has been updated to the desired child icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Exit hovering over Child Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  The Grandchild Box's [PointerIcon.Hand] should win for its entire surface area regardless of
-     *  whether there's a Child Box or Parent Box present. This is because the Parent Box and Child
-     *  Box have overrideDescendants = false and should therefore not have their custom icons take
-     *  priority over the Grandchild Box's custom icon. The Child Box should win for its remaining
-     *  surface area not covered by the Grandchild Box. The Parent Box's [PointerIcon.Crosshair]
-     *  should win for its remaining surface area not covered by the Child Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *      ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun dynamicallyAddAndRemoveNestedChild_noOverrideDescendants() {
-        val isChildVisible = mutableStateOf(false)
-        val grandchild = movableContentOf {
-            Box(
-                modifier = Modifier
-                    .requiredSize(100.dp)
-                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                    .testTag(grandchildIconTag)
-                    .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = false)
-            )
-        }
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
-                    contentAlignment = Alignment.Center
-                ) {
-                    if (isChildVisible.value) {
-                        Box(
-                            modifier = Modifier
-                                .requiredSize(150.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false),
-                            contentAlignment = Alignment.Center
-                        ) {
-                            grandchild()
-                        }
-                    } else {
-                        grandchild()
-                    }
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over center of Grandchild Box
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            enter(center)
-        }
-        // Verify Grandchild Box has the desired grandchild icon and dynamically add the Child Box
-        // under the cursor
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-            isChildVisible.value = true
-        }
-        // Verify the icon stays as the desired grandchild icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move cursor within Grandchild Box and verify it still has the grandchild icon
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move cursor outside Grandchild Box within Child Box to verify icon is now the child icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor back to the center of the Grandchild Box
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Dynamically remove the Child Box
-        rule.runOnIdle {
-            isChildVisible.value = false
-        }
-        // Verify the icon has been updated to the desired grandchild icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Exit hovering over Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
-     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  The Grandchild Box's [PointerIcon.Hand] should win for its entire surface area regardless of
-     *  whether there's a Child Box or Parent Box present. This is because the Parent Box and Child
-     *  Box have overrideDescendants = false and should therefore not have thei custom icona take
-     *  priority over the Grandchild Box's custom icon. The Child Box should win for its remaining
-     *  surface area not covered by the Grandchild Box. The Parent Box's [PointerIcon.Crosshair]
-     *  should win for its remaining surface area not covered by the Child Box.
-     *  Initially, the Parent Box's [PointerIcon.Crosshair] should win for its entire surface area
-     *  because it has no competition in the hierarchy for any other custom icons. After the Child
-     *  Box and the Grandchild Box are dynamically added under the cursor, the Child Box's
-     *  [PointerIcon.Text] should win for the entire surface area of the Child Box. This includes
-     *  the Grandchild Box's [PointerIcon.Text] should win for the remaining surface area of the Child Box not
-     *  covered by the Grandchild Box. This also requires updating the user facing cursor icon to
-     *  reflect the Child Box and Grandchild Box that were added under the cursor.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *      ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun dynamicallyAddAndRemoveNestedChild_ChildOverridesDescendants() {
-        val isChildVisible = mutableStateOf(false)
-        val grandchild = movableContentOf {
-            Box(
-                modifier = Modifier
-                    .requiredSize(100.dp)
-                    .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                    .testTag(grandchildIconTag)
-                    .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = false)
-            )
-        }
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
-                    contentAlignment = Alignment.Center
-                ) {
-                    if (isChildVisible.value) {
-                        Box(
-                            modifier = Modifier
-                                .requiredSize(150.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = true),
-                            contentAlignment = Alignment.Center
-                        ) {
-                            grandchild()
-                        }
-                    } else {
-                        grandchild()
-                    }
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over center of Grandchild Box
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            enter(center)
-        }
-        // Verify Grandchild Box has the desired grandchild icon and dynamically add the Child Box
-        // under the cursor
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-            isChildVisible.value = true
-        }
-        // Verify the icon has been updated to the desired child icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor within Grandchild Box and verify it still has the child icon
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor outside Grandchild Box within Child Box to verify it still has the child icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomCenter)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor back to center of the Grandchild Box
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Dynamically remove the Child Box
-        rule.runOnIdle {
-            isChildVisible.value = false
-        }
-        // Verify the icon has been updated to the desired grandchild icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Exit hovering over Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Grandparent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  After hovering over the corner of the Grandparent Box that doesn't overlap with any
-     *  descendant, the hierarchy of the screen updates to:
-     *  Grandparent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *      ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *         ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  The Grandchild Box's [PointerIcon.Hand] should win for its entire surface area regardless of
-     *  whether there's a Child, Parent, or Grandparent Box present. This is because the
-     *  Grandparent, Parent, and Child Boxes have overrideDescendants = false and should therefore
-     *  not have their custom icons take priority over the Grandchild Box's custom icon. The Child
-     *  Box should win for its remaining surface area not covered by the Grandchild Box. The Parent
-     *  Box's [PointerIcon.Crosshair] should win for its remaining surface area not covered by the
-     *  Child Box. And the Grandparent Box should win for its remaining surface area not covered by
-     *  the Parent Box.
-     *
-     *  Grandparent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Parent Box (output icon = [PointerIcon.Crosshair])
-     *      ⤷ Child Box (output icon = [PointerIcon.Text])
-     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun dynamicallyAddAndRemoveNestedChild_notHoveredOverChild() {
-        val grandparentIconTag = "myGrandparentIcon"
-        val desiredGrandparentIcon = desiredParentIcon
-        val isChildVisible = mutableStateOf(false)
-        val grandchild = movableContentOf {
-            Box(
-                modifier = Modifier
-                    .requiredSize(100.dp)
-                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                    .testTag(grandchildIconTag)
-                    .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = false)
-            )
-        }
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(grandparentIconTag)
-                        .pointerHoverIcon(desiredGrandparentIcon, overrideDescendants = false),
-                    contentAlignment = Alignment.Center
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .requiredSize(175.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                            .testTag(parentIconTag)
-                            .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
-                        contentAlignment = Alignment.Center
-                    ) {
-                        if (isChildVisible.value) {
-                            Box(
-                                modifier = Modifier
-                                    .requiredSize(150.dp)
-                                    .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                    .testTag(childIconTag)
-                                    .pointerHoverIcon(
-                                        desiredChildIcon,
-                                        overrideDescendants = false
-                                    ),
-                                contentAlignment = Alignment.Center
-                            ) {
-                                grandchild()
-                            }
-                        } else {
-                            grandchild()
-                        }
-                    }
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over center of Grandchild Box
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            enter(center)
-        }
-        // Verify Grandchild Box has the desired grandchild icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move to corner of Grandparent Box where no descendants are under the cursor
-        rule.onNodeWithTag(grandparentIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        // Verify the icon is the desired grandparent icon and dynamically add the Child Box
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandparentIcon)
-            isChildVisible.value = true
-        }
-        // Verify the icon stays as the desired grandparent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandparentIcon)
-        }
-        // Move cursor within Grandparent Box and verify it still has the grandparent icon
-        rule.onNodeWithTag(grandparentIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandparentIcon)
-        }
-        // Move cursor outside Grandparent Box to Parent Box to verify icon is now the parent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor back to corner of Grandparent Box where no descendants are under the cursor
-        rule.onNodeWithTag(grandparentIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        // Dynamically remove the Child Box
-        rule.runOnIdle {
-            isChildVisible.value = false
-        }
-        // Verify the icon stays as the grandparent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandparentIcon)
-        }
-        // Exit hovering over Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  After hovering over the corner of the Parent Box that doesn't overlap with any descendant,
-     *  the hierarchy of the screen updates to:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  The Grandchild Box's [PointerIcon.Hand] should win for its entire surface area regardless of
-     *  whether there's a Child or Parent Box present. This is because the Parent and Child Boxes
-     *  have overrideDescendants = false and should therefore not have their custom icons take
-     *  priority over the Grandchild Box's custom icon. The Child Box should win for its remaining
-     *  surface area not covered by the Grandchild Box. The Parent Box's [PointerIcon.Crosshair]
-     *  should win for its remaining surface area not covered by the Child Box.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *      ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun dynamicallyAddAndRemoveGrandchild_notHoveredOverGrandchild() {
-        val isGrandchildVisible = mutableStateOf(false)
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
-                    contentAlignment = Alignment.Center
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .requiredSize(150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                            .testTag(childIconTag)
-                            .pointerHoverIcon(
-                                desiredChildIcon,
-                                overrideDescendants = false
-                            ),
-                        contentAlignment = Alignment.Center
-                    ) {
-                        if (isGrandchildVisible.value) {
-                            Box(
-                                modifier = Modifier
-                                    .requiredSize(100.dp)
-                                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                    .testTag(grandchildIconTag)
-                                    .pointerHoverIcon(
-                                        desiredGrandchildIcon,
-                                        overrideDescendants = false
-                                    )
-                            )
-                        }
-                    }
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over center of Child Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            enter(center)
-        }
-        // Verify Child Box has the desired child icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move to corner of Parent Box where no descendants are under the cursor
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        // Verify the icon is the desired parent icon and dynamically add the Grandchild Box
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-            isGrandchildVisible.value = true
-        }
-        // Verify the icon stays as the desired parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor within Parent Box and verify it still has the grandparent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move cursor outside Parent Box to Child Box to verify icon is now the child icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor back to corner of Parent Box where no descendants are under the cursor
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        // Dynamically remove the Grandchild Box
-        rule.runOnIdle {
-            isGrandchildVisible.value = false
-        }
-        // Verify the icon stays as the parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Exit hovering over Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  After hovering over the area where ChildB will be, the hierarchy of the screen updates to:
-     *  Parent Box (no custom icon set)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Regardless of the presence of ChildB Box, ChildA Box's [PointerIcon.Text] should win for
-     *  its entire surface area. Once ChildB Box appears, ChildB Box's [PointerIcon.Hand] should
-     *  win for its entire surface area. Initially, Parent Box's [PointerIcon.Crosshair] should win
-     *  for its entire surface area not covered by ChildA Box. Once ChildA Box appears, Parent Box
-     *  should win for its entire surface not covered by either ChildA or ChildB Boxes.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *    ⤷ Child Box (output icon = [PointerIcon.Hand])
-     */
-    @Ignore("b/271277248 - Remove Ignore annotation once input event bug is fixed")
-    @Test
-    fun dynamicallyAddAndRemoveSibling_hoveredOverAppearingSibling() {
-        val isChildBVisible = mutableStateOf(false)
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(150.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Column {
-                        Box(
-                            Modifier
-                                .requiredSize(50.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(
-                                    desiredChildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                        if (isChildBVisible.value) {
-                            // Referencing grandchild tag/icon for ChildB in this test
-                            Box(
-                                Modifier
-                                    .requiredSize(50.dp)
-                                    .offset(y = 100.dp)
-                                    .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                    .testTag(grandchildIconTag)
-                                    .pointerHoverIcon(
-                                        desiredGrandchildIcon,
-                                        overrideDescendants = false
-                                    )
-                            )
-                        }
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over corner of Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            enter(bottomRight)
-        }
-        // Verify Parent Box has the desired parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move to center of ChildA Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Verify ChildA Box has the desired child icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move to left corner of Parent Box where ChildB will be added
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomLeft)
-        }
-        // Dynamically add the ChildB Box
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-            isChildBVisible.value = true
-        }
-        // Verify the icon is updated to the desired ChildB icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move to corner of ChildB Box
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        // Verify ChildB Box has the desired grandchild icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move cursor back to the center of ChildA Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Verify that icon is updated to the desired ChildA icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor back to the location of ChildB
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Dynamically remove the ChildB Box
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-            isChildBVisible.value = false
-        }
-        // Verify the icon updates to the parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Exit hovering over ChildA Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for the initial setup of this test is:
-     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *
-     *  After hovering over ChildA, the hierarchy of the screen updates to:
-     *  Parent Box (no custom icon set)
-     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Regardless of the presence of ChildB Box, ChildA Box's [PointerIcon.Text] should win for
-     *  its entire surface area. Once ChildB Box appears, ChildB Box's [PointerIcon.Hand] should
-     *  win for its entire surface area. Initially, Parent Box's [PointerIcon.Crosshair] should win
-     *  for its entire surface area not covered by ChildA Box. Once ChildA Box appears, Parent Box
-     *  should win for its entire surface not covered by either ChildA or ChildB Boxes.
-     *
-     *  Parent Box (output icon = [PointerIcon.Crosshair])
-     *    ⤷ Child Box (output icon = [PointerIcon.Text])
-     *    ⤷ Child Box (output icon = [PointerIcon.Hand])
-     */
-    @Ignore("b/271277248 - Remove Ignore annotation once input event bug is fixed")
-    @Test
-    fun dynamicallyAddAndRemoveSibling_notHoveredOverAppearingSibling() {
-        val isChildBVisible = mutableStateOf(false)
-
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .requiredSize(200.dp)
-                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                        .testTag(parentIconTag)
-                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                ) {
-                    Column {
-                        Box(
-                            Modifier
-                                .requiredSize(50.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(
-                                    desiredChildIcon,
-                                    overrideDescendants = false
-                                )
-                        )
-                        if (isChildBVisible.value) {
-                            // Referencing grandchild tag/icon for ChildB in this test
-                            Box(
-                                Modifier
-                                    .requiredSize(50.dp)
-                                    .offset(y = 100.dp)
-                                    .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                    .testTag(grandchildIconTag)
-                                    .pointerHoverIcon(
-                                        desiredGrandchildIcon,
-                                        overrideDescendants = false
-                                    )
-                            )
-                        }
-                    }
-                }
-            }
-        }
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over corner of Parent Box
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            enter(bottomRight)
-        }
-        // Verify Parent Box has the desired parent icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-        // Move to center of ChildA Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Verify ChildA Box has the desired child icon and dynamically add the ChildB Box
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-            isChildBVisible.value = true
-        }
-        // Verify the icon stays as the desired child icon since the cursor hasn't moved
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move to corner of ChildB Box
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        // Verify ChildB Box has the desired grandchild icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move cursor back to the center of ChildA Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(center)
-        }
-        // Dynamically remove the ChildB Box
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-            isChildBVisible.value = false
-        }
-        // Verify the icon stays as the desired child icon since the cursor hasn't moved
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Exit hovering over ChildA Box
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            exit()
-        }
-    }
-
-    /**
-     * Setup:
-     * The hierarchy for this test is setup as:
-     *  Default Box (no custom icon set)
-     *    ⤷ Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
-     *        ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
-     *            ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
-     *
-     *  Expected Output:
-     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of the Grandchild Box.
-     *  Child Box's [PointerIcon.Text] wins for the remaining surface area of the Child Box not
-     *  covered by the Grandchild Box. Parent Box’s [PointerIcon.Crosshair] wins for the remaining
-     *  surface area not covered by the Child Box. [PointerIcon.Default] wins for the remaining
-     *  surface area of
-     *
-     *  Default Box (output icon = [PointerIcon.Default]
-     *    ⤷ Parent Box (output icon = [PointerIcon.Crosshair])
-     *        ⤷ Child Box (output icon = [PointerIcon.Text])
-     *            ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
-     */
-    @Test
-    fun childNotFullyContainedInParent_noOverrideDescendants() {
-        val defaultIconTag = "myDefaultWrapper"
-        rule.setContent {
-            CompositionLocalProvider(LocalPointerIconService provides iconService) {
-                Box(
-                    modifier = Modifier
-                        .fillMaxSize()
-                        .border(BorderStroke(2.dp, SolidColor(Color.Yellow)))
-                        .testTag(defaultIconTag)
-                ) {
-                    Box(
-                        modifier = Modifier
-                            .requiredSize(width = 200.dp, height = 150.dp)
-                            .border(BorderStroke(2.dp, SolidColor(Color.Red)))
-                            .testTag(parentIconTag)
-                            .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
-                    ) {
-                        Box(
-                            Modifier
-                                .requiredSize(width = 150.dp, height = 125.dp)
-                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
-                                .testTag(childIconTag)
-                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
-                        ) {
-                            Box(
-                                Modifier
-                                    .requiredSize(width = 300.dp, height = 100.dp)
-                                    .offset(x = 100.dp)
-                                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
-                                    .testTag(grandchildIconTag)
-                                    .pointerHoverIcon(
-                                        desiredGrandchildIcon,
-                                        overrideDescendants = false
-                                    )
-                            )
-                        }
-                    }
-                }
-            }
-        }
-
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over the default wrapping box and verify the cursor is still the default icon
-        rule.onNodeWithTag(defaultIconTag).performMouseInput {
-            enter(center)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Move cursor to the corner of the Grandchild Box and verify it has the desired grandchild
-        // icon
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move cursor to the center right of the Child Box and verify it still has the desired
-        // grandchild icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move cursor to the corner of the Child Box and verify it has updated to the desired child
-        // icon
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-        // Move cursor to the center right of the Parent Box and verify it has the desired
-        // grandchild icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(centerRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-        // Move cursor to the corner of the Parent Box and verify it has the desired parent icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
-        }
-    }
-
-    private fun verifyIconOnHover(tag: String, expectedIcon: PointerIcon) {
-        // Hover over element with specified tag
-        rule.onNodeWithTag(tag).performMouseInput {
-            enter(bottomRight)
-        }
-        // Verify the current icon is the expected icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(expectedIcon)
-        }
-        // Exit hovering over element
-        rule.onNodeWithTag(tag).performMouseInput {
-            exit()
-        }
-    }
-
-    private fun verifyOverlappingSiblings() {
-        // Verify initial state of pointer icon
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-        // Hover over ChildB (bottom right corner) and verify desired ChildB icon
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            enter(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-
-        // Then hover to parent (bottom right corner) and verify default arrow icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomRight)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-
-        // Then hover back over ChildB in area that overlaps with sibling (bottom left corner) and
-        // verify desired ChildB icon
-        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
-            moveTo(bottomLeft)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
-        }
-
-        // Then hover to ChildA (bottom left corner) and verify desired ChildA icon (hand)
-        rule.onNodeWithTag(childIconTag).performMouseInput {
-            moveTo(bottomLeft)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
-        }
-
-        // Then hover over parent (bottom left corner) and verify default arrow icon
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            moveTo(bottomLeft)
-        }
-        rule.runOnIdle {
-            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
-        }
-
-        // Exit hovering
-        rule.onNodeWithTag(parentIconTag).performMouseInput {
-            exit()
-        }
-    }
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
deleted file mode 100644
index a9970b5..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ /dev/null
@@ -1,3473 +0,0 @@
-/*
- * Copyright 2020 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.input.pointer
-
-import android.view.InputDevice
-import android.view.KeyEvent as AndroidKeyEvent
-import android.view.MotionEvent
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.Autofill
-import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.draganddrop.DragAndDropInfo
-import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusOwner
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Canvas
-import androidx.compose.ui.graphics.Matrix
-import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.InputModeManager
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.layout.layout
-import androidx.compose.ui.modifier.ModifierLocalManager
-import androidx.compose.ui.node.InternalCoreApi
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.LayoutNodeDrawScope
-import androidx.compose.ui.node.MeasureAndLayoutDelegate
-import androidx.compose.ui.node.OwnedLayer
-import androidx.compose.ui.node.Owner
-import androidx.compose.ui.node.OwnerSnapshotObserver
-import androidx.compose.ui.node.RootForTest
-import androidx.compose.ui.platform.AccessibilityManager
-import androidx.compose.ui.platform.ClipboardManager
-import androidx.compose.ui.platform.PlatformTextInputSessionScope
-import androidx.compose.ui.platform.TextToolbar
-import androidx.compose.ui.platform.ViewConfiguration
-import androidx.compose.ui.platform.WindowInfo
-import androidx.compose.ui.text.font.Font
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.input.TextInputService
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.minus
-import androidx.compose.ui.unit.toOffset
-import androidx.compose.ui.util.fastMaxBy
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import java.util.concurrent.Executors
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.asCoroutineDispatcher
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-// TODO(shepshapard): Write the following PointerInputEvent to PointerInputChangeEvent tests
-// 2 down, 2 move, 2 up, converted correctly
-// 3 down, 3 move, 3 up, converted correctly
-// down, up, down, up, converted correctly
-// 2 down, 1 up, same down, both up, converted correctly
-// 2 down, 1 up, new down, both up, converted correctly
-// new is up, throws exception
-
-// TODO(shepshapard): Write the following hit testing tests
-// 2 down, one hits, target receives correct event
-// 2 down, one moves in, one out, 2 up, target receives correct event stream
-// down, up, receives down and up
-// down, move, up, receives all 3
-// down, up, then down and misses, target receives down and up
-// down, misses, moves in bounds, up, target does not receive event
-// down, hits, moves out of bounds, up, target receives all events
-
-// TODO(shepshapard): Write the following offset testing tests
-// 3 simultaneous moves, offsets are correct
-
-// TODO(shepshapard): Write the following pointer input dispatch path tests:
-// down, move, up, on 2, hits all 5 passes
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class PointerInputEventProcessorTest {
-
-    private lateinit var pointerInputEventProcessor: PointerInputEventProcessor
-    private lateinit var testOwner: TestOwner
-    private val positionCalculator = object : PositionCalculator {
-        override fun screenToLocal(positionOnScreen: Offset): Offset = positionOnScreen
-
-        override fun localToScreen(localPosition: Offset): Offset = localPosition
-
-        override fun localToScreen(localTransform: Matrix) {}
-    }
-
-    @Before
-    fun setup() {
-        testOwner = TestOwner()
-        pointerInputEventProcessor = PointerInputEventProcessor(testOwner.root)
-    }
-
-    private fun addToRoot(vararg layoutNodes: LayoutNode) {
-        layoutNodes.forEachIndexed { index, node ->
-            testOwner.root.insertAt(index, node)
-        }
-        testOwner.measureAndLayout()
-    }
-
-    @Test
-    @OptIn(ExperimentalComposeUiApi::class)
-    fun pointerTypePassed() {
-        val pointerTypes = listOf(
-            PointerType.Unknown,
-            PointerType.Touch,
-            PointerType.Mouse,
-            PointerType.Stylus,
-            PointerType.Eraser
-        )
-
-        // Arrange
-        val pointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            0,
-            0,
-            500,
-            500,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-
-        addToRoot(layoutNode)
-
-        val offset = Offset(100f, 200f)
-        val previousEvents = mutableListOf<PointerInputEventData>()
-        val events = pointerTypes.mapIndexed { index, pointerType ->
-            previousEvents += PointerInputEventData(
-                id = PointerId(index.toLong()),
-                uptime = index.toLong(),
-                positionOnScreen = Offset(offset.x + index, offset.y + index),
-                position = Offset(offset.x + index, offset.y + index),
-                down = true,
-                pressure = 1.0f,
-                type = pointerType
-            )
-            val data = previousEvents.map {
-                it.copy(uptime = index.toLong())
-            }
-            PointerInputEvent(index.toLong(), data)
-        }
-
-        // Act
-
-        events.forEach { pointerInputEventProcessor.process(it) }
-
-        // Assert
-
-        val log = pointerInputFilter.log.getOnPointerEventFilterLog()
-
-        // Verify call count
-        assertThat(log)
-            .hasSize(PointerEventPass.values().size * pointerTypes.size)
-
-        // Verify types of the pointers
-        repeat(pointerTypes.size) { eventIndex ->
-            PointerEventPass.values().forEachIndexed { passIndex, pass ->
-                val item = log[passIndex + (eventIndex * PointerEventPass.values().size)]
-                assertThat(item.pass).isEqualTo(pass)
-
-                val changes = item.pointerEvent.changes
-                assertThat(changes.size).isEqualTo(eventIndex + 1)
-
-                for (i in 0..eventIndex) {
-                    val pointerType = pointerTypes[i]
-                    val change = changes[i]
-                    assertThat(change.type).isEqualTo(pointerType)
-                }
-            }
-        }
-    }
-
-    /**
-     * PointerInputEventProcessor doesn't currently support reentrancy and
-     * b/233209795 indicates that it is likely causing a crash. This test
-     * ensures that if we have reentrancy that we exit without handling
-     * the event. This test can be replaced with tests supporting reentrant
-     * behavior when reentrancy is supported.
-     */
-    @Test
-    fun noReentrancy() {
-        var reentrancyCount = 0
-        // Arrange
-        val reentrantPointerInputFilter = object : PointerInputFilter() {
-            override fun onPointerEvent(
-                pointerEvent: PointerEvent,
-                pass: PointerEventPass,
-                bounds: IntSize
-            ) {
-                if (pass != PointerEventPass.Initial) {
-                    return
-                }
-                if (reentrancyCount > 1) {
-                    // Don't allow infinite recursion. Just enough to break the test.
-                    return
-                }
-                val oldId = pointerEvent.changes.fastMaxBy { it.id.value }!!.id.value.toInt()
-                val event = PointerInputEvent(oldId + 1, 14, Offset.Zero, true)
-                // force a reentrant call
-                val result = pointerInputEventProcessor.process(event)
-                assertThat(result.anyMovementConsumed).isFalse()
-                assertThat(result.dispatchedToAPointerInputModifier).isFalse()
-                pointerEvent.changes.forEach { it.consume() }
-                reentrancyCount++
-            }
-
-            override fun onCancel() {
-            }
-        }
-
-        val layoutNode = LayoutNode(
-            0,
-            0,
-            500,
-            500,
-            PointerInputModifierImpl2(reentrantPointerInputFilter)
-        )
-
-        addToRoot(layoutNode)
-
-        // Act
-
-        val result =
-            pointerInputEventProcessor.process(PointerInputEvent(8712, 3, Offset.Zero, true))
-
-        // Assert
-
-        assertThat(reentrancyCount).isEqualTo(1)
-
-        assertThat(result.anyMovementConsumed).isFalse()
-        assertThat(result.dispatchedToAPointerInputModifier).isTrue()
-    }
-
-    @Test
-    fun process_downMoveUp_convertedCorrectlyAndTraversesAllPassesInCorrectOrder() {
-
-        // Arrange
-        val pointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            0,
-            0,
-            500,
-            500,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-
-        addToRoot(layoutNode)
-
-        val offset = Offset(100f, 200f)
-        val offset2 = Offset(300f, 400f)
-
-        val events = arrayOf(
-            PointerInputEvent(8712, 3, offset, true),
-            PointerInputEvent(8712, 11, offset2, true),
-            PointerInputEvent(8712, 13, offset2, false)
-        )
-
-        val down = down(8712, 3, offset.x, offset.y)
-        val move = down.moveTo(11, offset2.x, offset2.y)
-        val up = move.up(13)
-
-        val expectedChanges = arrayOf(down, move, up)
-
-        // Act
-
-        events.forEach { pointerInputEventProcessor.process(it) }
-
-        // Assert
-
-        val log = pointerInputFilter.log.getOnPointerEventFilterLog()
-
-        // Verify call count
-        assertThat(log)
-            .hasSize(PointerEventPass.values().size * expectedChanges.size)
-
-        // Verify call values
-        var count = 0
-        expectedChanges.forEach { change ->
-            PointerEventPass.values().forEach { pass ->
-                val item = log[count]
-                PointerEventSubject
-                    .assertThat(item.pointerEvent)
-                    .isStructurallyEqualTo(pointerEventOf(change))
-                assertThat(item.pass).isEqualTo(pass)
-                count++
-            }
-        }
-    }
-
-    @Test
-    fun process_downHits_targetReceives() {
-
-        // Arrange
-
-        val childOffset = Offset(100f, 200f)
-        val pointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            100, 200, 301, 401,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-
-        addToRoot(layoutNode)
-
-        val offsets = arrayOf(
-            Offset(100f, 200f),
-            Offset(300f, 200f),
-            Offset(100f, 400f),
-            Offset(300f, 400f)
-        )
-
-        val events = Array(4) { index ->
-            PointerInputEvent(index, 5, offsets[index], true)
-        }
-
-        val expectedChanges = Array(4) { index ->
-            PointerInputChange(
-                id = PointerId(index.toLong()),
-                5,
-                offsets[index] - childOffset,
-                true,
-                5,
-                offsets[index] - childOffset,
-                false,
-                isInitiallyConsumed = false
-            )
-        }
-
-        // Act
-
-        events.forEach {
-            pointerInputEventProcessor.process(it)
-        }
-
-        // Assert
-
-        val log =
-            pointerInputFilter
-                .log
-                .getOnPointerEventFilterLog()
-                .filter { it.pass == PointerEventPass.Initial }
-
-        // Verify call count
-        assertThat(log)
-            .hasSize(expectedChanges.size)
-
-        // Verify call values
-        expectedChanges.forEachIndexed { index, change ->
-            val item = log[index]
-            PointerEventSubject
-                .assertThat(item.pointerEvent)
-                .isStructurallyEqualTo(pointerEventOf(change))
-        }
-    }
-
-    @Test
-    fun process_downMisses_targetDoesNotReceive() {
-
-        // Arrange
-
-        val pointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            100, 200, 301, 401,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-
-        addToRoot(layoutNode)
-
-        val offsets = arrayOf(
-            Offset(99f, 200f),
-            Offset(99f, 400f),
-            Offset(100f, 199f),
-            Offset(100f, 401f),
-            Offset(300f, 199f),
-            Offset(300f, 401f),
-            Offset(301f, 200f),
-            Offset(301f, 400f)
-        )
-
-        val events = Array(8) { index ->
-            PointerInputEvent(index, 0, offsets[index], true)
-        }
-
-        // Act
-
-        events.forEach {
-            pointerInputEventProcessor.process(it)
-        }
-
-        // Assert
-
-        assertThat(pointerInputFilter.log.getOnPointerEventFilterLog()).hasSize(0)
-    }
-
-    @Test
-    fun process_downHits3of3_all3PointerNodesReceive() {
-        process_partialTreeHits(3)
-    }
-
-    @Test
-    fun process_downHits2of3_correct2PointerNodesReceive() {
-        process_partialTreeHits(2)
-    }
-
-    @Test
-    fun process_downHits1of3_onlyCorrectPointerNodesReceives() {
-        process_partialTreeHits(1)
-    }
-
-    private fun process_partialTreeHits(numberOfChildrenHit: Int) {
-        // Arrange
-
-        val log = mutableListOf<LogEntry>()
-        val childPointerInputFilter = PointerInputFilterMock(log)
-        val middlePointerInputFilter = PointerInputFilterMock(log)
-        val parentPointerInputFilter = PointerInputFilterMock(log)
-
-        val childLayoutNode =
-            LayoutNode(
-                100, 100, 200, 200,
-                PointerInputModifierImpl2(
-                    childPointerInputFilter
-                )
-            )
-        val middleLayoutNode: LayoutNode =
-            LayoutNode(
-                100, 100, 400, 400,
-                PointerInputModifierImpl2(
-                    middlePointerInputFilter
-                )
-            ).apply {
-                insertAt(0, childLayoutNode)
-            }
-        val parentLayoutNode: LayoutNode =
-            LayoutNode(
-                0, 0, 500, 500,
-                PointerInputModifierImpl2(
-                    parentPointerInputFilter
-                )
-            ).apply {
-                insertAt(0, middleLayoutNode)
-            }
-        addToRoot(parentLayoutNode)
-
-        val offset = when (numberOfChildrenHit) {
-            3 -> Offset(250f, 250f)
-            2 -> Offset(150f, 150f)
-            1 -> Offset(50f, 50f)
-            else -> throw IllegalStateException()
-        }
-
-        val event = PointerInputEvent(0, 5, offset, true)
-
-        // Act
-
-        pointerInputEventProcessor.process(event)
-
-        // Assert
-
-        val filteredLog = log.getOnPointerEventFilterLog().filter {
-            it.pass == PointerEventPass.Initial
-        }
-
-        when (numberOfChildrenHit) {
-            3 -> {
-                assertThat(filteredLog).hasSize(3)
-                assertThat(filteredLog[0].pointerInputFilter)
-                    .isSameInstanceAs(parentPointerInputFilter)
-                assertThat(filteredLog[1].pointerInputFilter)
-                    .isSameInstanceAs(middlePointerInputFilter)
-                assertThat(filteredLog[2].pointerInputFilter)
-                    .isSameInstanceAs(childPointerInputFilter)
-            }
-            2 -> {
-                assertThat(filteredLog).hasSize(2)
-                assertThat(filteredLog[0].pointerInputFilter)
-                    .isSameInstanceAs(parentPointerInputFilter)
-                assertThat(filteredLog[1].pointerInputFilter)
-                    .isSameInstanceAs(middlePointerInputFilter)
-            }
-            1 -> {
-                assertThat(filteredLog).hasSize(1)
-                assertThat(filteredLog[0].pointerInputFilter)
-                    .isSameInstanceAs(parentPointerInputFilter)
-            }
-            else -> throw IllegalStateException()
-        }
-    }
-
-    @Test
-    fun process_modifiedChange_isPassedToNext() {
-
-        // Arrange
-
-        val expectedInput = PointerInputChange(
-            id = PointerId(0),
-            5,
-            Offset(100f, 0f),
-            true,
-            3,
-            Offset(0f, 0f),
-            true,
-            isInitiallyConsumed = false
-        )
-        val expectedOutput = PointerInputChange(
-            id = PointerId(0),
-            5,
-            Offset(100f, 0f),
-            true,
-            3,
-            Offset(0f, 0f),
-            true,
-            isInitiallyConsumed = true
-        )
-
-        val pointerInputFilter = PointerInputFilterMock(
-            mutableListOf(),
-            pointerEventHandler = { pointerEvent, pass, _ ->
-                if (pass == PointerEventPass.Initial) {
-                    val change = pointerEvent
-                        .changes
-                        .first()
-
-                    if (change.positionChanged()) change.consume()
-                }
-            }
-        )
-
-        val layoutNode = LayoutNode(
-            0, 0, 500, 500,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-
-        addToRoot(layoutNode)
-
-        val down = PointerInputEvent(
-            0,
-            3,
-            Offset(0f, 0f),
-            true
-        )
-        val move = PointerInputEvent(
-            0,
-            5,
-            Offset(100f, 0f),
-            true
-        )
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-        pointerInputFilter.log.clear()
-        pointerInputEventProcessor.process(move)
-
-        // Assert
-
-        val log = pointerInputFilter.log.getOnPointerEventFilterLog()
-
-        assertThat(log).hasSize(3)
-        PointerInputChangeSubject
-            .assertThat(log[0].pointerEvent.changes.first())
-            .isStructurallyEqualTo(expectedInput)
-        PointerInputChangeSubject
-            .assertThat(log[1].pointerEvent.changes.first())
-            .isStructurallyEqualTo(expectedOutput)
-    }
-
-    @Test
-    fun process_nodesAndAdditionalOffsetIncreasinglyInset_dispatchInfoIsCorrect() {
-        process_dispatchInfoIsCorrect(
-            0, 0, 100, 100,
-            2, 11, 100, 100,
-            23, 31, 100, 100,
-            43, 51,
-            99, 99
-        )
-    }
-
-    @Test
-    fun process_nodesAndAdditionalOffsetIncreasinglyOutset_dispatchInfoIsCorrect() {
-        process_dispatchInfoIsCorrect(
-            0, 0, 100, 100,
-            -2, -11, 100, 100,
-            -23, -31, 100, 100,
-            -43, -51,
-            1, 1
-        )
-    }
-
-    @Test
-    fun process_nodesAndAdditionalOffsetNotOffset_dispatchInfoIsCorrect() {
-        process_dispatchInfoIsCorrect(
-            0, 0, 100, 100,
-            0, 0, 100, 100,
-            0, 0, 100, 100,
-            0, 0,
-            50, 50
-        )
-    }
-
-    @Suppress("SameParameterValue")
-    private fun process_dispatchInfoIsCorrect(
-        pX1: Int,
-        pY1: Int,
-        pX2: Int,
-        pY2: Int,
-        mX1: Int,
-        mY1: Int,
-        mX2: Int,
-        mY2: Int,
-        cX1: Int,
-        cY1: Int,
-        cX2: Int,
-        cY2: Int,
-        aOX: Int,
-        aOY: Int,
-        pointerX: Int,
-        pointerY: Int
-    ) {
-
-        // Arrange
-
-        val log = mutableListOf<LogEntry>()
-        val childPointerInputFilter = PointerInputFilterMock(log)
-        val middlePointerInputFilter = PointerInputFilterMock(log)
-        val parentPointerInputFilter = PointerInputFilterMock(log)
-
-        val childOffset = Offset(cX1.toFloat(), cY1.toFloat())
-        val childLayoutNode = LayoutNode(
-            cX1, cY1, cX2, cY2,
-            PointerInputModifierImpl2(
-                childPointerInputFilter
-            )
-        )
-        val middleOffset = Offset(mX1.toFloat(), mY1.toFloat())
-        val middleLayoutNode: LayoutNode = LayoutNode(
-            mX1, mY1, mX2, mY2,
-            PointerInputModifierImpl2(
-                middlePointerInputFilter
-            )
-        ).apply {
-            insertAt(0, childLayoutNode)
-        }
-        val parentLayoutNode: LayoutNode = LayoutNode(
-            pX1, pY1, pX2, pY2,
-            PointerInputModifierImpl2(
-                parentPointerInputFilter
-            )
-        ).apply {
-            insertAt(0, middleLayoutNode)
-        }
-
-        val outerLayoutNode = LayoutNode(
-            aOX,
-            aOY,
-            aOX + parentLayoutNode.width,
-            aOY + parentLayoutNode.height
-        )
-
-        outerLayoutNode.insertAt(0, parentLayoutNode)
-        addToRoot(outerLayoutNode)
-
-        val additionalOffset = IntOffset(aOX, aOY)
-
-        val offset = Offset(pointerX.toFloat(), pointerY.toFloat())
-
-        val down = PointerInputEvent(0, 7, offset, true)
-
-        val expectedPointerInputChanges = arrayOf(
-            PointerInputChange(
-                id = PointerId(0),
-                7,
-                offset - additionalOffset,
-                true,
-                7,
-                offset - additionalOffset,
-                false,
-                isInitiallyConsumed = false
-            ),
-            PointerInputChange(
-                id = PointerId(0),
-                7,
-                offset - middleOffset - additionalOffset,
-                true,
-                7,
-                offset - middleOffset - additionalOffset,
-                false,
-                isInitiallyConsumed = false
-            ),
-            PointerInputChange(
-                id = PointerId(0),
-                7,
-                offset - middleOffset - childOffset - additionalOffset,
-                true,
-                7,
-                offset - middleOffset - childOffset - additionalOffset,
-                false,
-                isInitiallyConsumed = false
-            )
-        )
-
-        val expectedSizes = arrayOf(
-            IntSize(pX2 - pX1, pY2 - pY1),
-            IntSize(mX2 - mX1, mY2 - mY1),
-            IntSize(cX2 - cX1, cY2 - cY1)
-        )
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-
-        // Assert
-
-        val filteredLog = log.getOnPointerEventFilterLog()
-
-        // Verify call count
-        assertThat(filteredLog).hasSize(PointerEventPass.values().size * 3)
-
-        // Verify call values
-        filteredLog.verifyOnPointerEventCall(
-            0,
-            parentPointerInputFilter,
-            pointerEventOf(expectedPointerInputChanges[0]),
-            PointerEventPass.Initial,
-            expectedSizes[0]
-        )
-        filteredLog.verifyOnPointerEventCall(
-            1,
-            middlePointerInputFilter,
-            pointerEventOf(expectedPointerInputChanges[1]),
-            PointerEventPass.Initial,
-            expectedSizes[1]
-        )
-        filteredLog.verifyOnPointerEventCall(
-            2,
-            childPointerInputFilter,
-            pointerEventOf(expectedPointerInputChanges[2]),
-            PointerEventPass.Initial,
-            expectedSizes[2]
-        )
-        filteredLog.verifyOnPointerEventCall(
-            3,
-            childPointerInputFilter,
-            pointerEventOf(expectedPointerInputChanges[2]),
-            PointerEventPass.Main,
-            expectedSizes[2]
-        )
-        filteredLog.verifyOnPointerEventCall(
-            4,
-            middlePointerInputFilter,
-            pointerEventOf(expectedPointerInputChanges[1]),
-            PointerEventPass.Main,
-            expectedSizes[1]
-        )
-        filteredLog.verifyOnPointerEventCall(
-            5,
-            parentPointerInputFilter,
-            pointerEventOf(expectedPointerInputChanges[0]),
-            PointerEventPass.Main,
-            expectedSizes[0]
-        )
-        filteredLog.verifyOnPointerEventCall(
-            6,
-            parentPointerInputFilter,
-            pointerEventOf(expectedPointerInputChanges[0]),
-            PointerEventPass.Final,
-            expectedSizes[0]
-        )
-        filteredLog.verifyOnPointerEventCall(
-            7,
-            middlePointerInputFilter,
-            pointerEventOf(expectedPointerInputChanges[1]),
-            PointerEventPass.Final,
-            expectedSizes[1]
-        )
-        filteredLog.verifyOnPointerEventCall(
-            8,
-            childPointerInputFilter,
-            pointerEventOf(expectedPointerInputChanges[2]),
-            PointerEventPass.Final,
-            expectedSizes[2]
-        )
-    }
-
-    /**
-     * This test creates a layout of this shape:
-     *
-     *  -------------
-     *  |     |     |
-     *  |  t  |     |
-     *  |     |     |
-     *  |-----|     |
-     *  |           |
-     *  |     |-----|
-     *  |     |     |
-     *  |     |  t  |
-     *  |     |     |
-     *  -------------
-     *
-     * Where there is one child in the top right, and one in the bottom left, and 2 down touches,
-     * one in the top left and one in the bottom right.
-     */
-    @Test
-    fun process_2DownOn2DifferentPointerNodes_hitAndDispatchInfoAreCorrect() {
-
-        // Arrange
-
-        val log = mutableListOf<LogEntry>()
-        val childPointerInputFilter1 = PointerInputFilterMock(log)
-        val childPointerInputFilter2 = PointerInputFilterMock(log)
-
-        val childLayoutNode1 =
-            LayoutNode(
-                0, 0, 50, 50,
-                PointerInputModifierImpl2(
-                    childPointerInputFilter1
-                )
-            )
-        val childLayoutNode2 =
-            LayoutNode(
-                50, 50, 100, 100,
-                PointerInputModifierImpl2(
-                    childPointerInputFilter2
-                )
-            )
-        addToRoot(childLayoutNode1, childLayoutNode2)
-
-        val offset1 = Offset(25f, 25f)
-        val offset2 = Offset(75f, 75f)
-
-        val down = PointerInputEvent(
-            5,
-            listOf(
-                PointerInputEventData(0, 5, offset1, true),
-                PointerInputEventData(1, 5, offset2, true)
-            )
-        )
-
-        val expectedChange1 =
-            PointerInputChange(
-                id = PointerId(0),
-                5,
-                offset1,
-                true,
-                5,
-                offset1,
-                false,
-                isInitiallyConsumed = false
-            )
-        val expectedChange2 =
-            PointerInputChange(
-                id = PointerId(1),
-                5,
-                offset2 - Offset(50f, 50f),
-                true,
-                5,
-                offset2 - Offset(50f, 50f),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-
-        // Assert
-
-        // Verify call count
-
-        val child1Log = log.getOnPointerEventFilterLog().filter {
-            it.pointerInputFilter === childPointerInputFilter1
-        }
-        val child2Log = log.getOnPointerEventFilterLog().filter {
-            it.pointerInputFilter === childPointerInputFilter2
-        }
-        assertThat(child1Log).hasSize(PointerEventPass.values().size)
-        assertThat(child2Log).hasSize(PointerEventPass.values().size)
-
-        // Verify call values
-
-        val expectedBounds = IntSize(50, 50)
-
-        child1Log.verifyOnPointerEventCall(
-            0,
-            null,
-            pointerEventOf(expectedChange1),
-            PointerEventPass.Initial,
-            expectedBounds
-        )
-        child1Log.verifyOnPointerEventCall(
-            1,
-            null,
-            pointerEventOf(expectedChange1),
-            PointerEventPass.Main,
-            expectedBounds
-        )
-        child1Log.verifyOnPointerEventCall(
-            2,
-            null,
-            pointerEventOf(expectedChange1),
-            PointerEventPass.Final,
-            expectedBounds
-        )
-
-        child2Log.verifyOnPointerEventCall(
-            0,
-            null,
-            pointerEventOf(expectedChange2),
-            PointerEventPass.Initial,
-            expectedBounds
-        )
-        child2Log.verifyOnPointerEventCall(
-            1,
-            null,
-            pointerEventOf(expectedChange2),
-            PointerEventPass.Main,
-            expectedBounds
-        )
-        child2Log.verifyOnPointerEventCall(
-            2,
-            null,
-            pointerEventOf(expectedChange2),
-            PointerEventPass.Final,
-            expectedBounds
-        )
-    }
-
-    /**
-     * This test creates a layout of this shape:
-     *
-     *  ---------------
-     *  | t      |    |
-     *  |        |    |
-     *  |  |-------|  |
-     *  |  | t     |  |
-     *  |  |       |  |
-     *  |  |       |  |
-     *  |--|  |-------|
-     *  |  |  | t     |
-     *  |  |  |       |
-     *  |  |  |       |
-     *  |  |--|       |
-     *  |     |       |
-     *  ---------------
-     *
-     * There are 3 staggered children and 3 down events, the first is on child 1, the second is on
-     * child 2 in a space that overlaps child 1, and the third is in a space that overlaps both
-     * child 2.
-     */
-    @Test
-    fun process_3DownOnOverlappingPointerNodes_hitAndDispatchInfoAreCorrect() {
-
-        val log = mutableListOf<LogEntry>()
-        val childPointerInputFilter1 = PointerInputFilterMock(log)
-        val childPointerInputFilter2 = PointerInputFilterMock(log)
-        val childPointerInputFilter3 = PointerInputFilterMock(log)
-
-        val childLayoutNode1 = LayoutNode(
-            0, 0, 100, 100,
-            PointerInputModifierImpl2(
-                childPointerInputFilter1
-            )
-        )
-        val childLayoutNode2 = LayoutNode(
-            50, 50, 150, 150,
-            PointerInputModifierImpl2(
-                childPointerInputFilter2
-            )
-        )
-        val childLayoutNode3 = LayoutNode(
-            100, 100, 200, 200,
-            PointerInputModifierImpl2(
-                childPointerInputFilter3
-            )
-        )
-
-        addToRoot(childLayoutNode1, childLayoutNode2, childLayoutNode3)
-
-        val offset1 = Offset(25f, 25f)
-        val offset2 = Offset(75f, 75f)
-        val offset3 = Offset(125f, 125f)
-
-        val down = PointerInputEvent(
-            5,
-            listOf(
-                PointerInputEventData(0, 5, offset1, true),
-                PointerInputEventData(1, 5, offset2, true),
-                PointerInputEventData(2, 5, offset3, true)
-            )
-        )
-
-        val expectedChange1 =
-            PointerInputChange(
-                id = PointerId(0),
-                5,
-                offset1,
-                true,
-                5,
-                offset1,
-                false,
-                isInitiallyConsumed = false
-            )
-        val expectedChange2 =
-            PointerInputChange(
-                id = PointerId(1),
-                5,
-                offset2 - Offset(50f, 50f),
-                true,
-                5,
-                offset2 - Offset(50f, 50f),
-                false,
-                isInitiallyConsumed = false
-            )
-        val expectedChange3 =
-            PointerInputChange(
-                id = PointerId(2),
-                5,
-                offset3 - Offset(100f, 100f),
-                true,
-                5,
-                offset3 - Offset(100f, 100f),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-
-        // Assert
-
-        val child1Log = log.getOnPointerEventFilterLog().filter {
-            it.pointerInputFilter === childPointerInputFilter1
-        }
-        val child2Log = log.getOnPointerEventFilterLog().filter {
-            it.pointerInputFilter === childPointerInputFilter2
-        }
-        val child3Log = log.getOnPointerEventFilterLog().filter {
-            it.pointerInputFilter === childPointerInputFilter3
-        }
-        assertThat(child1Log).hasSize(PointerEventPass.values().size)
-        assertThat(child2Log).hasSize(PointerEventPass.values().size)
-        assertThat(child3Log).hasSize(PointerEventPass.values().size)
-
-        // Verify call values
-
-        val expectedBounds = IntSize(100, 100)
-
-        child1Log.verifyOnPointerEventCall(
-            0,
-            null,
-            pointerEventOf(expectedChange1),
-            PointerEventPass.Initial,
-            expectedBounds
-        )
-        child1Log.verifyOnPointerEventCall(
-            1,
-            null,
-            pointerEventOf(expectedChange1),
-            PointerEventPass.Main,
-            expectedBounds
-        )
-        child1Log.verifyOnPointerEventCall(
-            2,
-            null,
-            pointerEventOf(expectedChange1),
-            PointerEventPass.Final,
-            expectedBounds
-        )
-
-        child2Log.verifyOnPointerEventCall(
-            0,
-            null,
-            pointerEventOf(expectedChange2),
-            PointerEventPass.Initial,
-            expectedBounds
-        )
-        child2Log.verifyOnPointerEventCall(
-            1,
-            null,
-            pointerEventOf(expectedChange2),
-            PointerEventPass.Main,
-            expectedBounds
-        )
-        child2Log.verifyOnPointerEventCall(
-            2,
-            null,
-            pointerEventOf(expectedChange2),
-            PointerEventPass.Final,
-            expectedBounds
-        )
-
-        child3Log.verifyOnPointerEventCall(
-            0,
-            null,
-            pointerEventOf(expectedChange3),
-            PointerEventPass.Initial,
-            expectedBounds
-        )
-        child3Log.verifyOnPointerEventCall(
-            1,
-            null,
-            pointerEventOf(expectedChange3),
-            PointerEventPass.Main,
-            expectedBounds
-        )
-        child3Log.verifyOnPointerEventCall(
-            2,
-            null,
-            pointerEventOf(expectedChange3),
-            PointerEventPass.Final,
-            expectedBounds
-        )
-    }
-
-    /**
-     * This test creates a layout of this shape:
-     *
-     *  ---------------
-     *  |             |
-     *  |      t      |
-     *  |             |
-     *  |  |-------|  |
-     *  |  |       |  |
-     *  |  |   t   |  |
-     *  |  |       |  |
-     *  |  |-------|  |
-     *  |             |
-     *  |      t      |
-     *  |             |
-     *  ---------------
-     *
-     * There are 3 staggered children and 3 down events, the first is on child 1, the second is on
-     * child 2 in a space that overlaps child 1, and the third is in a space that overlaps both
-     * child 2.
-     */
-    @Test
-    fun process_3DownOnFloatingPointerNodeV_hitAndDispatchInfoAreCorrect() {
-
-        val childPointerInputFilter1 = PointerInputFilterMock()
-        val childPointerInputFilter2 = PointerInputFilterMock()
-
-        val childLayoutNode1 = LayoutNode(
-            0, 0, 100, 150,
-            PointerInputModifierImpl2(
-                childPointerInputFilter1
-            )
-        )
-        val childLayoutNode2 = LayoutNode(
-            25, 50, 75, 100,
-            PointerInputModifierImpl2(
-                childPointerInputFilter2
-            )
-        )
-
-        addToRoot(childLayoutNode1, childLayoutNode2)
-
-        val offset1 = Offset(50f, 25f)
-        val offset2 = Offset(50f, 75f)
-        val offset3 = Offset(50f, 125f)
-
-        val down = PointerInputEvent(
-            7,
-            listOf(
-                PointerInputEventData(0, 7, offset1, true),
-                PointerInputEventData(1, 7, offset2, true),
-                PointerInputEventData(2, 7, offset3, true)
-            )
-        )
-
-        val expectedChange1 =
-            PointerInputChange(
-                id = PointerId(0),
-                7,
-                offset1,
-                true,
-                7,
-                offset1,
-                false,
-                isInitiallyConsumed = false
-            )
-        val expectedChange2 =
-            PointerInputChange(
-                id = PointerId(1),
-                7,
-                offset2 - Offset(25f, 50f),
-                true,
-                7,
-                offset2 - Offset(25f, 50f),
-                false,
-                isInitiallyConsumed = false
-            )
-        val expectedChange3 =
-            PointerInputChange(
-                id = PointerId(2),
-                7,
-                offset3,
-                true,
-                7,
-                offset3,
-                false,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-
-        // Assert
-
-        val log1 = childPointerInputFilter1.log.getOnPointerEventFilterLog()
-        val log2 = childPointerInputFilter2.log.getOnPointerEventFilterLog()
-
-        // Verify call count
-        assertThat(log1).hasSize(PointerEventPass.values().size)
-        assertThat(log2).hasSize(PointerEventPass.values().size)
-
-        // Verify call values
-        PointerEventPass.values().forEachIndexed { index, pass ->
-            log1.verifyOnPointerEventCall(
-                index,
-                null,
-                pointerEventOf(expectedChange1, expectedChange3),
-                pass,
-                IntSize(100, 150)
-            )
-            log2.verifyOnPointerEventCall(
-                index,
-                null,
-                pointerEventOf(expectedChange2),
-                pass,
-                IntSize(50, 50)
-            )
-        }
-    }
-
-    /**
-     * This test creates a layout of this shape:
-     *
-     *  -----------------
-     *  |               |
-     *  |   |-------|   |
-     *  |   |       |   |
-     *  | t |   t   | t |
-     *  |   |       |   |
-     *  |   |-------|   |
-     *  |               |
-     *  -----------------
-     *
-     * There are 3 staggered children and 3 down events, the first is on child 1, the second is on
-     * child 2 in a space that overlaps child 1, and the third is in a space that overlaps both
-     * child 2.
-     */
-    @Test
-    fun process_3DownOnFloatingPointerNodeH_hitAndDispatchInfoAreCorrect() {
-
-        val childPointerInputFilter1 = PointerInputFilterMock()
-        val childPointerInputFilter2 = PointerInputFilterMock()
-
-        val childLayoutNode1 = LayoutNode(
-            0, 0, 150, 100,
-            PointerInputModifierImpl2(
-                childPointerInputFilter1
-            )
-        )
-        val childLayoutNode2 = LayoutNode(
-            50, 25, 100, 75,
-            PointerInputModifierImpl2(
-                childPointerInputFilter2
-            )
-        )
-
-        addToRoot(childLayoutNode1, childLayoutNode2)
-
-        val offset1 = Offset(25f, 50f)
-        val offset2 = Offset(75f, 50f)
-        val offset3 = Offset(125f, 50f)
-
-        val down = PointerInputEvent(
-            11,
-            listOf(
-                PointerInputEventData(0, 11, offset1, true),
-                PointerInputEventData(1, 11, offset2, true),
-                PointerInputEventData(2, 11, offset3, true)
-            )
-        )
-
-        val expectedChange1 =
-            PointerInputChange(
-                id = PointerId(0),
-                11,
-                offset1,
-                true,
-                11,
-                offset1,
-                false,
-                isInitiallyConsumed = false
-            )
-        val expectedChange2 =
-            PointerInputChange(
-                id = PointerId(1),
-                11,
-                offset2 - Offset(50f, 25f),
-                true,
-                11,
-                offset2 - Offset(50f, 25f),
-                false,
-                isInitiallyConsumed = false
-            )
-        val expectedChange3 =
-            PointerInputChange(
-                id = PointerId(2),
-                11,
-                offset3,
-                true,
-                11,
-                offset3,
-                false,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-
-        // Assert
-
-        val log1 = childPointerInputFilter1.log.getOnPointerEventFilterLog()
-        val log2 = childPointerInputFilter2.log.getOnPointerEventFilterLog()
-
-        // Verify call count
-        assertThat(log1).hasSize(PointerEventPass.values().size)
-        assertThat(log2).hasSize(PointerEventPass.values().size)
-
-        // Verify call values
-        PointerEventPass.values().forEachIndexed { index, pass ->
-            log1.verifyOnPointerEventCall(
-                index,
-                null,
-                pointerEventOf(expectedChange1, expectedChange3),
-                pass,
-                IntSize(150, 100)
-            )
-            log2.verifyOnPointerEventCall(
-                index,
-                null,
-                pointerEventOf(expectedChange2),
-                pass,
-                IntSize(50, 50)
-            )
-        }
-    }
-
-    /**
-     * This test creates a layout of this shape:
-     *     0   1   2   3   4
-     *   .........   .........
-     * 0 .     t .   . t     .
-     *   .   |---|---|---|   .
-     * 1 . t | t |   | t | t .
-     *   ....|---|   |---|....
-     * 2     |           |
-     *   ....|---|   |---|....
-     * 3 . t | t |   | t | t .
-     *   .   |---|---|---|   .
-     * 4 .     t .   . t     .
-     *   .........   .........
-     *
-     * 4 LayoutNodes with PointerInputModifiers that are clipped by their parent LayoutNode. 4
-     * touches touch just inside the parent LayoutNode and inside the child LayoutNodes. 8
-     * touches touch just outside the parent LayoutNode but inside the child LayoutNodes.
-     *
-     * Because layout node bounds are not used to clip pointer input hit testing, all pointers
-     * should hit.
-     */
-    @Test
-    fun process_4DownInClippedAreaOfLnsWithPims_onlyCorrectPointersHit() {
-
-        // Arrange
-
-        val pointerInputFilterTopLeft = PointerInputFilterMock()
-        val pointerInputFilterTopRight = PointerInputFilterMock()
-        val pointerInputFilterBottomLeft = PointerInputFilterMock()
-        val pointerInputFilterBottomRight = PointerInputFilterMock()
-
-        val layoutNodeTopLeft = LayoutNode(
-            -1, -1, 1, 1,
-            PointerInputModifierImpl2(
-                pointerInputFilterTopLeft
-            )
-        )
-        val layoutNodeTopRight = LayoutNode(
-            2, -1, 4, 1,
-            PointerInputModifierImpl2(
-                pointerInputFilterTopRight
-            )
-        )
-        val layoutNodeBottomLeft = LayoutNode(
-            -1, 2, 1, 4,
-            PointerInputModifierImpl2(
-                pointerInputFilterBottomLeft
-            )
-        )
-        val layoutNodeBottomRight = LayoutNode(
-            2, 2, 4, 4,
-            PointerInputModifierImpl2(
-                pointerInputFilterBottomRight
-            )
-        )
-
-        val parentLayoutNode = LayoutNode(1, 1, 4, 4).apply {
-            insertAt(0, layoutNodeTopLeft)
-            insertAt(1, layoutNodeTopRight)
-            insertAt(2, layoutNodeBottomLeft)
-            insertAt(3, layoutNodeBottomRight)
-        }
-        addToRoot(parentLayoutNode)
-
-        val offsetsTopLeft =
-            listOf(
-                Offset(0f, 1f),
-                Offset(1f, 0f),
-                Offset(1f, 1f)
-            )
-
-        val offsetsTopRight =
-            listOf(
-                Offset(3f, 0f),
-                Offset(3f, 1f),
-                Offset(4f, 1f)
-            )
-
-        val offsetsBottomLeft =
-            listOf(
-                Offset(0f, 3f),
-                Offset(1f, 3f),
-                Offset(1f, 4f)
-            )
-
-        val offsetsBottomRight =
-            listOf(
-                Offset(3f, 3f),
-                Offset(3f, 4f),
-                Offset(4f, 3f)
-            )
-
-        val allOffsets = offsetsTopLeft + offsetsTopRight + offsetsBottomLeft + offsetsBottomRight
-
-        val pointerInputEvent =
-            PointerInputEvent(
-                11,
-                (allOffsets.indices).map {
-                    PointerInputEventData(it, 11, allOffsets[it], true)
-                }
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(pointerInputEvent)
-
-        // Assert
-
-        val expectedChangesTopLeft =
-            (offsetsTopLeft.indices).map {
-                PointerInputChange(
-                    id = PointerId(it.toLong()),
-                    11,
-                    Offset(
-                        offsetsTopLeft[it].x,
-                        offsetsTopLeft[it].y
-                    ),
-                    true,
-                    11,
-                    Offset(
-                        offsetsTopLeft[it].x,
-                        offsetsTopLeft[it].y
-                    ),
-                    false,
-                    isInitiallyConsumed = false
-                )
-            }
-
-        val expectedChangesTopRight =
-            (offsetsTopLeft.indices).map {
-                PointerInputChange(
-                    id = PointerId(it.toLong() + 3),
-                    11,
-                    Offset(
-                        offsetsTopRight[it].x - 3f,
-                        offsetsTopRight[it].y
-                    ),
-                    true,
-                    11,
-                    Offset(
-                        offsetsTopRight[it].x - 3f,
-                        offsetsTopRight[it].y
-                    ),
-                    false,
-                    isInitiallyConsumed = false
-                )
-            }
-
-        val expectedChangesBottomLeft =
-            (offsetsTopLeft.indices).map {
-                PointerInputChange(
-                    id = PointerId(it.toLong() + 6),
-                    11,
-                    Offset(
-                        offsetsBottomLeft[it].x,
-                        offsetsBottomLeft[it].y - 3f
-                    ),
-                    true,
-                    11,
-                    Offset(
-                        offsetsBottomLeft[it].x,
-                        offsetsBottomLeft[it].y - 3f
-                    ),
-                    false,
-                    isInitiallyConsumed = false
-                )
-            }
-
-        val expectedChangesBottomRight =
-            (offsetsTopLeft.indices).map {
-                PointerInputChange(
-                    id = PointerId(it.toLong() + 9),
-                    11,
-                    Offset(
-                        offsetsBottomRight[it].x - 3f,
-                        offsetsBottomRight[it].y - 3f
-                    ),
-                    true,
-                    11,
-                    Offset(
-                        offsetsBottomRight[it].x - 3f,
-                        offsetsBottomRight[it].y - 3f
-                    ),
-                    false,
-                    isInitiallyConsumed = false
-                )
-            }
-
-        // Verify call values
-
-        val logTopLeft = pointerInputFilterTopLeft.log.getOnPointerEventFilterLog()
-        val logTopRight = pointerInputFilterTopRight.log.getOnPointerEventFilterLog()
-        val logBottomLeft = pointerInputFilterBottomLeft.log.getOnPointerEventFilterLog()
-        val logBottomRight = pointerInputFilterBottomRight.log.getOnPointerEventFilterLog()
-
-        PointerEventPass.values().forEachIndexed { index, pass ->
-            logTopLeft.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(*expectedChangesTopLeft.toTypedArray()),
-                expectedPass = pass
-            )
-            logTopRight.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(*expectedChangesTopRight.toTypedArray()),
-                expectedPass = pass
-            )
-            logBottomLeft.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(*expectedChangesBottomLeft.toTypedArray()),
-                expectedPass = pass
-            )
-            logBottomRight.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(*expectedChangesBottomRight.toTypedArray()),
-                expectedPass = pass
-            )
-        }
-    }
-
-    /**
-     * This test creates a layout of this shape:
-     *
-     *   |---|
-     *   |tt |
-     *   |t  |
-     *   |---|t
-     *       tt
-     *
-     *   But where the additional offset suggest something more like this shape.
-     *
-     *   tt
-     *   t|---|
-     *    |  t|
-     *    | tt|
-     *    |---|
-     *
-     *   Without the additional offset, it would be expected that only the top left 3 pointers would
-     *   hit, but with the additional offset, only the bottom right 3 hit.
-     */
-    @Test
-    fun process_rootIsOffset_onlyCorrectPointersHit() {
-
-        // Arrange
-        val singlePointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            0, 0, 2, 2,
-            PointerInputModifierImpl2(
-                singlePointerInputFilter
-            )
-        )
-        val outerLayoutNode = LayoutNode(1, 1, 3, 3)
-        outerLayoutNode.insertAt(0, layoutNode)
-        addToRoot(outerLayoutNode)
-        val offsetsThatHit =
-            listOf(
-                Offset(2f, 2f),
-                Offset(2f, 1f),
-                Offset(1f, 2f)
-            )
-        val offsetsThatMiss =
-            listOf(
-                Offset(0f, 0f),
-                Offset(0f, 1f),
-                Offset(1f, 0f)
-            )
-        val allOffsets = offsetsThatHit + offsetsThatMiss
-        val pointerInputEvent =
-            PointerInputEvent(
-                11,
-                (allOffsets.indices).map {
-                    PointerInputEventData(it, 11, allOffsets[it], true)
-                }
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(pointerInputEvent)
-
-        // Assert
-
-        val expectedChanges =
-            (offsetsThatHit.indices).map {
-                PointerInputChange(
-                    id = PointerId(it.toLong()),
-                    11,
-                    offsetsThatHit[it] - Offset(1f, 1f),
-                    true,
-                    11,
-                    offsetsThatHit[it] - Offset(1f, 1f),
-                    false,
-                    isInitiallyConsumed = false
-                )
-            }
-
-        val log = singlePointerInputFilter.log.getOnPointerEventFilterLog()
-
-        // Verify call count
-        assertThat(log).hasSize(PointerEventPass.values().size)
-
-        // Verify call values
-        PointerEventPass.values().forEachIndexed { index, pass ->
-            log.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(*expectedChanges.toTypedArray()),
-                expectedPass = pass
-            )
-        }
-    }
-
-    @Test
-    fun process_downOn3NestedPointerInputModifiers_hitAndDispatchInfoAreCorrect() {
-
-        val pointerInputFilter1 = PointerInputFilterMock()
-        val pointerInputFilter2 = PointerInputFilterMock()
-        val pointerInputFilter3 = PointerInputFilterMock()
-
-        val modifier = PointerInputModifierImpl2(pointerInputFilter1) then
-            PointerInputModifierImpl2(pointerInputFilter2) then
-            PointerInputModifierImpl2(pointerInputFilter3)
-
-        val layoutNode = LayoutNode(
-            25, 50, 75, 100,
-            modifier
-        )
-
-        addToRoot(layoutNode)
-
-        val offset1 = Offset(50f, 75f)
-
-        val down = PointerInputEvent(
-            7,
-            listOf(
-                PointerInputEventData(0, 7, offset1, true)
-            )
-        )
-
-        val expectedChange =
-            PointerInputChange(
-                id = PointerId(0),
-                7,
-                offset1 - Offset(25f, 50f),
-                true,
-                7,
-                offset1 - Offset(25f, 50f),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-
-        // Assert
-
-        val log1 = pointerInputFilter1.log.getOnPointerEventFilterLog()
-        val log2 = pointerInputFilter2.log.getOnPointerEventFilterLog()
-        val log3 = pointerInputFilter3.log.getOnPointerEventFilterLog()
-
-        // Verify call count
-        assertThat(log1).hasSize(PointerEventPass.values().size)
-        assertThat(log2).hasSize(PointerEventPass.values().size)
-        assertThat(log3).hasSize(PointerEventPass.values().size)
-
-        // Verify call values
-        PointerEventPass.values().forEachIndexed { index, pass ->
-            log1.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedChange),
-                expectedPass = pass,
-                expectedBounds = IntSize(50, 50)
-            )
-            log2.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedChange),
-                expectedPass = pass,
-                expectedBounds = IntSize(50, 50)
-            )
-            log3.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedChange),
-                expectedPass = pass,
-                expectedBounds = IntSize(50, 50)
-            )
-        }
-    }
-
-    @Test
-    fun process_downOnDeeplyNestedPointerInputModifier_hitAndDispatchInfoAreCorrect() {
-
-        val pointerInputFilter = PointerInputFilterMock()
-
-        val layoutNode1 =
-            LayoutNode(
-                1, 5, 500, 500,
-                PointerInputModifierImpl2(pointerInputFilter)
-            )
-        val layoutNode2: LayoutNode = LayoutNode(2, 6, 500, 500).apply {
-            insertAt(0, layoutNode1)
-        }
-        val layoutNode3: LayoutNode = LayoutNode(3, 7, 500, 500).apply {
-            insertAt(0, layoutNode2)
-        }
-        val layoutNode4: LayoutNode = LayoutNode(4, 8, 500, 500).apply {
-            insertAt(0, layoutNode3)
-        }
-        addToRoot(layoutNode4)
-
-        val offset1 = Offset(499f, 499f)
-
-        val downEvent = PointerInputEvent(
-            7,
-            listOf(
-                PointerInputEventData(0, 7, offset1, true)
-            )
-        )
-
-        val expectedChange =
-            PointerInputChange(
-                id = PointerId(0),
-                7,
-                offset1 - Offset(1f + 2f + 3f + 4f, 5f + 6f + 7f + 8f),
-                true,
-                7,
-                offset1 - Offset(1f + 2f + 3f + 4f, 5f + 6f + 7f + 8f),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(downEvent)
-
-        // Assert
-
-        val log = pointerInputFilter.log.getOnPointerEventFilterLog()
-
-        // Verify call count
-        assertThat(log).hasSize(PointerEventPass.values().size)
-
-        // Verify call values
-        PointerEventPass.values().forEachIndexed { index, pass ->
-            log.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedChange),
-                expectedPass = pass,
-                expectedBounds = IntSize(499, 495)
-            )
-        }
-    }
-
-    @Test
-    fun process_downOnComplexPointerAndLayoutNodePath_hitAndDispatchInfoAreCorrect() {
-
-        val pointerInputFilter1 = PointerInputFilterMock()
-        val pointerInputFilter2 = PointerInputFilterMock()
-        val pointerInputFilter3 = PointerInputFilterMock()
-        val pointerInputFilter4 = PointerInputFilterMock()
-
-        val layoutNode1 = LayoutNode(
-            1, 6, 500, 500,
-            PointerInputModifierImpl2(pointerInputFilter1)
-                then PointerInputModifierImpl2(pointerInputFilter2)
-        )
-        val layoutNode2: LayoutNode = LayoutNode(2, 7, 500, 500).apply {
-            insertAt(0, layoutNode1)
-        }
-        val layoutNode3 =
-            LayoutNode(
-                3, 8, 500, 500,
-                PointerInputModifierImpl2(pointerInputFilter3)
-                    then PointerInputModifierImpl2(pointerInputFilter4)
-            ).apply {
-                insertAt(0, layoutNode2)
-            }
-
-        val layoutNode4: LayoutNode = LayoutNode(4, 9, 500, 500).apply {
-            insertAt(0, layoutNode3)
-        }
-        val layoutNode5: LayoutNode = LayoutNode(5, 10, 500, 500).apply {
-            insertAt(0, layoutNode4)
-        }
-        addToRoot(layoutNode5)
-
-        val offset1 = Offset(499f, 499f)
-
-        val downEvent = PointerInputEvent(
-            3,
-            listOf(
-                PointerInputEventData(0, 3, offset1, true)
-            )
-        )
-
-        val expectedChange1 =
-            PointerInputChange(
-                id = PointerId(0),
-                3,
-                offset1 - Offset(
-                    1f + 2f + 3f + 4f + 5f,
-                    6f + 7f + 8f + 9f + 10f
-                ),
-                true,
-                3,
-                offset1 - Offset(
-                    1f + 2f + 3f + 4f + 5f,
-                    6f + 7f + 8f + 9f + 10f
-                ),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        val expectedChange2 =
-            PointerInputChange(
-                id = PointerId(0),
-                3,
-                offset1 - Offset(3f + 4f + 5f, 8f + 9f + 10f),
-                true,
-                3,
-                offset1 - Offset(3f + 4f + 5f, 8f + 9f + 10f),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(downEvent)
-
-        // Assert
-
-        val log1 = pointerInputFilter1.log.getOnPointerEventFilterLog()
-        val log2 = pointerInputFilter2.log.getOnPointerEventFilterLog()
-        val log3 = pointerInputFilter3.log.getOnPointerEventFilterLog()
-        val log4 = pointerInputFilter4.log.getOnPointerEventFilterLog()
-
-        // Verify call count
-        assertThat(log1).hasSize(PointerEventPass.values().size)
-        assertThat(log2).hasSize(PointerEventPass.values().size)
-        assertThat(log3).hasSize(PointerEventPass.values().size)
-        assertThat(log4).hasSize(PointerEventPass.values().size)
-
-        // Verify call values
-        PointerEventPass.values().forEachIndexed { index, pass ->
-            log1.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedChange1),
-                expectedPass = pass,
-                expectedBounds = IntSize(499, 494)
-            )
-            log2.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedChange1),
-                expectedPass = pass,
-                expectedBounds = IntSize(499, 494)
-            )
-            log3.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedChange2),
-                expectedPass = pass,
-                expectedBounds = IntSize(497, 492)
-            )
-            log4.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedChange2),
-                expectedPass = pass,
-                expectedBounds = IntSize(497, 492)
-            )
-        }
-    }
-
-    @Test
-    fun process_downOnFullyOverlappingPointerInputModifiers_onlyTopPointerInputModifierReceives() {
-
-        val pointerInputFilter1 = PointerInputFilterMock()
-        val pointerInputFilter2 = PointerInputFilterMock()
-
-        val layoutNode1 = LayoutNode(
-            0, 0, 100, 100,
-            PointerInputModifierImpl2(
-                pointerInputFilter1
-            )
-        )
-        val layoutNode2 = LayoutNode(
-            0, 0, 100, 100,
-            PointerInputModifierImpl2(
-                pointerInputFilter2
-            )
-        )
-
-        addToRoot(layoutNode1, layoutNode2)
-
-        val down = PointerInputEvent(
-            1, 0, Offset(50f, 50f), true
-        )
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-
-        // Assert
-        assertThat(pointerInputFilter2.log.getOnPointerEventFilterLog()).hasSize(3)
-        assertThat(pointerInputFilter1.log.getOnPointerEventFilterLog()).hasSize(0)
-    }
-
-    @Test
-    fun process_downOnPointerInputModifierInLayoutNodeWithNoSize_downNotReceived() {
-
-        val pointerInputFilter1 = PointerInputFilterMock()
-
-        val layoutNode1 = LayoutNode(
-            0, 0, 0, 0,
-            PointerInputModifierImpl2(pointerInputFilter1)
-        )
-
-        addToRoot(layoutNode1)
-
-        val down = PointerInputEvent(
-            1, 0, Offset(0f, 0f), true
-        )
-
-        // Act
-        pointerInputEventProcessor.process(down)
-
-        // Assert
-        assertThat(pointerInputFilter1.log.getOnPointerEventFilterLog()).hasSize(0)
-    }
-
-    // Cancel Handlers
-
-    @Test
-    fun processCancel_noPointers_doesntCrash() {
-        pointerInputEventProcessor.processCancel()
-    }
-
-    @Test
-    fun processCancel_downThenCancel_pimOnlyReceivesCorrectDownThenCancel() {
-
-        // Arrange
-
-        val pointerInputFilter = PointerInputFilterMock()
-
-        val layoutNode = LayoutNode(
-            0, 0, 500, 500,
-            PointerInputModifierImpl2(pointerInputFilter)
-        )
-
-        addToRoot(layoutNode)
-
-        val pointerInputEvent =
-            PointerInputEvent(
-                7,
-                5,
-                Offset(250f, 250f),
-                true
-            )
-
-        val expectedChange =
-            PointerInputChange(
-                id = PointerId(7),
-                5,
-                Offset(250f, 250f),
-                true,
-                5,
-                Offset(250f, 250f),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(pointerInputEvent)
-        pointerInputEventProcessor.processCancel()
-
-        // Assert
-
-        val log = pointerInputFilter.log.filter {
-            it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
-        }
-
-        // Verify call count
-        assertThat(log).hasSize(PointerEventPass.values().size + 1)
-
-        // Verify call values
-        PointerEventPass.values().forEachIndexed { index, pass ->
-            log.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedChange),
-                expectedPass = pass
-            )
-        }
-        log.verifyOnCancelCall(PointerEventPass.values().size)
-    }
-
-    @Test
-    fun processCancel_downDownOnSamePimThenCancel_pimOnlyReceivesCorrectChangesThenCancel() {
-
-        // Arrange
-
-        val pointerInputFilter = PointerInputFilterMock()
-
-        val layoutNode = LayoutNode(
-            0, 0, 500, 500,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-
-        addToRoot(layoutNode)
-
-        val pointerInputEvent1 =
-            PointerInputEvent(
-                7,
-                5,
-                Offset(200f, 200f),
-                true
-            )
-
-        val pointerInputEvent2 =
-            PointerInputEvent(
-                10,
-                listOf(
-                    PointerInputEventData(
-                        7,
-                        10,
-                        Offset(200f, 200f),
-                        true
-                    ),
-                    PointerInputEventData(
-                        9,
-                        10,
-                        Offset(300f, 300f),
-                        true
-                    )
-                )
-            )
-
-        val expectedChanges1 =
-            listOf(
-                PointerInputChange(
-                    id = PointerId(7),
-                    5,
-                    Offset(200f, 200f),
-                    true,
-                    5,
-                    Offset(200f, 200f),
-                    false,
-                    isInitiallyConsumed = false
-                )
-            )
-
-        val expectedChanges2 =
-            listOf(
-                PointerInputChange(
-                    id = PointerId(7),
-                    10,
-                    Offset(200f, 200f),
-                    true,
-                    5,
-                    Offset(200f, 200f),
-                    true,
-                    isInitiallyConsumed = false
-                ),
-                PointerInputChange(
-                    id = PointerId(9),
-                    10,
-                    Offset(300f, 300f),
-                    true,
-                    10,
-                    Offset(300f, 300f),
-                    false,
-                    isInitiallyConsumed = false
-                )
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(pointerInputEvent1)
-        pointerInputEventProcessor.process(pointerInputEvent2)
-        pointerInputEventProcessor.processCancel()
-
-        // Assert
-
-        val log = pointerInputFilter.log.filter {
-            it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
-        }
-
-        // Verify call count
-        assertThat(log).hasSize(PointerEventPass.values().size * 2 + 1)
-
-        // Verify call values
-        var index = 0
-        PointerEventPass.values().forEach { pass ->
-            log.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(*expectedChanges1.toTypedArray()),
-                expectedPass = pass
-            )
-            index++
-        }
-        PointerEventPass.values().forEach { pass ->
-            log.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(*expectedChanges2.toTypedArray()),
-                expectedPass = pass
-            )
-            index++
-        }
-        log.verifyOnCancelCall(index)
-    }
-
-    @Test
-    fun processCancel_downOn2DifferentPimsThenCancel_pimsOnlyReceiveCorrectDownsThenCancel() {
-
-        // Arrange
-
-        val pointerInputFilter1 = PointerInputFilterMock()
-        val layoutNode1 = LayoutNode(
-            0, 0, 199, 199,
-            PointerInputModifierImpl2(pointerInputFilter1)
-        )
-
-        val pointerInputFilter2 = PointerInputFilterMock()
-        val layoutNode2 = LayoutNode(
-            200, 200, 399, 399,
-            PointerInputModifierImpl2(pointerInputFilter2)
-        )
-
-        addToRoot(layoutNode1, layoutNode2)
-
-        val pointerInputEventData1 =
-            PointerInputEventData(
-                7,
-                5,
-                Offset(100f, 100f),
-                true
-            )
-
-        val pointerInputEventData2 =
-            PointerInputEventData(
-                9,
-                5,
-                Offset(300f, 300f),
-                true
-            )
-
-        val pointerInputEvent = PointerInputEvent(
-            5,
-            listOf(pointerInputEventData1, pointerInputEventData2)
-        )
-
-        val expectedChange1 =
-            PointerInputChange(
-                id = PointerId(7),
-                5,
-                Offset(100f, 100f),
-                true,
-                5,
-                Offset(100f, 100f),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        val expectedChange2 =
-            PointerInputChange(
-                id = PointerId(9),
-                5,
-                Offset(100f, 100f),
-                true,
-                5,
-                Offset(100f, 100f),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(pointerInputEvent)
-        pointerInputEventProcessor.processCancel()
-
-        // Assert
-
-        val log1 =
-            pointerInputFilter1.log.filter {
-                it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
-            }
-        val log2 =
-            pointerInputFilter2.log.filter {
-                it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
-            }
-
-        // Verify call count
-        assertThat(log1).hasSize(PointerEventPass.values().size + 1)
-        assertThat(log2).hasSize(PointerEventPass.values().size + 1)
-
-        // Verify call values
-        var index = 0
-        PointerEventPass.values().forEach { pass ->
-            log1.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedChange1),
-                expectedPass = pass
-            )
-            log2.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedChange2),
-                expectedPass = pass
-            )
-            index++
-        }
-        log1.verifyOnCancelCall(index)
-        log2.verifyOnCancelCall(index)
-    }
-
-    @Test
-    fun processCancel_downMoveCancel_pimOnlyReceivesCorrectDownMoveCancel() {
-
-        // Arrange
-
-        val pointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            0, 0, 500, 500,
-            PointerInputModifierImpl2(pointerInputFilter)
-        )
-
-        addToRoot(layoutNode)
-
-        val down =
-            PointerInputEvent(
-                7,
-                5,
-                Offset(200f, 200f),
-                true
-            )
-
-        val move =
-            PointerInputEvent(
-                7,
-                10,
-                Offset(300f, 300f),
-                true
-            )
-
-        val expectedDown =
-            PointerInputChange(
-                id = PointerId(7),
-                5,
-                Offset(200f, 200f),
-                true,
-                5,
-                Offset(200f, 200f),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        val expectedMove =
-            PointerInputChange(
-                id = PointerId(7),
-                10,
-                Offset(300f, 300f),
-                true,
-                5,
-                Offset(200f, 200f),
-                true,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-        pointerInputEventProcessor.process(move)
-        pointerInputEventProcessor.processCancel()
-
-        // Assert
-
-        val log = pointerInputFilter.log.filter {
-            it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
-        }
-
-        // Verify call count
-        assertThat(log).hasSize(PointerEventPass.values().size * 2 + 1)
-
-        // Verify call values
-        var index = 0
-        PointerEventPass.values().forEach { pass ->
-            log.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedDown),
-                expectedPass = pass
-            )
-            index++
-        }
-        PointerEventPass.values().forEach { pass ->
-            log.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedMove),
-                expectedPass = pass
-            )
-            index++
-        }
-        log.verifyOnCancelCall(index)
-    }
-
-    @Test
-    fun processCancel_downCancelMoveUp_pimOnlyReceivesCorrectDownCancel() {
-
-        // Arrange
-
-        val pointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            0, 0, 500, 500,
-            PointerInputModifierImpl2(pointerInputFilter)
-        )
-
-        addToRoot(layoutNode)
-
-        val down =
-            PointerInputEvent(
-                7,
-                5,
-                Offset(200f, 200f),
-                true
-            )
-
-        val expectedDown =
-            PointerInputChange(
-                id = PointerId(7),
-                5,
-                Offset(200f, 200f),
-                true,
-                5,
-                Offset(200f, 200f),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-        pointerInputEventProcessor.processCancel()
-
-        // Assert
-
-        val log = pointerInputFilter.log.filter {
-            it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
-        }
-
-        // Verify call count
-        assertThat(log).hasSize(PointerEventPass.values().size + 1)
-
-        // Verify call values
-        var index = 0
-        PointerEventPass.values().forEach { pass ->
-            log.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedDown),
-                expectedPass = pass
-            )
-            index++
-        }
-        log.verifyOnCancelCall(index)
-    }
-
-    @Test
-    fun processCancel_downCancelDown_pimOnlyReceivesCorrectDownCancelDown() {
-
-        // Arrange
-
-        val pointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            0, 0, 500, 500,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-
-        addToRoot(layoutNode)
-
-        val down1 =
-            PointerInputEvent(
-                7,
-                5,
-                Offset(200f, 200f),
-                true
-            )
-
-        val down2 =
-            PointerInputEvent(
-                7,
-                10,
-                Offset(200f, 200f),
-                true
-            )
-
-        val expectedDown1 =
-            PointerInputChange(
-                id = PointerId(7),
-                5,
-                Offset(200f, 200f),
-                true,
-                5,
-                Offset(200f, 200f),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        val expectedDown2 =
-            PointerInputChange(
-                id = PointerId(7),
-                10,
-                Offset(200f, 200f),
-                true,
-                10,
-                Offset(200f, 200f),
-                false,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(down1)
-        pointerInputEventProcessor.processCancel()
-        pointerInputEventProcessor.process(down2)
-
-        // Assert
-
-        val log = pointerInputFilter.log.filter {
-            it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
-        }
-
-        // Verify call count
-        assertThat(log).hasSize(PointerEventPass.values().size * 2 + 1)
-
-        // Verify call values
-        var index = 0
-        PointerEventPass.values().forEach { pass ->
-            log.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedDown1),
-                expectedPass = pass
-            )
-            index++
-        }
-        log.verifyOnCancelCall(index)
-        index++
-        PointerEventPass.values().forEach { pass ->
-            log.verifyOnPointerEventCall(
-                index = index,
-                expectedEvent = pointerEventOf(expectedDown2),
-                expectedPass = pass
-            )
-            index++
-        }
-    }
-
-    @Test
-    fun process_layoutNodeRemovedDuringInput_correctPointerInputChangesReceived() {
-
-        // Arrange
-
-        val childPointerInputFilter = PointerInputFilterMock()
-        val childLayoutNode = LayoutNode(
-            0, 0, 100, 100,
-            PointerInputModifierImpl2(childPointerInputFilter)
-        )
-
-        val parentPointerInputFilter = PointerInputFilterMock()
-        val parentLayoutNode: LayoutNode = LayoutNode(
-            0, 0, 100, 100,
-            PointerInputModifierImpl2(parentPointerInputFilter)
-        ).apply {
-            insertAt(0, childLayoutNode)
-        }
-
-        addToRoot(parentLayoutNode)
-
-        val offset = Offset(50f, 50f)
-
-        val down = PointerInputEvent(0, 7, offset, true)
-        val up = PointerInputEvent(0, 11, offset, false)
-
-        val expectedDownChange =
-            PointerInputChange(
-                id = PointerId(0),
-                7,
-                offset,
-                true,
-                7,
-                offset,
-                false,
-                isInitiallyConsumed = false
-            )
-
-        val expectedUpChange =
-            PointerInputChange(
-                id = PointerId(0),
-                11,
-                offset,
-                false,
-                7,
-                offset,
-                true,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-        parentLayoutNode.removeAt(0, 1)
-        pointerInputEventProcessor.process(up)
-
-        // Assert
-
-        val parentLog = parentPointerInputFilter.log.getOnPointerEventFilterLog()
-        val childLog = childPointerInputFilter.log.getOnPointerEventFilterLog()
-
-        // Verify call count
-        assertThat(parentLog).hasSize(PointerEventPass.values().size * 2)
-        assertThat(childLog).hasSize(PointerEventPass.values().size)
-
-        // Verify call values
-
-        parentLog.verifyOnPointerEventCall(
-            index = 0,
-            expectedEvent = pointerEventOf(expectedDownChange),
-            expectedPass = PointerEventPass.Initial
-        )
-        parentLog.verifyOnPointerEventCall(
-            index = 1,
-            expectedEvent = pointerEventOf(expectedDownChange),
-            expectedPass = PointerEventPass.Main
-        )
-        parentLog.verifyOnPointerEventCall(
-            index = 2,
-            expectedEvent = pointerEventOf(expectedDownChange),
-            expectedPass = PointerEventPass.Final
-        )
-        parentLog.verifyOnPointerEventCall(
-            index = 3,
-            expectedEvent = pointerEventOf(expectedUpChange),
-            expectedPass = PointerEventPass.Initial
-        )
-        parentLog.verifyOnPointerEventCall(
-            index = 4,
-            expectedEvent = pointerEventOf(expectedUpChange),
-            expectedPass = PointerEventPass.Main
-        )
-        parentLog.verifyOnPointerEventCall(
-            index = 5,
-            expectedEvent = pointerEventOf(expectedUpChange),
-            expectedPass = PointerEventPass.Final
-        )
-
-        childLog.verifyOnPointerEventCall(
-            index = 0,
-            expectedEvent = pointerEventOf(expectedDownChange),
-            expectedPass = PointerEventPass.Initial
-        )
-        childLog.verifyOnPointerEventCall(
-            index = 1,
-            expectedEvent = pointerEventOf(expectedDownChange),
-            expectedPass = PointerEventPass.Main
-        )
-        childLog.verifyOnPointerEventCall(
-            index = 2,
-            expectedEvent = pointerEventOf(expectedDownChange),
-            expectedPass = PointerEventPass.Final
-        )
-    }
-
-    @Test
-    fun process_layoutNodeRemovedDuringInput_cancelDispatchedToCorrectPointerInputModifierImpl2() {
-
-        // Arrange
-
-        val childPointerInputFilter = PointerInputFilterMock()
-        val childLayoutNode = LayoutNode(
-            0, 0, 100, 100,
-            PointerInputModifierImpl2(childPointerInputFilter)
-        )
-
-        val parentPointerInputFilter = PointerInputFilterMock()
-        val parentLayoutNode: LayoutNode = LayoutNode(
-            0, 0, 100, 100,
-            PointerInputModifierImpl2(parentPointerInputFilter)
-        ).apply {
-            insertAt(0, childLayoutNode)
-        }
-
-        addToRoot(parentLayoutNode)
-
-        val down =
-            PointerInputEvent(0, 7, Offset(50f, 50f), true)
-
-        val up = PointerInputEvent(0, 11, Offset(50f, 50f), false)
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-        parentLayoutNode.removeAt(0, 1)
-        pointerInputEventProcessor.process(up)
-
-        // Assert
-        assertThat(childPointerInputFilter.log.getOnCancelFilterLog()).hasSize(1)
-        assertThat(parentPointerInputFilter.log.getOnCancelFilterLog()).hasSize(0)
-    }
-
-    @Test
-    fun process_pointerInputModifierRemovedDuringInput_correctPointerInputChangesReceived() {
-
-        // Arrange
-
-        val childPointerInputFilter = PointerInputFilterMock()
-        val childLayoutNode = LayoutNode(
-            0, 0, 100, 100,
-            PointerInputModifierImpl2(
-                childPointerInputFilter
-            )
-        )
-
-        val parentPointerInputFilter = PointerInputFilterMock()
-        val parentLayoutNode: LayoutNode = LayoutNode(
-            0, 0, 100, 100,
-            PointerInputModifierImpl2(
-                parentPointerInputFilter
-            )
-        ).apply {
-            insertAt(0, childLayoutNode)
-        }
-
-        addToRoot(parentLayoutNode)
-
-        val offset = Offset(50f, 50f)
-
-        val down = PointerInputEvent(0, 7, offset, true)
-        val up = PointerInputEvent(0, 11, offset, false)
-
-        val expectedDownChange =
-            PointerInputChange(
-                id = PointerId(0),
-                7,
-                offset,
-                true,
-                7,
-                offset,
-                false,
-                isInitiallyConsumed = false
-            )
-
-        val expectedUpChange =
-            PointerInputChange(
-                id = PointerId(0),
-                11,
-                offset,
-                false,
-                7,
-                offset,
-                true,
-                isInitiallyConsumed = false
-            )
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-        childLayoutNode.modifier = Modifier
-        pointerInputEventProcessor.process(up)
-
-        // Assert
-
-        val parentLog = parentPointerInputFilter.log.getOnPointerEventFilterLog()
-        val childLog = childPointerInputFilter.log.getOnPointerEventFilterLog()
-
-        // Verify call count
-        assertThat(parentLog).hasSize(PointerEventPass.values().size * 2)
-        assertThat(childLog).hasSize(PointerEventPass.values().size)
-
-        // Verify call values
-
-        parentLog.verifyOnPointerEventCall(
-            index = 0,
-            expectedEvent = pointerEventOf(expectedDownChange),
-            expectedPass = PointerEventPass.Initial
-        )
-        parentLog.verifyOnPointerEventCall(
-            index = 1,
-            expectedEvent = pointerEventOf(expectedDownChange),
-            expectedPass = PointerEventPass.Main
-        )
-        parentLog.verifyOnPointerEventCall(
-            index = 2,
-            expectedEvent = pointerEventOf(expectedDownChange),
-            expectedPass = PointerEventPass.Final
-        )
-        parentLog.verifyOnPointerEventCall(
-            index = 3,
-            expectedEvent = pointerEventOf(expectedUpChange),
-            expectedPass = PointerEventPass.Initial
-        )
-        parentLog.verifyOnPointerEventCall(
-            index = 4,
-            expectedEvent = pointerEventOf(expectedUpChange),
-            expectedPass = PointerEventPass.Main
-        )
-        parentLog.verifyOnPointerEventCall(
-            index = 5,
-            expectedEvent = pointerEventOf(expectedUpChange),
-            expectedPass = PointerEventPass.Final
-        )
-
-        childLog.verifyOnPointerEventCall(
-            index = 0,
-            expectedEvent = pointerEventOf(expectedDownChange),
-            expectedPass = PointerEventPass.Initial
-        )
-        childLog.verifyOnPointerEventCall(
-            index = 1,
-            expectedEvent = pointerEventOf(expectedDownChange),
-            expectedPass = PointerEventPass.Main
-        )
-        childLog.verifyOnPointerEventCall(
-            index = 2,
-            expectedEvent = pointerEventOf(expectedDownChange),
-            expectedPass = PointerEventPass.Final
-        )
-    }
-
-    @Test
-    fun process_pointerInputModifierRemovedDuringInput_cancelDispatchedToCorrectPim() {
-
-        // Arrange
-
-        val childPointerInputFilter = PointerInputFilterMock()
-        val childLayoutNode = LayoutNode(
-            0, 0, 100, 100,
-            PointerInputModifierImpl2(childPointerInputFilter)
-        )
-
-        val parentPointerInputFilter = PointerInputFilterMock()
-        val parentLayoutNode: LayoutNode = LayoutNode(
-            0, 0, 100, 100,
-            PointerInputModifierImpl2(parentPointerInputFilter)
-        ).apply {
-            insertAt(0, childLayoutNode)
-        }
-
-        addToRoot(parentLayoutNode)
-
-        val down =
-            PointerInputEvent(0, 7, Offset(50f, 50f), true)
-
-        val up =
-            PointerInputEvent(0, 11, Offset(50f, 50f), false)
-
-        // Act
-
-        pointerInputEventProcessor.process(down)
-        childLayoutNode.modifier = Modifier
-        pointerInputEventProcessor.process(up)
-
-        // Assert
-        assertThat(childPointerInputFilter.log.getOnCancelFilterLog()).hasSize(1)
-        assertThat(parentPointerInputFilter.log.getOnCancelFilterLog()).hasSize(0)
-    }
-
-    @Test
-    fun process_downNoPointerInputModifiers_nothingInteractedWithAndNoMovementConsumed() {
-        val pointerInputEvent =
-            PointerInputEvent(0, 7, Offset(0f, 0f), true)
-
-        val result: ProcessResult = pointerInputEventProcessor.process(pointerInputEvent)
-
-        assertThat(result).isEqualTo(
-            ProcessResult(
-                dispatchedToAPointerInputModifier = false,
-                anyMovementConsumed = false
-            )
-        )
-    }
-
-    @Test
-    fun process_downNoPointerInputModifiersHit_nothingInteractedWithAndNoMovementConsumed() {
-
-        // Arrange
-
-        val pointerInputFilter = PointerInputFilterMock()
-
-        val layoutNode = LayoutNode(
-            0, 0, 1, 1,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-
-        addToRoot(layoutNode)
-
-        val offsets =
-            listOf(
-                Offset(-1f, 0f),
-                Offset(0f, -1f),
-                Offset(1f, 0f),
-                Offset(0f, 1f)
-            )
-        val pointerInputEvent =
-            PointerInputEvent(
-                11,
-                (offsets.indices).map {
-                    PointerInputEventData(it, 11, offsets[it], true)
-                }
-            )
-
-        // Act
-
-        val result: ProcessResult = pointerInputEventProcessor.process(pointerInputEvent)
-
-        // Assert
-
-        assertThat(result).isEqualTo(
-            ProcessResult(
-                dispatchedToAPointerInputModifier = false,
-                anyMovementConsumed = false
-            )
-        )
-    }
-
-    @Test
-    fun process_downPointerInputModifierHit_somethingInteractedWithAndNoMovementConsumed() {
-
-        // Arrange
-
-        val pointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            0, 0, 1, 1,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-        addToRoot(layoutNode)
-        val pointerInputEvent =
-            PointerInputEvent(0, 11, Offset(0f, 0f), true)
-
-        // Act
-
-        val result = pointerInputEventProcessor.process(pointerInputEvent)
-
-        // Assert
-
-        assertThat(result).isEqualTo(
-            ProcessResult(
-                dispatchedToAPointerInputModifier = true,
-                anyMovementConsumed = false
-            )
-        )
-    }
-
-    @Test
-    fun process_downHitsPifRemovedPointerMoves_nothingInteractedWithAndNoMovementConsumed() {
-
-        // Arrange
-
-        val pointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            0, 0, 1, 1,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-        addToRoot(layoutNode)
-        val down = PointerInputEvent(0, 11, Offset(0f, 0f), true)
-        pointerInputEventProcessor.process(down)
-        val move = PointerInputEvent(0, 11, Offset(1f, 0f), true)
-
-        // Act
-
-        testOwner.root.removeAt(0, 1)
-        val result = pointerInputEventProcessor.process(move)
-
-        // Assert
-
-        assertThat(result).isEqualTo(
-            ProcessResult(
-                dispatchedToAPointerInputModifier = false,
-                anyMovementConsumed = false
-            )
-        )
-    }
-
-    @Test
-    fun process_downHitsPointerMovesNothingConsumed_somethingInteractedWithAndNoMovementConsumed() {
-
-        // Arrange
-
-        val pointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            0, 0, 1, 1,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-        addToRoot(layoutNode)
-        val down = PointerInputEvent(0, 11, Offset(0f, 0f), true)
-        pointerInputEventProcessor.process(down)
-        val move = PointerInputEvent(0, 11, Offset(1f, 0f), true)
-
-        // Act
-
-        val result = pointerInputEventProcessor.process(move)
-
-        // Assert
-
-        assertThat(result).isEqualTo(
-            ProcessResult(
-                dispatchedToAPointerInputModifier = true,
-                anyMovementConsumed = false
-            )
-        )
-    }
-
-    @Test
-    fun process_downHitsPointerMovementConsumed_somethingInteractedWithAndMovementConsumed() {
-
-        // Arrange
-
-        val pointerInputFilter: PointerInputFilter =
-            PointerInputFilterMock(
-                pointerEventHandler = { pointerEvent, pass, _ ->
-                    if (pass == PointerEventPass.Initial) {
-                        pointerEvent.changes.forEach {
-                            if (it.positionChange() != Offset.Zero) it.consume()
-                        }
-                    }
-                }
-            )
-
-        val layoutNode = LayoutNode(
-            0, 0, 1, 1,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-        addToRoot(layoutNode)
-        val down = PointerInputEvent(0, 11, Offset(0f, 0f), true)
-        pointerInputEventProcessor.process(down)
-        val move = PointerInputEvent(0, 11, Offset(1f, 0f), true)
-
-        // Act
-
-        val result = pointerInputEventProcessor.process(move)
-
-        // Assert
-
-        assertThat(result).isEqualTo(
-            ProcessResult(
-                dispatchedToAPointerInputModifier = true,
-                anyMovementConsumed = true
-            )
-        )
-    }
-
-    @Test
-    fun processResult_trueTrue_propValuesAreCorrect() {
-        val processResult1 = ProcessResult(
-            dispatchedToAPointerInputModifier = true,
-            anyMovementConsumed = true
-        )
-        assertThat(processResult1.dispatchedToAPointerInputModifier).isTrue()
-        assertThat(processResult1.anyMovementConsumed).isTrue()
-    }
-
-    @Test
-    fun processResult_trueFalse_propValuesAreCorrect() {
-        val processResult1 = ProcessResult(
-            dispatchedToAPointerInputModifier = true,
-            anyMovementConsumed = false
-        )
-        assertThat(processResult1.dispatchedToAPointerInputModifier).isTrue()
-        assertThat(processResult1.anyMovementConsumed).isFalse()
-    }
-
-    @Test
-    fun processResult_falseTrue_propValuesAreCorrect() {
-        val processResult1 = ProcessResult(
-            dispatchedToAPointerInputModifier = false,
-            anyMovementConsumed = true
-        )
-        assertThat(processResult1.dispatchedToAPointerInputModifier).isFalse()
-        assertThat(processResult1.anyMovementConsumed).isTrue()
-    }
-
-    @Test
-    fun processResult_falseFalse_propValuesAreCorrect() {
-        val processResult1 = ProcessResult(
-            dispatchedToAPointerInputModifier = false,
-            anyMovementConsumed = false
-        )
-        assertThat(processResult1.dispatchedToAPointerInputModifier).isFalse()
-        assertThat(processResult1.anyMovementConsumed).isFalse()
-    }
-
-    @Test
-    fun buttonsPressed() {
-        // Arrange
-        val pointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            0,
-            0,
-            500,
-            500,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-        addToRoot(layoutNode)
-
-        class ButtonValidation(
-            vararg pressedValues: Int,
-            val primary: Boolean = false,
-            val secondary: Boolean = false,
-            val tertiary: Boolean = false,
-            val back: Boolean = false,
-            val forward: Boolean = false,
-            val anyPressed: Boolean = true,
-        ) {
-            val pressedValues = pressedValues
-        }
-
-        val buttonCheckerMap = mapOf(
-            MotionEvent.BUTTON_PRIMARY to ButtonValidation(0, primary = true),
-            MotionEvent.BUTTON_SECONDARY to ButtonValidation(1, secondary = true),
-            MotionEvent.BUTTON_TERTIARY to ButtonValidation(2, tertiary = true),
-            MotionEvent.BUTTON_STYLUS_PRIMARY to ButtonValidation(0, primary = true),
-            MotionEvent.BUTTON_STYLUS_SECONDARY to ButtonValidation(1, secondary = true),
-            MotionEvent.BUTTON_BACK to ButtonValidation(3, back = true),
-            MotionEvent.BUTTON_FORWARD to ButtonValidation(4, forward = true),
-            MotionEvent.BUTTON_PRIMARY or MotionEvent.BUTTON_TERTIARY to
-                ButtonValidation(0, 2, primary = true, tertiary = true),
-            MotionEvent.BUTTON_BACK or MotionEvent.BUTTON_STYLUS_PRIMARY to
-                ButtonValidation(0, 3, primary = true, back = true),
-            0 to ButtonValidation(anyPressed = false)
-        )
-
-        for (entry in buttonCheckerMap) {
-            val buttonState = entry.key
-            val validator = entry.value
-            val event = PointerInputEvent(
-                0,
-                listOf(PointerInputEventData(0, 0L, Offset.Zero, true)),
-                MotionEvent.obtain(
-                    0L,
-                    0L,
-                    MotionEvent.ACTION_DOWN,
-                    1,
-                    arrayOf(PointerProperties(1, MotionEvent.TOOL_TYPE_MOUSE)),
-                    arrayOf(PointerCoords(0f, 0f)),
-                    0,
-                    buttonState,
-                    0.1f,
-                    0.1f,
-                    0,
-                    0,
-                    InputDevice.SOURCE_MOUSE,
-                    0
-                )
-            )
-            pointerInputEventProcessor.process(event)
-
-            with(
-                (pointerInputFilter.log.last() as OnPointerEventFilterEntry).pointerEvent.buttons
-            ) {
-                assertThat(isPrimaryPressed).isEqualTo(validator.primary)
-                assertThat(isSecondaryPressed).isEqualTo(validator.secondary)
-                assertThat(isTertiaryPressed).isEqualTo(validator.tertiary)
-                assertThat(isBackPressed).isEqualTo(validator.back)
-                assertThat(isForwardPressed).isEqualTo(validator.forward)
-                assertThat(areAnyPressed).isEqualTo(validator.anyPressed)
-                val firstIndex = validator.pressedValues.firstOrNull() ?: -1
-                val lastIndex = validator.pressedValues.lastOrNull() ?: -1
-                assertThat(indexOfFirstPressed()).isEqualTo(firstIndex)
-                assertThat(indexOfLastPressed()).isEqualTo(lastIndex)
-                for (i in 0..10) {
-                    assertThat(isPressed(i)).isEqualTo(validator.pressedValues.contains(i))
-                }
-            }
-        }
-    }
-
-    @Test
-    fun metaState() {
-        // Arrange
-        val pointerInputFilter = PointerInputFilterMock()
-        val layoutNode = LayoutNode(
-            0,
-            0,
-            500,
-            500,
-            PointerInputModifierImpl2(
-                pointerInputFilter
-            )
-        )
-        addToRoot(layoutNode)
-
-        class MetaValidation(
-            val control: Boolean = false,
-            val meta: Boolean = false,
-            val alt: Boolean = false,
-            val shift: Boolean = false,
-            val sym: Boolean = false,
-            val function: Boolean = false,
-            val capsLock: Boolean = false,
-            val scrollLock: Boolean = false,
-            val numLock: Boolean = false
-        )
-
-        val buttonCheckerMap = mapOf(
-            AndroidKeyEvent.META_CTRL_ON to MetaValidation(control = true),
-            AndroidKeyEvent.META_META_ON to MetaValidation(meta = true),
-            AndroidKeyEvent.META_ALT_ON to MetaValidation(alt = true),
-            AndroidKeyEvent.META_SYM_ON to MetaValidation(sym = true),
-            AndroidKeyEvent.META_SHIFT_ON to MetaValidation(shift = true),
-            AndroidKeyEvent.META_FUNCTION_ON to MetaValidation(function = true),
-            AndroidKeyEvent.META_CAPS_LOCK_ON to MetaValidation(capsLock = true),
-            AndroidKeyEvent.META_SCROLL_LOCK_ON to MetaValidation(scrollLock = true),
-            AndroidKeyEvent.META_NUM_LOCK_ON to MetaValidation(numLock = true),
-            AndroidKeyEvent.META_CTRL_ON or AndroidKeyEvent.META_SHIFT_ON or
-                AndroidKeyEvent.META_NUM_LOCK_ON to
-                MetaValidation(control = true, shift = true, numLock = true),
-            0 to MetaValidation(),
-        )
-
-        for (entry in buttonCheckerMap) {
-            val metaState = entry.key
-            val validator = entry.value
-            val event = PointerInputEvent(
-                0,
-                listOf(PointerInputEventData(0, 0L, Offset.Zero, true)),
-                MotionEvent.obtain(
-                    0L,
-                    0L,
-                    MotionEvent.ACTION_DOWN,
-                    1,
-                    arrayOf(PointerProperties(1, MotionEvent.TOOL_TYPE_MOUSE)),
-                    arrayOf(PointerCoords(0f, 0f)),
-                    metaState,
-                    0,
-                    0.1f,
-                    0.1f,
-                    0,
-                    0,
-                    InputDevice.SOURCE_MOUSE,
-                    0
-                )
-            )
-            pointerInputEventProcessor.process(event)
-
-            val keyboardModifiers = (pointerInputFilter.log.last() as OnPointerEventFilterEntry)
-                .pointerEvent.keyboardModifiers
-            with(keyboardModifiers) {
-                assertThat(isCtrlPressed).isEqualTo(validator.control)
-                assertThat(isMetaPressed).isEqualTo(validator.meta)
-                assertThat(isAltPressed).isEqualTo(validator.alt)
-                assertThat(isAltGraphPressed).isFalse()
-                assertThat(isSymPressed).isEqualTo(validator.sym)
-                assertThat(isShiftPressed).isEqualTo(validator.shift)
-                assertThat(isFunctionPressed).isEqualTo(validator.function)
-                assertThat(isCapsLockOn).isEqualTo(validator.capsLock)
-                assertThat(isScrollLockOn).isEqualTo(validator.scrollLock)
-                assertThat(isNumLockOn).isEqualTo(validator.numLock)
-            }
-        }
-    }
-
-    private fun PointerInputEventProcessor.process(event: PointerInputEvent) =
-        process(event, positionCalculator)
-}
-
-private class PointerInputModifierImpl2(override val pointerInputFilter: PointerInputFilter) :
-    PointerInputModifier
-
-internal fun LayoutNode(x: Int, y: Int, x2: Int, y2: Int, modifier: Modifier = Modifier) =
-    LayoutNode().apply {
-        this.modifier = Modifier
-            .layout { measurable, constraints ->
-                val placeable = measurable.measure(constraints)
-                layout(placeable.width, placeable.height) {
-                    placeable.place(x, y)
-                }
-            }
-            .then(modifier)
-        measurePolicy = object : LayoutNode.NoIntrinsicsMeasurePolicy("not supported") {
-            override fun MeasureScope.measure(
-                measurables: List<Measurable>,
-                constraints: Constraints
-            ): MeasureResult =
-                innerCoordinator.layout(x2 - x, y2 - y) {
-                    measurables.forEach { it.measure(constraints).place(0, 0) }
-                }
-        }
-    }
-
-@OptIn(ExperimentalComposeUiApi::class, InternalCoreApi::class)
-private class TestOwner : Owner {
-    val onEndListeners = mutableListOf<() -> Unit>()
-    var position: IntOffset = IntOffset.Zero
-    override val root = LayoutNode(0, 0, 500, 500)
-
-    private val delegate = MeasureAndLayoutDelegate(root)
-
-    init {
-        root.attach(this)
-        delegate.updateRootConstraints(Constraints(maxWidth = 500, maxHeight = 500))
-    }
-
-    override fun requestFocus(): Boolean = false
-    override val rootForTest: RootForTest
-        get() = TODO("Not yet implemented")
-    override val hapticFeedBack: HapticFeedback
-        get() = TODO("Not yet implemented")
-    override val inputModeManager: InputModeManager
-        get() = TODO("Not yet implemented")
-    override val clipboardManager: ClipboardManager
-        get() = TODO("Not yet implemented")
-    override val accessibilityManager: AccessibilityManager
-        get() = TODO("Not yet implemented")
-    override val textToolbar: TextToolbar
-        get() = TODO("Not yet implemented")
-    override val autofillTree: AutofillTree
-        get() = TODO("Not yet implemented")
-    override val autofill: Autofill?
-        get() = null
-    override val density: Density
-        get() = Density(1f)
-    override val textInputService: TextInputService
-        get() = TODO("Not yet implemented")
-
-    override suspend fun textInputSession(
-        session: suspend PlatformTextInputSessionScope.() -> Nothing
-    ): Nothing {
-        TODO("Not yet implemented")
-    }
-
-    override val pointerIconService: PointerIconService
-        get() = TODO("Not yet implemented")
-    override val focusOwner: FocusOwner
-        get() = TODO("Not yet implemented")
-    override val windowInfo: WindowInfo
-        get() = TODO("Not yet implemented")
-
-    @Deprecated(
-        "fontLoader is deprecated, use fontFamilyResolver",
-        replaceWith = ReplaceWith("fontFamilyResolver")
-    )
-    @Suppress("OverridingDeprecatedMember", "DEPRECATION")
-    override val fontLoader: Font.ResourceLoader
-        get() = TODO("Not yet implemented")
-    override val fontFamilyResolver: FontFamily.Resolver
-        get() = TODO("Not yet implemented")
-    override val layoutDirection: LayoutDirection
-        get() = LayoutDirection.Ltr
-    override var showLayoutBounds: Boolean
-        get() = false
-        set(@Suppress("UNUSED_PARAMETER") value) {}
-
-    override fun onRequestMeasure(
-        layoutNode: LayoutNode,
-        affectsLookahead: Boolean,
-        forceRequest: Boolean,
-        scheduleMeasureAndLayout: Boolean
-    ) {
-        if (affectsLookahead) {
-            delegate.requestLookaheadRemeasure(layoutNode)
-        } else {
-            delegate.requestRemeasure(layoutNode)
-        }
-    }
-
-    override fun onRequestRelayout(
-        layoutNode: LayoutNode,
-        affectsLookahead: Boolean,
-        forceRequest: Boolean
-    ) {
-        if (affectsLookahead) {
-            delegate.requestLookaheadRelayout(layoutNode)
-        } else {
-            delegate.requestRelayout(layoutNode)
-        }
-    }
-
-    override fun requestOnPositionedCallback(layoutNode: LayoutNode) {
-        TODO("Not yet implemented")
-    }
-
-    override fun onAttach(node: LayoutNode) {
-    }
-
-    override fun onDetach(node: LayoutNode) {
-    }
-
-    override fun calculatePositionInWindow(localPosition: Offset): Offset =
-        localPosition + position.toOffset()
-
-    override fun calculateLocalPosition(positionInWindow: Offset): Offset =
-        positionInWindow - position.toOffset()
-
-    override fun measureAndLayout(sendPointerUpdate: Boolean) {
-        delegate.measureAndLayout()
-    }
-
-    override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
-        delegate.measureAndLayout(layoutNode, constraints)
-    }
-
-    override fun forceMeasureTheSubtree(layoutNode: LayoutNode, affectsLookahead: Boolean) {
-        delegate.forceMeasureTheSubtree(layoutNode, affectsLookahead)
-    }
-
-    override fun createLayer(
-        drawBlock: (Canvas) -> Unit,
-        invalidateParentLayer: () -> Unit
-    ): OwnedLayer {
-        TODO("Not yet implemented")
-    }
-
-    override fun onSemanticsChange() {
-    }
-
-    override fun onLayoutChange(layoutNode: LayoutNode) {
-    }
-
-    override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
-        TODO("Not yet implemented")
-    }
-
-    override val measureIteration: Long
-        get() = 0
-
-    override val viewConfiguration: ViewConfiguration
-        get() = TODO("Not yet implemented")
-    override val snapshotObserver = OwnerSnapshotObserver { it.invoke() }
-    override val modifierLocalManager: ModifierLocalManager = ModifierLocalManager(this)
-
-    override val coroutineContext: CoroutineContext =
-        Executors.newFixedThreadPool(3).asCoroutineDispatcher()
-    override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
-        onEndListeners += listener
-    }
-
-    override fun onEndApplyChanges() {
-        while (onEndListeners.isNotEmpty()) {
-            onEndListeners.removeAt(0).invoke()
-        }
-    }
-
-    override fun drag(dragAndDropInfo: DragAndDropInfo): Boolean {
-        TODO("Not yet implemented")
-    }
-
-    override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
-        TODO("Not yet implemented")
-    }
-
-    override val sharedDrawScope = LayoutNodeDrawScope()
-}
-
-private fun List<LogEntry>.verifyOnPointerEventCall(
-    index: Int,
-    expectedPif: PointerInputFilter? = null,
-    expectedEvent: PointerEvent,
-    expectedPass: PointerEventPass,
-    expectedBounds: IntSize? = null
-) {
-    val logEntry = this[index]
-    assertThat(logEntry).isInstanceOf(OnPointerEventFilterEntry::class.java)
-    val entry = logEntry as OnPointerEventFilterEntry
-    if (expectedPif != null) {
-        assertThat(entry.pointerInputFilter).isSameInstanceAs(expectedPif)
-    }
-    PointerEventSubject
-        .assertThat(entry.pointerEvent)
-        .isStructurallyEqualTo(expectedEvent)
-    assertThat(entry.pass).isEqualTo(expectedPass)
-    if (expectedBounds != null) {
-        assertThat(entry.bounds).isEqualTo(expectedBounds)
-    }
-}
-
-private fun List<LogEntry>.verifyOnCancelCall(
-    index: Int
-) {
-    val logEntry = this[index]
-    assertThat(logEntry).isInstanceOf(OnCancelFilterEntry::class.java)
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
deleted file mode 100644
index ffe8b60..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ /dev/null
@@ -1,1775 +0,0 @@
-/*
- * Copyright 2020 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.viewinterop
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.Paint
-import android.os.Build
-import android.os.Bundle
-import android.os.Parcelable
-import android.util.DisplayMetrics
-import android.util.TypedValue
-import android.view.LayoutInflater
-import android.view.SurfaceView
-import android.view.View
-import android.view.View.OnAttachStateChangeListener
-import android.view.ViewGroup
-import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import android.view.accessibility.AccessibilityNodeInfo
-import android.widget.EditText
-import android.widget.FrameLayout
-import android.widget.RelativeLayout
-import android.widget.TextView
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.DisallowComposableCalls
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.ReusableContent
-import androidx.compose.runtime.ReusableContentHost
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.compositionLocalOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.movableContentOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveableStateHolder
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.withFrameNanos
-import androidx.compose.testutils.assertPixels
-import androidx.compose.ui.AbsoluteAlignment
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalLifecycleOwner
-import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
-import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.platform.findViewTreeCompositionContext
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.TestActivity
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.tests.R
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnCreate
-import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnRelease
-import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnReset
-import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnUpdate
-import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnViewAttach
-import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnViewDetach
-import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.ViewLifecycleEvent
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.Lifecycle.Event.ON_CREATE
-import androidx.lifecycle.Lifecycle.Event.ON_PAUSE
-import androidx.lifecycle.Lifecycle.Event.ON_RESUME
-import androidx.lifecycle.Lifecycle.Event.ON_START
-import androidx.lifecycle.Lifecycle.Event.ON_STOP
-import androidx.lifecycle.LifecycleEventObserver
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.findViewTreeLifecycleOwner
-import androidx.lifecycle.testing.TestLifecycleOwner
-import androidx.savedstate.SavedStateRegistry
-import androidx.savedstate.SavedStateRegistryOwner
-import androidx.savedstate.findViewTreeSavedStateRegistryOwner
-import androidx.test.espresso.Espresso
-import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.action.ViewActions.typeText
-import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
-import androidx.test.espresso.assertion.ViewAssertions.matches
-import androidx.test.espresso.matcher.ViewMatchers.Visibility
-import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
-import androidx.test.espresso.matcher.ViewMatchers.withClassName
-import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
-import androidx.test.espresso.matcher.ViewMatchers.withText
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth.assertThat
-import kotlin.math.roundToInt
-import kotlin.test.assertIs
-import org.hamcrest.CoreMatchers.endsWith
-import org.hamcrest.CoreMatchers.equalTo
-import org.hamcrest.CoreMatchers.instanceOf
-import org.junit.Assert.assertEquals
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalComposeUiApi::class)
-class AndroidViewTest {
-    @get:Rule
-    val rule = createAndroidComposeRule<TestActivity>()
-
-    @Test
-    fun androidViewWithConstructor() {
-        rule.setContent {
-            AndroidView({ TextView(it).apply { text = "Test" } })
-        }
-        Espresso
-            .onView(instanceOf(TextView::class.java))
-            .check(matches(isDisplayed()))
-    }
-
-    @Test
-    fun androidViewWithResourceTest() {
-        rule.setContent {
-            AndroidView({ LayoutInflater.from(it).inflate(R.layout.test_layout, null) })
-        }
-        Espresso
-            .onView(instanceOf(RelativeLayout::class.java))
-            .check(matches(isDisplayed()))
-    }
-
-    @Test
-    fun androidViewInvalidatingDuringDrawTest() {
-        var drawCount = 0
-        val timesToInvalidate = 10
-        var customView: InvalidatedTextView? = null
-        rule.setContent {
-            AndroidView(
-                factory = {
-                    val view: View = LayoutInflater.from(it)
-                        .inflate(R.layout.test_multiple_invalidation_layout, null)
-                    customView = view.findViewById<InvalidatedTextView>(R.id.custom_draw_view)
-                    customView!!.timesToInvalidate = timesToInvalidate
-                    view.viewTreeObserver?.addOnPreDrawListener {
-                        ++drawCount
-                        true
-                    }
-                    view
-                })
-        }
-        // the first drawn was not caused by invalidation, thus add it to expected draw count.
-        var expectedDraws = timesToInvalidate + 1
-        repeat(expectedDraws) {
-            rule.mainClock.advanceTimeByFrame()
-        }
-
-        // Ensure we wait until the time advancement actually happened as sometimes we can race if
-        // we use runOnIdle directly making the test fail, so providing a big enough timeout to
-        // give plenty of time for the frame advancement to happen.
-        rule.waitUntil(3000) {
-            drawCount == expectedDraws
-        }
-
-        rule.runOnIdle {
-            // Verify that we only drew once per invalidation
-            assertThat(drawCount).isEqualTo(expectedDraws)
-            assertThat(drawCount).isEqualTo(customView!!.timesDrawn)
-        }
-    }
-
-    @Test
-    fun androidViewWithViewTest() {
-        lateinit var frameLayout: FrameLayout
-        rule.activityRule.scenario.onActivity { activity ->
-            frameLayout = FrameLayout(activity).apply {
-                layoutParams = ViewGroup.LayoutParams(300, 300)
-            }
-        }
-        rule.setContent {
-            AndroidView({ frameLayout })
-        }
-        Espresso
-            .onView(equalTo(frameLayout))
-            .check(matches(isDisplayed()))
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
-    fun androidViewAccessibilityDelegate() {
-        rule.setContent {
-             AndroidView({ TextView(it).apply { text = "Test"; setScreenReaderFocusable(true) } })
-        }
-        Espresso
-            .onView(instanceOf(TextView::class.java))
-            .check(matches(isDisplayed()))
-            .check { view, exception ->
-                val viewParent = view.getParent()
-                if (viewParent !is View) {
-                    throw exception
-                }
-                val delegate = viewParent.getAccessibilityDelegate()
-                if (viewParent.getAccessibilityDelegate() == null) {
-                    throw exception
-                }
-                val info: AccessibilityNodeInfo = AccessibilityNodeInfo()
-                delegate.onInitializeAccessibilityNodeInfo(view, info)
-                if (!info.isScreenReaderFocusable()) {
-                    throw exception
-                }
-            }
-    }
-
-    @Test
-    fun androidViewWithResourceTest_preservesLayoutParams() {
-        rule.setContent {
-            AndroidView({
-                LayoutInflater.from(it).inflate(R.layout.test_layout, FrameLayout(it), false)
-            })
-        }
-        Espresso
-            .onView(withClassName(endsWith("RelativeLayout")))
-            .check(matches(isDisplayed()))
-            .check { view, exception ->
-                if (view.layoutParams.width != 300.dp.toPx(view.context.resources.displayMetrics)) {
-                    throw exception
-                }
-                if (view.layoutParams.height != WRAP_CONTENT) {
-                    throw exception
-                }
-            }
-    }
-
-    @Test
-    fun androidViewProperlyDetached() {
-        lateinit var frameLayout: FrameLayout
-        rule.activityRule.scenario.onActivity { activity ->
-            frameLayout = FrameLayout(activity).apply {
-                layoutParams = ViewGroup.LayoutParams(300, 300)
-            }
-        }
-        var emit by mutableStateOf(true)
-        rule.setContent {
-            if (emit) {
-                AndroidView({ frameLayout })
-            }
-        }
-
-        // Assert view initially attached
-        rule.runOnUiThread {
-            assertThat(frameLayout.parent).isNotNull()
-            emit = false
-        }
-
-        // Assert view detached when removed from composition hierarchy
-        rule.runOnIdle {
-            assertThat(frameLayout.parent).isNull()
-            emit = true
-        }
-
-        // Assert view reattached when added back to the composition hierarchy
-        rule.runOnIdle {
-            assertThat(frameLayout.parent).isNotNull()
-        }
-    }
-
-    @Test
-    @LargeTest
-    fun androidView_attachedAfterDetached_addsViewBack() {
-        lateinit var root: FrameLayout
-        lateinit var composeView: ComposeView
-        lateinit var viewInsideCompose: View
-        rule.activityRule.scenario.onActivity { activity ->
-            root = FrameLayout(activity)
-            composeView = ComposeView(activity)
-            composeView.setViewCompositionStrategy(
-                ViewCompositionStrategy.DisposeOnLifecycleDestroyed(activity)
-            )
-            viewInsideCompose = View(activity)
-
-            activity.setContentView(root)
-            root.addView(composeView)
-            composeView.setContent {
-                AndroidView({ viewInsideCompose })
-            }
-        }
-
-        var viewInsideComposeHolder: ViewGroup? = null
-        rule.runOnUiThread {
-            assertThat(viewInsideCompose.parent).isNotNull()
-            viewInsideComposeHolder = viewInsideCompose.parent as ViewGroup
-            root.removeView(composeView)
-        }
-
-        rule.runOnIdle {
-            // Views don't detach from the parent when the parent is detached
-            assertThat(viewInsideCompose.parent).isNotNull()
-            assertThat(viewInsideComposeHolder?.childCount).isEqualTo(1)
-            root.addView(composeView)
-        }
-
-        rule.runOnIdle {
-            assertThat(viewInsideCompose.parent).isEqualTo(viewInsideComposeHolder)
-            assertThat(viewInsideComposeHolder?.childCount).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun androidViewWithResource_modifierIsApplied() {
-        val size = 20.dp
-        rule.setContent {
-            AndroidView(
-                { LayoutInflater.from(it).inflate(R.layout.test_layout, null) },
-                Modifier.requiredSize(size)
-            )
-        }
-        Espresso
-            .onView(instanceOf(RelativeLayout::class.java))
-            .check(matches(isDisplayed()))
-            .check { view, exception ->
-                val expectedSize = size.toPx(view.context.resources.displayMetrics)
-                if (view.width != expectedSize || view.height != expectedSize) {
-                    throw exception
-                }
-            }
-    }
-
-    @Test
-    fun androidViewWithView_modifierIsApplied() {
-        val size = 20.dp
-        lateinit var frameLayout: FrameLayout
-        rule.activityRule.scenario.onActivity { activity ->
-            frameLayout = FrameLayout(activity)
-        }
-        rule.setContent {
-            AndroidView({ frameLayout }, Modifier.requiredSize(size))
-        }
-
-        Espresso
-            .onView(equalTo(frameLayout))
-            .check(matches(isDisplayed()))
-            .check { view, exception ->
-                val expectedSize = size.toPx(view.context.resources.displayMetrics)
-                if (view.width != expectedSize || view.height != expectedSize) {
-                    throw exception
-                }
-            }
-    }
-
-    @Test
-    @LargeTest
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    fun androidViewWithView_drawModifierIsApplied() {
-        val size = 300
-        lateinit var frameLayout: FrameLayout
-        rule.activityRule.scenario.onActivity { activity ->
-            frameLayout = FrameLayout(activity).apply {
-                layoutParams = ViewGroup.LayoutParams(size, size)
-            }
-        }
-        rule.setContent {
-            AndroidView({ frameLayout },
-                Modifier
-                    .testTag("view")
-                    .background(color = Color.Blue))
-        }
-
-        rule.onNodeWithTag("view").captureToImage().assertPixels(IntSize(size, size)) {
-            Color.Blue
-        }
-    }
-
-    @Test
-    fun androidViewWithResource_modifierIsCorrectlyChanged() {
-        val size = mutableStateOf(20.dp)
-        rule.setContent {
-            AndroidView(
-                { LayoutInflater.from(it).inflate(R.layout.test_layout, null) },
-                Modifier.requiredSize(size.value)
-            )
-        }
-        Espresso
-            .onView(instanceOf(RelativeLayout::class.java))
-            .check(matches(isDisplayed()))
-            .check { view, exception ->
-                val expectedSize = size.value.toPx(view.context.resources.displayMetrics)
-                if (view.width != expectedSize || view.height != expectedSize) {
-                    throw exception
-                }
-            }
-        rule.runOnIdle { size.value = 30.dp }
-        Espresso
-            .onView(instanceOf(RelativeLayout::class.java))
-            .check(matches(isDisplayed()))
-            .check { view, exception ->
-                val expectedSize = size.value.toPx(view.context.resources.displayMetrics)
-                if (view.width != expectedSize || view.height != expectedSize) {
-                    throw exception
-                }
-            }
-    }
-
-    @Test
-    fun androidView_notDetachedFromWindowTwice() {
-        // Should not crash.
-        rule.setContent {
-            Box {
-                AndroidView(::ComposeView) {
-                    it.setContent {
-                        Box(Modifier)
-                    }
-                }
-            }
-        }
-    }
-
-    @Test
-    fun androidView_updateIsRanInitially() {
-        rule.setContent {
-            Box {
-                AndroidView(::UpdateTestView) { view ->
-                    view.counter = 1
-                }
-            }
-        }
-
-        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
-            assertIs<UpdateTestView>(view)
-            assertThat(view.counter).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun androidView_updateObservesMultipleStateChanges() {
-        var counter by mutableStateOf(1)
-
-        rule.setContent {
-            Box {
-                AndroidView(::UpdateTestView) { view ->
-                    view.counter = counter
-                }
-            }
-        }
-
-        counter = 2
-        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
-            assertIs<UpdateTestView>(view)
-            assertThat(view.counter).isEqualTo(counter)
-        }
-
-        counter = 3
-        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
-            assertIs<UpdateTestView>(view)
-            assertThat(view.counter).isEqualTo(counter)
-        }
-
-        counter = 4
-        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
-            assertIs<UpdateTestView>(view)
-            assertThat(view.counter).isEqualTo(counter)
-        }
-    }
-
-    @Test
-    fun androidView_updateObservesStateChanges_fromDisposableEffect() {
-        var counter by mutableStateOf(1)
-
-        rule.setContent {
-            DisposableEffect(Unit) {
-                counter = 2
-                onDispose {}
-            }
-
-            Box {
-                AndroidView(::UpdateTestView) { view ->
-                    view.counter = counter
-                }
-            }
-        }
-
-        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
-            assertIs<UpdateTestView>(view)
-            assertThat(view.counter).isEqualTo(2)
-        }
-    }
-
-    @Test
-    fun androidView_updateObservesStateChanges_fromLaunchedEffect() {
-        var counter by mutableStateOf(1)
-
-        rule.setContent {
-            LaunchedEffect(Unit) {
-                counter = 2
-            }
-
-            Box {
-                AndroidView(::UpdateTestView) { view ->
-                    view.counter = counter
-                }
-            }
-        }
-
-        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
-            assertIs<UpdateTestView>(view)
-            assertThat(view.counter).isEqualTo(2)
-        }
-    }
-
-    @Test
-    fun androidView_updateObservesMultipleStateChanges_fromEffect() {
-        var counter by mutableStateOf(1)
-
-        rule.setContent {
-            LaunchedEffect(Unit) {
-                counter = 2
-                withFrameNanos {
-                    counter = 3
-                }
-            }
-
-            Box {
-                AndroidView(::UpdateTestView) { view ->
-                    view.counter = counter
-                }
-            }
-        }
-
-        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
-            assertIs<UpdateTestView>(view)
-            assertThat(view.counter).isEqualTo(3)
-        }
-    }
-
-    @Test
-    fun androidView_updateObservesLayoutStateChanges() {
-        var size by mutableStateOf(20)
-        var obtainedSize: IntSize = IntSize.Zero
-        rule.setContent {
-            Box {
-                AndroidView(
-                    ::View,
-                    Modifier.onGloballyPositioned { obtainedSize = it.size }
-                ) { view ->
-                    view.layoutParams = ViewGroup.LayoutParams(size, size)
-                }
-            }
-        }
-        rule.runOnIdle {
-            assertThat(obtainedSize).isEqualTo(IntSize(size, size))
-            size = 40
-        }
-        rule.runOnIdle {
-            assertThat(obtainedSize).isEqualTo(IntSize(size, size))
-        }
-    }
-
-    @Test
-    fun androidView_propagatesDensity() {
-        rule.setContent {
-            val size = 50.dp
-            val density = Density(3f)
-            val sizeIpx = with(density) { size.roundToPx() }
-            CompositionLocalProvider(LocalDensity provides density) {
-                AndroidView(
-                    { FrameLayout(it) },
-                    Modifier
-                        .requiredSize(size)
-                        .onGloballyPositioned {
-                            assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
-                        }
-                )
-            }
-        }
-        rule.waitForIdle()
-    }
-
-    @Test
-    fun androidView_propagatesViewTreeCompositionContext() {
-        lateinit var parentComposeView: ComposeView
-        lateinit var compositionChildView: View
-        rule.activityRule.scenario.onActivity { activity ->
-            parentComposeView = ComposeView(activity).apply {
-                setContent {
-                    AndroidView(::View) {
-                        compositionChildView = it
-                    }
-                }
-                activity.setContentView(this)
-            }
-        }
-        rule.runOnIdle {
-            assertThat(compositionChildView.findViewTreeCompositionContext())
-                .isNotEqualTo(parentComposeView.findViewTreeCompositionContext())
-        }
-    }
-
-    @Test
-    fun androidView_propagatesLocalsToComposeViewChildren() {
-        val ambient = compositionLocalOf { "unset" }
-        var childComposedAmbientValue = "uncomposed"
-        rule.setContent {
-            CompositionLocalProvider(ambient provides "setByParent") {
-                AndroidView(
-                    factory = {
-                        ComposeView(it).apply {
-                            setContent {
-                                childComposedAmbientValue = ambient.current
-                            }
-                        }
-                    }
-                )
-            }
-        }
-        rule.runOnIdle {
-            assertThat(childComposedAmbientValue).isEqualTo("setByParent")
-        }
-    }
-
-    @Test
-    fun androidView_propagatesLayoutDirectionToComposeViewChildren() {
-        var childViewLayoutDirection: Int = Int.MIN_VALUE
-        var childCompositionLayoutDirection: LayoutDirection? = null
-        rule.setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                AndroidView(
-                    factory = {
-                        FrameLayout(it).apply {
-                            addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
-                                childViewLayoutDirection = layoutDirection
-                            }
-                            addView(
-                                ComposeView(it).apply {
-                                    // The view hierarchy's layout direction should always override
-                                    // the ambient layout direction from the parent composition.
-                                    layoutDirection = android.util.LayoutDirection.LTR
-                                    setContent {
-                                        childCompositionLayoutDirection =
-                                            LocalLayoutDirection.current
-                                    }
-                                },
-                                ViewGroup.LayoutParams(
-                                    ViewGroup.LayoutParams.MATCH_PARENT,
-                                    ViewGroup.LayoutParams.MATCH_PARENT
-                                )
-                            )
-                        }
-                    }
-                )
-            }
-        }
-        rule.runOnIdle {
-            assertThat(childViewLayoutDirection).isEqualTo(android.util.LayoutDirection.RTL)
-            assertThat(childCompositionLayoutDirection).isEqualTo(LayoutDirection.Ltr)
-        }
-    }
-
-    @Test
-    fun androidView_propagatesLocalLifecycleOwnerAsViewTreeOwner() {
-        lateinit var parentLifecycleOwner: LifecycleOwner
-        val compositionLifecycleOwner = TestLifecycleOwner()
-        var childViewTreeLifecycleOwner: LifecycleOwner? = null
-
-        rule.setContent {
-            LocalLifecycleOwner.current.also {
-                SideEffect {
-                    parentLifecycleOwner = it
-                }
-            }
-
-            CompositionLocalProvider(LocalLifecycleOwner provides compositionLifecycleOwner) {
-                AndroidView(
-                    factory = {
-                        object : FrameLayout(it) {
-                            override fun onAttachedToWindow() {
-                                super.onAttachedToWindow()
-                                childViewTreeLifecycleOwner = findViewTreeLifecycleOwner()
-                            }
-                        }
-                    }
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(childViewTreeLifecycleOwner).isSameInstanceAs(compositionLifecycleOwner)
-            assertThat(childViewTreeLifecycleOwner).isNotSameInstanceAs(parentLifecycleOwner)
-        }
-    }
-
-    @Test
-    fun androidView_propagatesLocalSavedStateRegistryOwnerAsViewTreeOwner() {
-        lateinit var parentSavedStateRegistryOwner: SavedStateRegistryOwner
-        val compositionSavedStateRegistryOwner =
-            object : SavedStateRegistryOwner, LifecycleOwner by TestLifecycleOwner() {
-                // We don't actually need to ever get actual instance.
-                override val savedStateRegistry: SavedStateRegistry
-                    get() = throw UnsupportedOperationException()
-            }
-        var childViewTreeSavedStateRegistryOwner: SavedStateRegistryOwner? = null
-
-        rule.setContent {
-            LocalSavedStateRegistryOwner.current.also {
-                SideEffect {
-                    parentSavedStateRegistryOwner = it
-                }
-            }
-
-            CompositionLocalProvider(
-                LocalSavedStateRegistryOwner provides compositionSavedStateRegistryOwner
-            ) {
-                AndroidView(
-                    factory = {
-                        object : FrameLayout(it) {
-                            override fun onAttachedToWindow() {
-                                super.onAttachedToWindow()
-                                childViewTreeSavedStateRegistryOwner =
-                                    findViewTreeSavedStateRegistryOwner()
-                            }
-                        }
-                    }
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(childViewTreeSavedStateRegistryOwner)
-                .isSameInstanceAs(compositionSavedStateRegistryOwner)
-            assertThat(childViewTreeSavedStateRegistryOwner)
-                .isNotSameInstanceAs(parentSavedStateRegistryOwner)
-        }
-    }
-
-    @Test
-    fun androidView_runsFactoryExactlyOnce_afterFirstComposition() {
-        var factoryRunCount = 0
-        rule.setContent {
-            val view = remember { View(rule.activity) }
-            AndroidView({ ++factoryRunCount; view })
-        }
-        rule.runOnIdle {
-            assertThat(factoryRunCount).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun androidView_runsFactoryExactlyOnce_evenWhenFactoryIsChanged() {
-        var factoryRunCount = 0
-        var first by mutableStateOf(true)
-        rule.setContent {
-            val view = remember { View(rule.activity) }
-            AndroidView(
-                if (first) {
-                    { ++factoryRunCount; view }
-                } else {
-                    { ++factoryRunCount; view }
-                }
-            )
-        }
-        rule.runOnIdle {
-            assertThat(factoryRunCount).isEqualTo(1)
-            first = false
-        }
-        rule.runOnIdle {
-            assertThat(factoryRunCount).isEqualTo(1)
-        }
-    }
-
-    @Ignore
-    @Test
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    fun androidView_clipsToBounds() {
-        val size = 20
-        val sizeDp = with(rule.density) { size.toDp() }
-        rule.setContent {
-            Column {
-                Box(
-                    Modifier
-                        .size(sizeDp)
-                        .background(Color.Blue)
-                        .testTag("box"))
-                AndroidView(factory = { SurfaceView(it) })
-            }
-        }
-
-        rule.onNodeWithTag("box").captureToImage().assertPixels(IntSize(size, size)) {
-            Color.Blue
-        }
-    }
-
-    @Test
-    fun androidView_callsOnRelease() {
-        var releaseCount = 0
-        var showContent by mutableStateOf(true)
-        rule.setContent {
-            if (showContent) {
-                AndroidView(
-                    factory = { TextView(it) },
-                    update = { it.text = "onRelease test" },
-                    onRelease = { releaseCount++ }
-                )
-            }
-        }
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(isDisplayed()))
-
-        assertEquals("onRelease() was called unexpectedly", 0, releaseCount)
-
-        showContent = false
-
-        onView(instanceOf(TextView::class.java))
-            .check(doesNotExist())
-
-        assertEquals(
-            "onRelease() should be called exactly once after " +
-                "removing the view from the composition hierarchy",
-            1, releaseCount
-        )
-    }
-
-    @Test
-    fun androidView_restoresState() {
-        var result = ""
-
-        @Composable
-        fun <T : Any> Navigation(
-            currentScreen: T,
-            modifier: Modifier = Modifier,
-            content: @Composable (T) -> Unit
-        ) {
-            val saveableStateHolder = rememberSaveableStateHolder()
-            Box(modifier) {
-                saveableStateHolder.SaveableStateProvider(currentScreen) {
-                    content(currentScreen)
-                }
-            }
-        }
-
-        var screen by mutableStateOf("screen1")
-        rule.setContent {
-            Navigation(screen) { currentScreen ->
-                if (currentScreen == "screen1") {
-                    AndroidView({
-                        StateSavingView(
-                            "testKey",
-                            "testValue",
-                            { restoredValue -> result = restoredValue },
-                            it
-                        )
-                    })
-                } else {
-                    Box(Modifier)
-                }
-            }
-        }
-
-        rule.runOnIdle { screen = "screen2" }
-        rule.runOnIdle { screen = "screen1" }
-        rule.runOnIdle {
-            assertThat(result).isEqualTo("testValue")
-        }
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    fun androidView_noClip() {
-        rule.setContent {
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .background(Color.White)) {
-                with(LocalDensity.current) {
-                    Box(
-                        Modifier
-                            .requiredSize(150.toDp())
-                            .testTag("box")) {
-                        Box(
-                            Modifier
-                                .size(100.toDp(), 100.toDp())
-                                .align(AbsoluteAlignment.TopLeft)
-                        ) {
-                            AndroidView(factory = { context ->
-                                object : View(context) {
-                                    init {
-                                        clipToOutline = false
-                                    }
-
-                                    override fun onDraw(canvas: Canvas) {
-                                        val paint = Paint()
-                                        paint.color = Color.Blue.toArgb()
-                                        paint.style = Paint.Style.FILL
-                                        canvas.drawRect(0f, 0f, 150f, 150f, paint)
-                                    }
-                                }
-                            })
-                        }
-                    }
-                }
-            }
-        }
-        rule.onNodeWithTag("box").captureToImage().assertPixels(IntSize(150, 150)) {
-            Color.Blue
-        }
-    }
-
-    @Test
-    fun testInitialComposition_causesViewToBecomeActive() {
-        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
-        rule.setContent {
-            ReusableContent("never-changes") {
-                ReusableAndroidViewWithLifecycleTracking(
-                    factory = { TextView(it).apply { text = "Test" } },
-                    onLifecycleEvent = lifecycleEvents::add
-                )
-            }
-        }
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(isDisplayed()))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "added to the composition hierarchy",
-            listOf(
-                OnCreate,
-                OnUpdate,
-                OnViewAttach,
-                ViewLifecycleEvent(ON_CREATE),
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-    }
-
-    @Test
-    fun testViewRecomposition_onlyInvokesUpdate() {
-        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
-        var state by mutableStateOf(0)
-        rule.setContent {
-            ReusableContent("never-changes") {
-                ReusableAndroidViewWithLifecycleTracking(
-                    factory = { TextView(it) },
-                    update = { it.text = "Text $state" },
-                    onLifecycleEvent = lifecycleEvents::add
-                )
-            }
-        }
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(isDisplayed()))
-            .check(matches(withText("Text 0")))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "added to the composition hierarchy",
-            listOf(
-                OnCreate,
-                OnUpdate,
-                OnViewAttach,
-                ViewLifecycleEvent(ON_CREATE),
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        state++
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(isDisplayed()))
-            .check(matches(withText("Text 1")))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when recomposed",
-            listOf(OnUpdate),
-            lifecycleEvents
-        )
-    }
-
-    @Test
-    fun testViewDeactivation_causesViewResetAndDetach() {
-        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
-        var attached by mutableStateOf(true)
-        rule.setContent {
-            ReusableContentHost(attached) {
-                ReusableAndroidViewWithLifecycleTracking(
-                    factory = { TextView(it).apply { text = "Test" } },
-                    onLifecycleEvent = lifecycleEvents::add
-                )
-            }
-        }
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "added to the composition hierarchy",
-            listOf(
-                OnCreate,
-                OnUpdate,
-                OnViewAttach,
-                ViewLifecycleEvent(ON_CREATE),
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        attached = false
-
-        onView(instanceOf(TextView::class.java))
-            .check(doesNotExist())
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "removed from the composition hierarchy and retained by Compose",
-            listOf(OnReset, OnViewDetach),
-            lifecycleEvents
-        )
-    }
-
-    @Test
-    fun testViewReattachment_causesViewToBecomeReusedAndReactivated() {
-        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
-        var attached by mutableStateOf(true)
-        rule.setContent {
-            ReusableContentHost(attached) {
-                ReusableAndroidViewWithLifecycleTracking(
-                    factory = { TextView(it).apply { text = "Test" } },
-                    onLifecycleEvent = lifecycleEvents::add
-                )
-            }
-        }
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(isDisplayed()))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "added to the composition hierarchy",
-            listOf(
-                OnCreate,
-                OnUpdate,
-                OnViewAttach,
-                ViewLifecycleEvent(ON_CREATE),
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        attached = false
-
-        onView(instanceOf(TextView::class.java))
-            .check(doesNotExist())
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "removed from the composition hierarchy and retained by Compose",
-            listOf(OnReset, OnViewDetach),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        attached = true
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(isDisplayed()))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "reattached to the composition hierarchy",
-            listOf(OnViewAttach, OnUpdate),
-            lifecycleEvents
-        )
-    }
-
-    @Test
-    fun testViewDisposalWhenDetached_causesViewToBeReleased() {
-        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
-        var active by mutableStateOf(true)
-        var emit by mutableStateOf(true)
-        rule.setContent {
-            if (emit) {
-                ReusableContentHost(active) {
-                    ReusableAndroidViewWithLifecycleTracking(
-                        factory = { TextView(it).apply { text = "Test" } },
-                        onLifecycleEvent = lifecycleEvents::add
-                    )
-                }
-            }
-        }
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "added to the composition hierarchy",
-            listOf(
-                OnCreate,
-                OnUpdate,
-                OnViewAttach,
-                ViewLifecycleEvent(ON_CREATE),
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        active = false
-
-        onView(instanceOf(TextView::class.java))
-            .check(doesNotExist())
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "removed from the composition hierarchy and retained by Compose",
-            listOf(OnReset, OnViewDetach),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        emit = false
-
-        onView(instanceOf(TextView::class.java))
-            .check(doesNotExist())
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "removed from the composition hierarchy while deactivated",
-            listOf(OnRelease),
-            lifecycleEvents
-        )
-    }
-
-    @Test
-    fun testViewRemovedFromComposition_causesViewToBeReleased() {
-        var includeViewInComposition by mutableStateOf(true)
-        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
-        rule.setContent {
-            if (includeViewInComposition) {
-                ReusableAndroidViewWithLifecycleTracking(
-                    factory = { TextView(it).apply { text = "Test" } },
-                    onLifecycleEvent = lifecycleEvents::add
-                )
-            }
-        }
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(isDisplayed()))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "added to the composition hierarchy",
-            listOf(
-                OnCreate,
-                OnUpdate,
-                OnViewAttach,
-                ViewLifecycleEvent(ON_CREATE),
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        includeViewInComposition = false
-
-        onView(instanceOf(TextView::class.java))
-            .check(doesNotExist())
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "removed from composition while visible",
-            listOf(OnViewDetach, OnRelease),
-            lifecycleEvents
-        )
-    }
-
-    @Test
-    fun testViewReusedInComposition_invokesReuseCallbackSequence() {
-        var key by mutableStateOf(0)
-        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
-        rule.setContent {
-            ReusableContent(key) {
-                ReusableAndroidViewWithLifecycleTracking(
-                    factory = { TextView(it) },
-                    update = { it.text = "Test" },
-                    onLifecycleEvent = lifecycleEvents::add
-                )
-            }
-        }
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(isDisplayed()))
-            .check(matches(withText("Test")))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "added to the composition hierarchy",
-            listOf(
-                OnCreate,
-                OnUpdate,
-                OnViewAttach,
-                ViewLifecycleEvent(ON_CREATE),
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        key++
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(isDisplayed()))
-            .check(matches(withText("Test")))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "reused in composition",
-            listOf(OnReset, OnUpdate),
-            lifecycleEvents
-        )
-    }
-
-    @Test
-    fun testViewInComposition_experiencesHostLifecycle_andDoesNotRecreateView() {
-        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
-        rule.setContent {
-            ReusableContentHost(active = true) {
-                ReusableAndroidViewWithLifecycleTracking(
-                    factory = { TextView(it).apply { text = "Test" } },
-                    onLifecycleEvent = lifecycleEvents::add
-                )
-            }
-        }
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "added to the composition hierarchy",
-            listOf(
-                OnCreate,
-                OnUpdate,
-                OnViewAttach,
-                ViewLifecycleEvent(ON_CREATE),
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-
-        rule.activityRule.scenario.moveToState(Lifecycle.State.CREATED)
-        rule.runOnIdle { /* Ensure lifecycle callbacks propagate */ }
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "its host transitioned from RESUMED to CREATED while the view was attached",
-            listOf(
-                ViewLifecycleEvent(ON_PAUSE),
-                ViewLifecycleEvent(ON_STOP)
-            ),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        rule.activityRule.scenario.moveToState(Lifecycle.State.RESUMED)
-        rule.runOnIdle { /* Ensure lifecycle callbacks propagate */ }
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "its host transitioned from CREATED to RESUMED while the view was attached",
-            listOf(
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-    }
-
-    @Test
-    fun testReactivationWithChangingKey_onlyResetsOnce() {
-        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
-        var attach by mutableStateOf(true)
-        var key by mutableStateOf(1)
-        rule.setContent {
-            ReusableContentHost(active = attach) {
-                ReusableContent(key = key) {
-                    ReusableAndroidViewWithLifecycleTracking(
-                        factory = { TextView(it).apply { text = "Test" } },
-                        onLifecycleEvent = lifecycleEvents::add
-                    )
-                }
-            }
-        }
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "added to the composition hierarchy",
-            listOf(
-                OnCreate,
-                OnUpdate,
-                OnViewAttach,
-                ViewLifecycleEvent(ON_CREATE),
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        attach = false
-
-        onView(instanceOf(TextView::class.java))
-            .check(doesNotExist())
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "detached from the composition hierarchy",
-            listOf(OnReset, OnViewDetach),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        rule.runOnUiThread {
-            // Make sure both changes are applied in the same composition.
-            attach = true
-            key++
-        }
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "simultaneously reactivating and changing reuse keys",
-            listOf(OnViewAttach, OnUpdate),
-            lifecycleEvents
-        )
-    }
-
-    @Test
-    fun testViewDetachedFromComposition_stillExperiencesHostLifecycle() {
-        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
-        var attached by mutableStateOf(true)
-        rule.setContent {
-            ReusableContentHost(attached) {
-                ReusableAndroidViewWithLifecycleTracking(
-                    factory = { TextView(it).apply { text = "Test" } },
-                    onLifecycleEvent = lifecycleEvents::add
-                )
-            }
-        }
-
-        onView(instanceOf(TextView::class.java))
-            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "added to the composition hierarchy",
-            listOf(
-                OnCreate,
-                OnUpdate,
-                OnViewAttach,
-                ViewLifecycleEvent(ON_CREATE),
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        attached = false
-
-        onView(instanceOf(TextView::class.java))
-            .check(doesNotExist())
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "removed from the composition hierarchy and retained by Compose",
-            listOf(OnReset, OnViewDetach),
-            lifecycleEvents
-        )
-        lifecycleEvents.clear()
-
-        rule.activityRule.scenario.moveToState(Lifecycle.State.CREATED)
-        rule.runOnIdle { /* Ensure lifecycle callbacks propagate */ }
-
-        assertEquals(
-            "AndroidView did not receive callbacks when its host transitioned from " +
-                "RESUMED to CREATED while the view was detached",
-            listOf(
-                ViewLifecycleEvent(ON_PAUSE),
-                ViewLifecycleEvent(ON_STOP)
-            ),
-            lifecycleEvents
-        )
-
-        lifecycleEvents.clear()
-        rule.activityRule.scenario.moveToState(Lifecycle.State.RESUMED)
-        rule.runOnIdle { /* Wait for UI to settle */ }
-
-        assertEquals(
-            "AndroidView did not receive callbacks when its host transitioned from " +
-                "CREATED to RESUMED while the view was detached",
-            listOf(
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-    }
-
-    @Test
-    fun testViewIsReused_whenMoved() {
-        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
-        var slotWithContent by mutableStateOf(0)
-
-        rule.setContent {
-            val movableContext = remember {
-                movableContentOf {
-                    ReusableAndroidViewWithLifecycleTracking(
-                        factory = {
-                            EditText(it).apply { id = R.id.testContentViewId }
-                        },
-                        onLifecycleEvent = lifecycleEvents::add
-                    )
-                }
-            }
-
-            Column {
-                repeat(10) { slot ->
-                    if (slot == slotWithContent) {
-                        ReusableContent(Unit) {
-                            movableContext()
-                        }
-                    } else {
-                        Text("Slot $slot")
-                    }
-                }
-            }
-        }
-
-        onView(instanceOf(EditText::class.java))
-            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
-            .perform(typeText("Input"))
-
-        assertEquals(
-            "AndroidView did not experience the expected lifecycle when " +
-                "added to the composition hierarchy",
-            listOf(
-                OnCreate,
-                OnUpdate,
-                OnViewAttach,
-                ViewLifecycleEvent(ON_CREATE),
-                ViewLifecycleEvent(ON_START),
-                ViewLifecycleEvent(ON_RESUME)
-            ),
-            lifecycleEvents
-        )
-        lifecycleEvents.clear()
-        slotWithContent++
-
-        rule.runOnIdle { /* Wait for UI to settle */ }
-
-        assertEquals(
-            "AndroidView experienced unexpected lifecycle events when " +
-                "moved in the composition",
-            emptyList<AndroidViewLifecycleEvent>(),
-            lifecycleEvents
-        )
-
-        // Check that the state of the view is retained
-        onView(instanceOf(EditText::class.java))
-            .check(matches(isDisplayed()))
-            .check(matches(withText("Input")))
-    }
-
-    @Test
-    fun testViewRestoresState_whenRemovedAndRecreatedWithNoReuse() {
-        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
-        var screen by mutableStateOf("screen1")
-        rule.setContent {
-            with(rememberSaveableStateHolder()) {
-                if (screen == "screen1") {
-                    SaveableStateProvider("screen1") {
-                        ReusableAndroidViewWithLifecycleTracking(
-                            factory = {
-                                EditText(it).apply { id = R.id.testContentViewId }
-                            },
-                            onLifecycleEvent = lifecycleEvents::add
-                        )
-                    }
-                }
-            }
-        }
-
-        onView(instanceOf(EditText::class.java))
-            .check(matches(isDisplayed()))
-            .perform(typeText("User Input"))
-
-        rule.runOnIdle { screen = "screen2" }
-
-        onView(instanceOf(EditText::class.java))
-            .check(doesNotExist())
-
-        rule.runOnIdle { screen = "screen1" }
-
-        onView(instanceOf(EditText::class.java))
-            .check(matches(isDisplayed()))
-            .check(matches(withText("User Input")))
-    }
-
-    @Test
-    fun androidView_withParentDataModifier() {
-        val columnHeight = 100
-        val columnHeightDp = with(rule.density) { columnHeight.toDp() }
-        var viewSize = IntSize.Zero
-        rule.setContent {
-            Column(
-                Modifier
-                    .height(columnHeightDp)
-                    .fillMaxWidth()) {
-                AndroidView(
-                    factory = { View(it) },
-                    modifier = Modifier
-                        .weight(1f)
-                        .onGloballyPositioned { viewSize = it.size }
-                )
-
-                Box(Modifier.height(columnHeightDp / 4))
-            }
-        }
-
-        rule.runOnIdle {
-            assertEquals(columnHeight * 3 / 4, viewSize.height)
-        }
-    }
-
-    @Test
-    fun androidView_visibilityGone() {
-        var view: View? = null
-        var drawCount = 0
-        val viewSizeDp = 50.dp
-        val viewSize = with(rule.density) { viewSizeDp.roundToPx() }
-        rule.setContent {
-            AndroidView(
-                modifier = Modifier
-                    .testTag("wrapper")
-                    .heightIn(max = viewSizeDp),
-                factory = {
-                    object : View(it) {
-                        override fun dispatchDraw(canvas: Canvas) {
-                            drawCount++
-                            super.dispatchDraw(canvas)
-                        }
-                    }
-                },
-                update = {
-                    view = it
-                    it.layoutParams = ViewGroup.LayoutParams(viewSize, WRAP_CONTENT)
-                },
-            )
-        }
-
-        rule.onNodeWithTag("wrapper")
-            .assertHeightIsEqualTo(viewSizeDp)
-
-        rule.runOnUiThread {
-            drawCount = 0
-            view?.visibility = View.GONE
-        }
-
-        rule.onNodeWithTag("wrapper")
-            .assertHeightIsEqualTo(0.dp)
-        assertEquals(0, drawCount)
-    }
-
-    @Test
-    fun androidView_visibilityGone_column() {
-        var view: View? = null
-        val viewSizeDp = 50.dp
-        val viewSize = with(rule.density) { viewSizeDp.roundToPx() }
-        rule.setContent {
-            Column {
-                AndroidView(
-                    modifier = Modifier
-                        .testTag("wrapper")
-                        .heightIn(max = viewSizeDp),
-                    factory = {
-                        View(it)
-                    },
-                    update = {
-                        view = it
-                        it.layoutParams = ViewGroup.LayoutParams(viewSize, WRAP_CONTENT)
-                    },
-                )
-
-                Box(
-                    Modifier
-                        .size(viewSizeDp)
-                        .testTag("box")
-                )
-            }
-        }
-
-        rule.onNodeWithTag("box")
-            .assertTopPositionInRootIsEqualTo(viewSizeDp)
-            .assertLeftPositionInRootIsEqualTo(0.dp)
-
-        rule.runOnUiThread {
-            view?.visibility = View.GONE
-        }
-
-        rule.onNodeWithTag("box")
-            .assertTopPositionInRootIsEqualTo(0.dp)
-            .assertLeftPositionInRootIsEqualTo(0.dp)
-    }
-
-    @ExperimentalComposeUiApi
-    @Composable
-    private inline fun <T : View> ReusableAndroidViewWithLifecycleTracking(
-        crossinline factory: (Context) -> T,
-        noinline onLifecycleEvent: @DisallowComposableCalls (AndroidViewLifecycleEvent) -> Unit,
-        modifier: Modifier = Modifier,
-        crossinline update: (T) -> Unit = { },
-        crossinline reuse: (T) -> Unit = { },
-        crossinline release: (T) -> Unit = { }
-    ) {
-        AndroidView(
-            factory = {
-                onLifecycleEvent(OnCreate)
-                factory(it).apply {
-                    addOnAttachStateChangeListener(
-                        object : OnAttachStateChangeListener, LifecycleEventObserver {
-                            override fun onViewAttachedToWindow(v: View) {
-                                onLifecycleEvent(OnViewAttach)
-                                findViewTreeLifecycleOwner()!!.lifecycle.addObserver(this)
-                            }
-
-                            override fun onViewDetachedFromWindow(v: View) {
-                                onLifecycleEvent(OnViewDetach)
-                            }
-
-                            override fun onStateChanged(
-                                source: LifecycleOwner,
-                                event: Lifecycle.Event
-                            ) {
-                                onLifecycleEvent(ViewLifecycleEvent(event))
-                            }
-                        }
-                    )
-                }
-            },
-            modifier = modifier,
-            update = {
-                onLifecycleEvent(OnUpdate)
-                update(it)
-            },
-            onReset = {
-                onLifecycleEvent(OnReset)
-                reuse(it)
-            },
-            onRelease = {
-                onLifecycleEvent(OnRelease)
-                release(it)
-            }
-        )
-    }
-
-    private sealed class AndroidViewLifecycleEvent {
-        override fun toString(): String {
-            return javaClass.simpleName
-        }
-
-        // Sent when the factory lambda is invoked
-        object OnCreate : AndroidViewLifecycleEvent()
-
-        object OnUpdate : AndroidViewLifecycleEvent()
-        object OnReset : AndroidViewLifecycleEvent()
-        object OnRelease : AndroidViewLifecycleEvent()
-
-        object OnViewAttach : AndroidViewLifecycleEvent()
-        object OnViewDetach : AndroidViewLifecycleEvent()
-
-        data class ViewLifecycleEvent(
-            val event: Lifecycle.Event
-        ) : AndroidViewLifecycleEvent() {
-            override fun toString() = "ViewLifecycleEvent($event)"
-        }
-    }
-
-    private class StateSavingView(
-        private val key: String,
-        private val value: String,
-        private val onRestoredValue: (String) -> Unit,
-        context: Context
-    ) : View(context) {
-        init {
-            id = 73
-        }
-
-        override fun onSaveInstanceState(): Parcelable {
-            val superState = super.onSaveInstanceState()
-            val bundle = Bundle()
-            bundle.putParcelable("superState", superState)
-            bundle.putString(key, value)
-            return bundle
-        }
-
-        @Suppress("DEPRECATION")
-        override fun onRestoreInstanceState(state: Parcelable?) {
-            super.onRestoreInstanceState((state as Bundle).getParcelable("superState"))
-            onRestoredValue(state.getString(key)!!)
-        }
-    }
-
-    private class UpdateTestView(context: Context) : View(context) {
-        var counter = 0
-    }
-
-    private fun Dp.toPx(displayMetrics: DisplayMetrics) =
-        TypedValue.applyDimension(
-            TypedValue.COMPLEX_UNIT_DIP,
-            value,
-            displayMetrics
-        ).roundToInt()
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/AndroidManifest.xml b/compose/ui/ui/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/AndroidManifest.xml
rename to compose/ui/ui/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AccessibilityIteratorsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AccessibilityIteratorsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AccessibilityIteratorsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AccessibilityIteratorsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/GoldenCommon.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/GoldenCommon.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/GoldenCommon.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/GoldenCommon.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/OpenComposeView.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/OpenComposeView.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/OpenComposeView.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/OpenComposeView.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/ParentDataModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/ParentDataModifierTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/ParentDataModifierTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/ParentDataModifierTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/RecyclerViewIntegrationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/RecyclerViewIntegrationTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/RecyclerViewIntegrationTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/RecyclerViewIntegrationTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/SnapshotFlowTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/SnapshotFlowTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/SnapshotFlowTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/SnapshotFlowTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/ZIndexNodeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/ZIndexNodeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/ZIndexNodeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/ZIndexNodeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/accessibility/CollectionInfoTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/CollectionInfoTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/accessibility/CollectionInfoTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/CollectionInfoTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/AlphaTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/AlphaTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/AlphaTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/AlphaTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/BlurTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/BlurTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/BlurTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/BlurTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
new file mode 100644
index 0000000..299ab79
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
@@ -0,0 +1,855 @@
+/*
+ * Copyright 2020 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.draw
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.AtLeastSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.LayoutModifierImpl
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.elementFor
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class DrawModifierTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Before
+    fun before() {
+        isDebugInspectorInfoEnabled = true
+    }
+
+    @After
+    fun after() {
+        isDebugInspectorInfoEnabled = false
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testCacheHitWithStateChange() {
+        // Verify that a state change outside of the cache block does not
+        // require the cache block to be invalidated
+        val testTag = "testTag"
+        var cacheBuildCount = 0
+        val size = 200
+        rule.setContent {
+            var rectColor by remember { mutableStateOf(Color.Blue) }
+            AtLeastSize(
+                size = size,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawWithCache {
+                        val drawSize = this.size
+                        val path = Path().apply {
+                            lineTo(drawSize.width / 2f, 0f)
+                            lineTo(drawSize.width / 2f, drawSize.height)
+                            lineTo(0f, drawSize.height)
+                            close()
+                        }
+                        cacheBuildCount++
+                        onDrawBehind {
+                            drawRect(rectColor)
+                            drawPath(path, Color.Red)
+                        }
+                    }
+                    .clickable {
+                        if (rectColor == Color.Blue) {
+                            rectColor = Color.Green
+                        } else {
+                            rectColor = Color.Blue
+                        }
+                    }
+            ) { }
+        }
+
+        rule.onNodeWithTag(testTag).apply {
+            // Verify that the path was created only once
+            assertEquals(1, cacheBuildCount)
+            captureToBitmap().apply {
+                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
+                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 2, 1))
+                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 2, height / 2 - 2))
+                assertEquals(Color.Red.toArgb(), getPixel(1, height / 2 - 2))
+
+                assertEquals(Color.Blue.toArgb(), getPixel(width / 2 + 1, 1))
+                assertEquals(Color.Blue.toArgb(), getPixel(width - 2, 1))
+                assertEquals(Color.Blue.toArgb(), getPixel(width / 2 + 1, height - 2))
+                assertEquals(Color.Blue.toArgb(), getPixel(width - 2, height - 2))
+            }
+            performClick()
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(testTag).apply {
+            // Verify that the path was re-used and only built once
+            assertEquals(1, cacheBuildCount)
+            captureToBitmap().apply {
+                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
+                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 2, 1))
+                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 2, height / 2 - 1))
+                assertEquals(Color.Red.toArgb(), getPixel(1, height / 2 - 2))
+
+                assertEquals(Color.Green.toArgb(), getPixel(width / 2 + 1, 1))
+                assertEquals(Color.Green.toArgb(), getPixel(width - 2, 1))
+                assertEquals(Color.Green.toArgb(), getPixel(width / 2 + 1, height - 2))
+                assertEquals(Color.Green.toArgb(), getPixel(width - 2, height - 2))
+            }
+        }
+    }
+
+    @Test
+    fun invalidationForDrawWithCache() {
+        var size by mutableStateOf(10f)
+        var drawCount = 0
+        rule.setContent {
+            Box(Modifier.fillMaxSize()) {
+                Box(Modifier
+                    .graphicsLayer { }
+                    .size(50.dp)
+                    .drawWithCache {
+                        val rectSize = Size(size, size)
+                        onDrawBehind {
+                            drawRect(Color.Blue, Offset.Zero, rectSize)
+                            drawCount++
+                        }
+                    }
+                    .graphicsLayer { }
+                )
+            }
+        }
+        rule.waitForIdle()
+        assertThat(drawCount).isEqualTo(1)
+
+        size = 15f
+        rule.waitForIdle()
+        assertThat(drawCount).isEqualTo(2)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testCacheInvalidatedAfterStateChange() {
+        // Verify that a state change within the cache block does
+        // require the cache block to be invalidated
+        val testTag = "testTag"
+        var cacheBuildCount = 0
+        val size = 200
+
+        rule.setContent {
+            var pathFillBounds by remember { mutableStateOf(false) }
+            AtLeastSize(
+                size = size,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawWithCache {
+                        val pathSize = if (pathFillBounds) this.size else this.size / 2f
+                        val path = Path().apply {
+                            lineTo(pathSize.width, 0f)
+                            lineTo(pathSize.width, pathSize.height)
+                            lineTo(0f, pathSize.height)
+                            close()
+                        }
+                        cacheBuildCount++
+                        onDrawBehind {
+                            drawRect(Color.Red)
+                            drawPath(path, Color.Blue)
+                        }
+                    }
+                    .clickable {
+                        pathFillBounds = !pathFillBounds
+                    }
+            ) { }
+        }
+
+        rule.onNodeWithTag(testTag).apply {
+            // Verify that the path was created only once
+            assertEquals(1, cacheBuildCount)
+            captureToBitmap().apply {
+                assertEquals(Color.Blue.toArgb(), getPixel(1, 1))
+                assertEquals(Color.Blue.toArgb(), getPixel(width / 2 - 2, 1))
+                assertEquals(Color.Blue.toArgb(), getPixel(width / 2 - 2, height / 2 - 2))
+                assertEquals(Color.Blue.toArgb(), getPixel(1, height / 2 - 1))
+
+                assertEquals(Color.Red.toArgb(), getPixel(width / 2 + 1, 1))
+                assertEquals(Color.Red.toArgb(), getPixel(width / 2 + 1, height / 2 - 1))
+                assertEquals(Color.Red.toArgb(), getPixel(width / 2 + 1, height / 2 - 2))
+                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 2, height / 2 + 1))
+                assertEquals(Color.Red.toArgb(), getPixel(1, height / 2 + 1))
+
+                assertEquals(Color.Red.toArgb(), getPixel(1, height - 2))
+                assertEquals(Color.Red.toArgb(), getPixel(width - 2, 1))
+                assertEquals(Color.Red.toArgb(), getPixel(width - 2, height - 2))
+            }
+            performClick()
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(testTag).apply {
+            assertEquals(2, cacheBuildCount)
+            captureToBitmap().apply {
+                assertEquals(Color.Blue.toArgb(), getPixel(1, 1))
+                assertEquals(Color.Blue.toArgb(), getPixel(size - 2, 1))
+                assertEquals(Color.Blue.toArgb(), getPixel(size - 2, size - 2))
+                assertEquals(Color.Blue.toArgb(), getPixel(1, size - 2))
+            }
+        }
+    }
+
+    @Test
+    fun combinedModifiers_drawingSizesAreUsingTheSizeDefinedByLayoutModifier() {
+        var drawingSize: Size = Size.Unspecified
+        var drawingCacheSize: Size = Size.Unspecified
+        val modifier = object : LayoutModifier, DrawCacheModifier {
+            override fun onBuildCache(params: BuildDrawCacheParams) {
+                drawingCacheSize = params.size
+            }
+
+            override fun ContentDrawScope.draw() {
+                drawingSize = size
+            }
+
+            override fun MeasureScope.measure(
+                measurable: Measurable,
+                constraints: Constraints
+            ): MeasureResult {
+                val placeable = measurable.measure(Constraints.fixed(10, 10))
+                return layout(20, 20) {
+                    placeable.place(0, 0)
+                }
+            }
+        }
+        rule.setContent {
+            Box(modifier)
+        }
+
+        rule.runOnIdle {
+            val expectedSize = Size(10f, 10f)
+            assertThat(drawingSize).isEqualTo(expectedSize)
+            assertThat(drawingCacheSize).isEqualTo(expectedSize)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testCacheInvalidatedAfterSizeChange() {
+        // Verify that a size change does cause the cache block to be invalidated
+        val testTag = "testTag"
+        var cacheBuildCount = 0
+        val startSize = 200
+        val endSize = 400
+        rule.setContent {
+            var size by remember { mutableStateOf(startSize) }
+            AtLeastSize(
+                size = size,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawWithCache {
+                        val drawSize = this.size
+                        val path = Path().apply {
+                            lineTo(drawSize.width, 0f)
+                            lineTo(drawSize.height, drawSize.height)
+                            lineTo(0f, drawSize.height)
+                            close()
+                        }
+                        cacheBuildCount++
+                        onDrawBehind {
+                            drawPath(path, Color.Red)
+                        }
+                    }
+                    .clickable {
+                        if (size == startSize) {
+                            size = endSize
+                        } else {
+                            size = startSize
+                        }
+                    }
+            ) { }
+        }
+
+        rule.onNodeWithTag(testTag).apply {
+            // Verify that the path was created only once
+            assertEquals(1, cacheBuildCount)
+            captureToBitmap().apply {
+                assertEquals(startSize, this.width)
+                assertEquals(startSize, this.height)
+                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
+                assertEquals(Color.Red.toArgb(), getPixel(width - 2, height - 2))
+            }
+            performClick()
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(testTag).apply {
+            // Verify that the path was re-used and only built once
+            assertEquals(2, cacheBuildCount)
+            captureToBitmap().apply {
+                assertEquals(endSize, this.width)
+                assertEquals(endSize, this.height)
+                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
+                assertEquals(Color.Red.toArgb(), getPixel(width - 2, height - 2))
+            }
+        }
+    }
+
+    @Test
+    fun testCacheInvalidatedAfterLayoutDirectionChange() {
+        var layoutDirection by mutableStateOf(LayoutDirection.Ltr)
+        var realLayoutDirection: LayoutDirection? = null
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                AtLeastSize(
+                    size = 10,
+                    modifier = Modifier.drawWithCache {
+                        realLayoutDirection = layoutDirection
+                        onDrawBehind {}
+                    }
+                ) { }
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(LayoutDirection.Ltr, realLayoutDirection)
+            layoutDirection = LayoutDirection.Rtl
+        }
+
+        rule.runOnIdle {
+            assertEquals(LayoutDirection.Rtl, realLayoutDirection)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testCacheInvalidatedWithHelperModifier() {
+        // If Modifier.drawWithCache is used as part of the implementation for another modifier
+        // defined in a helper function, make sure that an change in state parameter ends up calling
+        // ModifiedDrawNode.onModifierChanged and updates the internal cache for
+        // Modifier.drawWithCache
+        val testTag = "testTag"
+        val startSize = 200
+        rule.setContent {
+            val color = remember { mutableStateOf(Color.Red) }
+            AtLeastSize(
+                size = startSize,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawPathHelperModifier(color.value)
+                    .clickable {
+                        if (color.value == Color.Red) {
+                            color.value = Color.Blue
+                        } else {
+                            color.value = Color.Red
+                        }
+                    }
+            ) { }
+        }
+
+        rule.onNodeWithTag(testTag).apply {
+            // Verify that the path was created only once
+            captureToBitmap().apply {
+                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
+                assertEquals(Color.Red.toArgb(), getPixel(width - 2, height - 2))
+            }
+            performClick()
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(testTag).apply {
+            // Verify that the path was re-used and only built once
+            captureToBitmap().apply {
+                assertEquals(Color.Blue.toArgb(), getPixel(1, 1))
+                assertEquals(Color.Blue.toArgb(), getPixel(width - 2, height - 2))
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testGraphicsLayerCacheInvalidatedAfterStateChange() {
+        // Verify that a state change within the cache block does
+        // require the cache block to be invalidated if a graphicsLayer is also
+        // configured on the composable and the state parameter is configured elsewhere
+        val boxTag = "boxTag"
+        val clickTag = "clickTag"
+
+        var cacheBuildCount = 0
+
+        rule.setContent {
+            val flag = remember { mutableStateOf(false) }
+            Column {
+                AtLeastSize(
+                    size = 50,
+                    modifier = Modifier
+                        .testTag(boxTag)
+                        .graphicsLayer()
+                        .drawWithCache {
+                            // State read of flag
+                            val color = if (flag.value) Color.Red else Color.Blue
+                            cacheBuildCount++
+
+                            onDrawBehind {
+                                drawRect(color)
+                            }
+                        }
+                )
+
+                Box(
+                    Modifier
+                        .testTag(clickTag)
+                        .size(20.dp)
+                        .clickable {
+                            flag.value = !flag.value
+                        }
+                )
+            }
+        }
+
+        rule.onNodeWithTag(boxTag).apply {
+            // Verify that the cache lambda was invoked once
+            assertEquals(1, cacheBuildCount)
+            captureToImage().assertPixels { Color.Blue }
+        }
+
+        rule.onNodeWithTag(clickTag).performClick()
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(boxTag).apply {
+            // Verify the cache lambda was invoked again and the
+            // rect is drawn with the updated color
+            assertEquals(2, cacheBuildCount)
+            captureToImage().assertPixels { Color.Red }
+        }
+    }
+
+    // Helper Modifier that uses Modifier.drawWithCache internally. If the color
+    // parameter
+    private fun Modifier.drawPathHelperModifier(color: Color) =
+        this.then(
+            Modifier.drawWithCache {
+                val drawSize = this.size
+                val path = Path().apply {
+                    lineTo(drawSize.width, 0f)
+                    lineTo(drawSize.height, drawSize.height)
+                    lineTo(0f, drawSize.height)
+                    close()
+                }
+                onDrawBehind {
+                    drawPath(path, color)
+                }
+            }
+        )
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testDrawWithCacheContentDrawnImplicitly() {
+        // Verify that drawContent is invoked even if it is not explicitly called within
+        // the implementation of the callback provided in the onDraw method
+        // in Modifier.drawWithCache
+        val testTag = "testTag"
+        val testSize = 200
+        rule.setContent {
+            AtLeastSize(
+                size = testSize,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawWithCache {
+                        onDrawBehind {
+                            drawRect(Color.Red, size = Size(size.width / 2, size.height))
+                        }
+                    }
+                    .background(Color.Blue)
+            )
+        }
+
+        rule.onNodeWithTag(testTag).apply {
+            captureToBitmap().apply {
+                assertEquals(Color.Blue.toArgb(), getPixel(0, 0))
+                assertEquals(Color.Blue.toArgb(), getPixel(width - 1, 0))
+                assertEquals(Color.Blue.toArgb(), getPixel(width - 1, height - 1))
+                assertEquals(Color.Blue.toArgb(), getPixel(0, height - 1))
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testDrawWithCacheOverContent() {
+        // Verify that drawContent is not invoked implicitly if it is explicitly called within
+        // the implementation of the callback provided in the onDraw method
+        // in Modifier.drawWithCache. That is the red rectangle is drawn above the contents
+        val testTag = "testTag"
+        val testSize = 200
+        rule.setContent {
+            AtLeastSize(
+                size = testSize,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawWithCache {
+                        onDrawWithContent {
+                            drawContent()
+                            drawRect(Color.Red, size = Size(size.width / 2, size.height))
+                        }
+                    }
+                    .background(Color.Blue)
+            )
+        }
+
+        rule.onNodeWithTag(testTag).apply {
+            captureToBitmap().apply {
+                assertEquals(Color.Blue.toArgb(), getPixel(width / 2 + 1, 0))
+                assertEquals(Color.Blue.toArgb(), getPixel(width - 1, 0))
+                assertEquals(Color.Blue.toArgb(), getPixel(width - 1, height - 1))
+                assertEquals(Color.Blue.toArgb(), getPixel(width / 2 + 1, height - 1))
+
+                assertEquals(Color.Red.toArgb(), getPixel(0, 0))
+                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 1, 0))
+                assertEquals(Color.Red.toArgb(), getPixel(width / 2 - 1, height - 1))
+                assertEquals(Color.Red.toArgb(), getPixel(0, height - 1))
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testDrawWithCacheBlendsContent() {
+        // Verify that the drawing commands of drawContent are blended against the green
+        // rectangle with the specified BlendMode
+        val testTag = "testTag"
+        val testSize = 200
+        rule.setContent {
+            AtLeastSize(
+                size = testSize,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawWithCache {
+                        onDrawWithContent {
+                            drawContent()
+                            drawRect(Color.Green, blendMode = BlendMode.Plus)
+                        }
+                    }
+                    .background(Color.Blue)
+            )
+        }
+
+        rule.onNodeWithTag(testTag).apply {
+            captureToBitmap().apply {
+                assertEquals(Color.Cyan.toArgb(), getPixel(0, 0))
+                assertEquals(Color.Cyan.toArgb(), getPixel(width - 1, 0))
+                assertEquals(Color.Cyan.toArgb(), getPixel(width - 1, height - 1))
+                assertEquals(Color.Cyan.toArgb(), getPixel(0, height - 1))
+            }
+        }
+    }
+
+    @Test
+    fun testInspectorValueForDrawBehind() {
+        val onDraw: DrawScope.() -> Unit = {}
+        rule.setContent {
+            val modifier = Modifier.drawBehind(onDraw) as InspectableValue
+            assertThat(modifier.nameFallback).isEqualTo("drawBehind")
+            assertThat(modifier.valueOverride).isNull()
+            assertThat(modifier.inspectableElements.map { it.name }.asIterable())
+                .containsExactly("onDraw")
+        }
+    }
+
+    @Test
+    fun testInspectorValueForDrawWithCache() {
+        val onBuildDrawCache: CacheDrawScope.() -> DrawResult = { DrawResult {} }
+        rule.setContent {
+            val modifier = Modifier.drawWithCache(onBuildDrawCache) as InspectableValue
+            assertThat(modifier.nameFallback).isEqualTo("drawWithCache")
+            assertThat(modifier.valueOverride).isNull()
+            assertThat(modifier.inspectableElements.map { it.name }.asIterable())
+                .containsExactly("onBuildDrawCache")
+        }
+    }
+
+    @Test
+    fun testInspectorValueForDrawWithContent() {
+        val onDraw: DrawScope.() -> Unit = {}
+        rule.setContent {
+            val modifier = Modifier.drawWithContent(onDraw) as InspectableValue
+            assertThat(modifier.nameFallback).isEqualTo("drawWithContent")
+            assertThat(modifier.valueOverride).isNull()
+            assertThat(modifier.inspectableElements.map { it.name }.asIterable())
+                .containsExactly("onDraw")
+        }
+    }
+
+    @Test
+    fun recompositionWithTheSameDrawBehindLambdaIsNotTriggeringRedraw() {
+        val recompositionCounter = mutableStateOf(0)
+        var redrawCounter = 0
+        val drawBlock: DrawScope.() -> Unit = {
+            redrawCounter++
+        }
+        rule.setContent {
+            recompositionCounter.value
+            Layout({}, modifier = Modifier.drawBehind(drawBlock)) { _, _ ->
+                layout(100, 100) {}
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(redrawCounter).isEqualTo(1)
+            recompositionCounter.value = 1
+        }
+
+        rule.runOnIdle {
+            assertThat(redrawCounter).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun recompositionWithTheSameDrawWithContentLambdaIsNotTriggeringRedraw() {
+        val recompositionCounter = mutableStateOf(0)
+        var redrawCounter = 0
+        val drawBlock: ContentDrawScope.() -> Unit = {
+            redrawCounter++
+        }
+        rule.setContent {
+            recompositionCounter.value
+            Layout({}, modifier = Modifier.drawWithContent(drawBlock)) { _, _ ->
+                layout(100, 100) {}
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(redrawCounter).isEqualTo(1)
+            recompositionCounter.value = 1
+        }
+
+        rule.runOnIdle {
+            assertThat(redrawCounter).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun recompositionWithTheSameDrawWithCacheLambdaIsNotTriggeringRedraw() {
+        val recompositionCounter = mutableStateOf(0)
+        var cacheRebuildCounter = 0
+        var redrawCounter = 0
+        val drawBlock: CacheDrawScope.() -> DrawResult = {
+            cacheRebuildCounter++
+            onDrawBehind {
+                redrawCounter++
+            }
+        }
+        rule.setContent {
+            recompositionCounter.value
+            Layout({}, modifier = Modifier.drawWithCache(drawBlock)) { _, _ ->
+                layout(100, 100) {}
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(cacheRebuildCounter).isEqualTo(1)
+            assertThat(redrawCounter).isEqualTo(1)
+            recompositionCounter.value = 1
+        }
+
+        rule.runOnIdle {
+            assertThat(cacheRebuildCounter).isEqualTo(1)
+            assertThat(redrawCounter).isEqualTo(1)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testDelegatedDrawNodesDraw() {
+        val testTag = "testTag"
+        val size = 200
+
+        val node = object : DelegatingNode() {
+            val draw = delegate(DrawBackgroundModifier {
+                drawRect(Color.Red)
+            })
+        }
+
+        rule.setContent {
+            AtLeastSize(
+                size = size,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .elementFor(node)
+            ) { }
+        }
+
+        rule.onNodeWithTag(testTag).apply {
+            captureToBitmap().apply {
+                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testMultipleDelegatedDrawNodes() {
+        val testTag = "testTag"
+
+        val node = object : DelegatingNode() {
+            val a = delegate(DrawBackgroundModifier {
+                drawRect(
+                    Color.Red,
+                    size = Size(10f, 10f)
+                )
+            })
+
+            val b = delegate(DrawBackgroundModifier {
+                drawRect(
+                    Color.Blue,
+                    topLeft = Offset(10f, 0f),
+                    size = Size(10f, 10f))
+            })
+        }
+
+        rule.setContent {
+            AtLeastSize(
+                size = 200,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .elementFor(node)
+            ) { }
+        }
+
+        rule.onNodeWithTag(testTag).apply {
+            captureToBitmap().apply {
+                assertEquals(Color.Red.toArgb(), getPixel(1, 1))
+                assertEquals(Color.Blue.toArgb(), getPixel(11, 1))
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testDelegatedLayoutModifierNode() {
+        val testTag = "testTag"
+
+        val node = object : DelegatingNode() {
+            val a = delegate(LayoutModifierImpl { measurable, constraints ->
+                val p = measurable.measure(constraints)
+                layout(10.dp.roundToPx(), 10.dp.roundToPx()) {
+                    p.place(0, 0)
+                }
+            })
+        }
+
+        rule.setContent {
+            Box(
+                modifier = Modifier
+                    .testTag(testTag)
+                    .elementFor(node)
+            )
+        }
+
+        rule
+            .onNodeWithTag(testTag)
+            .assertWidthIsEqualTo(10.dp)
+            .assertHeightIsEqualTo(10.dp)
+    }
+
+    @Test
+    fun testInvalidationInsideOnSizeChanged() {
+        var someState by mutableStateOf(1)
+        var drawCount = 0
+
+        rule.setContent {
+            Box(
+                Modifier
+                    .drawBehind {
+                        @Suppress("UNUSED_EXPRESSION")
+                        someState
+                        drawCount++
+                    }
+                    .onSizeChanged {
+                        // assert that draw hasn't happened yet
+                        assertEquals(0, drawCount)
+                        someState++
+                    }
+                    .size(10.dp)
+            )
+        }
+        rule.runOnIdle {
+            // assert that state invalidation inside of onSizeChanged
+            // doesn't schedule additional draw
+            assertEquals(1, drawCount)
+        }
+    }
+
+    // captureToImage() requires API level 26
+    @RequiresApi(Build.VERSION_CODES.O)
+    private fun SemanticsNodeInteraction.captureToBitmap() = captureToImage().asAndroidBitmap()
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawReorderingTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawReorderingTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawReorderingTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawReorderingTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/GraphicsLayerModifierTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerModifierTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/GraphicsLayerModifierTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/InvalidatingNotPlacedChildTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/InvalidatingNotPlacedChildTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/InvalidatingNotPlacedChildTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/InvalidatingNotPlacedChildTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/NotHardwareAcceleratedActivityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/NotHardwareAcceleratedActivityTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/NotHardwareAcceleratedActivityTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/NotHardwareAcceleratedActivityTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ShadowTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/ShadowTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ShadowTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/ShadowTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CancelFocusMoveTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/CancelFocusMoveTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CancelFocusMoveTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/CancelFocusMoveTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusExitTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ClearFocusExitTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusExitTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ClearFocusExitTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CombinedFocusModifierNodeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/CombinedFocusModifierNodeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CombinedFocusModifierNodeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/CombinedFocusModifierNodeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ComposeViewKeyEventInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ComposeViewKeyEventInteropTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ComposeViewKeyEventInteropTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ComposeViewKeyEventInteropTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusAggregationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusAggregationTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusAggregationTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusAggregationTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedCountTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusChangedCountTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedCountTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusChangedCountTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt
new file mode 100644
index 0000000..469567e
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt
@@ -0,0 +1,224 @@
+/*
+ * 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.ui.focus
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.focusGroup
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.key
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalFoundationApi
+@OptIn(ExperimentalComposeUiApi::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class FocusRestorerTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun restoresSavedChild() {
+        // Arrange.
+        val (parent, child2) = FocusRequester.createRefs()
+        lateinit var focusManager: FocusManager
+        lateinit var child1State: FocusState
+        lateinit var child2State: FocusState
+        rule.setFocusableContent {
+            focusManager = LocalFocusManager.current
+            Box(
+                Modifier
+                    .size(10.dp)
+                    .focusRequester(parent)
+                    .focusRestorer()
+                    .focusGroup()
+            ) {
+                key(1) {
+                    Box(
+                        Modifier
+                            .size(10.dp)
+                            .onFocusChanged { child1State = it }
+                            .focusTarget()
+                    )
+                }
+                key(2) {
+                    Box(
+                        Modifier
+                            .size(10.dp)
+                            .focusRequester(child2)
+                            .onFocusChanged { child2State = it }
+                            .focusTarget()
+                    )
+                }
+            }
+        }
+        rule.runOnIdle { child2.requestFocus() }
+
+        // Act.
+        rule.runOnIdle { focusManager.clearFocus() }
+        rule.runOnIdle { parent.requestFocus() }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(child1State.isFocused).isFalse()
+            assertThat(child2State.isFocused).isTrue()
+        }
+    }
+
+    @Test
+    fun withoutUniqueKeysRestoresFirstMatchingChild() {
+        // Arrange.
+        val (parent, child2) = FocusRequester.createRefs()
+        lateinit var focusManager: FocusManager
+        lateinit var child1State: FocusState
+        lateinit var child2State: FocusState
+        rule.setFocusableContent {
+            focusManager = LocalFocusManager.current
+            Box(
+                Modifier
+                    .size(10.dp)
+                    .focusRequester(parent)
+                    .focusRestorer()
+                    .focusGroup()
+            ) {
+                Box(
+                    Modifier
+                        .size(10.dp)
+                        .onFocusChanged { child1State = it }
+                        .focusTarget()
+                )
+                Box(
+                    Modifier
+                        .size(10.dp)
+                        .focusRequester(child2)
+                        .onFocusChanged { child2State = it }
+                        .focusTarget()
+                )
+            }
+        }
+        rule.runOnIdle { child2.requestFocus() }
+
+        // Act.
+        rule.runOnIdle { focusManager.clearFocus() }
+        rule.runOnIdle { parent.requestFocus() }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(child1State.isFocused).isTrue()
+            assertThat(child2State.isFocused).isFalse()
+        }
+    }
+
+    @Test
+    fun doesNotRestoreGrandChild_butFocusesOnChildInstead() {
+        // Arrange.
+        val (parent, grandChild) = FocusRequester.createRefs()
+        lateinit var focusManager: FocusManager
+        lateinit var childState: FocusState
+        lateinit var grandChildState: FocusState
+        rule.setFocusableContent {
+            focusManager = LocalFocusManager.current
+            Box(
+                Modifier
+                    .size(10.dp)
+                    .focusRequester(parent)
+                    .focusRestorer()
+                    .focusGroup()
+            ) {
+                Box(
+                    Modifier
+                        .size(10.dp)
+                        .onFocusChanged { childState = it }
+                        .focusTarget()
+                ) {
+                    Box(
+                        Modifier
+                            .size(10.dp)
+                            .focusRequester(grandChild)
+                            .onFocusChanged { grandChildState = it }
+                            .focusTarget()
+                    )
+                }
+            }
+        }
+        rule.runOnIdle { grandChild.requestFocus() }
+
+        // Act.
+        rule.runOnIdle { focusManager.clearFocus() }
+        rule.runOnIdle { parent.requestFocus() }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(childState.isFocused).isTrue()
+            assertThat(grandChildState.isFocused).isFalse()
+        }
+    }
+
+    @Test
+    fun restorationFailed_fallbackToOnRestoreFailedDestination() {
+        // Arrange.
+        val (parent, child2) = FocusRequester.createRefs()
+        lateinit var child1State: FocusState
+        lateinit var child2State: FocusState
+        rule.setFocusableContent {
+            Box(
+                Modifier
+                    .size(10.dp)
+                    .focusRequester(parent)
+                    .focusRestorer { child2 }
+                    .focusGroup()
+            ) {
+                key(1) {
+                    Box(
+                        Modifier
+                            .size(10.dp)
+                            .onFocusChanged { child1State = it }
+                            .focusTarget()
+                    )
+                }
+                key(2) {
+                    Box(
+                        Modifier
+                            .size(10.dp)
+                            .focusRequester(child2)
+                            .onFocusChanged { child2State = it }
+                            .focusTarget()
+                    )
+                }
+            }
+        }
+
+        // Act.
+        rule.runOnIdle { parent.requestFocus() }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(child1State.isFocused).isFalse()
+            assertThat(child2State.isFocused).isTrue()
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusSearchNonPlacedItemsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusSearchNonPlacedItemsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusSearchNonPlacedItemsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusSearchNonPlacedItemsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTransactionsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTransactionsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTransactionsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTransactionsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/KeyEventToFocusDirectionTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/KeyEventToFocusDirectionTest.kt
new file mode 100644
index 0000000..fb9d369
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/KeyEventToFocusDirectionTest.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2021 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.focus
+
+import android.view.KeyEvent as AndroidKeyEvent
+import android.view.KeyEvent.ACTION_DOWN as KeyDown
+import android.view.KeyEvent.META_SHIFT_ON as Shift
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.focus.FocusDirection.Companion.Down
+import androidx.compose.ui.focus.FocusDirection.Companion.Enter
+import androidx.compose.ui.focus.FocusDirection.Companion.Exit
+import androidx.compose.ui.focus.FocusDirection.Companion.Left
+import androidx.compose.ui.focus.FocusDirection.Companion.Next
+import androidx.compose.ui.focus.FocusDirection.Companion.Previous
+import androidx.compose.ui.focus.FocusDirection.Companion.Right
+import androidx.compose.ui.focus.FocusDirection.Companion.Up
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.nativeKeyCode
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalComposeUiApi::class)
+class KeyEventToFocusDirectionTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    private lateinit var owner: Owner
+
+    @Before
+    fun setup() {
+        rule.setContent {
+            owner = LocalView.current as Owner
+        }
+    }
+
+    @Test
+    fun left() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionLeft.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        assertThat(focusDirection).isEqualTo(Left)
+    }
+
+    @Test
+    fun right() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionRight.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        assertThat(focusDirection).isEqualTo(Right)
+    }
+
+    @Test
+    fun up() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionUp.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        assertThat(focusDirection).isEqualTo(Up)
+    }
+
+    @Test
+    fun down() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionDown.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        assertThat(focusDirection).isEqualTo(Down)
+    }
+
+    @Test
+    fun page_up() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.PageUp.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        assertThat(focusDirection).isEqualTo(Up)
+    }
+
+    @Test
+    fun page_down() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.PageDown.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        assertThat(focusDirection).isEqualTo(Down)
+    }
+
+    @Test
+    fun tab_next() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.Tab.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        assertThat(focusDirection).isEqualTo(Next)
+    }
+
+    @Test
+    fun shiftTab_previous() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(0L, 0L, KeyDown, Key.Tab.nativeKeyCode, 0, Shift))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        assertThat(focusDirection).isEqualTo(Previous)
+    }
+
+    @Test
+    fun dpadCenter_enter() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionCenter.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        @OptIn(ExperimentalComposeUiApi::class)
+        assertThat(focusDirection).isEqualTo(Enter)
+    }
+
+    @Test
+    fun enter_enter() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.Enter.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        @OptIn(ExperimentalComposeUiApi::class)
+        assertThat(focusDirection).isEqualTo(Enter)
+    }
+
+    @Test
+    fun numPadEnter_enter() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.NumPadEnter.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        @OptIn(ExperimentalComposeUiApi::class)
+        assertThat(focusDirection).isEqualTo(Enter)
+    }
+
+    @Test
+    fun back_exit() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.Back.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        @OptIn(ExperimentalComposeUiApi::class)
+        assertThat(focusDirection).isEqualTo(Exit)
+    }
+
+    @Test
+    fun esc_exit() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.Escape.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        @OptIn(ExperimentalComposeUiApi::class)
+        assertThat(focusDirection).isEqualTo(Exit)
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusEnterExitTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/RequestFocusEnterExitTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusEnterExitTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/RequestFocusEnterExitTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusEnterTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/RequestFocusEnterTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusEnterTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/RequestFocusEnterTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusExitTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/RequestFocusExitTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusExitTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/RequestFocusExitTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RestoreFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/RestoreFocusTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RestoreFocusTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/RestoreFocusTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalEnterTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalEnterTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalEnterTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalEnterTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalExitTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalExitTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalExitTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalExitTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalImplicitEnterTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalImplicitEnterTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalImplicitEnterTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalImplicitEnterTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalImplicitExitTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalImplicitExitTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalImplicitExitTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalImplicitExitTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocusTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocusTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocusTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/gesture/Utils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/gesture/Utils.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/gesture/Utils.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/gesture/Utils.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
new file mode 100644
index 0000000..6edf095
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -0,0 +1,1478 @@
+/*
+ * Copyright 2019 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.graphics.vector
+
+import android.app.Application
+import android.content.ComponentCallbacks2
+import android.content.pm.ActivityInfo
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.os.Build
+import androidx.activity.ComponentActivity
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.AtLeastSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.background
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.paint
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.CompositingStrategy
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.ImageBitmapConfig
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.graphics.toPixelMap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalImageVectorCache
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.ImageVectorCache
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.tests.R
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Assert
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class VectorTest {
+
+    @get:Rule
+    val rule = createAndroidComposeRule<ComponentActivity>()
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorTint() {
+        rule.setContent {
+            VectorTint()
+        }
+
+        takeScreenShot(200).apply {
+            assertEquals(getPixel(100, 100), Color.Cyan.toArgb())
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorIntrinsicTint() {
+        rule.setContent {
+            val background = Modifier.paint(
+                createTestVectorPainter(200, Color.Magenta),
+                alignment = Alignment.Center
+            )
+            AtLeastSize(size = 200, modifier = background) {
+            }
+        }
+        takeScreenShot(200).apply {
+            assertEquals(getPixel(100, 100), Color.Magenta.toArgb())
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorIntrinsicTintFirstFrame() {
+        var vector: VectorPainter? = null
+        rule.setContent {
+            vector = createTestVectorPainter(200, Color.Magenta)
+
+            val bitmap = remember {
+                val bitmap = ImageBitmap(200, 200)
+                val canvas = Canvas(bitmap)
+                val bitmapSize = Size(200f, 200f)
+                CanvasDrawScope().draw(
+                    Density(1f),
+                    LayoutDirection.Ltr,
+                    canvas,
+                    bitmapSize
+                ) {
+                    with(vector!!) {
+                        draw(bitmapSize)
+                    }
+                }
+                bitmap
+            }
+
+            val background = Modifier.paint(BitmapPainter(bitmap))
+
+            AtLeastSize(size = 200, modifier = background) {
+            }
+        }
+        takeScreenShot(200).apply {
+            assertEquals(getPixel(100, 100), Color.Magenta.toArgb())
+        }
+        assertEquals(ImageBitmapConfig.Alpha8, vector!!.bitmapConfig)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorAlignment() {
+        rule.setContent {
+            VectorTint(minimumSize = 450, alignment = Alignment.BottomEnd)
+        }
+
+        takeScreenShot(450).apply {
+            assertEquals(getPixel(430, 430), Color.Cyan.toArgb())
+        }
+    }
+
+    @Test
+    fun testVectorSkipsRecompositionOnNoChange() {
+        val state = mutableIntStateOf(0)
+        var composeCount = 0
+        var vectorComposeCount = 0
+
+        val composeVector: @Composable @VectorComposable (Float, Float) -> Unit = {
+                viewportWidth, viewportHeight ->
+
+            vectorComposeCount++
+            Path(
+                fill = SolidColor(Color.Blue),
+                pathData = PathData {
+                    lineTo(viewportWidth, 0f)
+                    lineTo(viewportWidth, viewportHeight)
+                    lineTo(0f, viewportHeight)
+                    close()
+                }
+            )
+        }
+
+        rule.setContent {
+            composeCount++
+            // Arbitrary read to force composition here and verify the subcomposition below skips
+            state.value
+            val vectorPainter = rememberVectorPainter(
+                defaultWidth = 10.dp,
+                defaultHeight = 10.dp,
+                autoMirror = false,
+                content = composeVector
+            )
+            Image(
+                vectorPainter,
+                null,
+                modifier = Modifier.size(20.dp)
+            )
+        }
+
+        state.value = 1
+        rule.waitForIdle()
+        assertEquals(2, composeCount) // Arbitrary state read should compose twice
+        assertEquals(1, vectorComposeCount) // Vector is identical so should compose once
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorInvalidation() {
+        val testCase = VectorInvalidationTestCase()
+        rule.setContent {
+            testCase.TestVector()
+        }
+
+        rule.waitUntil { testCase.measured }
+        val size = testCase.vectorSize
+        takeScreenShot(size).apply {
+            assertEquals(Color.Blue.toArgb(), getPixel(5, size - 5))
+            assertEquals(Color.White.toArgb(), getPixel(size - 5, 5))
+        }
+
+        testCase.measured = false
+        rule.runOnUiThread {
+            testCase.toggle()
+        }
+
+        rule.waitUntil { testCase.measured }
+
+        takeScreenShot(size).apply {
+            assertEquals(Color.White.toArgb(), getPixel(5, size - 5))
+            assertEquals(Color.Red.toArgb(), getPixel(size - 5, 5))
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorRendersOnceOnFirstFrame() {
+        var drawCount = 0
+        val testTag = "TestTag"
+        rule.setContent {
+            Box(modifier = Modifier
+                .wrapContentSize()
+                .drawBehind {
+                    drawCount++
+                }
+                .paint(painterResource(R.drawable.ic_triangle2))
+                .testTag(testTag))
+        }
+
+        rule.onNodeWithTag(testTag).captureToImage().toPixelMap().apply {
+            assertEquals(1, drawCount)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorClipPath() {
+        rule.setContent {
+            VectorClip()
+        }
+
+        takeScreenShot(200).apply {
+            assertEquals(getPixel(100, 50), Color.Cyan.toArgb())
+            assertEquals(getPixel(100, 150), Color.Black.toArgb())
+        }
+    }
+
+    @Test
+    fun testVectorZeroSizeDoesNotCrash() {
+        // Make sure that if we are given the size of zero we should not crash and instead
+        // act as a no-op
+        rule.setContent {
+            Box(modifier = Modifier.size(0.dp).paint(createTestVectorPainter()))
+        }
+    }
+
+    @Test
+    fun testVectorZeroWidthDoesNotCrash() {
+        rule.setContent {
+            Box(
+                modifier = Modifier.width(0.dp).height(100.dp).paint
+                (createTestVectorPainter())
+            )
+        }
+    }
+
+    @Test
+    fun testVectorZeroHeightDoesNotCrash() {
+        rule.setContent {
+            Box(
+                modifier = Modifier.width(50.dp).height(0.dp).paint(
+                    createTestVectorPainter()
+                )
+            )
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorTrimPath() {
+        rule.setContent {
+            VectorTrim()
+        }
+
+        takeScreenShot(200).apply {
+            assertEquals(Color.Yellow.toArgb(), getPixel(25, 100))
+            assertEquals(Color.Blue.toArgb(), getPixel(100, 100))
+            assertEquals(Color.Yellow.toArgb(), getPixel(175, 100))
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testImageVectorChangeOnStateChange() {
+        val defaultWidth = 48.dp
+        val defaultHeight = 48.dp
+        val viewportWidth = 24f
+        val viewportHeight = 24f
+
+        val icon1 = ImageVector.Builder(
+            defaultWidth = defaultWidth,
+            defaultHeight = defaultHeight,
+            viewportWidth = viewportWidth,
+            viewportHeight = viewportHeight
+        )
+            .addPath(
+                fill = SolidColor(Color.Black),
+                pathData = PathData {
+                    lineTo(viewportWidth, 0f)
+                    lineTo(viewportWidth, viewportHeight)
+                    lineTo(0f, 0f)
+                    close()
+                }
+            ).build()
+
+        val icon2 = ImageVector.Builder(
+            defaultWidth = defaultWidth,
+            defaultHeight = defaultHeight,
+            viewportWidth = viewportWidth,
+            viewportHeight = viewportHeight
+        )
+            .addPath(
+                fill = SolidColor(Color.Black),
+                pathData = PathData {
+                    lineTo(0f, viewportHeight)
+                    lineTo(viewportWidth, viewportHeight)
+                    lineTo(0f, 0f)
+                    close()
+                }
+            ).build()
+
+        val testTag = "iconClick"
+        rule.setContent {
+            val clickState = remember { mutableStateOf(false) }
+            Image(
+                imageVector = if (clickState.value) icon1 else icon2,
+                contentDescription = null,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .size(icon1.defaultWidth, icon1.defaultHeight)
+                    .background(Color.Red)
+                    .clickable { clickState.value = !clickState.value },
+                alignment = Alignment.TopStart,
+                contentScale = ContentScale.FillHeight
+            )
+        }
+
+        rule.onNodeWithTag(testTag).apply {
+            captureToImage().asAndroidBitmap().apply {
+                assertEquals(Color.Red.toArgb(), getPixel(width - 2, 0))
+                assertEquals(Color.Red.toArgb(), getPixel(2, 0))
+                assertEquals(Color.Red.toArgb(), getPixel(width - 1, height - 4))
+
+                assertEquals(Color.Black.toArgb(), getPixel(0, 2))
+                assertEquals(Color.Black.toArgb(), getPixel(0, height - 2))
+                assertEquals(Color.Black.toArgb(), getPixel(width - 4, height - 2))
+            }
+            performClick()
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(testTag).captureToImage().asAndroidBitmap().apply {
+            assertEquals(Color.Black.toArgb(), getPixel(width - 2, 0))
+            assertEquals(Color.Black.toArgb(), getPixel(2, 0))
+            assertEquals(Color.Black.toArgb(), getPixel(width - 1, height - 4))
+
+            assertEquals(Color.Red.toArgb(), getPixel(0, 2))
+            assertEquals(Color.Red.toArgb(), getPixel(0, height - 2))
+            assertEquals(Color.Red.toArgb(), getPixel(width - 4, height - 2))
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testDrawWithoutColorFilterAfterPreviouslyConfigured() {
+        val defaultWidth = 24.dp
+        val defaultHeight = 24.dp
+        val testTag = "testTag"
+        var vectorPainter: VectorPainter? = null
+
+        var tint: ColorFilter? by mutableStateOf(ColorFilter.tint(Color.Green))
+        rule.setContent {
+            vectorPainter = rememberVectorPainter(
+                defaultWidth = defaultWidth,
+                defaultHeight = defaultHeight,
+                autoMirror = false
+            ) { viewportWidth, viewportHeight ->
+                Path(
+                    fill = SolidColor(Color.Blue),
+                    pathData = PathData {
+                        lineTo(viewportWidth, 0f)
+                        lineTo(viewportWidth, viewportHeight)
+                        lineTo(0f, viewportHeight)
+                        close()
+                    }
+                )
+            }
+            Image(
+                painter = vectorPainter!!,
+                contentDescription = null,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .background(Color.Red),
+                contentScale = ContentScale.FillBounds,
+                colorFilter = tint
+            )
+        }
+
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Green }
+
+        tint = null
+        rule.waitForIdle()
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
+        assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testDrawWithColorFilterAfterNotPreviouslyConfigured() {
+        val defaultWidth = 24.dp
+        val defaultHeight = 24.dp
+        val testTag = "testTag"
+        var vectorPainter: VectorPainter? = null
+
+        var tint: ColorFilter? by mutableStateOf(null)
+        rule.setContent {
+            vectorPainter = rememberVectorPainter(
+                defaultWidth = defaultWidth,
+                defaultHeight = defaultHeight,
+                autoMirror = false
+            ) { viewportWidth, viewportHeight ->
+                Path(
+                    fill = SolidColor(Color.Blue),
+                    pathData = PathData {
+                        lineTo(viewportWidth, 0f)
+                        lineTo(viewportWidth, viewportHeight)
+                        lineTo(0f, viewportHeight)
+                        close()
+                    }
+                )
+            }
+            Image(
+                painter = vectorPainter!!,
+                contentDescription = null,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .background(Color.Red),
+                contentScale = ContentScale.FillBounds,
+                colorFilter = tint
+            )
+        }
+
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
+
+        tint = ColorFilter.tint(Color.Green)
+        rule.waitForIdle()
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Green }
+        assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicClearBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Clear)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicSrcBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Src)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicDstBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Dst)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicSrcOverBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.SrcOver, expectedConfig = ImageBitmapConfig.Alpha8)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicDstOverBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.DstOver)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicSrcInBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.SrcIn, expectedConfig = ImageBitmapConfig.Alpha8)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicDstInBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.DstIn)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicSrcOutBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.SrcOut)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicDstOutBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.DstOut)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicSrcAtopBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.SrcAtop)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicDstAtopBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.DstAtop)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicXorBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Xor)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicPlusBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Plus)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicModulateBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Modulate)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicScreenBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Screen)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicOverlayBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Overlay)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicDarkenBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Darken)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicLightenBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Lighten)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicColorDodgeBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.ColorDodge)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicColorBurnBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.ColorBurn)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicHardlightBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Hardlight)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicSoftLightBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Softlight)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicDifferenceBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Difference)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicExclusionBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Exclusion)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicMultiplyBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Multiply)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicHueBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Hue)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicSaturationBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Saturation)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicColorBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Color)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithIntrinsicLuminosityBlendMode() {
+        verifyAlphaMaskWithBlendModes(BlendMode.Luminosity)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawClearBlendMode() {
+        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Clear))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawSrcBlendMode() {
+        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Src))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawDstBlendMode() {
+        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Dst))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawSrcOverBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.SrcOver),
+            expectedConfig = ImageBitmapConfig.Alpha8
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawDstOverBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.DstOver))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawSrcInBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.SrcIn),
+            expectedConfig = ImageBitmapConfig.Alpha8
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawDstInBlendMode() {
+        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.DstIn))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawSrcOutBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.SrcOut))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawDstOutBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.DstOut))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawSrcAtopBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.SrcAtop))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawDstAtopBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.DstAtop))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawXorBlendMode() {
+        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Xor))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawPlusBlendMode() {
+        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Plus))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawModulateBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Modulate))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawScreenBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Screen))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawOverlayBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Overlay))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawDarkenBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Darken))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawLightenBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Lighten))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawColorDodgeBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.ColorDodge))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawColorBurnBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.ColorBurn))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawHardlightBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Hardlight))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawSoftLightBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Softlight))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawDifferenceBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Difference))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawExclusionBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Exclusion))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawMultiplyBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Multiply))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawHueBlendMode() {
+        verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Hue))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawSaturationBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Saturation))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawColorBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Color))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testAlphaMaskWithDrawLuminosityBlendMode() {
+        verifyAlphaMaskWithBlendModes(
+            colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Luminosity))
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    private fun verifyAlphaMaskWithBlendModes(
+        intrinsicBlendMode: BlendMode = BlendMode.SrcIn,
+        colorFilter: ColorFilter? = null,
+        expectedConfig: ImageBitmapConfig? = null,
+    ) {
+        val defaultWidth = 24.dp
+        val defaultHeight = 24.dp
+        val testTag = "testTag"
+        var vectorPainter: VectorPainter? = null
+
+        // Create a gradient of the same color as a solid in order to verify behavior
+        // of intrinsic color filter usage both with and without the optimization to tint
+        // use a tinted alpha channel bitmap instead of a ARGB8888
+        val solidBlueGradient = Brush.horizontalGradient(listOf(Color.Blue, Color.Blue))
+        val solidBlueColor = SolidColor(Color.Blue)
+        var targetBrush: Brush by mutableStateOf(solidBlueColor)
+        rule.setContent {
+            vectorPainter = rememberVectorPainter(
+                defaultWidth = defaultWidth,
+                defaultHeight = defaultHeight,
+                tintColor = Color.Cyan,
+                tintBlendMode = intrinsicBlendMode,
+                autoMirror = false
+            ) { viewportWidth, viewportHeight ->
+                Path(
+                    fill = targetBrush,
+                    pathData = PathData {
+                        lineTo(viewportWidth, 0f)
+                        lineTo(viewportWidth, viewportHeight)
+                        lineTo(0f, viewportHeight)
+                        close()
+                    }
+                )
+            }
+            Image(
+                painter = vectorPainter!!,
+                contentDescription = null,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .background(
+                        Brush.horizontalGradient(
+                            listOf(Color.Transparent, Color.Yellow, Color.Transparent)
+                        )
+                    )
+                    .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen },
+                contentScale = ContentScale.FillBounds,
+                colorFilter = colorFilter
+            )
+        }
+
+        rule.waitForIdle()
+
+        val solidBrushImage = rule.onNodeWithTag(testTag).captureToImage()
+        if (expectedConfig != null) {
+            assertEquals(expectedConfig, vectorPainter!!.bitmapConfig)
+        }
+
+        targetBrush = solidBlueGradient
+        rule.waitForIdle()
+
+        val gradientBrushImage = rule.onNodeWithTag(testTag).captureToImage()
+
+        assertArrayEquals(
+            "Optimized vector does not match expected for $intrinsicBlendMode",
+            gradientBrushImage.toPixelMap().buffer,
+            solidBrushImage.toPixelMap().buffer
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testPathColorChangeUpdatesBitmapConfig() {
+        val defaultWidth = 24.dp
+        val defaultHeight = 24.dp
+        val testTag = "testTag"
+        var vectorPainter: VectorPainter? = null
+        var brush: Brush by mutableStateOf(SolidColor(Color.Blue))
+        rule.setContent {
+            vectorPainter = rememberVectorPainter(
+                defaultWidth = defaultWidth,
+                defaultHeight = defaultHeight,
+                autoMirror = false
+            ) { viewportWidth, viewportHeight ->
+                Path(
+                    fill = brush,
+                    pathData = PathData {
+                        lineTo(viewportWidth, 0f)
+                        lineTo(viewportWidth, viewportHeight)
+                        lineTo(0f, viewportHeight)
+                        close()
+                    }
+                )
+            }
+            Image(
+                painter = vectorPainter!!,
+                contentDescription = null,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .size(defaultWidth * 8, defaultHeight * 2)
+                    .background(Color.Red),
+                contentScale = ContentScale.FillBounds
+            )
+        }
+
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
+        assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig)
+
+        brush = Brush.horizontalGradient(listOf(Color.Blue, Color.Blue))
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
+        assertEquals(ImageBitmapConfig.Argb8888, vectorPainter!!.bitmapConfig)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testGroupPathColorChangeUpdatesBitmapConfig() {
+        val defaultWidth = 24.dp
+        val defaultHeight = 24.dp
+        val testTag = "testTag"
+        var vectorPainter: VectorPainter? = null
+        var brush: Brush by mutableStateOf(SolidColor(Color.Blue))
+        rule.setContent {
+            vectorPainter = rememberVectorPainter(
+                defaultWidth = defaultWidth,
+                defaultHeight = defaultHeight,
+                autoMirror = false
+            ) { viewportWidth, viewportHeight ->
+                Group {
+                    Path(
+                        fill = brush,
+                        pathData = PathData {
+                            lineTo(viewportWidth, 0f)
+                            lineTo(viewportWidth, viewportHeight)
+                            lineTo(0f, viewportHeight)
+                            close()
+                        }
+                    )
+                }
+            }
+            Image(
+                painter = vectorPainter!!,
+                contentDescription = null,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .size(defaultWidth * 8, defaultHeight * 2)
+                    .background(Color.Red),
+                contentScale = ContentScale.FillBounds
+            )
+        }
+
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
+        assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig)
+
+        brush = Brush.horizontalGradient(listOf(Color.Blue, Color.Blue))
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
+        assertEquals(ImageBitmapConfig.Argb8888, vectorPainter!!.bitmapConfig)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorScaleNonUniformly() {
+        val defaultWidth = 24.dp
+        val defaultHeight = 24.dp
+        val testTag = "testTag"
+        var vectorPainter: VectorPainter? = null
+        rule.setContent {
+            vectorPainter = rememberVectorPainter(
+                defaultWidth = defaultWidth,
+                defaultHeight = defaultHeight,
+                autoMirror = false
+            ) { viewportWidth, viewportHeight ->
+                Path(
+                    fill = SolidColor(Color.Blue),
+                    pathData = PathData {
+                        lineTo(viewportWidth, 0f)
+                        lineTo(viewportWidth, viewportHeight)
+                        lineTo(0f, viewportHeight)
+                        close()
+                    }
+                )
+            }
+            Image(
+                painter = vectorPainter!!,
+                contentDescription = null,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .size(defaultWidth * 8, defaultHeight * 2)
+                    .background(Color.Red),
+                contentScale = ContentScale.FillBounds
+            )
+        }
+
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
+        assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorChangeSize() {
+        val size = mutableStateOf(200)
+        val color = mutableStateOf(Color.Magenta)
+
+        rule.setContent {
+            val background = Modifier.background(Color.Red).paint(
+                createTestVectorPainter(size.value, color.value),
+                alignment = Alignment.TopStart
+            )
+            AtLeastSize(size = 400, modifier = background) {
+            }
+        }
+
+        takeScreenShot(400).apply {
+            assertEquals(getPixel(100, 100), Color.Magenta.toArgb())
+            assertEquals(getPixel(300, 300), Color.Red.toArgb())
+        }
+
+        size.value = 400
+        color.value = Color.Cyan
+
+        takeScreenShot(400).apply {
+            assertEquals(getPixel(100, 100), Color.Cyan.toArgb())
+            assertEquals(getPixel(300, 300), Color.Cyan.toArgb())
+        }
+
+        size.value = 50
+        color.value = Color.Yellow
+
+        takeScreenShot(400).apply {
+            assertEquals(getPixel(10, 10), Color.Yellow.toArgb())
+            assertEquals(getPixel(100, 100), Color.Red.toArgb())
+            assertEquals(getPixel(300, 300), Color.Red.toArgb())
+        }
+    }
+
+    @Test
+    fun testImageVectorCacheHit() {
+        var vectorInCache = false
+        rule.setContent {
+            val theme = LocalContext.current.theme
+            val density = LocalDensity.current
+            val imageVectorCache = LocalImageVectorCache.current
+            imageVectorCache.clear()
+            Image(
+                painterResource(R.drawable.ic_triangle),
+                contentDescription = null
+            )
+
+            val key = ImageVectorCache.Key(theme, R.drawable.ic_triangle, density)
+            vectorInCache = imageVectorCache[key] != null
+        }
+
+        assertTrue(vectorInCache)
+    }
+
+    @Test
+    fun testVectorPainterCacheHit() {
+        var vectorInCache = false
+        rule.setContent {
+            // obtaining the same painter resource should return the same instance root
+            // GroupComponent
+            val painter1 = painterResource(R.drawable.ic_triangle) as VectorPainter
+            val painter2 = painterResource(R.drawable.ic_triangle) as VectorPainter
+            vectorInCache = painter1.vector.root === painter2.vector.root
+        }
+
+        assertTrue(vectorInCache)
+    }
+
+    @Test
+    fun testImageVectorCacheCleared() {
+        var vectorInCache = false
+        var application: Application? = null
+        var theme: Resources.Theme? = null
+        var vectorCache: ImageVectorCache? = null
+        var density: Density? = null
+        rule.setContent {
+            application = LocalContext.current.applicationContext as Application
+            density = LocalDensity.current
+            theme = LocalContext.current.theme
+            val imageVectorCache = LocalImageVectorCache.current
+            imageVectorCache.clear()
+            Image(
+                painterResource(R.drawable.ic_triangle),
+                contentDescription = null
+            )
+
+            val key = ImageVectorCache.Key(theme!!, R.drawable.ic_triangle, density!!)
+            vectorInCache = imageVectorCache[key] != null
+
+            vectorCache = imageVectorCache
+        }
+
+        application?.onTrimMemory(0)
+
+        val cacheCleared = vectorCache?.let {
+            it[ImageVectorCache.Key(theme!!, R.drawable.ic_triangle, density!!)] == null
+        } ?: false
+
+        assertTrue("Vector was not inserted in cache after initial creation", vectorInCache)
+        assertTrue("Cache was not cleared after trim memory call", cacheCleared)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testImageVectorConfigChange() {
+        val tag = "testTag"
+        rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+
+        val latch = CountDownLatch(1)
+
+        rule.activity.application.registerComponentCallbacks(object : ComponentCallbacks2 {
+            override fun onConfigurationChanged(p0: Configuration) {
+                latch.countDown()
+            }
+
+            override fun onLowMemory() {
+                // NO-OP
+            }
+
+            override fun onTrimMemory(p0: Int) {
+                // NO-OP
+            }
+        })
+
+        try {
+            latch.await(1500, TimeUnit.MILLISECONDS)
+            rule.setContent {
+                Image(
+                    painterResource(R.drawable.ic_triangle_config),
+                    contentDescription = null,
+                    modifier = Modifier.testTag(tag)
+                )
+            }
+            rule.onNodeWithTag(tag).captureToImage().apply {
+                assertEquals(Color.Blue, toPixelMap()[width - 5, 5])
+            }
+        } catch (e: InterruptedException) {
+            fail("Unable to verify vector asset in landscape orientation")
+        } finally {
+            rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorMirror() {
+        val tag = "mirroredVector"
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                Image(
+                    painter = VectorMirror(20),
+                    contentDescription = null,
+                    modifier = Modifier.testTag(tag)
+                )
+            }
+        }
+        rule.onNodeWithTag(tag).captureToImage().toPixelMap().apply {
+            assertEquals(Color.Blue, this[2, 2])
+            assertEquals(Color.Blue, this[2, height - 3])
+            assertEquals(Color.Blue, this[width / 2 - 3, 2])
+            assertEquals(Color.Blue, this[width / 2 - 3, height - 3])
+
+            assertEquals(Color.Red, this[width - 3, 2])
+            assertEquals(Color.Red, this[width - 3, height - 3])
+            assertEquals(Color.Red, this[width / 2 + 3, 2])
+            assertEquals(Color.Red, this[width / 2 + 3, height - 3])
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorStrokeWidth() {
+        val strokeWidth = mutableStateOf(100)
+        rule.setContent {
+            VectorStroke(strokeWidth = strokeWidth.value)
+        }
+        takeScreenShot(200).apply {
+            assertEquals(Color.Yellow.toArgb(), getPixel(100, 25))
+            assertEquals(Color.Blue.toArgb(), getPixel(100, 75))
+        }
+        rule.runOnUiThread { strokeWidth.value = 200 }
+        rule.waitForIdle()
+        takeScreenShot(200).apply {
+            assertEquals(Color.Yellow.toArgb(), getPixel(100, 25))
+            assertEquals(Color.Yellow.toArgb(), getPixel(100, 75))
+        }
+    }
+
+    @Composable
+    private fun VectorTint(
+        size: Int = 200,
+        minimumSize: Int = size,
+        alignment: Alignment = Alignment.Center
+    ) {
+        val background = Modifier.paint(
+            createTestVectorPainter(size),
+            colorFilter = ColorFilter.tint(Color.Cyan),
+            alignment = alignment
+        )
+        AtLeastSize(size = minimumSize, modifier = background) {
+        }
+    }
+
+    @Composable
+    private fun createTestVectorPainter(
+        size: Int = 200,
+        tintColor: Color = Color.Unspecified
+    ): VectorPainter {
+        val sizePx = size.toFloat()
+        val sizeDp = (size / LocalDensity.current.density).dp
+        return rememberVectorPainter(
+            defaultWidth = sizeDp,
+            defaultHeight = sizeDp,
+            autoMirror = false,
+            content = { _, _ ->
+                Path(
+                    pathData = PathData {
+                        lineTo(sizePx, 0.0f)
+                        lineTo(sizePx, sizePx)
+                        lineTo(0.0f, sizePx)
+                        close()
+                    },
+                    fill = SolidColor(Color.Black)
+                )
+            },
+            tintColor = tintColor
+        )
+    }
+
+    @Composable
+    private fun VectorClip(
+        size: Int = 200,
+        minimumSize: Int = size,
+        alignment: Alignment = Alignment.Center
+    ) {
+        val sizePx = size.toFloat()
+        val sizeDp = (size / LocalDensity.current.density).dp
+        val background = Modifier.paint(
+            rememberVectorPainter(
+                defaultWidth = sizeDp,
+                defaultHeight = sizeDp,
+                autoMirror = false
+            ) { _, _ ->
+                Path(
+                    // Cyan background.
+                    pathData = PathData {
+                        lineTo(sizePx, 0.0f)
+                        lineTo(sizePx, sizePx)
+                        lineTo(0.0f, sizePx)
+                        close()
+                    },
+                    fill = SolidColor(Color.Cyan)
+                )
+                Group(
+                    // Only show the top half...
+                    clipPathData = PathData {
+                        lineTo(sizePx, 0.0f)
+                        lineTo(sizePx, sizePx / 2)
+                        lineTo(0.0f, sizePx / 2)
+                        close()
+                    },
+                    // And rotate it, resulting in the bottom half being black.
+                    pivotX = sizePx / 2,
+                    pivotY = sizePx / 2,
+                    rotation = 180f
+                ) {
+                    Path(
+                        pathData = PathData {
+                            lineTo(sizePx, 0.0f)
+                            lineTo(sizePx, sizePx)
+                            lineTo(0.0f, sizePx)
+                            close()
+                        },
+                        fill = SolidColor(Color.Black)
+                    )
+                }
+            },
+            alignment = alignment
+        )
+        AtLeastSize(size = minimumSize, modifier = background) {
+        }
+    }
+
+    @Composable
+    private fun VectorTrim(
+        size: Int = 200,
+        minimumSize: Int = size,
+        alignment: Alignment = Alignment.Center
+    ) {
+        val sizePx = size.toFloat()
+        val sizeDp = (size / LocalDensity.current.density).dp
+        val background = Modifier.paint(
+            rememberVectorPainter(
+                defaultWidth = sizeDp,
+                defaultHeight = sizeDp,
+                autoMirror = false
+            ) { _, _ ->
+                Path(
+                    pathData = PathData {
+                        lineTo(sizePx, 0.0f)
+                        lineTo(sizePx, sizePx)
+                        lineTo(0.0f, sizePx)
+                        close()
+                    },
+                    fill = SolidColor(Color.Blue)
+                )
+                // A thick stroke
+                Path(
+                    pathData = PathData {
+                        moveTo(0.0f, sizePx / 2)
+                        lineTo(sizePx, sizePx / 2)
+                    },
+                    stroke = SolidColor(Color.Yellow),
+                    strokeLineWidth = sizePx / 2,
+                    trimPathStart = 0.25f,
+                    trimPathEnd = 0.75f,
+                    trimPathOffset = 0.5f
+                )
+            },
+            alignment = alignment
+        )
+        AtLeastSize(size = minimumSize, modifier = background) {
+        }
+    }
+
+    @Composable
+    private fun VectorStroke(
+        size: Int = 200,
+        strokeWidth: Int = 100,
+        minimumSize: Int = size,
+        alignment: Alignment = Alignment.Center
+    ) {
+        val sizePx = size.toFloat()
+        val sizeDp = (size / LocalDensity.current.density).dp
+        val strokeWidthPx = strokeWidth.toFloat()
+        val background = Modifier.paint(
+            rememberVectorPainter(
+                defaultWidth = sizeDp,
+                defaultHeight = sizeDp,
+                autoMirror = false
+            ) { _, _ ->
+                Path(
+                    pathData = PathData {
+                        lineTo(sizePx, 0.0f)
+                        lineTo(sizePx, sizePx)
+                        lineTo(0.0f, sizePx)
+                        close()
+                    },
+                    fill = SolidColor(Color.Blue)
+                )
+                // A thick stroke
+                Path(
+                    pathData = PathData {
+                        moveTo(0.0f, 0.0f)
+                        lineTo(sizePx, 0.0f)
+                    },
+                    stroke = SolidColor(Color.Yellow),
+                    strokeLineWidth = strokeWidthPx,
+                )
+            },
+            alignment = alignment
+        )
+        AtLeastSize(size = minimumSize, modifier = background) {
+        }
+    }
+
+    @Composable
+    private fun VectorMirror(size: Int): VectorPainter {
+        val sizePx = size.toFloat()
+        val sizeDp = (size / LocalDensity.current.density).dp
+        return rememberVectorPainter(
+                defaultWidth = sizeDp,
+                defaultHeight = sizeDp,
+                autoMirror = true
+            ) { _, _ ->
+                Path(
+                    pathData = PathData {
+                        lineTo(sizePx / 2, 0f)
+                        lineTo(sizePx / 2, sizePx)
+                        lineTo(0f, sizePx)
+                        close()
+                    },
+                    fill = SolidColor(Color.Red)
+                )
+
+                Path(
+                    pathData = PathData {
+                        moveTo(sizePx / 2, 0f)
+                        lineTo(sizePx, 0f)
+                        lineTo(sizePx, sizePx)
+                        lineTo(sizePx / 2, sizePx)
+                        close()
+                    },
+                    fill = SolidColor(Color.Blue)
+                )
+            }
+    }
+
+    // captureToImage() requires API level 26
+    @RequiresApi(Build.VERSION_CODES.O)
+    private fun takeScreenShot(width: Int, height: Int = width): Bitmap {
+        val bitmap = rule.onRoot().captureToImage().asAndroidBitmap()
+        Assert.assertEquals(width, bitmap.width)
+        Assert.assertEquals(height, bitmap.height)
+        return bitmap
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/ClickNotPlacedChildTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/ClickNotPlacedChildTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/ClickNotPlacedChildTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/ClickNotPlacedChildTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/CursorAnchorInfoBuilderTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/CursorAnchorInfoBuilderTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/CursorAnchorInfoBuilderTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/CursorAnchorInfoBuilderTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/EditorInfoTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/EditorInfoTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/EditorInfoTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/EditorInfoTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/InputModeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/InputModeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/InputModeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/InputModeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/RecordingInputConnectionTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/RecordingInputConnectionTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/RecordingInputConnectionTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/RecordingInputConnectionTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidCursorAnchorInfoTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidCursorAnchorInfoTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidCursorAnchorInfoTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidCursorAnchorInfoTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidEmojiTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidEmojiTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidEmojiTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidEmojiTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidOnStateUpdateTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidOnStateUpdateTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidOnStateUpdateTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidOnStateUpdateTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/AndroidProcessKeyInputTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/AndroidProcessKeyInputTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/AndroidProcessKeyInputTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/AndroidProcessKeyInputTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/HardwareKeyInputTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/HardwareKeyInputTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/HardwareKeyInputTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/HardwareKeyInputTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/KeyTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/KeyTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/KeyTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/KeyTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/MetaKeyTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/MetaKeyTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/MetaKeyTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/MetaKeyTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/ClipPointerInputTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/ClipPointerInputTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/ClipPointerInputTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/ClipPointerInputTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/LayerTouchTransformTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/LayerTouchTransformTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/LayerTouchTransformTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/LayerTouchTransformTest.kt
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapterTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapterTest.kt
new file mode 100644
index 0000000..77cdb77
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapterTest.kt
@@ -0,0 +1,1853 @@
+/*
+ * Copyright 2019 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.input.pointer
+
+import android.util.SparseLongArray
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_CANCEL
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_HOVER_ENTER
+import android.view.MotionEvent.ACTION_HOVER_EXIT
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_POINTER_DOWN
+import android.view.MotionEvent.ACTION_POINTER_UP
+import android.view.MotionEvent.ACTION_SCROLL
+import android.view.MotionEvent.ACTION_UP
+import android.view.MotionEvent.AXIS_HSCROLL
+import android.view.MotionEvent.AXIS_VSCROLL
+import android.view.MotionEvent.TOOL_TYPE_FINGER
+import android.view.MotionEvent.TOOL_TYPE_MOUSE
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Matrix
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MotionEventAdapterTest {
+
+    private lateinit var motionEventAdapter: MotionEventAdapter
+    private val positionCalculator = object : PositionCalculator {
+        override fun screenToLocal(positionOnScreen: Offset): Offset = positionOnScreen
+
+        override fun localToScreen(localPosition: Offset): Offset = localPosition
+
+        override fun localToScreen(localTransform: Matrix) {}
+    }
+
+    @Before
+    fun setup() {
+        motionEventAdapter = MotionEventAdapter()
+    }
+
+    @Test
+    fun convertToolType() {
+        val types = mapOf(
+            MotionEvent.TOOL_TYPE_FINGER to PointerType.Touch,
+            MotionEvent.TOOL_TYPE_UNKNOWN to PointerType.Unknown,
+            MotionEvent.TOOL_TYPE_ERASER to PointerType.Eraser,
+            MotionEvent.TOOL_TYPE_STYLUS to PointerType.Stylus,
+            MotionEvent.TOOL_TYPE_MOUSE to PointerType.Mouse,
+        )
+        types.entries.forEach { (toolType, pointerType) ->
+            motionEventAdapter = MotionEventAdapter()
+            val motionEvent = MotionEvent(
+                2894,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(
+                    PointerProperties(1000, toolType),
+                ),
+                arrayOf(
+                    PointerCoords(2967f, 5928f),
+                )
+            )
+            val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)!!
+            assertPointerInputEventData(
+                pointerInputEvent.pointers[0],
+                PointerId(0),
+                true,
+                2967f,
+                5928f,
+                pointerType
+            )
+        }
+    }
+
+    @Test
+    fun hoverEventsStay() {
+        // When a hover event happens, the pointer ID should stick around until it is removed.
+        val hoverEnter = MotionEvent(
+            0,
+            ACTION_HOVER_ENTER,
+            1,
+            0,
+            arrayOf(PointerProperties(1, TOOL_TYPE_MOUSE)),
+            arrayOf(PointerCoords(10f, 10f))
+        )
+        val hoverEnterEvent = motionEventAdapter.convertToPointerInputEvent(hoverEnter)!!
+        assertThat(hoverEnterEvent.pointers).hasSize(1)
+        val hoverEnterId = hoverEnterEvent.pointers[0].id
+
+        val hoverExit = MotionEvent(
+            1,
+            ACTION_HOVER_EXIT,
+            1,
+            0,
+            arrayOf(PointerProperties(1, TOOL_TYPE_MOUSE)),
+            arrayOf(PointerCoords(10f, 10f))
+        )
+
+        val hoverExitEvent = motionEventAdapter.convertToPointerInputEvent(hoverExit)!!
+        assertThat(hoverExitEvent.pointers).hasSize(1)
+        assertThat(hoverExitEvent.pointers[0].id).isEqualTo(hoverEnterId)
+
+        val down = MotionEvent(
+            1,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(1, TOOL_TYPE_MOUSE)),
+            arrayOf(PointerCoords(10f, 10f))
+        )
+
+        val downEvent = motionEventAdapter.convertToPointerInputEvent(down)!!
+        assertThat(downEvent.pointers).hasSize(1)
+        assertThat(downEvent.pointers[0].id).isEqualTo(hoverEnterId)
+
+        val up = MotionEvent(
+            2,
+            ACTION_UP,
+            1,
+            0,
+            arrayOf(PointerProperties(1, TOOL_TYPE_MOUSE)),
+            arrayOf(PointerCoords(10f, 10f))
+        )
+
+        val upEvent = motionEventAdapter.convertToPointerInputEvent(up)!!
+        assertThat(upEvent.pointers).hasSize(1)
+        assertThat(upEvent.pointers[0].id).isEqualTo(hoverEnterId)
+
+        val hoverEnterEvent2 = motionEventAdapter.convertToPointerInputEvent(hoverEnter)!!
+        assertThat(hoverEnterEvent2.pointers).hasSize(1)
+        assertThat(hoverEnterEvent2.pointers[0].id).isEqualTo(hoverEnterId)
+        motionEventAdapter.convertToPointerInputEvent(hoverExit)!!
+
+        val touchDown = MotionEvent(
+            3,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(1, TOOL_TYPE_FINGER)),
+            arrayOf(PointerCoords(10f, 10f))
+        )
+        val touchDownEvent = motionEventAdapter.convertToPointerInputEvent(touchDown)!!
+        assertThat(touchDownEvent.pointers).hasSize(1)
+        assertThat(touchDownEvent.pointers[0].id).isNotEqualTo(hoverEnterId)
+        val touchDownId = touchDownEvent.pointers[0].id
+
+        val touchUp = MotionEvent(
+            4,
+            ACTION_UP,
+            1,
+            0,
+            arrayOf(PointerProperties(1, TOOL_TYPE_FINGER)),
+            arrayOf(PointerCoords(10f, 10f))
+        )
+        val touchUpEvent = motionEventAdapter.convertToPointerInputEvent(touchUp)!!
+        assertThat(touchUpEvent.pointers).hasSize(1)
+        assertThat(touchUpEvent.pointers[0].id).isEqualTo(touchDownEvent.pointers[0].id)
+
+        val hoverEnterEvent3 = motionEventAdapter.convertToPointerInputEvent(hoverEnter)!!
+        assertThat(hoverEnterEvent3.pointers).hasSize(1)
+        assertThat(hoverEnterEvent3.pointers[0].id).isNotEqualTo(touchDownId)
+        assertThat(hoverEnterEvent3.pointers[0].id).isNotEqualTo(hoverEnterId)
+    }
+
+    @Test
+    fun robustIdConversion() {
+        // When an ID shows up unexpectedly, it shouldn't crash
+        val hoverExit = MotionEvent(
+            3,
+            ACTION_HOVER_EXIT,
+            1,
+            0,
+            arrayOf(PointerProperties(1, TOOL_TYPE_MOUSE)),
+            arrayOf(PointerCoords(10f, 10f))
+        )
+        val event = motionEventAdapter.convertToPointerInputEvent(hoverExit)!!
+        assertThat(event.pointers).hasSize(1)
+    }
+
+    @Test
+    fun convertToPointerInputEvent_1pointerActionDown_convertsCorrectly() {
+        val motionEvent = MotionEvent(
+            2894,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(8290)),
+            arrayOf(PointerCoords(2967f, 5928f))
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        val platformEvent = pointerInputEvent.motionEvent
+        assertThat(uptime).isEqualTo(2_894L)
+        assertThat(pointers).hasSize(1)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            true,
+            2967f,
+            5928f
+        )
+        assertThat(platformEvent).isSameInstanceAs(motionEvent)
+    }
+
+    @Test
+    fun convertToPointerInputEvent_1pointerActionMove_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        val motionEvent = MotionEvent(
+            5,
+            ACTION_MOVE,
+            1,
+            0,
+            arrayOf(PointerProperties(2)),
+            arrayOf(PointerCoords(6f, 7f))
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(5L)
+        assertThat(pointers).hasSize(1)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            true,
+            6f,
+            7f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_1pointerActionUp_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                10,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(46)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        val motionEvent = MotionEvent(
+            34,
+            ACTION_UP,
+            1,
+            0,
+            arrayOf(PointerProperties(46)),
+            arrayOf(PointerCoords(3f, 4f))
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(34L)
+        assertThat(uptime).isEqualTo(34L)
+        assertThat(pointers).hasSize(1)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            false,
+            3f,
+            4f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_2pointers1stPointerActionPointerDown_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        val motionEvent = MotionEvent(
+            4,
+            ACTION_POINTER_DOWN,
+            2,
+            0,
+            arrayOf(
+                PointerProperties(5),
+                PointerProperties(2)
+            ),
+            arrayOf(
+                PointerCoords(7f, 8f),
+                PointerCoords(3f, 4f)
+            )
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(4L)
+        assertThat(pointers).hasSize(2)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(1),
+            true,
+            7f,
+            8f
+        )
+        assertPointerInputEventData(
+            pointers[1],
+            PointerId(0),
+            true,
+            3f,
+            4f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_2pointers2ndPointerActionPointerDown_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        val motionEvent = MotionEvent(
+            4,
+            ACTION_POINTER_DOWN,
+            2,
+            1,
+            arrayOf(
+                PointerProperties(2),
+                PointerProperties(5)
+            ),
+            arrayOf(
+                PointerCoords(3f, 4f),
+                PointerCoords(7f, 8f)
+            )
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(4L)
+        assertThat(pointers).hasSize(2)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            true,
+            3f,
+            4f
+        )
+        assertPointerInputEventData(
+            pointers[1],
+            PointerId(1),
+            true,
+            7f,
+            8f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_3pointers1stPointerActionPointerDown_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                4,
+                ACTION_POINTER_DOWN,
+                2,
+                1,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f)
+                )
+            )
+        )
+        val motionEvent =
+            MotionEvent(
+                12,
+                ACTION_POINTER_DOWN,
+                3,
+                0,
+                arrayOf(
+                    PointerProperties(9),
+                    PointerProperties(2),
+                    PointerProperties(5)
+                ),
+                arrayOf(
+                    PointerCoords(10f, 11f),
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f)
+                )
+            )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(12L)
+        assertThat(pointers).hasSize(3)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(2),
+            true,
+            10f,
+            11f
+        )
+        assertPointerInputEventData(
+            pointers[1],
+            PointerId(0),
+            true,
+            3f,
+            4f
+        )
+        assertPointerInputEventData(
+            pointers[2],
+            PointerId(1),
+            true,
+            7f,
+            8f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_3pointers2ndPointerActionPointerDown_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                4,
+                ACTION_POINTER_DOWN,
+                2,
+                1,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f)
+                )
+            )
+        )
+        val motionEvent =
+            MotionEvent(
+                12,
+                ACTION_POINTER_DOWN,
+                3,
+                1,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(9),
+                    PointerProperties(5)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(10f, 11f),
+                    PointerCoords(7f, 8f)
+                )
+            )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(12L)
+        assertThat(pointers).hasSize(3)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            true,
+            3f,
+            4f
+        )
+        assertPointerInputEventData(
+            pointers[1],
+            PointerId(2),
+            true,
+            10f,
+            11f
+        )
+        assertPointerInputEventData(
+            pointers[2],
+            PointerId(1),
+            true,
+            7f,
+            8f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_3pointers3rdPointerActionPointerDown_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                4,
+                ACTION_POINTER_DOWN,
+                2,
+                1,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f)
+                )
+            )
+        )
+        val motionEvent =
+            MotionEvent(
+                12,
+                ACTION_POINTER_DOWN,
+                3,
+                2,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5),
+                    PointerProperties(9)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f),
+                    PointerCoords(10f, 11f)
+                )
+            )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(12L)
+        assertThat(pointers).hasSize(3)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            true,
+            3f,
+            4f
+        )
+        assertPointerInputEventData(
+            pointers[1],
+            PointerId(1),
+            true,
+            7f,
+            8f
+        )
+        assertPointerInputEventData(
+            pointers[2],
+            PointerId(2),
+            true,
+            10f,
+            11f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_2pointersActionMove_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                4,
+                ACTION_POINTER_DOWN,
+                2,
+                1,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f)
+                )
+            )
+        )
+        val motionEvent = MotionEvent(
+            10,
+            ACTION_MOVE,
+            2,
+            0,
+            arrayOf(
+                PointerProperties(2),
+                PointerProperties(5)
+            ),
+            arrayOf(
+                PointerCoords(11f, 12f),
+                PointerCoords(13f, 15f)
+            )
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(10L)
+        assertThat(pointers).hasSize(2)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            true,
+            11f,
+            12f
+        )
+        assertPointerInputEventData(
+            pointers[1],
+            PointerId(1),
+            true,
+            13f,
+            15f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_2pointers1stPointerActionPointerUP_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                4,
+                ACTION_POINTER_DOWN,
+                2,
+                1,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f)
+                )
+            )
+        )
+
+        val motionEvent = MotionEvent(
+            10,
+            ACTION_POINTER_UP,
+            2,
+            0,
+            arrayOf(
+                PointerProperties(2),
+                PointerProperties(5)
+            ),
+            arrayOf(
+                PointerCoords(3f, 4f),
+                PointerCoords(7f, 8f)
+            )
+        )
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(10L)
+        assertThat(pointers).hasSize(2)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            false,
+            3f,
+            4f
+        )
+        assertPointerInputEventData(
+            pointers[1],
+            PointerId(1),
+            true,
+            7f,
+            8f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_2pointers2ndPointerActionPointerUp_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                4,
+                ACTION_POINTER_DOWN,
+                2,
+                1,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f)
+                )
+            )
+        )
+
+        val motionEvent = MotionEvent(
+            10,
+            ACTION_POINTER_UP,
+            2,
+            1,
+            arrayOf(
+                PointerProperties(2),
+                PointerProperties(5)
+            ),
+            arrayOf(
+                PointerCoords(3f, 4f),
+                PointerCoords(7f, 8f)
+            )
+        )
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(10L)
+        assertThat(pointers).hasSize(2)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            true,
+            3f,
+            4f
+        )
+        assertPointerInputEventData(
+            pointers[1],
+            PointerId(1),
+            false,
+            7f,
+            8f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_3pointers1stPointerActionPointerUp_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                4,
+                ACTION_POINTER_DOWN,
+                2,
+                1,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f)
+                )
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                12,
+                ACTION_POINTER_DOWN,
+                3,
+                2,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5),
+                    PointerProperties(9)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f),
+                    PointerCoords(10f, 11f)
+                )
+            )
+        )
+
+        val motionEvent = MotionEvent(
+            20,
+            ACTION_POINTER_UP,
+            3,
+            0,
+            arrayOf(
+                PointerProperties(2),
+                PointerProperties(5),
+                PointerProperties(9)
+            ),
+            arrayOf(
+                PointerCoords(3f, 4f),
+                PointerCoords(7f, 8f),
+                PointerCoords(10f, 11f)
+            )
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(20L)
+        assertThat(pointers).hasSize(3)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            false,
+            3f,
+            4f
+        )
+        assertPointerInputEventData(
+            pointers[1],
+            PointerId(1),
+            true,
+            7f,
+            8f
+        )
+        assertPointerInputEventData(
+            pointers[2],
+            PointerId(2),
+            true,
+            10f,
+            11f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_3pointers2ndPointerActionPointerUp_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                4,
+                ACTION_POINTER_DOWN,
+                2,
+                1,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f)
+                )
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                12,
+                ACTION_POINTER_DOWN,
+                3,
+                2,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5),
+                    PointerProperties(9)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f),
+                    PointerCoords(10f, 11f)
+                )
+            )
+        )
+
+        val motionEvent = MotionEvent(
+            20,
+            ACTION_POINTER_UP,
+            3,
+            1,
+            arrayOf(
+                PointerProperties(2),
+                PointerProperties(5),
+                PointerProperties(9)
+            ),
+            arrayOf(
+                PointerCoords(3f, 4f),
+                PointerCoords(7f, 8f),
+                PointerCoords(10f, 11f)
+            )
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(20L)
+        assertThat(pointers).hasSize(3)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            true,
+            3f,
+            4f
+        )
+        assertPointerInputEventData(
+            pointers[1],
+            PointerId(1),
+            false,
+            7f,
+            8f
+        )
+        assertPointerInputEventData(
+            pointers[2],
+            PointerId(2),
+            true,
+            10f,
+            11f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_3pointers3rdPointerActionPointerUp_convertsCorrectly() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                4,
+                ACTION_POINTER_DOWN,
+                2,
+                1,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f)
+                )
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                12,
+                ACTION_POINTER_DOWN,
+                3,
+                2,
+                arrayOf(
+                    PointerProperties(2),
+                    PointerProperties(5),
+                    PointerProperties(9)
+                ),
+                arrayOf(
+                    PointerCoords(3f, 4f),
+                    PointerCoords(7f, 8f),
+                    PointerCoords(10f, 11f)
+                )
+            )
+        )
+
+        val motionEvent = MotionEvent(
+            20,
+            ACTION_POINTER_UP,
+            3,
+            2,
+            arrayOf(
+                PointerProperties(2),
+                PointerProperties(5),
+                PointerProperties(9)
+            ),
+            arrayOf(
+                PointerCoords(3f, 4f),
+                PointerCoords(7f, 8f),
+                PointerCoords(10f, 11f)
+            )
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(20L)
+        assertThat(pointers).hasSize(3)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            true,
+            3f,
+            4f
+        )
+        assertPointerInputEventData(
+            pointers[1],
+            PointerId(1),
+            true,
+            7f,
+            8f
+        )
+        assertPointerInputEventData(
+            pointers[2],
+            PointerId(2),
+            false,
+            10f,
+            11f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_downUpDownUpDownUpSameMotionEventId_pointerIdsAreUnique() {
+        val down1 = MotionEvent(
+            100,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(0)),
+            arrayOf(PointerCoords(10f, 11f))
+        )
+
+        val up1 = MotionEvent(
+            200,
+            ACTION_UP,
+            1,
+            0,
+            arrayOf(PointerProperties(0)),
+            arrayOf(PointerCoords(10f, 11f))
+        )
+
+        val down2 = MotionEvent(
+            300,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(0)),
+            arrayOf(PointerCoords(20f, 21f))
+        )
+
+        val up2 = MotionEvent(
+            400,
+            ACTION_UP,
+            1,
+            0,
+            arrayOf(PointerProperties(0)),
+            arrayOf(PointerCoords(20f, 21f))
+        )
+
+        val down3 = MotionEvent(
+            500,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(0)),
+            arrayOf(PointerCoords(30f, 31f))
+        )
+
+        val up3 = MotionEvent(
+            600,
+            ACTION_UP,
+            1,
+            0,
+            arrayOf(PointerProperties(0)),
+            arrayOf(PointerCoords(30f, 31f))
+        )
+
+        // Test the different events sequentially, since the returned event contains a list that
+        // will be reused by convertToPointerInputEvent for performance, so it shouldn't be held
+        // for longer than needed during the sequential dispatch.
+
+        val pointerInputEventDown1 = motionEventAdapter.convertToPointerInputEvent(down1)
+        assertThat(pointerInputEventDown1).isNotNull()
+        assertThat(pointerInputEventDown1!!.pointers[0].id).isEqualTo(PointerId(0))
+
+        val pointerInputEventUp1 = motionEventAdapter.convertToPointerInputEvent(up1)
+        assertThat(pointerInputEventUp1).isNotNull()
+        assertThat(pointerInputEventUp1!!.pointers[0].id).isEqualTo(PointerId(0))
+
+        val pointerInputEventDown2 = motionEventAdapter.convertToPointerInputEvent(down2)
+        assertThat(pointerInputEventDown2).isNotNull()
+        assertThat(pointerInputEventDown2!!.pointers[0].id).isEqualTo(PointerId(1))
+
+        val pointerInputEventUp2 = motionEventAdapter.convertToPointerInputEvent(up2)
+        assertThat(pointerInputEventUp2).isNotNull()
+        assertThat(pointerInputEventUp2!!.pointers[0].id).isEqualTo(PointerId(1))
+
+        val pointerInputEventDown3 = motionEventAdapter.convertToPointerInputEvent(down3)
+        assertThat(pointerInputEventDown3).isNotNull()
+        assertThat(pointerInputEventDown3!!.pointers[0].id).isEqualTo(PointerId(2))
+
+        val pointerInputEventUp3 = motionEventAdapter.convertToPointerInputEvent(up3)
+        assertThat(pointerInputEventUp3).isNotNull()
+        assertThat(pointerInputEventUp3!!.pointers[0].id).isEqualTo(PointerId(2))
+    }
+
+    @Test
+    fun convertToPointerInputEvent_downDownDownRandomMotionEventIds_pointerIdsAreUnique() {
+        val down1 = MotionEvent(
+            100,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(
+                PointerProperties(9276)
+            ),
+            arrayOf(
+                PointerCoords(10f, 11f)
+            )
+        )
+
+        val down2 = MotionEvent(
+            200,
+            ACTION_POINTER_DOWN,
+            2,
+            1,
+            arrayOf(
+                PointerProperties(9276),
+                PointerProperties(1759)
+            ),
+            arrayOf(
+                PointerCoords(10f, 11f),
+                PointerCoords(20f, 21f)
+            )
+        )
+
+        val down3 = MotionEvent(
+            300,
+            ACTION_POINTER_DOWN,
+            3,
+            2,
+            arrayOf(
+                PointerProperties(9276),
+                PointerProperties(1759),
+                PointerProperties(5043)
+            ),
+            arrayOf(
+                PointerCoords(10f, 11f),
+                PointerCoords(20f, 21f),
+                PointerCoords(30f, 31f)
+            )
+        )
+
+        // Test the different events sequentially, since the returned event contains a list that
+        // will be reused by convertToPointerInputEvent for performance, so it shouldn't be held
+        // for longer than needed during the sequential dispatch.
+
+        val pointerInputEventDown1 = motionEventAdapter.convertToPointerInputEvent(down1)
+
+        assertThat(pointerInputEventDown1).isNotNull()
+        assertThat(pointerInputEventDown1!!.pointers).hasSize(1)
+        assertThat(pointerInputEventDown1.pointers[0].id).isEqualTo(PointerId(0))
+
+        val pointerInputEventDown2 = motionEventAdapter.convertToPointerInputEvent(down2)
+
+        assertThat(pointerInputEventDown2).isNotNull()
+        assertThat(pointerInputEventDown2!!.pointers).hasSize(2)
+        assertThat(pointerInputEventDown2.pointers[0].id).isEqualTo(PointerId(0))
+        assertThat(pointerInputEventDown2.pointers[1].id).isEqualTo(PointerId(1))
+
+        val pointerInputEventDown3 = motionEventAdapter.convertToPointerInputEvent(down3)
+
+        assertThat(pointerInputEventDown3).isNotNull()
+        assertThat(pointerInputEventDown3!!.pointers).hasSize(3)
+        assertThat(pointerInputEventDown2.pointers[0].id).isEqualTo(PointerId(0))
+        assertThat(pointerInputEventDown2.pointers[1].id).isEqualTo(PointerId(1))
+        assertThat(pointerInputEventDown3.pointers[2].id).isEqualTo(PointerId(2))
+    }
+
+    @Test
+    fun convertToPointerInputEvent_motionEventOffset_usesRawCoordinatesInsteadOfOffset() {
+        val motionEvent = MotionEvent(
+            0,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(0)),
+            arrayOf(PointerCoords(1f, 2f))
+        )
+
+        motionEvent.offsetLocation(10f, 20f)
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        val uptime = pointerInputEvent!!.uptime
+        val pointers = pointerInputEvent.pointers
+        assertThat(uptime).isEqualTo(0L)
+        assertThat(pointers).hasSize(1)
+        assertPointerInputEventData(
+            pointers[0],
+            PointerId(0),
+            true,
+            1f,
+            2f,
+            originalX = 11f,
+            originalY = 22f
+        )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_actionCancel_returnsNull() {
+        val motionEvent = MotionEvent(
+            0,
+            ACTION_CANCEL,
+            1,
+            0,
+            arrayOf(PointerProperties(0)),
+            arrayOf(PointerCoords(1f, 2f))
+        )
+
+        motionEvent.offsetLocation(10f, 20f)
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNull()
+    }
+
+    @Test
+    fun convertToPointerInputEvent_downUp_noPointersTracked() {
+        val motionEvent1 = MotionEvent(
+            2894,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(8290)),
+            arrayOf(PointerCoords(2967f, 5928f))
+        )
+        val motionEvent2 = MotionEvent(
+            2894,
+            ACTION_UP,
+            1,
+            0,
+            arrayOf(PointerProperties(8290)),
+            arrayOf(PointerCoords(2967f, 5928f))
+        )
+
+        motionEventAdapter.convertToPointerInputEvent(motionEvent1)
+        motionEventAdapter.convertToPointerInputEvent(motionEvent2)
+
+        assertThat(motionEventAdapter.motionEventToComposePointerIdMap.size()).isEqualTo(0)
+    }
+
+    @Test
+    fun convertToPointerInputEvent_downDown_correctPointersTracked() {
+        val motionEvent1 = MotionEvent(
+            1,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(2)),
+            arrayOf(PointerCoords(3f, 4f))
+        )
+        val motionEvent2 = MotionEvent(
+            4,
+            ACTION_POINTER_DOWN,
+            2,
+            0,
+            arrayOf(
+                PointerProperties(5),
+                PointerProperties(2)
+            ),
+            arrayOf(
+                PointerCoords(7f, 8f),
+                PointerCoords(3f, 4f)
+            )
+        )
+
+        motionEventAdapter.convertToPointerInputEvent(motionEvent1)
+        motionEventAdapter.convertToPointerInputEvent(motionEvent2)
+
+        assertThat(motionEventAdapter.motionEventToComposePointerIdMap.toMap())
+            .containsExactlyEntriesIn(
+                mapOf(
+                    2 to PointerId(0),
+                    5 to PointerId(1)
+                )
+            )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_downDownFirstUp_correctPointerTracked() {
+        val motionEvent1 = MotionEvent(
+            1,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(2)),
+            arrayOf(PointerCoords(3f, 4f))
+        )
+        val motionEvent2 = MotionEvent(
+            4,
+            ACTION_POINTER_DOWN,
+            2,
+            0,
+            arrayOf(
+                PointerProperties(5),
+                PointerProperties(2)
+            ),
+            arrayOf(
+                PointerCoords(7f, 8f),
+                PointerCoords(3f, 4f)
+            )
+        )
+        val motionEvent3 = MotionEvent(
+            10,
+            ACTION_POINTER_UP,
+            2,
+            0,
+            arrayOf(
+                PointerProperties(5),
+                PointerProperties(2)
+            ),
+            arrayOf(
+                PointerCoords(7f, 8f),
+                PointerCoords(3f, 4f)
+            )
+        )
+
+        motionEventAdapter.convertToPointerInputEvent(motionEvent1)
+        motionEventAdapter.convertToPointerInputEvent(motionEvent2)
+        motionEventAdapter.convertToPointerInputEvent(motionEvent3)
+
+        assertThat(motionEventAdapter.motionEventToComposePointerIdMap.toMap())
+            .containsExactlyEntriesIn(
+                mapOf(2 to PointerId(0))
+            )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_downDownSecondUp_correctPointerTracked() {
+        val motionEvent1 = MotionEvent(
+            1,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(2)),
+            arrayOf(PointerCoords(3f, 4f))
+        )
+        val motionEvent2 = MotionEvent(
+            4,
+            ACTION_POINTER_DOWN,
+            2,
+            0,
+            arrayOf(
+                PointerProperties(5),
+                PointerProperties(2)
+            ),
+            arrayOf(
+                PointerCoords(7f, 8f),
+                PointerCoords(3f, 4f)
+            )
+        )
+        val motionEvent3 = MotionEvent(
+            10,
+            ACTION_POINTER_UP,
+            2,
+            1,
+            arrayOf(
+                PointerProperties(5),
+                PointerProperties(2)
+            ),
+            arrayOf(
+                PointerCoords(7f, 8f),
+                PointerCoords(3f, 4f)
+            )
+        )
+
+        motionEventAdapter.convertToPointerInputEvent(motionEvent1)
+        motionEventAdapter.convertToPointerInputEvent(motionEvent2)
+        motionEventAdapter.convertToPointerInputEvent(motionEvent3)
+
+        assertThat(motionEventAdapter.motionEventToComposePointerIdMap.toMap())
+            .containsExactlyEntriesIn(
+                mapOf(5 to PointerId(1))
+            )
+    }
+
+    @Test
+    fun convertToPointerInputEvent_downDownUpUp_noPointersTracked() {
+        val motionEvent1 = MotionEvent(
+            1,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(2)),
+            arrayOf(PointerCoords(3f, 4f))
+        )
+        val motionEvent2 = MotionEvent(
+            4,
+            ACTION_POINTER_DOWN,
+            2,
+            0,
+            arrayOf(
+                PointerProperties(5),
+                PointerProperties(2)
+            ),
+            arrayOf(
+                PointerCoords(7f, 8f),
+                PointerCoords(3f, 4f)
+            )
+        )
+        val motionEvent3 = MotionEvent(
+            10,
+            ACTION_POINTER_UP,
+            2,
+            0,
+            arrayOf(
+                PointerProperties(5),
+                PointerProperties(2)
+            ),
+            arrayOf(
+                PointerCoords(7f, 8f),
+                PointerCoords(3f, 4f)
+            )
+        )
+        val motionEvent4 = MotionEvent(
+            20,
+            ACTION_UP,
+            1,
+            0,
+            arrayOf(PointerProperties(2)),
+            arrayOf(PointerCoords(3f, 4f))
+        )
+
+        motionEventAdapter.convertToPointerInputEvent(motionEvent1)
+        motionEventAdapter.convertToPointerInputEvent(motionEvent2)
+        motionEventAdapter.convertToPointerInputEvent(motionEvent3)
+        motionEventAdapter.convertToPointerInputEvent(motionEvent4)
+
+        assertThat(motionEventAdapter.motionEventToComposePointerIdMap.toMap()).isEmpty()
+    }
+
+    @Test
+    fun convertToPointerInputEvent_downCancel_noPointersTracked() {
+        val motionEvent1 = MotionEvent(
+            1,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(2)),
+            arrayOf(PointerCoords(3f, 4f))
+        )
+        val motionEvent2 = MotionEvent(
+            4,
+            ACTION_POINTER_DOWN,
+            2,
+            0,
+            arrayOf(
+                PointerProperties(5),
+                PointerProperties(2)
+            ),
+            arrayOf(
+                PointerCoords(7f, 8f),
+                PointerCoords(3f, 4f)
+            )
+        )
+        val motionEvent3 = MotionEvent(
+            10,
+            ACTION_CANCEL,
+            2,
+            0,
+            arrayOf(
+                PointerProperties(5),
+                PointerProperties(2)
+            ),
+            arrayOf(
+                PointerCoords(7f, 8f),
+                PointerCoords(3f, 4f)
+            )
+        )
+        motionEventAdapter.convertToPointerInputEvent(motionEvent1)
+        motionEventAdapter.convertToPointerInputEvent(motionEvent2)
+        motionEventAdapter.convertToPointerInputEvent(motionEvent3)
+
+        assertThat(motionEventAdapter.motionEventToComposePointerIdMap.toMap()).isEmpty()
+    }
+
+    @Test
+    fun convertToPointerInputEvent_doesNotSynchronouslyMutateMotionEvent() {
+        val motionEvent = MotionEvent(
+            1,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(2)),
+            arrayOf(PointerCoords(3f, 4f))
+        )
+        motionEvent.offsetLocation(10f, 100f)
+
+        motionEventAdapter.convertToPointerInputEvent(motionEvent)
+
+        assertThat(motionEvent.x).isEqualTo(13f)
+        assertThat(motionEvent.y).isEqualTo(104f)
+    }
+
+    @Test
+    fun convertToPointerInputEvent_1PointerActionDown_includesMotionEvent() {
+        val motionEvent = MotionEvent(
+            2894,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(8290)),
+            arrayOf(PointerCoords(2967f, 5928f))
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        assertThat(pointerInputEvent!!.motionEvent).isSameInstanceAs(motionEvent)
+    }
+
+    @Test
+    fun convertToPointerInputEvent_1pointerActionMove_includesMotionEvent() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                1,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(2)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        val motionEvent = MotionEvent(
+            5,
+            ACTION_MOVE,
+            1,
+            0,
+            arrayOf(PointerProperties(2)),
+            arrayOf(PointerCoords(6f, 7f))
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        assertThat(pointerInputEvent!!.motionEvent).isSameInstanceAs(motionEvent)
+    }
+
+    @Test
+    fun convertToPointerInputEvent_1pointerActionUp_includesMotionEvent() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                10,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(46)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        val motionEvent = MotionEvent(
+            34,
+            ACTION_UP,
+            1,
+            0,
+            arrayOf(PointerProperties(46)),
+            arrayOf(PointerCoords(3f, 4f))
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+
+        assertThat(pointerInputEvent!!.motionEvent).isSameInstanceAs(motionEvent)
+    }
+
+    @Test
+    fun convertToPointerInputEvent_differentCoordinateSpace_useOriginalPointCoordinate() {
+        motionEventAdapter.convertToPointerInputEvent(
+            MotionEvent(
+                10,
+                ACTION_DOWN,
+                1,
+                0,
+                arrayOf(PointerProperties(46)),
+                arrayOf(PointerCoords(3f, 4f))
+            )
+        )
+        val motionEvent = MotionEvent(
+            34,
+            ACTION_MOVE,
+            1,
+            0,
+            arrayOf(PointerProperties(46)),
+            arrayOf(PointerCoords(30f, 40f))
+        )
+
+        val positionCalculator = object : PositionCalculator by positionCalculator {
+            override fun screenToLocal(positionOnScreen: Offset): Offset {
+                return positionOnScreen / 2f
+            }
+        }
+
+        val pointerInputEvent =
+            motionEventAdapter.convertToPointerInputEvent(motionEvent, positionCalculator)
+        assertPointerInputEventData(
+            pointerInputEvent!!.pointers[0],
+            PointerId(0),
+            true,
+            30f,
+            40f,
+            originalX = 30f,
+            originalY = 40f
+        )
+    }
+
+    @Test
+    fun convertScrollEvent_horizontalPositive() {
+        val motionEvent = MotionEvent(
+            eventTime = 1,
+            action = ACTION_SCROLL,
+            numPointers = 1,
+            actionIndex = 0,
+            pointerProperties = arrayOf(PointerProperties(2)),
+            pointerCoords = arrayOf(
+                PointerCoords(3f, 4f).apply {
+                    setAxisValue(AXIS_HSCROLL, 5f)
+                }
+            )
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+        assertThat(pointerInputEvent!!.pointers[0].scrollDelta).isEqualTo(Offset(5f, 0f))
+        assertThat(pointerInputEvent.motionEvent).isSameInstanceAs(motionEvent)
+    }
+
+    @Test
+    fun convertScrollEvent_horizontalNegative() {
+        val motionEvent = MotionEvent(
+            eventTime = 1,
+            action = ACTION_SCROLL,
+            numPointers = 1,
+            actionIndex = 0,
+            pointerProperties = arrayOf(PointerProperties(2)),
+            pointerCoords = arrayOf(
+                PointerCoords(3f, 4f).apply {
+                    setAxisValue(AXIS_HSCROLL, -5f)
+                }
+            )
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+        assertThat(pointerInputEvent!!.pointers[0].scrollDelta).isEqualTo(Offset(-5f, 0f))
+        assertThat(pointerInputEvent.motionEvent).isSameInstanceAs(motionEvent)
+    }
+
+    @Test
+    fun convertScrollEvent_verticalPositive() {
+        val motionEvent = MotionEvent(
+            eventTime = 1,
+            action = ACTION_SCROLL,
+            numPointers = 1,
+            actionIndex = 0,
+            pointerProperties = arrayOf(PointerProperties(2)),
+            pointerCoords = arrayOf(
+                PointerCoords(3f, 4f).apply {
+                    setAxisValue(AXIS_VSCROLL, 5f)
+                }
+            )
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+        // Note: y is inverted, per https://r.android.com/2071209
+        assertThat(pointerInputEvent!!.pointers[0].scrollDelta).isEqualTo(Offset(0f, -5f))
+        assertThat(pointerInputEvent.motionEvent).isSameInstanceAs(motionEvent)
+    }
+
+    @Test
+    fun convertScrollEvent_verticalNegative() {
+        val motionEvent = MotionEvent(
+            eventTime = 1,
+            action = ACTION_SCROLL,
+            numPointers = 1,
+            actionIndex = 0,
+            pointerProperties = arrayOf(PointerProperties(2)),
+            pointerCoords = arrayOf(
+                PointerCoords(3f, 4f).apply {
+                    setAxisValue(AXIS_VSCROLL, -5f)
+                }
+            )
+        )
+
+        val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
+        assertThat(pointerInputEvent).isNotNull()
+        // Note: y is inverted, per https://r.android.com/2071209
+        assertThat(pointerInputEvent!!.pointers[0].scrollDelta).isEqualTo(Offset(0f, 5f))
+        assertThat(pointerInputEvent.motionEvent).isSameInstanceAs(motionEvent)
+    }
+
+    private fun MotionEventAdapter.convertToPointerInputEvent(motionEvent: MotionEvent) =
+        convertToPointerInputEvent(motionEvent, positionCalculator)
+
+    private fun SparseLongArray.toMap(): Map<Int, PointerId> {
+        val map = mutableMapOf<Int, PointerId>()
+        for (i in 0 until size()) {
+            val key = keyAt(i)
+            val value = valueAt(i)
+            map[key] = PointerId(value)
+        }
+        return map
+    }
+}
+
+// Private helper functions
+
+private fun MotionEvent(
+    eventTime: Int,
+    action: Int,
+    numPointers: Int,
+    actionIndex: Int,
+    pointerProperties: Array<MotionEvent.PointerProperties>,
+    pointerCoords: Array<MotionEvent.PointerCoords>,
+    downTime: Long = 0
+) = MotionEvent.obtain(
+    downTime,
+    eventTime.toLong(),
+    action + (actionIndex shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+    numPointers,
+    pointerProperties,
+    pointerCoords,
+    0,
+    0,
+    0f,
+    0f,
+    0,
+    0,
+    InputDevice.SOURCE_TOUCHSCREEN,
+    0
+)
+
+private fun assertPointerInputEventData(
+    actual: PointerInputEventData,
+    id: PointerId,
+    isDown: Boolean,
+    x: Float,
+    y: Float,
+    type: PointerType = PointerType.Touch,
+    originalX: Float = x,
+    originalY: Float = y,
+) {
+    assertThat(actual.id).isEqualTo(id)
+    assertThat(actual.down).isEqualTo(isDown)
+    assertThat(actual.positionOnScreen.x).isEqualTo(x)
+    assertThat(actual.positionOnScreen.y).isEqualTo(y)
+    assertThat(actual.originalEventPosition.x).isEqualTo(originalX)
+    assertThat(actual.originalEventPosition.y).isEqualTo(originalY)
+    assertThat(actual.type).isEqualTo(type)
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/MotionEventSpyTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/MotionEventSpyTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/MotionEventSpyTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/MotionEventSpyTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/MouseEventTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/MouseEventTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/MouseEventTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/MouseEventTest.kt
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt
new file mode 100644
index 0000000..508c2fc
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt
@@ -0,0 +1,4574 @@
+/*
+ * 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.ui.input.pointer
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.platform.LocalPointerIconService
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performMouseInput
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@OptIn(ExperimentalTestApi::class)
+@RunWith(AndroidJUnit4::class)
+class PointerIconTest {
+    @get:Rule
+    val rule = createComposeRule()
+    private val parentIconTag = "myParentIcon"
+    private val childIconTag = "myChildIcon"
+    private val grandchildIconTag = "myGrandchildIcon"
+    private val desiredParentIcon = PointerIcon.Crosshair // AndroidPointerIcon(type=1007)
+    private val desiredChildIcon = PointerIcon.Text // AndroidPointerIcon(type=1008)
+    private val desiredGrandchildIcon = PointerIcon.Hand // AndroidPointerIcon(type=1002)
+    private val desiredDefaultIcon = PointerIcon.Default // AndroidPointerIcon(type=1000)
+    private lateinit var iconService: PointerIconService
+
+    @Before
+    fun setup() {
+        iconService = object : PointerIconService {
+            private var currentIcon: PointerIcon = PointerIcon.Default
+            override fun getIcon(): PointerIcon {
+                return currentIcon
+            }
+
+            override fun setIcon(value: PointerIcon?) {
+                currentIcon = value ?: PointerIcon.Default
+            }
+        }
+    }
+
+    @Test
+    fun testInspectorValue() {
+        isDebugInspectorInfoEnabled = true
+        rule.setContent {
+            val modifier = Modifier.pointerHoverIcon(
+                PointerIcon.Hand,
+                overrideDescendants = false
+            ) as InspectableValue
+            assertThat(modifier.nameFallback).isEqualTo("pointerHoverIcon")
+            assertThat(modifier.valueOverride).isNull()
+            assertThat(modifier.inspectableElements.map { it.name }.asIterable()).containsExactly(
+                "icon",
+                "overrideDescendants",
+            )
+        }
+        isDebugInspectorInfoEnabled = false
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Because we don't move the cursor, the icon will be the default [PointerIcon.Default]. We
+     *  also want to check that when using a .pointerHoverIcon modifier with a composable,
+     *  composition only happens once (per composable).
+     */
+    @Test
+    fun parentChildFullOverlap_noOverrideDescendants_checkNumberOfCompositions() {
+
+        var numberOfCompositions = 0
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+
+                    numberOfCompositions++
+
+                    Box(
+                        Modifier
+                            .requiredSize(200.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    ) {
+                        numberOfCompositions++
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+            assertThat(numberOfCompositions).isEqualTo(2)
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Child Box’s [PointerIcon.Text] wins for the entire Box area because it’s lower in
+     *  the hierarchy than Parent Box. If the Parent Box's overrideDescendants = false, the Child
+     *  Box takes priority.
+     */
+    @Test
+    fun parentChildFullOverlap_noOverrideDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(200.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    )
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify Parent Box is respecting Child Box's icon
+        verifyIconOnHover(parentIconTag, desiredChildIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire Box area because it’s higher in
+     *  the hierarchy than Child Box. Also the Parent Box's overrideDescendants value is TRUE, so
+     *  as the topmost parent in the hierarchy with overrideDescendants = true, all its children
+     *  must respect it.
+     */
+    @Test
+    fun parentChildFullOverlap_parentOverridesDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(200.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    )
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+        // Verify Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Child Box’s [PointerIcon.Text] wins for the entire Box area because its lower in priority
+     *  than Parent Box. If the Parent Box's overrideDescendants = false, the Child Box takes
+     *  priority.
+     */
+    @Test
+    fun parentChildFullOverlap_childOverridesDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(200.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    )
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify Parent Box is respecting Child Box's icon
+        verifyIconOnHover(parentIconTag, desiredChildIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire Box area because its
+     *  overrideDescendants = true. The Parent Box takes precedence because it is the topmost parent
+     *  in the hierarchy with overrideDescendants = true.
+     */
+    @Test
+    fun parentChildFullOverlap_bothOverrideDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
+                ) {
+                    Box(
+                        Modifier
+                            .requiredSize(200.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    )
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+        // Verify Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Child Box’s [PointerIcon.Hand] wins for the entire Child Box surface area because there's
+     *  no parent in its hierarchy that has overrideDescendants = true. Parent Box's
+     *  [PointerIcon.Crosshair] wins for all remaining surface area of its Box that doesn't overlap
+     *  with Child Box.
+     */
+    @Test
+    fun parentChildPartialOverlap_noOverrideDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(100.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    )
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify remaining Parent Box's area is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Parent Box’s [PointerIcon.Hand] wins for the entire Box area because its
+     *  overrideDescendants = true, so every child underneath it in the hierarchy must respect its
+     *  pointer icon since it's the topmost parent in the hierarchy with overrideDescendants = true.
+     */
+    @Test
+    fun parentChildPartialOverlap_parentOverridesDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(100.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    )
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+        // Verify Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Child Box’s [PointerIcon.Hand] wins for the entire Child Box surface area because it’s lower
+     *  in the hierarchy than Parent Box. If Parent Box's overrideDescendants = false, the Child
+     *  Box takes priority.
+     */
+    @Test
+    fun parentChildPartialOverlap_childOverridesDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(100.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    )
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify remaining Parent Box's area is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire Box area because its
+     *  overrideDescendants = true. If multiple locations in the hierarchy set
+     *  overrideDescendants = true, the highest parent in the hierarchy takes precedence (in this
+     *  example, it was Parent Box).
+     */
+    @Test
+    fun parentChildPartialOverlap_bothOverrideDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(100.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    )
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+        // Verify Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (no custom icon)
+     *      ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Initially, the Child Box's [PointerIcon.Text] should win for its entire surface area
+     *  because it has no competition in the hierarchy for any other custom icons. After the Parent
+     *  Box dynamically has the pointerHoverIcon Modifier added to it, the Parent Box's
+     *  [PointerIcon.Crosshair] should win for the entire surface area of the Parent Box and Child
+     *  Box because the Parent Box has overrideDescendants = true.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
+     */
+    @Test
+    fun parentChildPartialOverlap_parentModifierDynamicallyAdded() {
+        val isVisible = mutableStateOf(false)
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .then(
+                            if (isVisible.value) Modifier.pointerHoverIcon(
+                                desiredParentIcon,
+                                overrideDescendants = true
+                            ) else Modifier
+                        )
+
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    )
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Child Box is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify Parent Box's icon is the desired default icon
+        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
+        // Dynamically add the pointerHoverIcon Modifier to the Parent Box
+        rule.runOnIdle {
+            isVisible.value = true
+        }
+        // Verify Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+        // Verify Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (no custom icon)
+     *      ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Initially, the Child Box's [PointerIcon.Text] should win for its entire surface area
+     *  because it has no competition in the hierarchy for any other custom icons. After the Parent
+     *  Box dynamically has the pointerHoverIcon Modifier added to it, the Parent Box's
+     *  [PointerIcon.Crosshair] should win for the entire surface area of the Parent Box and Child
+     *  Box because the Parent Box has overrideDescendants = true.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
+     */
+    @Ignore("b/299482894 - not yet implemented")
+    @Test
+    fun parentChildPartialOverlap_parentModifierDynamicallyAddedWithMoveEvents() {
+        val isVisible = mutableStateOf(false)
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .then(
+                            if (isVisible.value) Modifier.pointerHoverIcon(
+                                desiredParentIcon,
+                                overrideDescendants = true
+                            ) else Modifier
+                        )
+
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    )
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over Child Box and verify it has the desired child icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            enter(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move to Parent Box and verify its icon is the desired default icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Move back to the Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Dynamically add the pointerHoverIcon Modifier to the Parent Box
+        rule.runOnIdle {
+            isVisible.value = true
+        }
+        // Verify the Child Box has updated to respect the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move within the Child Box and verify it is still respecting the desired parent icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        // Move to the Parent Box and verify it also has the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Exit hovering over Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *      ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  After several assertions, it reverts back to false in the parent:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *
+     *  Expected Output:
+     *  Initially, the Child Box's [PointerIcon.Text] should win for its entire surface area
+     *  because the parent does not override descendants. After the Parent Box dynamically changes
+     *  overrideDescendants to true, the Parent Box's [PointerIcon.Crosshair] should win for the
+     *  entire surface area of the Parent Box and Child Box because the Parent Box has
+     *  overrideDescendants = true.
+     *
+     *  It should then revert back to Child Box's [PointerIcon.Text] after the Parent Box's
+     *  overrideDescendants is set back to false.
+     *
+     */
+    @Test
+    fun parentChildPartialOverlap_parentModifierDynamicallyChangedToOverrideWithMoveEvents() {
+        var parentOverrideDescendants by mutableStateOf(false)
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .then(
+                            Modifier.pointerHoverIcon(
+                                desiredParentIcon,
+                                overrideDescendants = parentOverrideDescendants
+                            )
+                        )
+
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    )
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over Child Box and verify it has the desired child icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            enter(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move to Parent Box and verify its icon is the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move back to the Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(center)
+        }
+
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+
+        // Dynamically change the pointerHoverIcon Modifier to the Parent Box to
+        // override descendants.
+        rule.runOnIdle {
+            parentOverrideDescendants = true
+        }
+
+        // Verify the Child Box has updated to respect the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+
+        // Move within the Child Box and verify it is still respecting the desired parent icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+
+        // Verify the Child Box has updated to respect the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+
+        // Move to the Parent Box and verify it also has the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+
+        // Move within the Child Box and verify it is still respecting the desired parent icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+
+        // Dynamically change the pointerHoverIcon Modifier to the Parent Box to NOT
+        // override descendants.
+        rule.runOnIdle {
+            parentOverrideDescendants = false
+        }
+
+        // Verify it's changed to child icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+
+        // Move to Parent Box and verify its icon is the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move back to the Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(center)
+        }
+
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+
+        // Exit hovering over Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     *  The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *      ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Initially, Child Box’s [PointerIcon.Hand] wins for the entire Child Box surface area because
+     *  there's no parent in its hierarchy that has overrideDescendants = true. Additionally, Parent
+     *  Box's [PointerIcon.Crosshair] would initially win for all remaining surface area of its Box
+     *  that doesn't overlap with Child Box. Once Parent Box's overrideDescendants parameter is
+     *  dynamically updated to true, the Parent Box's icon should win for its entire surface area,
+     *  including within Child Box.
+     */
+    @Test
+    fun parentChildPartialOverlap_parentOverrideDescendantsDynamicallyUpdated() {
+        val parentOverrideState = mutableStateOf(false)
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(
+                            desiredParentIcon,
+                            overrideDescendants = parentOverrideState.value
+                        )
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    )
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify remaining Parent Box's area is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+        rule.runOnIdle {
+            parentOverrideState.value = true
+        }
+        // Verify Child Box's icon is the desired parent icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+        // Verify Parent Box also has the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *      ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  After hovering over various parts of the screen and verify the results, we update the
+     *  parent's overrideDescendants to true:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  After several assertions, it reverts back to false in the parent:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *
+     *  Expected Output:
+     *  Initially, the Child Box's [PointerIcon.Text] should win for its entire surface area
+     *  because the parent does not override descendants. After the Parent Box dynamically changes
+     *  overrideDescendants to true, the Parent Box's [PointerIcon.Crosshair] should win for the
+     *  child's surface area within the Parent Box BUT NOT the portion of the Child Box that is
+     *  outside the Parent Box.
+     *
+     *  It should then revert back to Child Box's [PointerIcon.Text] (in all scenarios) after the
+     *  Parent Box's overrideDescendants is set back to false.
+     *
+     */
+    @Test
+    fun parentChildPartialOverlapAndExtendsBeyondParent_dynamicOverrideDescendants() {
+        var parentOverrideDescendants by mutableStateOf(false)
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+
+                Box(
+                    modifier = Modifier
+                        .requiredSize(300.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Green)))
+
+                ) {
+                    // This child extends beyond the borders of the parent (enabling this test)
+                    Box(
+                        modifier = Modifier
+                            .size(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                            .testTag(parentIconTag)
+                            .then(
+                                Modifier.pointerHoverIcon(
+                                    desiredParentIcon,
+                                    overrideDescendants = parentOverrideDescendants
+                                )
+                            )
+
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(20.dp)
+                                .offset(100.dp)
+                                .width(300.dp)
+                                .height(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over Child Box and verify it has the desired child icon (outside parent)
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            enter(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+
+        // Hover over Child Box and verify it has the desired child icon (inside parent)
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomLeft)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+
+        // Move to Parent Box and verify its icon is the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move back to the Child Box (portion inside parent)
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomLeft)
+        }
+
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+
+        // Dynamically change the pointerHoverIcon Modifier of the Parent Box to
+        // override descendants.
+        rule.runOnIdle {
+            parentOverrideDescendants = true
+        }
+
+        // Verify the Child Box has updated to respect the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+
+        // Hover over Child Box and verify it has the desired child icon (outside parent)
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+
+        // Move to the Parent Box and verify it also has the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+
+        // Move within the Child Box (portion inside parent) and verify it is still
+        // respecting the desired parent icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomLeft)
+        }
+
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+
+        // Dynamically change the pointerHoverIcon Modifier of the Parent Box to NOT
+        // override descendants.
+        rule.runOnIdle {
+            parentOverrideDescendants = false
+        }
+
+        // Verify it's changed to child icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+
+        // Move to Parent Box and verify its icon is the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move back to the Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomLeft)
+        }
+
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+
+        // Exit hovering over Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  ChildA Box’s [PointerIcon.Text] wins for the entire surface area of ChildA's Box. ChildB
+     *  Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
+     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that's not
+     *  covered by ChildA Box or ChildB Box. In this example, there's no competition for pointer
+     *  icons because the parent has no icon set and neither ChildA or ChildB Boxes overlap.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *    ⤷ Child Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun NonOverlappingSiblings_noOverrideDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Column {
+                        Box(
+                            Modifier
+                                .padding(20.dp)
+                                .requiredSize(50.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                        )
+                        // Referencing grandchild tag/icon for ChildB in this test
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(50.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify ChildA Box's icon is the desired ChildA icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify ChildB Box's icon is the desired ChildB icon
+        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
+        // Verify remaining Parent Box's icon is the default icon
+        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  ChildA Box’s [PointerIcon.Text] wins for the entire surface area of ChildA's Box. ChildB
+     *  Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
+     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that's not
+     *  covered by ChildA Box or ChildB Box. In this example, it doesn't matter whether ChildA Box's
+     *  overrideDescendants = true or false because there's no competition for pointer icons in
+     *  this example.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *    ⤷ Child Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun NonOverlappingSiblings_firstChildOverridesDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Column {
+                        Box(
+                            Modifier
+                                .padding(20.dp)
+                                .requiredSize(50.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                        )
+                        // Referencing grandchild tag/icon for ChildB in this test
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(50.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify ChildA Box's icon is the desired ChildA icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify ChildB Box's icon is the desired ChildB icon
+        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
+        // Verify remaining Parent Box's icon is the default icon
+        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  ChildA Box’s [PointerIcon.Text] wins for the entire surface area of ChildA's Box. ChildB
+     *  Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
+     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that's not
+     *  covered by ChildA Box or ChildB Box. In this example, it doesn't matter whether ChildB Box's
+     *  overrideDescendants = true or false because there's no competition for pointer icons in
+     *  this example.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *    ⤷ Child Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun NonOverlappingSiblings_secondChildOverridesDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Column {
+                        Box(
+                            Modifier
+                                .padding(20.dp)
+                                .requiredSize(50.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                        )
+                        // Referencing grandchild tag/icon for ChildB in this test
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(50.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = true)
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify ChildA Box's icon is the desired ChildA icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify ChildB Box's icon is the desired ChildB icon
+        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
+        // Verify remaining Parent Box's icon is the default icon
+        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  ChildA Box’s [PointerIcon.Text] wins for the entire surface area of ChildA's Box. ChildB
+     *  Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
+     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that's not
+     *  covered by ChildA Box or ChildB Box. In this example, it doesn't matter whether ChildA Box
+     *  and ChildB Box's overrideDescendants = true or false because there's no competition for
+     *  pointer icons in this example.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ ChildA Box (output icon = [PointerIcon.Text])
+     *    ⤷ ChildB Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun NonOverlappingSiblings_bothOverrideDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Column {
+                        Box(
+                            Modifier
+                                .padding(20.dp)
+                                .requiredSize(50.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                        )
+                        // Referencing grandchild tag/icon for ChildB in this test
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(50.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = true)
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify ChildA Box's icon is the desired ChildA icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify ChildB Box's icon is the desired ChildB icon
+        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
+        // Verify remaining Parent Box's icon is the default icon
+        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE) where
+     *        ChildB Box's surface area overlaps with its sibling, ChildA, within the Parent Box
+     *
+     *  Expected Output:
+     *  ChildB Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
+     *  ChildA Box's [PointerIcon.Text] wins for the remaining surface area of ChildA Box not
+     *  covered by ChildB Box. [PointerIcon.Default] wins for the remainder of the surface area of
+     *  Parent Box that's not covered by ChildA Box or ChildB Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ ChildA Box (output icon = [PointerIcon.Text])
+     *    ⤷ ChildB Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun OverlappingSiblings_noOverrideDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(120.dp, 60.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    )
+                    // Referencing grandchild tag/icon for ChildB in this test
+                    Box(
+                        Modifier
+                            .padding(horizontal = 100.dp, vertical = 40.dp)
+                            .requiredSize(120.dp, 20.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                            .testTag(grandchildIconTag)
+                            .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = false)
+                    )
+                }
+            }
+        }
+
+        verifyOverlappingSiblings()
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE) where
+     *        ChildB Box's surface area overlaps with its sibling, ChildA, within the Parent Box
+     *
+     *  Expected Output:
+     *  ChildB Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
+     *  ChildA Box's [PointerIcon.Text] wins for the remaining surface area of ChildA Box not
+     *  covered by ChildB Box. [PointerIcon.Default] wins for the remainder of the surface area of
+     *  Parent Box that's not covered by ChildA Box or ChildB Box. The overrideDescendants param
+     *  only affects that element's children. So in this example, it doesn't matter whether ChildA
+     *  Box's overrideDescendants = true because ChildB is its sibling and is therefore unaffected
+     *  by this param.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ ChildA Box (output icon = [PointerIcon.Text])
+     *    ⤷ ChildB Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun OverlappingSiblings_childAOverridesDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(120.dp, 60.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    )
+                    // Referencing grandchild tag/icon for ChildB in this test
+                    Box(
+                        Modifier
+                            .padding(horizontal = 100.dp, vertical = 40.dp)
+                            .requiredSize(120.dp, 20.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                            .testTag(grandchildIconTag)
+                            .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = false)
+                    )
+                }
+            }
+        }
+
+        verifyOverlappingSiblings()
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE) where
+     *        ChildB Box's surface area overlaps with its sibling, ChildA, within the Parent Box
+     *
+     *  Expected Output:
+     *  ChildB Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
+     *  ChildA Box's [PointerIcon.Text] wins for the remaining surface area of ChildA Box not
+     *  covered by ChildB Box. [PointerIcon.Default] wins for the remainder of the surface area of
+     *  Parent Box that's not covered by ChildA Box or ChildB Box. The overrideDescendants param
+     *  only affects that element's children. So in this example, it doesn't matter whether ChildB
+     *  Box's overrideDescendants = true because ChildA is its sibling and is therefore unaffected
+     *  by this param.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ ChildA Box (output icon = [PointerIcon.Text])
+     *    ⤷ ChildB Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun OverlappingSiblings_childBOverridesDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(120.dp, 60.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    )
+                    // Referencing grandchild tag/icon for ChildB in this test
+                    Box(
+                        Modifier
+                            .padding(horizontal = 100.dp, vertical = 40.dp)
+                            .requiredSize(120.dp, 20.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                            .testTag(grandchildIconTag)
+                            .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = true)
+                    )
+                }
+            }
+        }
+
+        verifyOverlappingSiblings()
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE) where
+     *        ChildB Box's surface area overlaps with its sibling, ChildA, within the Parent Box
+     *
+     *  Expected Output:
+     *  ChildB Box's [PointerIcon.Hand] wins for the entire surface area of ChildB's Box.
+     *  ChildA Box's [PointerIcon.Text] wins for the remaining surface area of ChildA Box not
+     *  covered by ChildB Box. [PointerIcon.Default] wins for the remainder of the surface area of
+     *  Parent Box that's not covered by ChildA Box or ChildB Box. The overrideDescendants param
+     *  only affects that element's children. So in this example, it doesn't matter whether ChildA
+     *  Box or ChildB Box's overrideDescendants = true because ChildA and ChildB Boxes are siblings
+     *  and are unaffected by each other's overrideDescendants param.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ ChildA Box (output icon = [PointerIcon.Text])
+     *    ⤷ ChildB Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun OverlappingSiblings_bothOverrideDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(120.dp, 60.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    )
+                    // Referencing grandchild tag/icon for ChildB in this test
+                    Box(
+                        Modifier
+                            .padding(horizontal = 100.dp, vertical = 40.dp)
+                            .requiredSize(120.dp, 20.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                            .testTag(grandchildIconTag)
+                            .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = true)
+                    )
+                }
+            }
+        }
+
+        verifyOverlappingSiblings()
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE) where
+     *        ChildB Box's surface area overlaps with its sibling, ChildA, within the Parent Box
+     *
+     *  Expected Output:
+     *  Parent Box's [PointerIcon.Crosshair] wins for the entire surface area of its box, including
+     *  the surface area within ChildA Box and ChildB Box. Parent Box has overrideDescendants =
+     *  true, which takes priority over any custom icon set by its children.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ ChildA Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ ChildB Box (output icon = [PointerIcon.Crosshair])
+     */
+    @Test
+    fun OverlappingSiblings_parentOverridesDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(120.dp, 60.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    )
+                    // Referencing grandchild tag/icon for ChildB in this test
+                    Box(
+                        Modifier
+                            .padding(horizontal = 100.dp, vertical = 40.dp)
+                            .requiredSize(120.dp, 20.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                            .testTag(grandchildIconTag)
+                            .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = true)
+                    )
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over ChildB (bottom right corner) and verify desired Parent icon
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            enter(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Then hover to parent (bottom right corner) and icon hasn't changed
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Then hover to ChildA (bottom left corner) and verify icon hasn't changed
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomLeft)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Exit hovering
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ Child Box (no custom icon set)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Grandchild Box’s [PointerIcon.Hand] wins for the entire surface area of Grandchild's Box.
+     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that isn't
+     *  covered by Grandchild Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ Child Box (output icon = [PointerIcon.Default])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun multiLayeredNesting_grandchildCustomIconNoOverride() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Grandchild Box's icon is the desired grandchild icon
+        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
+        // Verify remaining Child Box's area is the default arrow icon
+        verifyIconOnHover(childIconTag, desiredDefaultIcon)
+        // Verify remaining Parent Box's area is the default arrow icon
+        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ Child Box (no custom icon set)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Grandchild Box’s [PointerIcon.Hand] wins for the entire surface area of Grandchild's Box.
+     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that isn't
+     *  covered by Grandchild Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ Child Box (output icon = [PointerIcon.Default])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun multiLayeredNesting_grandchildCustomIconHasOverride() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = true
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Grandchild Box's icon is the desired grandchild icon
+        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
+        // Verify remaining Child Box's area is the default arrow icon
+        verifyIconOnHover(childIconTag, desiredDefaultIcon)
+        // Verify remaining Parent Box's area is the default arrow icon
+        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of the Grandchild Box.
+     *  Child Box’s [PointerIcon.Text] wins for remaining surface area of its Box not covered by the
+     *  Grandchild Box. [PointerIcon.Default] wins for the remainder of the surface area of Parent
+     *  Box that isn't covered by Child Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun multiLayeredNesting_childAndGrandchildCustomIconsNoOverrides() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Grandchild Box's icon is the desired grandchild icon
+        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
+        // Verify remaining Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify remaining Parent Box's area is the default arrow icon
+        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of the Grandchild Box.
+     *  Child Box’s [PointerIcon.Text] wins for the remainder of the Child Box's surface area
+     *  that's not covered by the Grandchild box. [PointerIcon.Default] wins for the remainder of
+     *  the surface area of Parent Box that isn't covered by Child Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun multiLayeredNesting_childCustomIconGrandchildHasOverride() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = true
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Grandchild Box's icon is the desired grandchild icon
+        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
+        // Verify remaining Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify remaining Parent Box's area is the default arrow icon
+        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Child Box’s [PointerIcon.Text] wins for the entire surface area of its Box (including all
+     *  of the Grandchild Box since it is contained within Child Box's surface area).
+     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that isn't
+     *  covered by Child Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Text])
+     */
+    @Test
+    fun multiLayeredNesting_grandchildCustomIconChildHasOverride() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify Grandchild Box is respecting Child Box's icon
+        verifyIconOnHover(grandchildIconTag, desiredChildIcon)
+        // Verify remaining Parent Box's area is the default arrow icon
+        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (no custom icon set)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Child Box’s [PointerIcon.Text] wins for the entire surface area of its Box (including all
+     *  of the Grandchild Box since it is contained within Child Box's surface area).
+     *  [PointerIcon.Default] wins for the remainder of the surface area of Parent Box that isn't
+     *  covered by Child Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Default])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Text])
+     */
+    @Test
+    fun multiLayeredNesting_childAndGrandchildOverrideDescendants() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = true
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify Grandchild Box is respecting Child Box's icon
+        verifyIconOnHover(grandchildIconTag, desiredChildIcon)
+        // Verify remaining Parent Box's area is the default arrow icon
+        verifyIconOnHover(parentIconTag, desiredDefaultIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (no icon set)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of the Grandchild Box.
+     *  Parent Box’s [PointerIcon.Crosshair] wins for the remaining surface area of the Pare Box
+     *  that's not covered by the Grandchild Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun multiLayeredNesting_parentAndGrandchildCustomIconNoOverrides() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Grandchild Box's icon is the desired grandchild icon
+        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
+        // Verify remaining Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+        // Verify remaining Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (no icon set)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of its Box. Parent
+     *  Box’s [PointerIcon.Crosshair] wins for the remaining surface area of the Pare Box that's
+     *  not covered by the Grandchild Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun multiLayeredNesting_parentCustomIconGrandchildOverrides() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = true
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Grandchild Box's icon is the desired grandchild icon
+        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
+        // Verify remaining Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+        // Verify remaining Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of the Grandchild Box.
+     *  Child Box's [PointerIcon.Text] wins for the remaining surface area of the Child Box not
+     *  covered by the Grandchild Box. Parent Box’s [PointerIcon.Crosshair] wins for the remaining
+     *  surface area not covered by the Child Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun multiLayeredNesting_allCustomIconsNoOverrides() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Grandchild Box's icon is the desired grandchild icon
+        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
+        // Verify remaining Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify remaining Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of the Grandchild Box.
+     *  Child Box's [PointerIcon.Text] wins for the remaining surface area of the Child Box not
+     *  covered by the Grandchild Box. Parent Box’s [PointerIcon.Crosshair] wins for the remaining
+     *  surface area not covered by the Child Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun multiLayeredNesting_allCustomIconsGrandchildOverrides() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = true
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Grandchild Box's icon is the desired grandchild icon
+        verifyIconOnHover(grandchildIconTag, desiredGrandchildIcon)
+        // Verify remaining Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify remaining Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Child Box's [PointerIcon.Hand] wins for the entire surface area of its Box. Parent
+     *  Box’s [PointerIcon.Crosshair] wins for the remaining surface area of its Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Text])
+     */
+    @Test
+    fun multiLayeredNesting_allCustomIconsChildOverrides() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify Grandchild Box is respecting Child Box's icon
+        verifyIconOnHover(grandchildIconTag, desiredChildIcon)
+        // Verify remaining Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Child Box's [PointerIcon.Hand] wins for the entire surface area of its Box. Parent
+     *  Box’s [PointerIcon.Crosshair] wins for the remaining surface area of its Box. The addition
+     *  of Grandchild Box’s overrideDescendants = true in this test doesn’t impact the outcome; this
+     *  is because Child Box is Grandchild Box's parent in the hierarchy and it already has
+     *  overrideDescendants = true, which takes priority over anything Grandchild Box sets.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Text])
+     */
+    @Test
+    fun multiLayeredNesting_allCustomIconsChildAndGrandchildOverrides() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = true
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Child Box's icon is the desired child icon
+        verifyIconOnHover(childIconTag, desiredChildIcon)
+        // Verify Grandchild Box is respecting Child Box's icon
+        verifyIconOnHover(grandchildIconTag, desiredChildIcon)
+        // Verify remaining Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (no icon set)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire surface area of its Box. Even
+     *  though the Grandchild Box’s icon was set, the Parent Box will always take priority because
+     *  it's the highestmost level in the hierarchy where overrideDescendants = true.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Crosshair])
+     */
+    @Test
+    fun multiLayeredNesting_parentGrandChildCustomIconsParentOverrides() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+        // Verify Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+        // Verify Grandchild Box is respecting Parent Box's icon
+        verifyIconOnHover(grandchildIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (no icon set)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire surface area of its Box. Even
+     *  though the Grandchild Box’s icon was set, the Parent Box will always take priority because
+     *  it's the highestmost level in the hierarchy where overrideDescendants = true.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Crosshair])
+     */
+    @Test
+    fun multiLayeredNesting_parentGrandChildCustomIconsBothOverride() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = true
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+        // Verify Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+        // Verify Grandchild Box is respecting Parent Box's icon
+        verifyIconOnHover(grandchildIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire surface area of its Box. Even
+     *  though the Child and Grandchild Box’s icons were set, the Parent Box will always take
+     *  priority because it's the highestmost level in the hierarchy where overrideDescendants =
+     *  true.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Crosshair])
+     */
+    @Test
+    fun multiLayeredNesting_allCustomIconsParentOverrides() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+        // Verify Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+        // Verify Grandchild Box is respecting Parent Box's icon
+        verifyIconOnHover(grandchildIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire surface area of its Box. Even
+     *  though the Child and Grandchild Box’s icons were set, the Parent Box will always take
+     *  priority because it's the highestmost level in the hierarchy where overrideDescendants =
+     *  true.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Crosshair])
+     */
+    @Test
+    fun multiLayeredNesting_allCustomIconsParentAndGrandchildOverride() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = true
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+        // Verify Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+        // Verify Grandchild Box is respecting Parent Box's icon
+        verifyIconOnHover(grandchildIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire surface area of its Box. Even
+     *  though the Child and Grandchild Box’s icons were set, the Parent Box will always take
+     *  priority because it's the highestmost level in the hierarchy where overrideDescendants =
+     *  true.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Crosshair])
+     */
+    @Test
+    fun multiLayeredNesting_allCustomIconsParentAndChildOverride() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+        // Verify Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+        // Verify Grandchild Box is respecting Parent Box's icon
+        verifyIconOnHover(grandchildIconTag, desiredParentIcon)
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *        ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Parent Box’s [PointerIcon.Crosshair] wins for the entire surface area of its Box. Even
+     *  though the Child and Grandchild Box’s icons were set, the Parent Box will always take
+     *  priority because it's the highestmost level in the hierarchy where overrideDescendants =
+     *  true.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Crosshair])
+     */
+    @Test
+    fun multiLayeredNesting_allIconsOverride() {
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(desiredChildIcon, overrideDescendants = true)
+                    ) {
+                        Box(
+                            Modifier
+                                .padding(40.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(grandchildIconTag)
+                                .pointerHoverIcon(
+                                    desiredGrandchildIcon,
+                                    overrideDescendants = true
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Verify Parent Box's icon is the desired parent icon
+        verifyIconOnHover(parentIconTag, desiredParentIcon)
+        // Verify Child Box is respecting Parent Box's icon
+        verifyIconOnHover(childIconTag, desiredParentIcon)
+        // Verify Grandchild Box is respecting Parent Box's icon
+        verifyIconOnHover(grandchildIconTag, desiredParentIcon)
+    }
+
+    /**
+     * This test takes an existing Box with a custom icon and changes the custom icon to a different
+     * custom icon while the cursor is hovered over the box.
+     */
+    @Test
+    fun dynamicallyUpdatedIcon() {
+        val icon = mutableStateOf(desiredChildIcon)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Box(
+                        Modifier
+                            .padding(20.dp)
+                            .requiredSize(100.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(icon.value, overrideDescendants = false)
+                    )
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            enter(bottomRight)
+        }
+        // Verify Child Box has the desired child icon and dynamically update the icon assigned to
+        // the Child Box while hovering over Child Box
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+            icon.value = desiredGrandchildIcon
+        }
+        // Verify the icon has been updated to the desired grandchild icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move cursor within Child Box and verify it still has the updated icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Exit hovering over Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Initially, the Parent Box's [PointerIcon.Crosshair] should win for its entire surface area
+     *  because it has no competition in the hierarchy for any other custom icons. After the Child
+     *  Box is dynamically added under the cursor, the Child Box's [PointerIcon.Text] should win
+     *  for the entire surface area of the Child Box. This also requires updating the user facing
+     *  cursor icon to reflect the Child Box that was added under the cursor.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     */
+    @Test
+    fun dynamicallyAddAndRemoveChild_noOverrideDescendants() {
+        val isChildVisible = mutableStateOf(false)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
+                    contentAlignment = Alignment.Center
+                ) {
+                    if (isChildVisible.value) {
+                        Box(
+                            modifier = Modifier
+                                .padding(20.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                        )
+                    }
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over center of Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            enter(center)
+        }
+        // Verify Parent Box has the desired parent icon and dynamically add the Child Box under the
+        // cursor
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+            isChildVisible.value = true
+        }
+        // Verify the icon has been updated to the desired child icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor within Child Box and verify it still has the updated icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomCenter)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor back to the center of the Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Dynamically remove the Child Box
+        rule.runOnIdle {
+            isChildVisible.value = false
+        }
+        // Verify the icon has been updated to the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Exit hovering over Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  The Parent Box's [PointerIcon.Crosshair] should win for its entire surface area regardless
+     *  of whether the Child Box is visible or not. This is because the Parent Box's
+     *  overrideDescendants = true, so its children should always respect Parent Box's custom icon.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
+     */
+    @Test
+    fun dynamicallyAddAndRemoveChild_parentOverridesDescendants() {
+        val isChildVisible = mutableStateOf(false)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = true),
+                    contentAlignment = Alignment.Center
+                ) {
+                    if (isChildVisible.value) {
+                        Box(
+                            modifier = Modifier
+                                .padding(20.dp)
+                                .requiredSize(100.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                        )
+                    }
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over center of Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            enter(center)
+        }
+        // Verify Parent Box has the desired parent icon and dynamically add the Child Box under the
+        // cursor
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+            isChildVisible.value = true
+        }
+        // Verify the icon stays as the parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor within Child Box and verify it still is the parent icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomCenter)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor back to the center of the Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Dynamically remove the Child Box
+        rule.runOnIdle {
+            isChildVisible.value = false
+        }
+        // Verify the icon still the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Exit hovering over Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Initially, the Parent Box's [PointerIcon.Crosshair] should win for its entire surface area
+     *  because it has no competition in the hierarchy for any other custom icons. After the Child
+     *  Box and the Grandchild Box are dynamically added under the cursor, the Grandchild Box's
+     *  [PointerIcon.Hand] should win for the entire surface area of the Grandchild Box. The Child
+     *  Box's [PointerIcon.Text] should win for the remaining surface area of the Child Box not
+     *  covered by the Grandchild Box. This also requires updating the user facing cursor icon to
+     *  reflect the Child Box and Grandchild Box that were added under the cursor.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *      ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun dynamicallyAddAndRemoveChildAndGrandchild_noOverrideDescendants() {
+        val areDescendantsVisible = mutableStateOf(false)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
+                    contentAlignment = Alignment.Center
+                ) {
+                    if (areDescendantsVisible.value) {
+                        Box(
+                            modifier = Modifier
+                                .padding(20.dp)
+                                .requiredSize(150.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            Box(
+                                modifier = Modifier
+                                    .padding(40.dp)
+                                    .requiredSize(100.dp)
+                                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                    .testTag(grandchildIconTag)
+                                    .pointerHoverIcon(
+                                        desiredGrandchildIcon,
+                                        overrideDescendants = false
+                                    )
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over center of Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            enter(center)
+        }
+        // Verify Parent Box has the desired parent icon and dynamically add the Child Box and
+        // Grandchild Box under the cursor
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+            areDescendantsVisible.value = true
+        }
+        // Verify the icon has been updated to the desired grandchild icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move cursor within Grandchild Box and verify it still has the grandchild icon
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move cursor outside Grandchild Box within Child Box and verify it has the child icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomCenter)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor back to the center of the Grandchild Box
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Dynamically remove the Child Box and Grandchild Box
+        rule.runOnIdle {
+            areDescendantsVisible.value = false
+        }
+        // Verify the icon has been updated to the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Exit hovering over Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = TRUE)
+     *
+     *  Expected Output:
+     *  Initially, the Parent Box's [PointerIcon.Crosshair] should win for its entire surface area
+     *  because it has no competition in the hierarchy for any other custom icons. After the Child
+     *  Box and the Grandchild Box are dynamically added under the cursor, the Grandchild Box's
+     *  [PointerIcon.Hand] should win for the entire surface area of the Grandchild Box. Because the
+     *  Grandchild Box is the lowest level in the hierarchy, the outcome doesn't change whether it
+     *  has overrideDescendants = true or not. The Child Box's [PointerIcon.Text] should win for the
+     *  remaining surface area of the Child Box not covered by the Grandchild Box. This also
+     *  requires updating the user facing cursor icon to reflect the Child Box and Grandchild Box
+     *  that were added under the cursor.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *      ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun dynamicallyAddAndRemoveChildAndGrandchild_grandchildOverridesDescendants() {
+        val areDescendantsVisible = mutableStateOf(false)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
+                    contentAlignment = Alignment.Center
+                ) {
+                    if (areDescendantsVisible.value) {
+                        Box(
+                            modifier = Modifier
+                                .padding(20.dp)
+                                .requiredSize(150.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            Box(
+                                modifier = Modifier
+                                    .padding(40.dp)
+                                    .requiredSize(100.dp)
+                                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                    .testTag(grandchildIconTag)
+                                    .pointerHoverIcon(
+                                        desiredGrandchildIcon,
+                                        overrideDescendants = true
+                                    )
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over center of Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            enter(center)
+        }
+        // Verify Parent Box has the desired parent icon and dynamically add the Child Box and
+        // Grandchild Box under the cursor
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+            areDescendantsVisible.value = true
+        }
+        // Verify the icon has been updated to the desired grandchild icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move cursor within Grandchild Box and verify it still has the grandchild icon
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move cursor outside Grandchild Box within Child Box and verify it has the child icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomCenter)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor back to the center of the Grandchild Box
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Dynamically remove the Child Box and Grandchild Box
+        rule.runOnIdle {
+            areDescendantsVisible.value = false
+        }
+        // Verify the icon has been updated to the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Exit hovering over Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Initially, the Parent Box's [PointerIcon.Crosshair] should win for its entire surface area
+     *  because it has no competition in the hierarchy for any other custom icons. After the Child
+     *  Box and the Grandchild Box are dynamically added under the cursor, the Child Box's
+     *  [PointerIcon.Text] should win for the entire surface area of the Child Box. This includes
+     *  the Grandchild Box's [PointerIcon.Text] should win for the remaining surface area of the
+     *  Child Box not covered by the Grandchild Box. This also requires updating the user facing
+     *  cursor icon to reflect the Child Box and Grandchild Box that were added under the cursor.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *      ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun dynamicallyAddAndRemoveChildAndGrandchild_childOverridesDescendants() {
+        val areDescendantsVisible = mutableStateOf(false)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
+                    contentAlignment = Alignment.Center
+                ) {
+                    if (areDescendantsVisible.value) {
+                        Box(
+                            modifier = Modifier
+                                .padding(20.dp)
+                                .requiredSize(150.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = true),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            Box(
+                                modifier = Modifier
+                                    .padding(40.dp)
+                                    .requiredSize(100.dp)
+                                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                    .testTag(grandchildIconTag)
+                                    .pointerHoverIcon(
+                                        desiredGrandchildIcon,
+                                        overrideDescendants = false
+                                    )
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over center of Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            enter(center)
+        }
+        // Verify Parent Box has the desired parent icon, then dynamically add the Child Box and
+        // Grandchild Box under the cursor
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+            areDescendantsVisible.value = true
+        }
+        // Verify the icon has been updated to the desired child icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor within Grandchild Box and verify it still has the child icon
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor outside Grandchild Box within Child Box to verify it still has the child icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomCenter)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor back to the center of the Grandchild Box
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Dynamically remove the Child Box and Grandchild Box
+        rule.runOnIdle {
+            areDescendantsVisible.value = false
+        }
+        // Verify the icon has been updated to the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Exit hovering over Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  The Child Box's [PointerIcon.Text] should win for its entire surface area regardless of
+     *  whether there's a Parent Box present or not. This is because the Parent Box has
+     *  overrideDescendants = false and should therefore not have its custom icon take priority over
+     *  the Child Box's custom icon. The Parent Box's [PointerIcon.Crosshair] should win for its
+     *  remaining surface area not covered by the Child Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     */
+    @Test
+    fun dynamicallyAddAndRemoveParent_noOverrideDescendants() {
+        val isParentVisible = mutableStateOf(false)
+        val child = movableContentOf {
+            Box(
+                modifier = Modifier
+                    .requiredSize(150.dp)
+                    .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                    .testTag(childIconTag)
+                    .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+            )
+        }
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                if (isParentVisible.value) {
+                    Box(
+                        modifier = Modifier
+                            .requiredSize(200.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                            .testTag(parentIconTag)
+                            .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
+                        contentAlignment = Alignment.Center
+                    ) {
+                        child()
+                    }
+                } else {
+                    child()
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over center of Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            enter(center)
+        }
+        // Verify Child Box has the desired child icon and dynamically add the Parent Box under the
+        // cursor
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+            isParentVisible.value = true
+        }
+        // Verify the icon stays as the desired child icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor within Child Box and verify it still has the child icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomCenter)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor back to the center of the Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Dynamically remove the Parent Box
+        rule.runOnIdle {
+            isParentVisible.value = false
+        }
+        // Verify the icon stays as the desired child icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Exit hovering over Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = TRUE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  The Child Box's [PointerIcon.Text] should win for its entire surface area when the Parent
+     *  Box isn't present. Once the Parent Box becomes visible, the Parent Box's
+     *  [PointerIcon.Crosshair] should win for its entire surface area. This is because the Parent
+     *  Box's overrideDescendants = true, so its children should always respect Parent Box's custom
+     *  icon. This also requires updating the user facing cursor icon to reflect the Parent Box that
+     *  was added under the cursor.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
+     */
+    @Test
+    fun dynamicallyAddAndRemoveParent_parentOverridesDescendants() {
+        val isParentVisible = mutableStateOf(false)
+        val child = movableContentOf {
+            Box(
+                modifier = Modifier
+                    .requiredSize(150.dp)
+                    .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                    .testTag(childIconTag)
+                    .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+            )
+        }
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                if (isParentVisible.value) {
+                    Box(
+                        modifier = Modifier
+                            .requiredSize(200.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                            .testTag(parentIconTag)
+                            .pointerHoverIcon(desiredParentIcon, overrideDescendants = true),
+                        contentAlignment = Alignment.Center
+                    ) {
+                        child()
+                    }
+                } else {
+                    child()
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over center of Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            enter(center)
+        }
+        // Verify Child Box has the desired child icon and dynamically add the Parent Box under the
+        // cursor
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+            isParentVisible.value = true
+        }
+        // Verify the icon has been updated to the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor within Child Box and verify it still has the parent icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor outside Child Box and verify the icon is still the parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomCenter)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor back to the center of the Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Dynamically remove the Parent Box
+        rule.runOnIdle {
+            isParentVisible.value = false
+        }
+        // Verify the icon has been updated to the desired child icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Exit hovering over Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  The Grandchild Box's [PointerIcon.Hand] should win for its entire surface area regardless of
+     *  whether there's a Child Box or Parent Box present. This is because the Parent Box and Child
+     *  Box have overrideDescendants = false and should therefore not have their custom icons take
+     *  priority over the Grandchild Box's custom icon. The Child Box should win for its remaining
+     *  surface area not covered by the Grandchild Box. The Parent Box's [PointerIcon.Crosshair]
+     *  should win for its remaining surface area not covered by the Child Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *      ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun dynamicallyAddAndRemoveNestedChild_noOverrideDescendants() {
+        val isChildVisible = mutableStateOf(false)
+        val grandchild = movableContentOf {
+            Box(
+                modifier = Modifier
+                    .requiredSize(100.dp)
+                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                    .testTag(grandchildIconTag)
+                    .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = false)
+            )
+        }
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
+                    contentAlignment = Alignment.Center
+                ) {
+                    if (isChildVisible.value) {
+                        Box(
+                            modifier = Modifier
+                                .requiredSize(150.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            grandchild()
+                        }
+                    } else {
+                        grandchild()
+                    }
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over center of Grandchild Box
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            enter(center)
+        }
+        // Verify Grandchild Box has the desired grandchild icon and dynamically add the Child Box
+        // under the cursor
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+            isChildVisible.value = true
+        }
+        // Verify the icon stays as the desired grandchild icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move cursor within Grandchild Box and verify it still has the grandchild icon
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move cursor outside Grandchild Box within Child Box to verify icon is now the child icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor back to the center of the Grandchild Box
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Dynamically remove the Child Box
+        rule.runOnIdle {
+            isChildVisible.value = false
+        }
+        // Verify the icon has been updated to the desired grandchild icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Exit hovering over Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  After hovering over the center of the screen, the hierarchy under the cursor updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = TRUE)
+     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  The Grandchild Box's [PointerIcon.Hand] should win for its entire surface area regardless of
+     *  whether there's a Child Box or Parent Box present. This is because the Parent Box and Child
+     *  Box have overrideDescendants = false and should therefore not have thei custom icona take
+     *  priority over the Grandchild Box's custom icon. The Child Box should win for its remaining
+     *  surface area not covered by the Grandchild Box. The Parent Box's [PointerIcon.Crosshair]
+     *  should win for its remaining surface area not covered by the Child Box.
+     *  Initially, the Parent Box's [PointerIcon.Crosshair] should win for its entire surface area
+     *  because it has no competition in the hierarchy for any other custom icons. After the Child
+     *  Box and the Grandchild Box are dynamically added under the cursor, the Child Box's
+     *  [PointerIcon.Text] should win for the entire surface area of the Child Box. This includes
+     *  the Grandchild Box's [PointerIcon.Text] should win for the remaining surface area of the Child Box not
+     *  covered by the Grandchild Box. This also requires updating the user facing cursor icon to
+     *  reflect the Child Box and Grandchild Box that were added under the cursor.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *      ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun dynamicallyAddAndRemoveNestedChild_ChildOverridesDescendants() {
+        val isChildVisible = mutableStateOf(false)
+        val grandchild = movableContentOf {
+            Box(
+                modifier = Modifier
+                    .requiredSize(100.dp)
+                    .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                    .testTag(grandchildIconTag)
+                    .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = false)
+            )
+        }
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
+                    contentAlignment = Alignment.Center
+                ) {
+                    if (isChildVisible.value) {
+                        Box(
+                            modifier = Modifier
+                                .requiredSize(150.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = true),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            grandchild()
+                        }
+                    } else {
+                        grandchild()
+                    }
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over center of Grandchild Box
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            enter(center)
+        }
+        // Verify Grandchild Box has the desired grandchild icon and dynamically add the Child Box
+        // under the cursor
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+            isChildVisible.value = true
+        }
+        // Verify the icon has been updated to the desired child icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor within Grandchild Box and verify it still has the child icon
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor outside Grandchild Box within Child Box to verify it still has the child icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor outside Child Box and verify the icon is updated to the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomCenter)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor back to center of the Grandchild Box
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Dynamically remove the Child Box
+        rule.runOnIdle {
+            isChildVisible.value = false
+        }
+        // Verify the icon has been updated to the desired grandchild icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Exit hovering over Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Grandparent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  After hovering over the corner of the Grandparent Box that doesn't overlap with any
+     *  descendant, the hierarchy of the screen updates to:
+     *  Grandparent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *      ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *         ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  The Grandchild Box's [PointerIcon.Hand] should win for its entire surface area regardless of
+     *  whether there's a Child, Parent, or Grandparent Box present. This is because the
+     *  Grandparent, Parent, and Child Boxes have overrideDescendants = false and should therefore
+     *  not have their custom icons take priority over the Grandchild Box's custom icon. The Child
+     *  Box should win for its remaining surface area not covered by the Grandchild Box. The Parent
+     *  Box's [PointerIcon.Crosshair] should win for its remaining surface area not covered by the
+     *  Child Box. And the Grandparent Box should win for its remaining surface area not covered by
+     *  the Parent Box.
+     *
+     *  Grandparent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Parent Box (output icon = [PointerIcon.Crosshair])
+     *      ⤷ Child Box (output icon = [PointerIcon.Text])
+     *        ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun dynamicallyAddAndRemoveNestedChild_notHoveredOverChild() {
+        val grandparentIconTag = "myGrandparentIcon"
+        val desiredGrandparentIcon = desiredParentIcon
+        val isChildVisible = mutableStateOf(false)
+        val grandchild = movableContentOf {
+            Box(
+                modifier = Modifier
+                    .requiredSize(100.dp)
+                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                    .testTag(grandchildIconTag)
+                    .pointerHoverIcon(desiredGrandchildIcon, overrideDescendants = false)
+            )
+        }
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(grandparentIconTag)
+                        .pointerHoverIcon(desiredGrandparentIcon, overrideDescendants = false),
+                    contentAlignment = Alignment.Center
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .requiredSize(175.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                            .testTag(parentIconTag)
+                            .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
+                        contentAlignment = Alignment.Center
+                    ) {
+                        if (isChildVisible.value) {
+                            Box(
+                                modifier = Modifier
+                                    .requiredSize(150.dp)
+                                    .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                    .testTag(childIconTag)
+                                    .pointerHoverIcon(
+                                        desiredChildIcon,
+                                        overrideDescendants = false
+                                    ),
+                                contentAlignment = Alignment.Center
+                            ) {
+                                grandchild()
+                            }
+                        } else {
+                            grandchild()
+                        }
+                    }
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over center of Grandchild Box
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            enter(center)
+        }
+        // Verify Grandchild Box has the desired grandchild icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move to corner of Grandparent Box where no descendants are under the cursor
+        rule.onNodeWithTag(grandparentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        // Verify the icon is the desired grandparent icon and dynamically add the Child Box
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandparentIcon)
+            isChildVisible.value = true
+        }
+        // Verify the icon stays as the desired grandparent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandparentIcon)
+        }
+        // Move cursor within Grandparent Box and verify it still has the grandparent icon
+        rule.onNodeWithTag(grandparentIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandparentIcon)
+        }
+        // Move cursor outside Grandparent Box to Parent Box to verify icon is now the parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor back to corner of Grandparent Box where no descendants are under the cursor
+        rule.onNodeWithTag(grandparentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        // Dynamically remove the Child Box
+        rule.runOnIdle {
+            isChildVisible.value = false
+        }
+        // Verify the icon stays as the grandparent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandparentIcon)
+        }
+        // Exit hovering over Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  After hovering over the corner of the Parent Box that doesn't overlap with any descendant,
+     *  the hierarchy of the screen updates to:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *      ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  The Grandchild Box's [PointerIcon.Hand] should win for its entire surface area regardless of
+     *  whether there's a Child or Parent Box present. This is because the Parent and Child Boxes
+     *  have overrideDescendants = false and should therefore not have their custom icons take
+     *  priority over the Grandchild Box's custom icon. The Child Box should win for its remaining
+     *  surface area not covered by the Grandchild Box. The Parent Box's [PointerIcon.Crosshair]
+     *  should win for its remaining surface area not covered by the Child Box.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *      ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun dynamicallyAddAndRemoveGrandchild_notHoveredOverGrandchild() {
+        val isGrandchildVisible = mutableStateOf(false)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false),
+                    contentAlignment = Alignment.Center
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .requiredSize(150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                            .testTag(childIconTag)
+                            .pointerHoverIcon(
+                                desiredChildIcon,
+                                overrideDescendants = false
+                            ),
+                        contentAlignment = Alignment.Center
+                    ) {
+                        if (isGrandchildVisible.value) {
+                            Box(
+                                modifier = Modifier
+                                    .requiredSize(100.dp)
+                                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                    .testTag(grandchildIconTag)
+                                    .pointerHoverIcon(
+                                        desiredGrandchildIcon,
+                                        overrideDescendants = false
+                                    )
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over center of Child Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            enter(center)
+        }
+        // Verify Child Box has the desired child icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move to corner of Parent Box where no descendants are under the cursor
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        // Verify the icon is the desired parent icon and dynamically add the Grandchild Box
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+            isGrandchildVisible.value = true
+        }
+        // Verify the icon stays as the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor within Parent Box and verify it still has the grandparent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move cursor outside Parent Box to Child Box to verify icon is now the child icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor back to corner of Parent Box where no descendants are under the cursor
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        // Dynamically remove the Grandchild Box
+        rule.runOnIdle {
+            isGrandchildVisible.value = false
+        }
+        // Verify the icon stays as the parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Exit hovering over Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  After hovering over the area where ChildB will be, the hierarchy of the screen updates to:
+     *  Parent Box (no custom icon set)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Regardless of the presence of ChildB Box, ChildA Box's [PointerIcon.Text] should win for
+     *  its entire surface area. Once ChildB Box appears, ChildB Box's [PointerIcon.Hand] should
+     *  win for its entire surface area. Initially, Parent Box's [PointerIcon.Crosshair] should win
+     *  for its entire surface area not covered by ChildA Box. Once ChildA Box appears, Parent Box
+     *  should win for its entire surface not covered by either ChildA or ChildB Boxes.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *    ⤷ Child Box (output icon = [PointerIcon.Hand])
+     */
+    @Ignore("b/271277248 - Remove Ignore annotation once input event bug is fixed")
+    @Test
+    fun dynamicallyAddAndRemoveSibling_hoveredOverAppearingSibling() {
+        val isChildBVisible = mutableStateOf(false)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(150.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Column {
+                        Box(
+                            Modifier
+                                .requiredSize(50.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(
+                                    desiredChildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                        if (isChildBVisible.value) {
+                            // Referencing grandchild tag/icon for ChildB in this test
+                            Box(
+                                Modifier
+                                    .requiredSize(50.dp)
+                                    .offset(y = 100.dp)
+                                    .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                    .testTag(grandchildIconTag)
+                                    .pointerHoverIcon(
+                                        desiredGrandchildIcon,
+                                        overrideDescendants = false
+                                    )
+                            )
+                        }
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over corner of Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            enter(bottomRight)
+        }
+        // Verify Parent Box has the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move to center of ChildA Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Verify ChildA Box has the desired child icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move to left corner of Parent Box where ChildB will be added
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomLeft)
+        }
+        // Dynamically add the ChildB Box
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+            isChildBVisible.value = true
+        }
+        // Verify the icon is updated to the desired ChildB icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move to corner of ChildB Box
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        // Verify ChildB Box has the desired grandchild icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move cursor back to the center of ChildA Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Verify that icon is updated to the desired ChildA icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor back to the location of ChildB
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Dynamically remove the ChildB Box
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+            isChildBVisible.value = false
+        }
+        // Verify the icon updates to the parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Exit hovering over ChildA Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for the initial setup of this test is:
+     *  Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *
+     *  After hovering over ChildA, the hierarchy of the screen updates to:
+     *  Parent Box (no custom icon set)
+     *    ⤷ ChildA Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *    ⤷ ChildB Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Regardless of the presence of ChildB Box, ChildA Box's [PointerIcon.Text] should win for
+     *  its entire surface area. Once ChildB Box appears, ChildB Box's [PointerIcon.Hand] should
+     *  win for its entire surface area. Initially, Parent Box's [PointerIcon.Crosshair] should win
+     *  for its entire surface area not covered by ChildA Box. Once ChildA Box appears, Parent Box
+     *  should win for its entire surface not covered by either ChildA or ChildB Boxes.
+     *
+     *  Parent Box (output icon = [PointerIcon.Crosshair])
+     *    ⤷ Child Box (output icon = [PointerIcon.Text])
+     *    ⤷ Child Box (output icon = [PointerIcon.Hand])
+     */
+    @Ignore("b/271277248 - Remove Ignore annotation once input event bug is fixed")
+    @Test
+    fun dynamicallyAddAndRemoveSibling_notHoveredOverAppearingSibling() {
+        val isChildBVisible = mutableStateOf(false)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .requiredSize(200.dp)
+                        .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                        .testTag(parentIconTag)
+                        .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                ) {
+                    Column {
+                        Box(
+                            Modifier
+                                .requiredSize(50.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(
+                                    desiredChildIcon,
+                                    overrideDescendants = false
+                                )
+                        )
+                        if (isChildBVisible.value) {
+                            // Referencing grandchild tag/icon for ChildB in this test
+                            Box(
+                                Modifier
+                                    .requiredSize(50.dp)
+                                    .offset(y = 100.dp)
+                                    .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                    .testTag(grandchildIconTag)
+                                    .pointerHoverIcon(
+                                        desiredGrandchildIcon,
+                                        overrideDescendants = false
+                                    )
+                            )
+                        }
+                    }
+                }
+            }
+        }
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over corner of Parent Box
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            enter(bottomRight)
+        }
+        // Verify Parent Box has the desired parent icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+        // Move to center of ChildA Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Verify ChildA Box has the desired child icon and dynamically add the ChildB Box
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+            isChildBVisible.value = true
+        }
+        // Verify the icon stays as the desired child icon since the cursor hasn't moved
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move to corner of ChildB Box
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        // Verify ChildB Box has the desired grandchild icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move cursor back to the center of ChildA Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(center)
+        }
+        // Dynamically remove the ChildB Box
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+            isChildBVisible.value = false
+        }
+        // Verify the icon stays as the desired child icon since the cursor hasn't moved
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Exit hovering over ChildA Box
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            exit()
+        }
+    }
+
+    /**
+     * Setup:
+     * The hierarchy for this test is setup as:
+     *  Default Box (no custom icon set)
+     *    ⤷ Parent Box (custom icon = [PointerIcon.Crosshair], overrideDescendants = FALSE)
+     *        ⤷ Child Box (custom icon = [PointerIcon.Text], overrideDescendants = FALSE)
+     *            ⤷ Grandchild Box (custom icon = [PointerIcon.Hand], overrideDescendants = FALSE)
+     *
+     *  Expected Output:
+     *  Grandchild Box's [PointerIcon.Hand] wins for the entire surface area of the Grandchild Box.
+     *  Child Box's [PointerIcon.Text] wins for the remaining surface area of the Child Box not
+     *  covered by the Grandchild Box. Parent Box’s [PointerIcon.Crosshair] wins for the remaining
+     *  surface area not covered by the Child Box. [PointerIcon.Default] wins for the remaining
+     *  surface area of
+     *
+     *  Default Box (output icon = [PointerIcon.Default]
+     *    ⤷ Parent Box (output icon = [PointerIcon.Crosshair])
+     *        ⤷ Child Box (output icon = [PointerIcon.Text])
+     *            ⤷ Grandchild Box (output icon = [PointerIcon.Hand])
+     */
+    @Test
+    fun childNotFullyContainedInParent_noOverrideDescendants() {
+        val defaultIconTag = "myDefaultWrapper"
+        rule.setContent {
+            CompositionLocalProvider(LocalPointerIconService provides iconService) {
+                Box(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .border(BorderStroke(2.dp, SolidColor(Color.Yellow)))
+                        .testTag(defaultIconTag)
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .requiredSize(width = 200.dp, height = 150.dp)
+                            .border(BorderStroke(2.dp, SolidColor(Color.Red)))
+                            .testTag(parentIconTag)
+                            .pointerHoverIcon(desiredParentIcon, overrideDescendants = false)
+                    ) {
+                        Box(
+                            Modifier
+                                .requiredSize(width = 150.dp, height = 125.dp)
+                                .border(BorderStroke(2.dp, SolidColor(Color.Black)))
+                                .testTag(childIconTag)
+                                .pointerHoverIcon(desiredChildIcon, overrideDescendants = false)
+                        ) {
+                            Box(
+                                Modifier
+                                    .requiredSize(width = 300.dp, height = 100.dp)
+                                    .offset(x = 100.dp)
+                                    .border(BorderStroke(2.dp, SolidColor(Color.Blue)))
+                                    .testTag(grandchildIconTag)
+                                    .pointerHoverIcon(
+                                        desiredGrandchildIcon,
+                                        overrideDescendants = false
+                                    )
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over the default wrapping box and verify the cursor is still the default icon
+        rule.onNodeWithTag(defaultIconTag).performMouseInput {
+            enter(center)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Move cursor to the corner of the Grandchild Box and verify it has the desired grandchild
+        // icon
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move cursor to the center right of the Child Box and verify it still has the desired
+        // grandchild icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move cursor to the corner of the Child Box and verify it has updated to the desired child
+        // icon
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+        // Move cursor to the center right of the Parent Box and verify it has the desired
+        // grandchild icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(centerRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+        // Move cursor to the corner of the Parent Box and verify it has the desired parent icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredParentIcon)
+        }
+    }
+
+    @Test
+    fun resetPointerIconWhenChildRemoved_parentDoesSetIcon_iconIsHand() {
+        val defaultIconTag = "myDefaultWrapper"
+        var show by mutableStateOf(true)
+        rule.setContent {
+            CompositionLocalProvider(
+                LocalPointerIconService provides iconService
+            ) {
+                Box(modifier = Modifier
+                    .fillMaxSize()
+                    .pointerHoverIcon(PointerIcon.Hand)
+                    .testTag(defaultIconTag)
+                ) {
+                    if (show) {
+                        Box(
+                            modifier = Modifier
+                                .pointerHoverIcon(PointerIcon.Text)
+                                .size(10.dp, 10.dp)
+                        )
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            // No mouse movement yet, should be default
+            assertThat(iconService.getIcon()).isEqualTo(PointerIcon.Default)
+        }
+
+        rule.onNodeWithTag(defaultIconTag).performMouseInput {
+            moveTo(Offset(x = 5f, y = 5f))
+        }
+
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(PointerIcon.Text)
+        }
+
+        show = false
+
+        rule.onNodeWithTag(defaultIconTag).performMouseInput {
+            moveTo(Offset(x = 6f, y = 6f))
+        }
+
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(PointerIcon.Hand)
+        }
+    }
+
+    @Test
+    fun resetPointerIconWhenChildRemoved_parentDoesNotSetIcon_iconIsDefault() {
+        val defaultIconTag = "myDefaultWrapper"
+        var show by mutableStateOf(true)
+        rule.setContent {
+            CompositionLocalProvider(
+                LocalPointerIconService provides iconService
+            ) {
+                Box(modifier = Modifier
+                    .fillMaxSize()
+                    .testTag(defaultIconTag)
+                ) {
+                    if (show) {
+                        Box(
+                            modifier = Modifier
+                                .pointerHoverIcon(PointerIcon.Text)
+                                .size(10.dp, 10.dp)
+                        )
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            // No mouse movement yet, should be default
+            assertThat(iconService.getIcon()).isEqualTo(PointerIcon.Default)
+        }
+
+        rule.onNodeWithTag(defaultIconTag).performMouseInput {
+            moveTo(Offset(x = 5f, y = 5f))
+        }
+
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(PointerIcon.Text)
+        }
+
+        show = false
+
+        rule.onNodeWithTag(defaultIconTag).performMouseInput {
+            moveTo(Offset(x = 6f, y = 6f))
+        }
+
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(PointerIcon.Default)
+        }
+    }
+
+    private fun verifyIconOnHover(tag: String, expectedIcon: PointerIcon) {
+        // Hover over element with specified tag
+        rule.onNodeWithTag(tag).performMouseInput {
+            enter(bottomRight)
+        }
+        // Verify the current icon is the expected icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(expectedIcon)
+        }
+        // Exit hovering over element
+        rule.onNodeWithTag(tag).performMouseInput {
+            exit()
+        }
+    }
+
+    private fun verifyOverlappingSiblings() {
+        // Verify initial state of pointer icon
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+        // Hover over ChildB (bottom right corner) and verify desired ChildB icon
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            enter(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+
+        // Then hover to parent (bottom right corner) and verify default arrow icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomRight)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+
+        // Then hover back over ChildB in area that overlaps with sibling (bottom left corner) and
+        // verify desired ChildB icon
+        rule.onNodeWithTag(grandchildIconTag).performMouseInput {
+            moveTo(bottomLeft)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredGrandchildIcon)
+        }
+
+        // Then hover to ChildA (bottom left corner) and verify desired ChildA icon (hand)
+        rule.onNodeWithTag(childIconTag).performMouseInput {
+            moveTo(bottomLeft)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredChildIcon)
+        }
+
+        // Then hover over parent (bottom left corner) and verify default arrow icon
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            moveTo(bottomLeft)
+        }
+        rule.runOnIdle {
+            assertThat(iconService.getIcon()).isEqualTo(desiredDefaultIcon)
+        }
+
+        // Exit hovering
+        rule.onNodeWithTag(parentIconTag).performMouseInput {
+            exit()
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputDensityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputDensityTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputDensityTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputDensityTest.kt
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
new file mode 100644
index 0000000..5d2266e
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -0,0 +1,3474 @@
+/*
+ * Copyright 2020 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.input.pointer
+
+import android.view.InputDevice
+import android.view.KeyEvent as AndroidKeyEvent
+import android.view.MotionEvent
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.autofill.Autofill
+import androidx.compose.ui.autofill.AutofillTree
+import androidx.compose.ui.draganddrop.DragAndDropInfo
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.FocusOwner
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.hapticfeedback.HapticFeedback
+import androidx.compose.ui.input.InputModeManager
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.modifier.ModifierLocalManager
+import androidx.compose.ui.node.InternalCoreApi
+import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.LayoutNodeDrawScope
+import androidx.compose.ui.node.MeasureAndLayoutDelegate
+import androidx.compose.ui.node.OwnedLayer
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.OwnerSnapshotObserver
+import androidx.compose.ui.node.RootForTest
+import androidx.compose.ui.platform.AccessibilityManager
+import androidx.compose.ui.platform.ClipboardManager
+import androidx.compose.ui.platform.PlatformTextInputSessionScope
+import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.input.TextInputService
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.minus
+import androidx.compose.ui.unit.toOffset
+import androidx.compose.ui.util.fastMaxBy
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executors
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.asCoroutineDispatcher
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO(shepshapard): Write the following PointerInputEvent to PointerInputChangeEvent tests
+// 2 down, 2 move, 2 up, converted correctly
+// 3 down, 3 move, 3 up, converted correctly
+// down, up, down, up, converted correctly
+// 2 down, 1 up, same down, both up, converted correctly
+// 2 down, 1 up, new down, both up, converted correctly
+// new is up, throws exception
+
+// TODO(shepshapard): Write the following hit testing tests
+// 2 down, one hits, target receives correct event
+// 2 down, one moves in, one out, 2 up, target receives correct event stream
+// down, up, receives down and up
+// down, move, up, receives all 3
+// down, up, then down and misses, target receives down and up
+// down, misses, moves in bounds, up, target does not receive event
+// down, hits, moves out of bounds, up, target receives all events
+
+// TODO(shepshapard): Write the following offset testing tests
+// 3 simultaneous moves, offsets are correct
+
+// TODO(shepshapard): Write the following pointer input dispatch path tests:
+// down, move, up, on 2, hits all 5 passes
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class PointerInputEventProcessorTest {
+
+    private lateinit var pointerInputEventProcessor: PointerInputEventProcessor
+    private lateinit var testOwner: TestOwner
+    private val positionCalculator = object : PositionCalculator {
+        override fun screenToLocal(positionOnScreen: Offset): Offset = positionOnScreen
+
+        override fun localToScreen(localPosition: Offset): Offset = localPosition
+
+        override fun localToScreen(localTransform: Matrix) {}
+    }
+
+    @Before
+    fun setup() {
+        testOwner = TestOwner()
+        pointerInputEventProcessor = PointerInputEventProcessor(testOwner.root)
+    }
+
+    private fun addToRoot(vararg layoutNodes: LayoutNode) {
+        layoutNodes.forEachIndexed { index, node ->
+            testOwner.root.insertAt(index, node)
+        }
+        testOwner.measureAndLayout()
+    }
+
+    @Test
+    @OptIn(ExperimentalComposeUiApi::class)
+    fun pointerTypePassed() {
+        val pointerTypes = listOf(
+            PointerType.Unknown,
+            PointerType.Touch,
+            PointerType.Mouse,
+            PointerType.Stylus,
+            PointerType.Eraser
+        )
+
+        // Arrange
+        val pointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            0,
+            0,
+            500,
+            500,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+
+        addToRoot(layoutNode)
+
+        val offset = Offset(100f, 200f)
+        val previousEvents = mutableListOf<PointerInputEventData>()
+        val events = pointerTypes.mapIndexed { index, pointerType ->
+            previousEvents += PointerInputEventData(
+                id = PointerId(index.toLong()),
+                uptime = index.toLong(),
+                positionOnScreen = Offset(offset.x + index, offset.y + index),
+                position = Offset(offset.x + index, offset.y + index),
+                originalEventPosition = Offset(offset.x + index, offset.y + index),
+                down = true,
+                pressure = 1.0f,
+                type = pointerType
+            )
+            val data = previousEvents.map {
+                it.copy(uptime = index.toLong())
+            }
+            PointerInputEvent(index.toLong(), data)
+        }
+
+        // Act
+
+        events.forEach { pointerInputEventProcessor.process(it) }
+
+        // Assert
+
+        val log = pointerInputFilter.log.getOnPointerEventFilterLog()
+
+        // Verify call count
+        assertThat(log)
+            .hasSize(PointerEventPass.values().size * pointerTypes.size)
+
+        // Verify types of the pointers
+        repeat(pointerTypes.size) { eventIndex ->
+            PointerEventPass.values().forEachIndexed { passIndex, pass ->
+                val item = log[passIndex + (eventIndex * PointerEventPass.values().size)]
+                assertThat(item.pass).isEqualTo(pass)
+
+                val changes = item.pointerEvent.changes
+                assertThat(changes.size).isEqualTo(eventIndex + 1)
+
+                for (i in 0..eventIndex) {
+                    val pointerType = pointerTypes[i]
+                    val change = changes[i]
+                    assertThat(change.type).isEqualTo(pointerType)
+                }
+            }
+        }
+    }
+
+    /**
+     * PointerInputEventProcessor doesn't currently support reentrancy and
+     * b/233209795 indicates that it is likely causing a crash. This test
+     * ensures that if we have reentrancy that we exit without handling
+     * the event. This test can be replaced with tests supporting reentrant
+     * behavior when reentrancy is supported.
+     */
+    @Test
+    fun noReentrancy() {
+        var reentrancyCount = 0
+        // Arrange
+        val reentrantPointerInputFilter = object : PointerInputFilter() {
+            override fun onPointerEvent(
+                pointerEvent: PointerEvent,
+                pass: PointerEventPass,
+                bounds: IntSize
+            ) {
+                if (pass != PointerEventPass.Initial) {
+                    return
+                }
+                if (reentrancyCount > 1) {
+                    // Don't allow infinite recursion. Just enough to break the test.
+                    return
+                }
+                val oldId = pointerEvent.changes.fastMaxBy { it.id.value }!!.id.value.toInt()
+                val event = PointerInputEvent(oldId + 1, 14, Offset.Zero, true)
+                // force a reentrant call
+                val result = pointerInputEventProcessor.process(event)
+                assertThat(result.anyMovementConsumed).isFalse()
+                assertThat(result.dispatchedToAPointerInputModifier).isFalse()
+                pointerEvent.changes.forEach { it.consume() }
+                reentrancyCount++
+            }
+
+            override fun onCancel() {
+            }
+        }
+
+        val layoutNode = LayoutNode(
+            0,
+            0,
+            500,
+            500,
+            PointerInputModifierImpl2(reentrantPointerInputFilter)
+        )
+
+        addToRoot(layoutNode)
+
+        // Act
+
+        val result =
+            pointerInputEventProcessor.process(PointerInputEvent(8712, 3, Offset.Zero, true))
+
+        // Assert
+
+        assertThat(reentrancyCount).isEqualTo(1)
+
+        assertThat(result.anyMovementConsumed).isFalse()
+        assertThat(result.dispatchedToAPointerInputModifier).isTrue()
+    }
+
+    @Test
+    fun process_downMoveUp_convertedCorrectlyAndTraversesAllPassesInCorrectOrder() {
+
+        // Arrange
+        val pointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            0,
+            0,
+            500,
+            500,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+
+        addToRoot(layoutNode)
+
+        val offset = Offset(100f, 200f)
+        val offset2 = Offset(300f, 400f)
+
+        val events = arrayOf(
+            PointerInputEvent(8712, 3, offset, true),
+            PointerInputEvent(8712, 11, offset2, true),
+            PointerInputEvent(8712, 13, offset2, false)
+        )
+
+        val down = down(8712, 3, offset.x, offset.y)
+        val move = down.moveTo(11, offset2.x, offset2.y)
+        val up = move.up(13)
+
+        val expectedChanges = arrayOf(down, move, up)
+
+        // Act
+
+        events.forEach { pointerInputEventProcessor.process(it) }
+
+        // Assert
+
+        val log = pointerInputFilter.log.getOnPointerEventFilterLog()
+
+        // Verify call count
+        assertThat(log)
+            .hasSize(PointerEventPass.values().size * expectedChanges.size)
+
+        // Verify call values
+        var count = 0
+        expectedChanges.forEach { change ->
+            PointerEventPass.values().forEach { pass ->
+                val item = log[count]
+                PointerEventSubject
+                    .assertThat(item.pointerEvent)
+                    .isStructurallyEqualTo(pointerEventOf(change))
+                assertThat(item.pass).isEqualTo(pass)
+                count++
+            }
+        }
+    }
+
+    @Test
+    fun process_downHits_targetReceives() {
+
+        // Arrange
+
+        val childOffset = Offset(100f, 200f)
+        val pointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            100, 200, 301, 401,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+
+        addToRoot(layoutNode)
+
+        val offsets = arrayOf(
+            Offset(100f, 200f),
+            Offset(300f, 200f),
+            Offset(100f, 400f),
+            Offset(300f, 400f)
+        )
+
+        val events = Array(4) { index ->
+            PointerInputEvent(index, 5, offsets[index], true)
+        }
+
+        val expectedChanges = Array(4) { index ->
+            PointerInputChange(
+                id = PointerId(index.toLong()),
+                5,
+                offsets[index] - childOffset,
+                true,
+                5,
+                offsets[index] - childOffset,
+                false,
+                isInitiallyConsumed = false
+            )
+        }
+
+        // Act
+
+        events.forEach {
+            pointerInputEventProcessor.process(it)
+        }
+
+        // Assert
+
+        val log =
+            pointerInputFilter
+                .log
+                .getOnPointerEventFilterLog()
+                .filter { it.pass == PointerEventPass.Initial }
+
+        // Verify call count
+        assertThat(log)
+            .hasSize(expectedChanges.size)
+
+        // Verify call values
+        expectedChanges.forEachIndexed { index, change ->
+            val item = log[index]
+            PointerEventSubject
+                .assertThat(item.pointerEvent)
+                .isStructurallyEqualTo(pointerEventOf(change))
+        }
+    }
+
+    @Test
+    fun process_downMisses_targetDoesNotReceive() {
+
+        // Arrange
+
+        val pointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            100, 200, 301, 401,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+
+        addToRoot(layoutNode)
+
+        val offsets = arrayOf(
+            Offset(99f, 200f),
+            Offset(99f, 400f),
+            Offset(100f, 199f),
+            Offset(100f, 401f),
+            Offset(300f, 199f),
+            Offset(300f, 401f),
+            Offset(301f, 200f),
+            Offset(301f, 400f)
+        )
+
+        val events = Array(8) { index ->
+            PointerInputEvent(index, 0, offsets[index], true)
+        }
+
+        // Act
+
+        events.forEach {
+            pointerInputEventProcessor.process(it)
+        }
+
+        // Assert
+
+        assertThat(pointerInputFilter.log.getOnPointerEventFilterLog()).hasSize(0)
+    }
+
+    @Test
+    fun process_downHits3of3_all3PointerNodesReceive() {
+        process_partialTreeHits(3)
+    }
+
+    @Test
+    fun process_downHits2of3_correct2PointerNodesReceive() {
+        process_partialTreeHits(2)
+    }
+
+    @Test
+    fun process_downHits1of3_onlyCorrectPointerNodesReceives() {
+        process_partialTreeHits(1)
+    }
+
+    private fun process_partialTreeHits(numberOfChildrenHit: Int) {
+        // Arrange
+
+        val log = mutableListOf<LogEntry>()
+        val childPointerInputFilter = PointerInputFilterMock(log)
+        val middlePointerInputFilter = PointerInputFilterMock(log)
+        val parentPointerInputFilter = PointerInputFilterMock(log)
+
+        val childLayoutNode =
+            LayoutNode(
+                100, 100, 200, 200,
+                PointerInputModifierImpl2(
+                    childPointerInputFilter
+                )
+            )
+        val middleLayoutNode: LayoutNode =
+            LayoutNode(
+                100, 100, 400, 400,
+                PointerInputModifierImpl2(
+                    middlePointerInputFilter
+                )
+            ).apply {
+                insertAt(0, childLayoutNode)
+            }
+        val parentLayoutNode: LayoutNode =
+            LayoutNode(
+                0, 0, 500, 500,
+                PointerInputModifierImpl2(
+                    parentPointerInputFilter
+                )
+            ).apply {
+                insertAt(0, middleLayoutNode)
+            }
+        addToRoot(parentLayoutNode)
+
+        val offset = when (numberOfChildrenHit) {
+            3 -> Offset(250f, 250f)
+            2 -> Offset(150f, 150f)
+            1 -> Offset(50f, 50f)
+            else -> throw IllegalStateException()
+        }
+
+        val event = PointerInputEvent(0, 5, offset, true)
+
+        // Act
+
+        pointerInputEventProcessor.process(event)
+
+        // Assert
+
+        val filteredLog = log.getOnPointerEventFilterLog().filter {
+            it.pass == PointerEventPass.Initial
+        }
+
+        when (numberOfChildrenHit) {
+            3 -> {
+                assertThat(filteredLog).hasSize(3)
+                assertThat(filteredLog[0].pointerInputFilter)
+                    .isSameInstanceAs(parentPointerInputFilter)
+                assertThat(filteredLog[1].pointerInputFilter)
+                    .isSameInstanceAs(middlePointerInputFilter)
+                assertThat(filteredLog[2].pointerInputFilter)
+                    .isSameInstanceAs(childPointerInputFilter)
+            }
+            2 -> {
+                assertThat(filteredLog).hasSize(2)
+                assertThat(filteredLog[0].pointerInputFilter)
+                    .isSameInstanceAs(parentPointerInputFilter)
+                assertThat(filteredLog[1].pointerInputFilter)
+                    .isSameInstanceAs(middlePointerInputFilter)
+            }
+            1 -> {
+                assertThat(filteredLog).hasSize(1)
+                assertThat(filteredLog[0].pointerInputFilter)
+                    .isSameInstanceAs(parentPointerInputFilter)
+            }
+            else -> throw IllegalStateException()
+        }
+    }
+
+    @Test
+    fun process_modifiedChange_isPassedToNext() {
+
+        // Arrange
+
+        val expectedInput = PointerInputChange(
+            id = PointerId(0),
+            5,
+            Offset(100f, 0f),
+            true,
+            3,
+            Offset(0f, 0f),
+            true,
+            isInitiallyConsumed = false
+        )
+        val expectedOutput = PointerInputChange(
+            id = PointerId(0),
+            5,
+            Offset(100f, 0f),
+            true,
+            3,
+            Offset(0f, 0f),
+            true,
+            isInitiallyConsumed = true
+        )
+
+        val pointerInputFilter = PointerInputFilterMock(
+            mutableListOf(),
+            pointerEventHandler = { pointerEvent, pass, _ ->
+                if (pass == PointerEventPass.Initial) {
+                    val change = pointerEvent
+                        .changes
+                        .first()
+
+                    if (change.positionChanged()) change.consume()
+                }
+            }
+        )
+
+        val layoutNode = LayoutNode(
+            0, 0, 500, 500,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+
+        addToRoot(layoutNode)
+
+        val down = PointerInputEvent(
+            0,
+            3,
+            Offset(0f, 0f),
+            true
+        )
+        val move = PointerInputEvent(
+            0,
+            5,
+            Offset(100f, 0f),
+            true
+        )
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+        pointerInputFilter.log.clear()
+        pointerInputEventProcessor.process(move)
+
+        // Assert
+
+        val log = pointerInputFilter.log.getOnPointerEventFilterLog()
+
+        assertThat(log).hasSize(3)
+        PointerInputChangeSubject
+            .assertThat(log[0].pointerEvent.changes.first())
+            .isStructurallyEqualTo(expectedInput)
+        PointerInputChangeSubject
+            .assertThat(log[1].pointerEvent.changes.first())
+            .isStructurallyEqualTo(expectedOutput)
+    }
+
+    @Test
+    fun process_nodesAndAdditionalOffsetIncreasinglyInset_dispatchInfoIsCorrect() {
+        process_dispatchInfoIsCorrect(
+            0, 0, 100, 100,
+            2, 11, 100, 100,
+            23, 31, 100, 100,
+            43, 51,
+            99, 99
+        )
+    }
+
+    @Test
+    fun process_nodesAndAdditionalOffsetIncreasinglyOutset_dispatchInfoIsCorrect() {
+        process_dispatchInfoIsCorrect(
+            0, 0, 100, 100,
+            -2, -11, 100, 100,
+            -23, -31, 100, 100,
+            -43, -51,
+            1, 1
+        )
+    }
+
+    @Test
+    fun process_nodesAndAdditionalOffsetNotOffset_dispatchInfoIsCorrect() {
+        process_dispatchInfoIsCorrect(
+            0, 0, 100, 100,
+            0, 0, 100, 100,
+            0, 0, 100, 100,
+            0, 0,
+            50, 50
+        )
+    }
+
+    @Suppress("SameParameterValue")
+    private fun process_dispatchInfoIsCorrect(
+        pX1: Int,
+        pY1: Int,
+        pX2: Int,
+        pY2: Int,
+        mX1: Int,
+        mY1: Int,
+        mX2: Int,
+        mY2: Int,
+        cX1: Int,
+        cY1: Int,
+        cX2: Int,
+        cY2: Int,
+        aOX: Int,
+        aOY: Int,
+        pointerX: Int,
+        pointerY: Int
+    ) {
+
+        // Arrange
+
+        val log = mutableListOf<LogEntry>()
+        val childPointerInputFilter = PointerInputFilterMock(log)
+        val middlePointerInputFilter = PointerInputFilterMock(log)
+        val parentPointerInputFilter = PointerInputFilterMock(log)
+
+        val childOffset = Offset(cX1.toFloat(), cY1.toFloat())
+        val childLayoutNode = LayoutNode(
+            cX1, cY1, cX2, cY2,
+            PointerInputModifierImpl2(
+                childPointerInputFilter
+            )
+        )
+        val middleOffset = Offset(mX1.toFloat(), mY1.toFloat())
+        val middleLayoutNode: LayoutNode = LayoutNode(
+            mX1, mY1, mX2, mY2,
+            PointerInputModifierImpl2(
+                middlePointerInputFilter
+            )
+        ).apply {
+            insertAt(0, childLayoutNode)
+        }
+        val parentLayoutNode: LayoutNode = LayoutNode(
+            pX1, pY1, pX2, pY2,
+            PointerInputModifierImpl2(
+                parentPointerInputFilter
+            )
+        ).apply {
+            insertAt(0, middleLayoutNode)
+        }
+
+        val outerLayoutNode = LayoutNode(
+            aOX,
+            aOY,
+            aOX + parentLayoutNode.width,
+            aOY + parentLayoutNode.height
+        )
+
+        outerLayoutNode.insertAt(0, parentLayoutNode)
+        addToRoot(outerLayoutNode)
+
+        val additionalOffset = IntOffset(aOX, aOY)
+
+        val offset = Offset(pointerX.toFloat(), pointerY.toFloat())
+
+        val down = PointerInputEvent(0, 7, offset, true)
+
+        val expectedPointerInputChanges = arrayOf(
+            PointerInputChange(
+                id = PointerId(0),
+                7,
+                offset - additionalOffset,
+                true,
+                7,
+                offset - additionalOffset,
+                false,
+                isInitiallyConsumed = false
+            ),
+            PointerInputChange(
+                id = PointerId(0),
+                7,
+                offset - middleOffset - additionalOffset,
+                true,
+                7,
+                offset - middleOffset - additionalOffset,
+                false,
+                isInitiallyConsumed = false
+            ),
+            PointerInputChange(
+                id = PointerId(0),
+                7,
+                offset - middleOffset - childOffset - additionalOffset,
+                true,
+                7,
+                offset - middleOffset - childOffset - additionalOffset,
+                false,
+                isInitiallyConsumed = false
+            )
+        )
+
+        val expectedSizes = arrayOf(
+            IntSize(pX2 - pX1, pY2 - pY1),
+            IntSize(mX2 - mX1, mY2 - mY1),
+            IntSize(cX2 - cX1, cY2 - cY1)
+        )
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+
+        // Assert
+
+        val filteredLog = log.getOnPointerEventFilterLog()
+
+        // Verify call count
+        assertThat(filteredLog).hasSize(PointerEventPass.values().size * 3)
+
+        // Verify call values
+        filteredLog.verifyOnPointerEventCall(
+            0,
+            parentPointerInputFilter,
+            pointerEventOf(expectedPointerInputChanges[0]),
+            PointerEventPass.Initial,
+            expectedSizes[0]
+        )
+        filteredLog.verifyOnPointerEventCall(
+            1,
+            middlePointerInputFilter,
+            pointerEventOf(expectedPointerInputChanges[1]),
+            PointerEventPass.Initial,
+            expectedSizes[1]
+        )
+        filteredLog.verifyOnPointerEventCall(
+            2,
+            childPointerInputFilter,
+            pointerEventOf(expectedPointerInputChanges[2]),
+            PointerEventPass.Initial,
+            expectedSizes[2]
+        )
+        filteredLog.verifyOnPointerEventCall(
+            3,
+            childPointerInputFilter,
+            pointerEventOf(expectedPointerInputChanges[2]),
+            PointerEventPass.Main,
+            expectedSizes[2]
+        )
+        filteredLog.verifyOnPointerEventCall(
+            4,
+            middlePointerInputFilter,
+            pointerEventOf(expectedPointerInputChanges[1]),
+            PointerEventPass.Main,
+            expectedSizes[1]
+        )
+        filteredLog.verifyOnPointerEventCall(
+            5,
+            parentPointerInputFilter,
+            pointerEventOf(expectedPointerInputChanges[0]),
+            PointerEventPass.Main,
+            expectedSizes[0]
+        )
+        filteredLog.verifyOnPointerEventCall(
+            6,
+            parentPointerInputFilter,
+            pointerEventOf(expectedPointerInputChanges[0]),
+            PointerEventPass.Final,
+            expectedSizes[0]
+        )
+        filteredLog.verifyOnPointerEventCall(
+            7,
+            middlePointerInputFilter,
+            pointerEventOf(expectedPointerInputChanges[1]),
+            PointerEventPass.Final,
+            expectedSizes[1]
+        )
+        filteredLog.verifyOnPointerEventCall(
+            8,
+            childPointerInputFilter,
+            pointerEventOf(expectedPointerInputChanges[2]),
+            PointerEventPass.Final,
+            expectedSizes[2]
+        )
+    }
+
+    /**
+     * This test creates a layout of this shape:
+     *
+     *  -------------
+     *  |     |     |
+     *  |  t  |     |
+     *  |     |     |
+     *  |-----|     |
+     *  |           |
+     *  |     |-----|
+     *  |     |     |
+     *  |     |  t  |
+     *  |     |     |
+     *  -------------
+     *
+     * Where there is one child in the top right, and one in the bottom left, and 2 down touches,
+     * one in the top left and one in the bottom right.
+     */
+    @Test
+    fun process_2DownOn2DifferentPointerNodes_hitAndDispatchInfoAreCorrect() {
+
+        // Arrange
+
+        val log = mutableListOf<LogEntry>()
+        val childPointerInputFilter1 = PointerInputFilterMock(log)
+        val childPointerInputFilter2 = PointerInputFilterMock(log)
+
+        val childLayoutNode1 =
+            LayoutNode(
+                0, 0, 50, 50,
+                PointerInputModifierImpl2(
+                    childPointerInputFilter1
+                )
+            )
+        val childLayoutNode2 =
+            LayoutNode(
+                50, 50, 100, 100,
+                PointerInputModifierImpl2(
+                    childPointerInputFilter2
+                )
+            )
+        addToRoot(childLayoutNode1, childLayoutNode2)
+
+        val offset1 = Offset(25f, 25f)
+        val offset2 = Offset(75f, 75f)
+
+        val down = PointerInputEvent(
+            5,
+            listOf(
+                PointerInputEventData(0, 5, offset1, true),
+                PointerInputEventData(1, 5, offset2, true)
+            )
+        )
+
+        val expectedChange1 =
+            PointerInputChange(
+                id = PointerId(0),
+                5,
+                offset1,
+                true,
+                5,
+                offset1,
+                false,
+                isInitiallyConsumed = false
+            )
+        val expectedChange2 =
+            PointerInputChange(
+                id = PointerId(1),
+                5,
+                offset2 - Offset(50f, 50f),
+                true,
+                5,
+                offset2 - Offset(50f, 50f),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+
+        // Assert
+
+        // Verify call count
+
+        val child1Log = log.getOnPointerEventFilterLog().filter {
+            it.pointerInputFilter === childPointerInputFilter1
+        }
+        val child2Log = log.getOnPointerEventFilterLog().filter {
+            it.pointerInputFilter === childPointerInputFilter2
+        }
+        assertThat(child1Log).hasSize(PointerEventPass.values().size)
+        assertThat(child2Log).hasSize(PointerEventPass.values().size)
+
+        // Verify call values
+
+        val expectedBounds = IntSize(50, 50)
+
+        child1Log.verifyOnPointerEventCall(
+            0,
+            null,
+            pointerEventOf(expectedChange1),
+            PointerEventPass.Initial,
+            expectedBounds
+        )
+        child1Log.verifyOnPointerEventCall(
+            1,
+            null,
+            pointerEventOf(expectedChange1),
+            PointerEventPass.Main,
+            expectedBounds
+        )
+        child1Log.verifyOnPointerEventCall(
+            2,
+            null,
+            pointerEventOf(expectedChange1),
+            PointerEventPass.Final,
+            expectedBounds
+        )
+
+        child2Log.verifyOnPointerEventCall(
+            0,
+            null,
+            pointerEventOf(expectedChange2),
+            PointerEventPass.Initial,
+            expectedBounds
+        )
+        child2Log.verifyOnPointerEventCall(
+            1,
+            null,
+            pointerEventOf(expectedChange2),
+            PointerEventPass.Main,
+            expectedBounds
+        )
+        child2Log.verifyOnPointerEventCall(
+            2,
+            null,
+            pointerEventOf(expectedChange2),
+            PointerEventPass.Final,
+            expectedBounds
+        )
+    }
+
+    /**
+     * This test creates a layout of this shape:
+     *
+     *  ---------------
+     *  | t      |    |
+     *  |        |    |
+     *  |  |-------|  |
+     *  |  | t     |  |
+     *  |  |       |  |
+     *  |  |       |  |
+     *  |--|  |-------|
+     *  |  |  | t     |
+     *  |  |  |       |
+     *  |  |  |       |
+     *  |  |--|       |
+     *  |     |       |
+     *  ---------------
+     *
+     * There are 3 staggered children and 3 down events, the first is on child 1, the second is on
+     * child 2 in a space that overlaps child 1, and the third is in a space that overlaps both
+     * child 2.
+     */
+    @Test
+    fun process_3DownOnOverlappingPointerNodes_hitAndDispatchInfoAreCorrect() {
+
+        val log = mutableListOf<LogEntry>()
+        val childPointerInputFilter1 = PointerInputFilterMock(log)
+        val childPointerInputFilter2 = PointerInputFilterMock(log)
+        val childPointerInputFilter3 = PointerInputFilterMock(log)
+
+        val childLayoutNode1 = LayoutNode(
+            0, 0, 100, 100,
+            PointerInputModifierImpl2(
+                childPointerInputFilter1
+            )
+        )
+        val childLayoutNode2 = LayoutNode(
+            50, 50, 150, 150,
+            PointerInputModifierImpl2(
+                childPointerInputFilter2
+            )
+        )
+        val childLayoutNode3 = LayoutNode(
+            100, 100, 200, 200,
+            PointerInputModifierImpl2(
+                childPointerInputFilter3
+            )
+        )
+
+        addToRoot(childLayoutNode1, childLayoutNode2, childLayoutNode3)
+
+        val offset1 = Offset(25f, 25f)
+        val offset2 = Offset(75f, 75f)
+        val offset3 = Offset(125f, 125f)
+
+        val down = PointerInputEvent(
+            5,
+            listOf(
+                PointerInputEventData(0, 5, offset1, true),
+                PointerInputEventData(1, 5, offset2, true),
+                PointerInputEventData(2, 5, offset3, true)
+            )
+        )
+
+        val expectedChange1 =
+            PointerInputChange(
+                id = PointerId(0),
+                5,
+                offset1,
+                true,
+                5,
+                offset1,
+                false,
+                isInitiallyConsumed = false
+            )
+        val expectedChange2 =
+            PointerInputChange(
+                id = PointerId(1),
+                5,
+                offset2 - Offset(50f, 50f),
+                true,
+                5,
+                offset2 - Offset(50f, 50f),
+                false,
+                isInitiallyConsumed = false
+            )
+        val expectedChange3 =
+            PointerInputChange(
+                id = PointerId(2),
+                5,
+                offset3 - Offset(100f, 100f),
+                true,
+                5,
+                offset3 - Offset(100f, 100f),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+
+        // Assert
+
+        val child1Log = log.getOnPointerEventFilterLog().filter {
+            it.pointerInputFilter === childPointerInputFilter1
+        }
+        val child2Log = log.getOnPointerEventFilterLog().filter {
+            it.pointerInputFilter === childPointerInputFilter2
+        }
+        val child3Log = log.getOnPointerEventFilterLog().filter {
+            it.pointerInputFilter === childPointerInputFilter3
+        }
+        assertThat(child1Log).hasSize(PointerEventPass.values().size)
+        assertThat(child2Log).hasSize(PointerEventPass.values().size)
+        assertThat(child3Log).hasSize(PointerEventPass.values().size)
+
+        // Verify call values
+
+        val expectedBounds = IntSize(100, 100)
+
+        child1Log.verifyOnPointerEventCall(
+            0,
+            null,
+            pointerEventOf(expectedChange1),
+            PointerEventPass.Initial,
+            expectedBounds
+        )
+        child1Log.verifyOnPointerEventCall(
+            1,
+            null,
+            pointerEventOf(expectedChange1),
+            PointerEventPass.Main,
+            expectedBounds
+        )
+        child1Log.verifyOnPointerEventCall(
+            2,
+            null,
+            pointerEventOf(expectedChange1),
+            PointerEventPass.Final,
+            expectedBounds
+        )
+
+        child2Log.verifyOnPointerEventCall(
+            0,
+            null,
+            pointerEventOf(expectedChange2),
+            PointerEventPass.Initial,
+            expectedBounds
+        )
+        child2Log.verifyOnPointerEventCall(
+            1,
+            null,
+            pointerEventOf(expectedChange2),
+            PointerEventPass.Main,
+            expectedBounds
+        )
+        child2Log.verifyOnPointerEventCall(
+            2,
+            null,
+            pointerEventOf(expectedChange2),
+            PointerEventPass.Final,
+            expectedBounds
+        )
+
+        child3Log.verifyOnPointerEventCall(
+            0,
+            null,
+            pointerEventOf(expectedChange3),
+            PointerEventPass.Initial,
+            expectedBounds
+        )
+        child3Log.verifyOnPointerEventCall(
+            1,
+            null,
+            pointerEventOf(expectedChange3),
+            PointerEventPass.Main,
+            expectedBounds
+        )
+        child3Log.verifyOnPointerEventCall(
+            2,
+            null,
+            pointerEventOf(expectedChange3),
+            PointerEventPass.Final,
+            expectedBounds
+        )
+    }
+
+    /**
+     * This test creates a layout of this shape:
+     *
+     *  ---------------
+     *  |             |
+     *  |      t      |
+     *  |             |
+     *  |  |-------|  |
+     *  |  |       |  |
+     *  |  |   t   |  |
+     *  |  |       |  |
+     *  |  |-------|  |
+     *  |             |
+     *  |      t      |
+     *  |             |
+     *  ---------------
+     *
+     * There are 3 staggered children and 3 down events, the first is on child 1, the second is on
+     * child 2 in a space that overlaps child 1, and the third is in a space that overlaps both
+     * child 2.
+     */
+    @Test
+    fun process_3DownOnFloatingPointerNodeV_hitAndDispatchInfoAreCorrect() {
+
+        val childPointerInputFilter1 = PointerInputFilterMock()
+        val childPointerInputFilter2 = PointerInputFilterMock()
+
+        val childLayoutNode1 = LayoutNode(
+            0, 0, 100, 150,
+            PointerInputModifierImpl2(
+                childPointerInputFilter1
+            )
+        )
+        val childLayoutNode2 = LayoutNode(
+            25, 50, 75, 100,
+            PointerInputModifierImpl2(
+                childPointerInputFilter2
+            )
+        )
+
+        addToRoot(childLayoutNode1, childLayoutNode2)
+
+        val offset1 = Offset(50f, 25f)
+        val offset2 = Offset(50f, 75f)
+        val offset3 = Offset(50f, 125f)
+
+        val down = PointerInputEvent(
+            7,
+            listOf(
+                PointerInputEventData(0, 7, offset1, true),
+                PointerInputEventData(1, 7, offset2, true),
+                PointerInputEventData(2, 7, offset3, true)
+            )
+        )
+
+        val expectedChange1 =
+            PointerInputChange(
+                id = PointerId(0),
+                7,
+                offset1,
+                true,
+                7,
+                offset1,
+                false,
+                isInitiallyConsumed = false
+            )
+        val expectedChange2 =
+            PointerInputChange(
+                id = PointerId(1),
+                7,
+                offset2 - Offset(25f, 50f),
+                true,
+                7,
+                offset2 - Offset(25f, 50f),
+                false,
+                isInitiallyConsumed = false
+            )
+        val expectedChange3 =
+            PointerInputChange(
+                id = PointerId(2),
+                7,
+                offset3,
+                true,
+                7,
+                offset3,
+                false,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+
+        // Assert
+
+        val log1 = childPointerInputFilter1.log.getOnPointerEventFilterLog()
+        val log2 = childPointerInputFilter2.log.getOnPointerEventFilterLog()
+
+        // Verify call count
+        assertThat(log1).hasSize(PointerEventPass.values().size)
+        assertThat(log2).hasSize(PointerEventPass.values().size)
+
+        // Verify call values
+        PointerEventPass.values().forEachIndexed { index, pass ->
+            log1.verifyOnPointerEventCall(
+                index,
+                null,
+                pointerEventOf(expectedChange1, expectedChange3),
+                pass,
+                IntSize(100, 150)
+            )
+            log2.verifyOnPointerEventCall(
+                index,
+                null,
+                pointerEventOf(expectedChange2),
+                pass,
+                IntSize(50, 50)
+            )
+        }
+    }
+
+    /**
+     * This test creates a layout of this shape:
+     *
+     *  -----------------
+     *  |               |
+     *  |   |-------|   |
+     *  |   |       |   |
+     *  | t |   t   | t |
+     *  |   |       |   |
+     *  |   |-------|   |
+     *  |               |
+     *  -----------------
+     *
+     * There are 3 staggered children and 3 down events, the first is on child 1, the second is on
+     * child 2 in a space that overlaps child 1, and the third is in a space that overlaps both
+     * child 2.
+     */
+    @Test
+    fun process_3DownOnFloatingPointerNodeH_hitAndDispatchInfoAreCorrect() {
+
+        val childPointerInputFilter1 = PointerInputFilterMock()
+        val childPointerInputFilter2 = PointerInputFilterMock()
+
+        val childLayoutNode1 = LayoutNode(
+            0, 0, 150, 100,
+            PointerInputModifierImpl2(
+                childPointerInputFilter1
+            )
+        )
+        val childLayoutNode2 = LayoutNode(
+            50, 25, 100, 75,
+            PointerInputModifierImpl2(
+                childPointerInputFilter2
+            )
+        )
+
+        addToRoot(childLayoutNode1, childLayoutNode2)
+
+        val offset1 = Offset(25f, 50f)
+        val offset2 = Offset(75f, 50f)
+        val offset3 = Offset(125f, 50f)
+
+        val down = PointerInputEvent(
+            11,
+            listOf(
+                PointerInputEventData(0, 11, offset1, true),
+                PointerInputEventData(1, 11, offset2, true),
+                PointerInputEventData(2, 11, offset3, true)
+            )
+        )
+
+        val expectedChange1 =
+            PointerInputChange(
+                id = PointerId(0),
+                11,
+                offset1,
+                true,
+                11,
+                offset1,
+                false,
+                isInitiallyConsumed = false
+            )
+        val expectedChange2 =
+            PointerInputChange(
+                id = PointerId(1),
+                11,
+                offset2 - Offset(50f, 25f),
+                true,
+                11,
+                offset2 - Offset(50f, 25f),
+                false,
+                isInitiallyConsumed = false
+            )
+        val expectedChange3 =
+            PointerInputChange(
+                id = PointerId(2),
+                11,
+                offset3,
+                true,
+                11,
+                offset3,
+                false,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+
+        // Assert
+
+        val log1 = childPointerInputFilter1.log.getOnPointerEventFilterLog()
+        val log2 = childPointerInputFilter2.log.getOnPointerEventFilterLog()
+
+        // Verify call count
+        assertThat(log1).hasSize(PointerEventPass.values().size)
+        assertThat(log2).hasSize(PointerEventPass.values().size)
+
+        // Verify call values
+        PointerEventPass.values().forEachIndexed { index, pass ->
+            log1.verifyOnPointerEventCall(
+                index,
+                null,
+                pointerEventOf(expectedChange1, expectedChange3),
+                pass,
+                IntSize(150, 100)
+            )
+            log2.verifyOnPointerEventCall(
+                index,
+                null,
+                pointerEventOf(expectedChange2),
+                pass,
+                IntSize(50, 50)
+            )
+        }
+    }
+
+    /**
+     * This test creates a layout of this shape:
+     *     0   1   2   3   4
+     *   .........   .........
+     * 0 .     t .   . t     .
+     *   .   |---|---|---|   .
+     * 1 . t | t |   | t | t .
+     *   ....|---|   |---|....
+     * 2     |           |
+     *   ....|---|   |---|....
+     * 3 . t | t |   | t | t .
+     *   .   |---|---|---|   .
+     * 4 .     t .   . t     .
+     *   .........   .........
+     *
+     * 4 LayoutNodes with PointerInputModifiers that are clipped by their parent LayoutNode. 4
+     * touches touch just inside the parent LayoutNode and inside the child LayoutNodes. 8
+     * touches touch just outside the parent LayoutNode but inside the child LayoutNodes.
+     *
+     * Because layout node bounds are not used to clip pointer input hit testing, all pointers
+     * should hit.
+     */
+    @Test
+    fun process_4DownInClippedAreaOfLnsWithPims_onlyCorrectPointersHit() {
+
+        // Arrange
+
+        val pointerInputFilterTopLeft = PointerInputFilterMock()
+        val pointerInputFilterTopRight = PointerInputFilterMock()
+        val pointerInputFilterBottomLeft = PointerInputFilterMock()
+        val pointerInputFilterBottomRight = PointerInputFilterMock()
+
+        val layoutNodeTopLeft = LayoutNode(
+            -1, -1, 1, 1,
+            PointerInputModifierImpl2(
+                pointerInputFilterTopLeft
+            )
+        )
+        val layoutNodeTopRight = LayoutNode(
+            2, -1, 4, 1,
+            PointerInputModifierImpl2(
+                pointerInputFilterTopRight
+            )
+        )
+        val layoutNodeBottomLeft = LayoutNode(
+            -1, 2, 1, 4,
+            PointerInputModifierImpl2(
+                pointerInputFilterBottomLeft
+            )
+        )
+        val layoutNodeBottomRight = LayoutNode(
+            2, 2, 4, 4,
+            PointerInputModifierImpl2(
+                pointerInputFilterBottomRight
+            )
+        )
+
+        val parentLayoutNode = LayoutNode(1, 1, 4, 4).apply {
+            insertAt(0, layoutNodeTopLeft)
+            insertAt(1, layoutNodeTopRight)
+            insertAt(2, layoutNodeBottomLeft)
+            insertAt(3, layoutNodeBottomRight)
+        }
+        addToRoot(parentLayoutNode)
+
+        val offsetsTopLeft =
+            listOf(
+                Offset(0f, 1f),
+                Offset(1f, 0f),
+                Offset(1f, 1f)
+            )
+
+        val offsetsTopRight =
+            listOf(
+                Offset(3f, 0f),
+                Offset(3f, 1f),
+                Offset(4f, 1f)
+            )
+
+        val offsetsBottomLeft =
+            listOf(
+                Offset(0f, 3f),
+                Offset(1f, 3f),
+                Offset(1f, 4f)
+            )
+
+        val offsetsBottomRight =
+            listOf(
+                Offset(3f, 3f),
+                Offset(3f, 4f),
+                Offset(4f, 3f)
+            )
+
+        val allOffsets = offsetsTopLeft + offsetsTopRight + offsetsBottomLeft + offsetsBottomRight
+
+        val pointerInputEvent =
+            PointerInputEvent(
+                11,
+                (allOffsets.indices).map {
+                    PointerInputEventData(it, 11, allOffsets[it], true)
+                }
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(pointerInputEvent)
+
+        // Assert
+
+        val expectedChangesTopLeft =
+            (offsetsTopLeft.indices).map {
+                PointerInputChange(
+                    id = PointerId(it.toLong()),
+                    11,
+                    Offset(
+                        offsetsTopLeft[it].x,
+                        offsetsTopLeft[it].y
+                    ),
+                    true,
+                    11,
+                    Offset(
+                        offsetsTopLeft[it].x,
+                        offsetsTopLeft[it].y
+                    ),
+                    false,
+                    isInitiallyConsumed = false
+                )
+            }
+
+        val expectedChangesTopRight =
+            (offsetsTopLeft.indices).map {
+                PointerInputChange(
+                    id = PointerId(it.toLong() + 3),
+                    11,
+                    Offset(
+                        offsetsTopRight[it].x - 3f,
+                        offsetsTopRight[it].y
+                    ),
+                    true,
+                    11,
+                    Offset(
+                        offsetsTopRight[it].x - 3f,
+                        offsetsTopRight[it].y
+                    ),
+                    false,
+                    isInitiallyConsumed = false
+                )
+            }
+
+        val expectedChangesBottomLeft =
+            (offsetsTopLeft.indices).map {
+                PointerInputChange(
+                    id = PointerId(it.toLong() + 6),
+                    11,
+                    Offset(
+                        offsetsBottomLeft[it].x,
+                        offsetsBottomLeft[it].y - 3f
+                    ),
+                    true,
+                    11,
+                    Offset(
+                        offsetsBottomLeft[it].x,
+                        offsetsBottomLeft[it].y - 3f
+                    ),
+                    false,
+                    isInitiallyConsumed = false
+                )
+            }
+
+        val expectedChangesBottomRight =
+            (offsetsTopLeft.indices).map {
+                PointerInputChange(
+                    id = PointerId(it.toLong() + 9),
+                    11,
+                    Offset(
+                        offsetsBottomRight[it].x - 3f,
+                        offsetsBottomRight[it].y - 3f
+                    ),
+                    true,
+                    11,
+                    Offset(
+                        offsetsBottomRight[it].x - 3f,
+                        offsetsBottomRight[it].y - 3f
+                    ),
+                    false,
+                    isInitiallyConsumed = false
+                )
+            }
+
+        // Verify call values
+
+        val logTopLeft = pointerInputFilterTopLeft.log.getOnPointerEventFilterLog()
+        val logTopRight = pointerInputFilterTopRight.log.getOnPointerEventFilterLog()
+        val logBottomLeft = pointerInputFilterBottomLeft.log.getOnPointerEventFilterLog()
+        val logBottomRight = pointerInputFilterBottomRight.log.getOnPointerEventFilterLog()
+
+        PointerEventPass.values().forEachIndexed { index, pass ->
+            logTopLeft.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(*expectedChangesTopLeft.toTypedArray()),
+                expectedPass = pass
+            )
+            logTopRight.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(*expectedChangesTopRight.toTypedArray()),
+                expectedPass = pass
+            )
+            logBottomLeft.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(*expectedChangesBottomLeft.toTypedArray()),
+                expectedPass = pass
+            )
+            logBottomRight.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(*expectedChangesBottomRight.toTypedArray()),
+                expectedPass = pass
+            )
+        }
+    }
+
+    /**
+     * This test creates a layout of this shape:
+     *
+     *   |---|
+     *   |tt |
+     *   |t  |
+     *   |---|t
+     *       tt
+     *
+     *   But where the additional offset suggest something more like this shape.
+     *
+     *   tt
+     *   t|---|
+     *    |  t|
+     *    | tt|
+     *    |---|
+     *
+     *   Without the additional offset, it would be expected that only the top left 3 pointers would
+     *   hit, but with the additional offset, only the bottom right 3 hit.
+     */
+    @Test
+    fun process_rootIsOffset_onlyCorrectPointersHit() {
+
+        // Arrange
+        val singlePointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            0, 0, 2, 2,
+            PointerInputModifierImpl2(
+                singlePointerInputFilter
+            )
+        )
+        val outerLayoutNode = LayoutNode(1, 1, 3, 3)
+        outerLayoutNode.insertAt(0, layoutNode)
+        addToRoot(outerLayoutNode)
+        val offsetsThatHit =
+            listOf(
+                Offset(2f, 2f),
+                Offset(2f, 1f),
+                Offset(1f, 2f)
+            )
+        val offsetsThatMiss =
+            listOf(
+                Offset(0f, 0f),
+                Offset(0f, 1f),
+                Offset(1f, 0f)
+            )
+        val allOffsets = offsetsThatHit + offsetsThatMiss
+        val pointerInputEvent =
+            PointerInputEvent(
+                11,
+                (allOffsets.indices).map {
+                    PointerInputEventData(it, 11, allOffsets[it], true)
+                }
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(pointerInputEvent)
+
+        // Assert
+
+        val expectedChanges =
+            (offsetsThatHit.indices).map {
+                PointerInputChange(
+                    id = PointerId(it.toLong()),
+                    11,
+                    offsetsThatHit[it] - Offset(1f, 1f),
+                    true,
+                    11,
+                    offsetsThatHit[it] - Offset(1f, 1f),
+                    false,
+                    isInitiallyConsumed = false
+                )
+            }
+
+        val log = singlePointerInputFilter.log.getOnPointerEventFilterLog()
+
+        // Verify call count
+        assertThat(log).hasSize(PointerEventPass.values().size)
+
+        // Verify call values
+        PointerEventPass.values().forEachIndexed { index, pass ->
+            log.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(*expectedChanges.toTypedArray()),
+                expectedPass = pass
+            )
+        }
+    }
+
+    @Test
+    fun process_downOn3NestedPointerInputModifiers_hitAndDispatchInfoAreCorrect() {
+
+        val pointerInputFilter1 = PointerInputFilterMock()
+        val pointerInputFilter2 = PointerInputFilterMock()
+        val pointerInputFilter3 = PointerInputFilterMock()
+
+        val modifier = PointerInputModifierImpl2(pointerInputFilter1) then
+            PointerInputModifierImpl2(pointerInputFilter2) then
+            PointerInputModifierImpl2(pointerInputFilter3)
+
+        val layoutNode = LayoutNode(
+            25, 50, 75, 100,
+            modifier
+        )
+
+        addToRoot(layoutNode)
+
+        val offset1 = Offset(50f, 75f)
+
+        val down = PointerInputEvent(
+            7,
+            listOf(
+                PointerInputEventData(0, 7, offset1, true)
+            )
+        )
+
+        val expectedChange =
+            PointerInputChange(
+                id = PointerId(0),
+                7,
+                offset1 - Offset(25f, 50f),
+                true,
+                7,
+                offset1 - Offset(25f, 50f),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+
+        // Assert
+
+        val log1 = pointerInputFilter1.log.getOnPointerEventFilterLog()
+        val log2 = pointerInputFilter2.log.getOnPointerEventFilterLog()
+        val log3 = pointerInputFilter3.log.getOnPointerEventFilterLog()
+
+        // Verify call count
+        assertThat(log1).hasSize(PointerEventPass.values().size)
+        assertThat(log2).hasSize(PointerEventPass.values().size)
+        assertThat(log3).hasSize(PointerEventPass.values().size)
+
+        // Verify call values
+        PointerEventPass.values().forEachIndexed { index, pass ->
+            log1.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedChange),
+                expectedPass = pass,
+                expectedBounds = IntSize(50, 50)
+            )
+            log2.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedChange),
+                expectedPass = pass,
+                expectedBounds = IntSize(50, 50)
+            )
+            log3.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedChange),
+                expectedPass = pass,
+                expectedBounds = IntSize(50, 50)
+            )
+        }
+    }
+
+    @Test
+    fun process_downOnDeeplyNestedPointerInputModifier_hitAndDispatchInfoAreCorrect() {
+
+        val pointerInputFilter = PointerInputFilterMock()
+
+        val layoutNode1 =
+            LayoutNode(
+                1, 5, 500, 500,
+                PointerInputModifierImpl2(pointerInputFilter)
+            )
+        val layoutNode2: LayoutNode = LayoutNode(2, 6, 500, 500).apply {
+            insertAt(0, layoutNode1)
+        }
+        val layoutNode3: LayoutNode = LayoutNode(3, 7, 500, 500).apply {
+            insertAt(0, layoutNode2)
+        }
+        val layoutNode4: LayoutNode = LayoutNode(4, 8, 500, 500).apply {
+            insertAt(0, layoutNode3)
+        }
+        addToRoot(layoutNode4)
+
+        val offset1 = Offset(499f, 499f)
+
+        val downEvent = PointerInputEvent(
+            7,
+            listOf(
+                PointerInputEventData(0, 7, offset1, true)
+            )
+        )
+
+        val expectedChange =
+            PointerInputChange(
+                id = PointerId(0),
+                7,
+                offset1 - Offset(1f + 2f + 3f + 4f, 5f + 6f + 7f + 8f),
+                true,
+                7,
+                offset1 - Offset(1f + 2f + 3f + 4f, 5f + 6f + 7f + 8f),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(downEvent)
+
+        // Assert
+
+        val log = pointerInputFilter.log.getOnPointerEventFilterLog()
+
+        // Verify call count
+        assertThat(log).hasSize(PointerEventPass.values().size)
+
+        // Verify call values
+        PointerEventPass.values().forEachIndexed { index, pass ->
+            log.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedChange),
+                expectedPass = pass,
+                expectedBounds = IntSize(499, 495)
+            )
+        }
+    }
+
+    @Test
+    fun process_downOnComplexPointerAndLayoutNodePath_hitAndDispatchInfoAreCorrect() {
+
+        val pointerInputFilter1 = PointerInputFilterMock()
+        val pointerInputFilter2 = PointerInputFilterMock()
+        val pointerInputFilter3 = PointerInputFilterMock()
+        val pointerInputFilter4 = PointerInputFilterMock()
+
+        val layoutNode1 = LayoutNode(
+            1, 6, 500, 500,
+            PointerInputModifierImpl2(pointerInputFilter1)
+                then PointerInputModifierImpl2(pointerInputFilter2)
+        )
+        val layoutNode2: LayoutNode = LayoutNode(2, 7, 500, 500).apply {
+            insertAt(0, layoutNode1)
+        }
+        val layoutNode3 =
+            LayoutNode(
+                3, 8, 500, 500,
+                PointerInputModifierImpl2(pointerInputFilter3)
+                    then PointerInputModifierImpl2(pointerInputFilter4)
+            ).apply {
+                insertAt(0, layoutNode2)
+            }
+
+        val layoutNode4: LayoutNode = LayoutNode(4, 9, 500, 500).apply {
+            insertAt(0, layoutNode3)
+        }
+        val layoutNode5: LayoutNode = LayoutNode(5, 10, 500, 500).apply {
+            insertAt(0, layoutNode4)
+        }
+        addToRoot(layoutNode5)
+
+        val offset1 = Offset(499f, 499f)
+
+        val downEvent = PointerInputEvent(
+            3,
+            listOf(
+                PointerInputEventData(0, 3, offset1, true)
+            )
+        )
+
+        val expectedChange1 =
+            PointerInputChange(
+                id = PointerId(0),
+                3,
+                offset1 - Offset(
+                    1f + 2f + 3f + 4f + 5f,
+                    6f + 7f + 8f + 9f + 10f
+                ),
+                true,
+                3,
+                offset1 - Offset(
+                    1f + 2f + 3f + 4f + 5f,
+                    6f + 7f + 8f + 9f + 10f
+                ),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        val expectedChange2 =
+            PointerInputChange(
+                id = PointerId(0),
+                3,
+                offset1 - Offset(3f + 4f + 5f, 8f + 9f + 10f),
+                true,
+                3,
+                offset1 - Offset(3f + 4f + 5f, 8f + 9f + 10f),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(downEvent)
+
+        // Assert
+
+        val log1 = pointerInputFilter1.log.getOnPointerEventFilterLog()
+        val log2 = pointerInputFilter2.log.getOnPointerEventFilterLog()
+        val log3 = pointerInputFilter3.log.getOnPointerEventFilterLog()
+        val log4 = pointerInputFilter4.log.getOnPointerEventFilterLog()
+
+        // Verify call count
+        assertThat(log1).hasSize(PointerEventPass.values().size)
+        assertThat(log2).hasSize(PointerEventPass.values().size)
+        assertThat(log3).hasSize(PointerEventPass.values().size)
+        assertThat(log4).hasSize(PointerEventPass.values().size)
+
+        // Verify call values
+        PointerEventPass.values().forEachIndexed { index, pass ->
+            log1.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedChange1),
+                expectedPass = pass,
+                expectedBounds = IntSize(499, 494)
+            )
+            log2.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedChange1),
+                expectedPass = pass,
+                expectedBounds = IntSize(499, 494)
+            )
+            log3.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedChange2),
+                expectedPass = pass,
+                expectedBounds = IntSize(497, 492)
+            )
+            log4.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedChange2),
+                expectedPass = pass,
+                expectedBounds = IntSize(497, 492)
+            )
+        }
+    }
+
+    @Test
+    fun process_downOnFullyOverlappingPointerInputModifiers_onlyTopPointerInputModifierReceives() {
+
+        val pointerInputFilter1 = PointerInputFilterMock()
+        val pointerInputFilter2 = PointerInputFilterMock()
+
+        val layoutNode1 = LayoutNode(
+            0, 0, 100, 100,
+            PointerInputModifierImpl2(
+                pointerInputFilter1
+            )
+        )
+        val layoutNode2 = LayoutNode(
+            0, 0, 100, 100,
+            PointerInputModifierImpl2(
+                pointerInputFilter2
+            )
+        )
+
+        addToRoot(layoutNode1, layoutNode2)
+
+        val down = PointerInputEvent(
+            1, 0, Offset(50f, 50f), true
+        )
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+
+        // Assert
+        assertThat(pointerInputFilter2.log.getOnPointerEventFilterLog()).hasSize(3)
+        assertThat(pointerInputFilter1.log.getOnPointerEventFilterLog()).hasSize(0)
+    }
+
+    @Test
+    fun process_downOnPointerInputModifierInLayoutNodeWithNoSize_downNotReceived() {
+
+        val pointerInputFilter1 = PointerInputFilterMock()
+
+        val layoutNode1 = LayoutNode(
+            0, 0, 0, 0,
+            PointerInputModifierImpl2(pointerInputFilter1)
+        )
+
+        addToRoot(layoutNode1)
+
+        val down = PointerInputEvent(
+            1, 0, Offset(0f, 0f), true
+        )
+
+        // Act
+        pointerInputEventProcessor.process(down)
+
+        // Assert
+        assertThat(pointerInputFilter1.log.getOnPointerEventFilterLog()).hasSize(0)
+    }
+
+    // Cancel Handlers
+
+    @Test
+    fun processCancel_noPointers_doesntCrash() {
+        pointerInputEventProcessor.processCancel()
+    }
+
+    @Test
+    fun processCancel_downThenCancel_pimOnlyReceivesCorrectDownThenCancel() {
+
+        // Arrange
+
+        val pointerInputFilter = PointerInputFilterMock()
+
+        val layoutNode = LayoutNode(
+            0, 0, 500, 500,
+            PointerInputModifierImpl2(pointerInputFilter)
+        )
+
+        addToRoot(layoutNode)
+
+        val pointerInputEvent =
+            PointerInputEvent(
+                7,
+                5,
+                Offset(250f, 250f),
+                true
+            )
+
+        val expectedChange =
+            PointerInputChange(
+                id = PointerId(7),
+                5,
+                Offset(250f, 250f),
+                true,
+                5,
+                Offset(250f, 250f),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(pointerInputEvent)
+        pointerInputEventProcessor.processCancel()
+
+        // Assert
+
+        val log = pointerInputFilter.log.filter {
+            it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
+        }
+
+        // Verify call count
+        assertThat(log).hasSize(PointerEventPass.values().size + 1)
+
+        // Verify call values
+        PointerEventPass.values().forEachIndexed { index, pass ->
+            log.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedChange),
+                expectedPass = pass
+            )
+        }
+        log.verifyOnCancelCall(PointerEventPass.values().size)
+    }
+
+    @Test
+    fun processCancel_downDownOnSamePimThenCancel_pimOnlyReceivesCorrectChangesThenCancel() {
+
+        // Arrange
+
+        val pointerInputFilter = PointerInputFilterMock()
+
+        val layoutNode = LayoutNode(
+            0, 0, 500, 500,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+
+        addToRoot(layoutNode)
+
+        val pointerInputEvent1 =
+            PointerInputEvent(
+                7,
+                5,
+                Offset(200f, 200f),
+                true
+            )
+
+        val pointerInputEvent2 =
+            PointerInputEvent(
+                10,
+                listOf(
+                    PointerInputEventData(
+                        7,
+                        10,
+                        Offset(200f, 200f),
+                        true
+                    ),
+                    PointerInputEventData(
+                        9,
+                        10,
+                        Offset(300f, 300f),
+                        true
+                    )
+                )
+            )
+
+        val expectedChanges1 =
+            listOf(
+                PointerInputChange(
+                    id = PointerId(7),
+                    5,
+                    Offset(200f, 200f),
+                    true,
+                    5,
+                    Offset(200f, 200f),
+                    false,
+                    isInitiallyConsumed = false
+                )
+            )
+
+        val expectedChanges2 =
+            listOf(
+                PointerInputChange(
+                    id = PointerId(7),
+                    10,
+                    Offset(200f, 200f),
+                    true,
+                    5,
+                    Offset(200f, 200f),
+                    true,
+                    isInitiallyConsumed = false
+                ),
+                PointerInputChange(
+                    id = PointerId(9),
+                    10,
+                    Offset(300f, 300f),
+                    true,
+                    10,
+                    Offset(300f, 300f),
+                    false,
+                    isInitiallyConsumed = false
+                )
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(pointerInputEvent1)
+        pointerInputEventProcessor.process(pointerInputEvent2)
+        pointerInputEventProcessor.processCancel()
+
+        // Assert
+
+        val log = pointerInputFilter.log.filter {
+            it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
+        }
+
+        // Verify call count
+        assertThat(log).hasSize(PointerEventPass.values().size * 2 + 1)
+
+        // Verify call values
+        var index = 0
+        PointerEventPass.values().forEach { pass ->
+            log.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(*expectedChanges1.toTypedArray()),
+                expectedPass = pass
+            )
+            index++
+        }
+        PointerEventPass.values().forEach { pass ->
+            log.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(*expectedChanges2.toTypedArray()),
+                expectedPass = pass
+            )
+            index++
+        }
+        log.verifyOnCancelCall(index)
+    }
+
+    @Test
+    fun processCancel_downOn2DifferentPimsThenCancel_pimsOnlyReceiveCorrectDownsThenCancel() {
+
+        // Arrange
+
+        val pointerInputFilter1 = PointerInputFilterMock()
+        val layoutNode1 = LayoutNode(
+            0, 0, 199, 199,
+            PointerInputModifierImpl2(pointerInputFilter1)
+        )
+
+        val pointerInputFilter2 = PointerInputFilterMock()
+        val layoutNode2 = LayoutNode(
+            200, 200, 399, 399,
+            PointerInputModifierImpl2(pointerInputFilter2)
+        )
+
+        addToRoot(layoutNode1, layoutNode2)
+
+        val pointerInputEventData1 =
+            PointerInputEventData(
+                7,
+                5,
+                Offset(100f, 100f),
+                true
+            )
+
+        val pointerInputEventData2 =
+            PointerInputEventData(
+                9,
+                5,
+                Offset(300f, 300f),
+                true
+            )
+
+        val pointerInputEvent = PointerInputEvent(
+            5,
+            listOf(pointerInputEventData1, pointerInputEventData2)
+        )
+
+        val expectedChange1 =
+            PointerInputChange(
+                id = PointerId(7),
+                5,
+                Offset(100f, 100f),
+                true,
+                5,
+                Offset(100f, 100f),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        val expectedChange2 =
+            PointerInputChange(
+                id = PointerId(9),
+                5,
+                Offset(100f, 100f),
+                true,
+                5,
+                Offset(100f, 100f),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(pointerInputEvent)
+        pointerInputEventProcessor.processCancel()
+
+        // Assert
+
+        val log1 =
+            pointerInputFilter1.log.filter {
+                it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
+            }
+        val log2 =
+            pointerInputFilter2.log.filter {
+                it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
+            }
+
+        // Verify call count
+        assertThat(log1).hasSize(PointerEventPass.values().size + 1)
+        assertThat(log2).hasSize(PointerEventPass.values().size + 1)
+
+        // Verify call values
+        var index = 0
+        PointerEventPass.values().forEach { pass ->
+            log1.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedChange1),
+                expectedPass = pass
+            )
+            log2.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedChange2),
+                expectedPass = pass
+            )
+            index++
+        }
+        log1.verifyOnCancelCall(index)
+        log2.verifyOnCancelCall(index)
+    }
+
+    @Test
+    fun processCancel_downMoveCancel_pimOnlyReceivesCorrectDownMoveCancel() {
+
+        // Arrange
+
+        val pointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            0, 0, 500, 500,
+            PointerInputModifierImpl2(pointerInputFilter)
+        )
+
+        addToRoot(layoutNode)
+
+        val down =
+            PointerInputEvent(
+                7,
+                5,
+                Offset(200f, 200f),
+                true
+            )
+
+        val move =
+            PointerInputEvent(
+                7,
+                10,
+                Offset(300f, 300f),
+                true
+            )
+
+        val expectedDown =
+            PointerInputChange(
+                id = PointerId(7),
+                5,
+                Offset(200f, 200f),
+                true,
+                5,
+                Offset(200f, 200f),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        val expectedMove =
+            PointerInputChange(
+                id = PointerId(7),
+                10,
+                Offset(300f, 300f),
+                true,
+                5,
+                Offset(200f, 200f),
+                true,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+        pointerInputEventProcessor.process(move)
+        pointerInputEventProcessor.processCancel()
+
+        // Assert
+
+        val log = pointerInputFilter.log.filter {
+            it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
+        }
+
+        // Verify call count
+        assertThat(log).hasSize(PointerEventPass.values().size * 2 + 1)
+
+        // Verify call values
+        var index = 0
+        PointerEventPass.values().forEach { pass ->
+            log.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedDown),
+                expectedPass = pass
+            )
+            index++
+        }
+        PointerEventPass.values().forEach { pass ->
+            log.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedMove),
+                expectedPass = pass
+            )
+            index++
+        }
+        log.verifyOnCancelCall(index)
+    }
+
+    @Test
+    fun processCancel_downCancelMoveUp_pimOnlyReceivesCorrectDownCancel() {
+
+        // Arrange
+
+        val pointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            0, 0, 500, 500,
+            PointerInputModifierImpl2(pointerInputFilter)
+        )
+
+        addToRoot(layoutNode)
+
+        val down =
+            PointerInputEvent(
+                7,
+                5,
+                Offset(200f, 200f),
+                true
+            )
+
+        val expectedDown =
+            PointerInputChange(
+                id = PointerId(7),
+                5,
+                Offset(200f, 200f),
+                true,
+                5,
+                Offset(200f, 200f),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+        pointerInputEventProcessor.processCancel()
+
+        // Assert
+
+        val log = pointerInputFilter.log.filter {
+            it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
+        }
+
+        // Verify call count
+        assertThat(log).hasSize(PointerEventPass.values().size + 1)
+
+        // Verify call values
+        var index = 0
+        PointerEventPass.values().forEach { pass ->
+            log.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedDown),
+                expectedPass = pass
+            )
+            index++
+        }
+        log.verifyOnCancelCall(index)
+    }
+
+    @Test
+    fun processCancel_downCancelDown_pimOnlyReceivesCorrectDownCancelDown() {
+
+        // Arrange
+
+        val pointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            0, 0, 500, 500,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+
+        addToRoot(layoutNode)
+
+        val down1 =
+            PointerInputEvent(
+                7,
+                5,
+                Offset(200f, 200f),
+                true
+            )
+
+        val down2 =
+            PointerInputEvent(
+                7,
+                10,
+                Offset(200f, 200f),
+                true
+            )
+
+        val expectedDown1 =
+            PointerInputChange(
+                id = PointerId(7),
+                5,
+                Offset(200f, 200f),
+                true,
+                5,
+                Offset(200f, 200f),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        val expectedDown2 =
+            PointerInputChange(
+                id = PointerId(7),
+                10,
+                Offset(200f, 200f),
+                true,
+                10,
+                Offset(200f, 200f),
+                false,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(down1)
+        pointerInputEventProcessor.processCancel()
+        pointerInputEventProcessor.process(down2)
+
+        // Assert
+
+        val log = pointerInputFilter.log.filter {
+            it is OnPointerEventFilterEntry || it is OnCancelFilterEntry
+        }
+
+        // Verify call count
+        assertThat(log).hasSize(PointerEventPass.values().size * 2 + 1)
+
+        // Verify call values
+        var index = 0
+        PointerEventPass.values().forEach { pass ->
+            log.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedDown1),
+                expectedPass = pass
+            )
+            index++
+        }
+        log.verifyOnCancelCall(index)
+        index++
+        PointerEventPass.values().forEach { pass ->
+            log.verifyOnPointerEventCall(
+                index = index,
+                expectedEvent = pointerEventOf(expectedDown2),
+                expectedPass = pass
+            )
+            index++
+        }
+    }
+
+    @Test
+    fun process_layoutNodeRemovedDuringInput_correctPointerInputChangesReceived() {
+
+        // Arrange
+
+        val childPointerInputFilter = PointerInputFilterMock()
+        val childLayoutNode = LayoutNode(
+            0, 0, 100, 100,
+            PointerInputModifierImpl2(childPointerInputFilter)
+        )
+
+        val parentPointerInputFilter = PointerInputFilterMock()
+        val parentLayoutNode: LayoutNode = LayoutNode(
+            0, 0, 100, 100,
+            PointerInputModifierImpl2(parentPointerInputFilter)
+        ).apply {
+            insertAt(0, childLayoutNode)
+        }
+
+        addToRoot(parentLayoutNode)
+
+        val offset = Offset(50f, 50f)
+
+        val down = PointerInputEvent(0, 7, offset, true)
+        val up = PointerInputEvent(0, 11, offset, false)
+
+        val expectedDownChange =
+            PointerInputChange(
+                id = PointerId(0),
+                7,
+                offset,
+                true,
+                7,
+                offset,
+                false,
+                isInitiallyConsumed = false
+            )
+
+        val expectedUpChange =
+            PointerInputChange(
+                id = PointerId(0),
+                11,
+                offset,
+                false,
+                7,
+                offset,
+                true,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+        parentLayoutNode.removeAt(0, 1)
+        pointerInputEventProcessor.process(up)
+
+        // Assert
+
+        val parentLog = parentPointerInputFilter.log.getOnPointerEventFilterLog()
+        val childLog = childPointerInputFilter.log.getOnPointerEventFilterLog()
+
+        // Verify call count
+        assertThat(parentLog).hasSize(PointerEventPass.values().size * 2)
+        assertThat(childLog).hasSize(PointerEventPass.values().size)
+
+        // Verify call values
+
+        parentLog.verifyOnPointerEventCall(
+            index = 0,
+            expectedEvent = pointerEventOf(expectedDownChange),
+            expectedPass = PointerEventPass.Initial
+        )
+        parentLog.verifyOnPointerEventCall(
+            index = 1,
+            expectedEvent = pointerEventOf(expectedDownChange),
+            expectedPass = PointerEventPass.Main
+        )
+        parentLog.verifyOnPointerEventCall(
+            index = 2,
+            expectedEvent = pointerEventOf(expectedDownChange),
+            expectedPass = PointerEventPass.Final
+        )
+        parentLog.verifyOnPointerEventCall(
+            index = 3,
+            expectedEvent = pointerEventOf(expectedUpChange),
+            expectedPass = PointerEventPass.Initial
+        )
+        parentLog.verifyOnPointerEventCall(
+            index = 4,
+            expectedEvent = pointerEventOf(expectedUpChange),
+            expectedPass = PointerEventPass.Main
+        )
+        parentLog.verifyOnPointerEventCall(
+            index = 5,
+            expectedEvent = pointerEventOf(expectedUpChange),
+            expectedPass = PointerEventPass.Final
+        )
+
+        childLog.verifyOnPointerEventCall(
+            index = 0,
+            expectedEvent = pointerEventOf(expectedDownChange),
+            expectedPass = PointerEventPass.Initial
+        )
+        childLog.verifyOnPointerEventCall(
+            index = 1,
+            expectedEvent = pointerEventOf(expectedDownChange),
+            expectedPass = PointerEventPass.Main
+        )
+        childLog.verifyOnPointerEventCall(
+            index = 2,
+            expectedEvent = pointerEventOf(expectedDownChange),
+            expectedPass = PointerEventPass.Final
+        )
+    }
+
+    @Test
+    fun process_layoutNodeRemovedDuringInput_cancelDispatchedToCorrectPointerInputModifierImpl2() {
+
+        // Arrange
+
+        val childPointerInputFilter = PointerInputFilterMock()
+        val childLayoutNode = LayoutNode(
+            0, 0, 100, 100,
+            PointerInputModifierImpl2(childPointerInputFilter)
+        )
+
+        val parentPointerInputFilter = PointerInputFilterMock()
+        val parentLayoutNode: LayoutNode = LayoutNode(
+            0, 0, 100, 100,
+            PointerInputModifierImpl2(parentPointerInputFilter)
+        ).apply {
+            insertAt(0, childLayoutNode)
+        }
+
+        addToRoot(parentLayoutNode)
+
+        val down =
+            PointerInputEvent(0, 7, Offset(50f, 50f), true)
+
+        val up = PointerInputEvent(0, 11, Offset(50f, 50f), false)
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+        parentLayoutNode.removeAt(0, 1)
+        pointerInputEventProcessor.process(up)
+
+        // Assert
+        assertThat(childPointerInputFilter.log.getOnCancelFilterLog()).hasSize(1)
+        assertThat(parentPointerInputFilter.log.getOnCancelFilterLog()).hasSize(0)
+    }
+
+    @Test
+    fun process_pointerInputModifierRemovedDuringInput_correctPointerInputChangesReceived() {
+
+        // Arrange
+
+        val childPointerInputFilter = PointerInputFilterMock()
+        val childLayoutNode = LayoutNode(
+            0, 0, 100, 100,
+            PointerInputModifierImpl2(
+                childPointerInputFilter
+            )
+        )
+
+        val parentPointerInputFilter = PointerInputFilterMock()
+        val parentLayoutNode: LayoutNode = LayoutNode(
+            0, 0, 100, 100,
+            PointerInputModifierImpl2(
+                parentPointerInputFilter
+            )
+        ).apply {
+            insertAt(0, childLayoutNode)
+        }
+
+        addToRoot(parentLayoutNode)
+
+        val offset = Offset(50f, 50f)
+
+        val down = PointerInputEvent(0, 7, offset, true)
+        val up = PointerInputEvent(0, 11, offset, false)
+
+        val expectedDownChange =
+            PointerInputChange(
+                id = PointerId(0),
+                7,
+                offset,
+                true,
+                7,
+                offset,
+                false,
+                isInitiallyConsumed = false
+            )
+
+        val expectedUpChange =
+            PointerInputChange(
+                id = PointerId(0),
+                11,
+                offset,
+                false,
+                7,
+                offset,
+                true,
+                isInitiallyConsumed = false
+            )
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+        childLayoutNode.modifier = Modifier
+        pointerInputEventProcessor.process(up)
+
+        // Assert
+
+        val parentLog = parentPointerInputFilter.log.getOnPointerEventFilterLog()
+        val childLog = childPointerInputFilter.log.getOnPointerEventFilterLog()
+
+        // Verify call count
+        assertThat(parentLog).hasSize(PointerEventPass.values().size * 2)
+        assertThat(childLog).hasSize(PointerEventPass.values().size)
+
+        // Verify call values
+
+        parentLog.verifyOnPointerEventCall(
+            index = 0,
+            expectedEvent = pointerEventOf(expectedDownChange),
+            expectedPass = PointerEventPass.Initial
+        )
+        parentLog.verifyOnPointerEventCall(
+            index = 1,
+            expectedEvent = pointerEventOf(expectedDownChange),
+            expectedPass = PointerEventPass.Main
+        )
+        parentLog.verifyOnPointerEventCall(
+            index = 2,
+            expectedEvent = pointerEventOf(expectedDownChange),
+            expectedPass = PointerEventPass.Final
+        )
+        parentLog.verifyOnPointerEventCall(
+            index = 3,
+            expectedEvent = pointerEventOf(expectedUpChange),
+            expectedPass = PointerEventPass.Initial
+        )
+        parentLog.verifyOnPointerEventCall(
+            index = 4,
+            expectedEvent = pointerEventOf(expectedUpChange),
+            expectedPass = PointerEventPass.Main
+        )
+        parentLog.verifyOnPointerEventCall(
+            index = 5,
+            expectedEvent = pointerEventOf(expectedUpChange),
+            expectedPass = PointerEventPass.Final
+        )
+
+        childLog.verifyOnPointerEventCall(
+            index = 0,
+            expectedEvent = pointerEventOf(expectedDownChange),
+            expectedPass = PointerEventPass.Initial
+        )
+        childLog.verifyOnPointerEventCall(
+            index = 1,
+            expectedEvent = pointerEventOf(expectedDownChange),
+            expectedPass = PointerEventPass.Main
+        )
+        childLog.verifyOnPointerEventCall(
+            index = 2,
+            expectedEvent = pointerEventOf(expectedDownChange),
+            expectedPass = PointerEventPass.Final
+        )
+    }
+
+    @Test
+    fun process_pointerInputModifierRemovedDuringInput_cancelDispatchedToCorrectPim() {
+
+        // Arrange
+
+        val childPointerInputFilter = PointerInputFilterMock()
+        val childLayoutNode = LayoutNode(
+            0, 0, 100, 100,
+            PointerInputModifierImpl2(childPointerInputFilter)
+        )
+
+        val parentPointerInputFilter = PointerInputFilterMock()
+        val parentLayoutNode: LayoutNode = LayoutNode(
+            0, 0, 100, 100,
+            PointerInputModifierImpl2(parentPointerInputFilter)
+        ).apply {
+            insertAt(0, childLayoutNode)
+        }
+
+        addToRoot(parentLayoutNode)
+
+        val down =
+            PointerInputEvent(0, 7, Offset(50f, 50f), true)
+
+        val up =
+            PointerInputEvent(0, 11, Offset(50f, 50f), false)
+
+        // Act
+
+        pointerInputEventProcessor.process(down)
+        childLayoutNode.modifier = Modifier
+        pointerInputEventProcessor.process(up)
+
+        // Assert
+        assertThat(childPointerInputFilter.log.getOnCancelFilterLog()).hasSize(1)
+        assertThat(parentPointerInputFilter.log.getOnCancelFilterLog()).hasSize(0)
+    }
+
+    @Test
+    fun process_downNoPointerInputModifiers_nothingInteractedWithAndNoMovementConsumed() {
+        val pointerInputEvent =
+            PointerInputEvent(0, 7, Offset(0f, 0f), true)
+
+        val result: ProcessResult = pointerInputEventProcessor.process(pointerInputEvent)
+
+        assertThat(result).isEqualTo(
+            ProcessResult(
+                dispatchedToAPointerInputModifier = false,
+                anyMovementConsumed = false
+            )
+        )
+    }
+
+    @Test
+    fun process_downNoPointerInputModifiersHit_nothingInteractedWithAndNoMovementConsumed() {
+
+        // Arrange
+
+        val pointerInputFilter = PointerInputFilterMock()
+
+        val layoutNode = LayoutNode(
+            0, 0, 1, 1,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+
+        addToRoot(layoutNode)
+
+        val offsets =
+            listOf(
+                Offset(-1f, 0f),
+                Offset(0f, -1f),
+                Offset(1f, 0f),
+                Offset(0f, 1f)
+            )
+        val pointerInputEvent =
+            PointerInputEvent(
+                11,
+                (offsets.indices).map {
+                    PointerInputEventData(it, 11, offsets[it], true)
+                }
+            )
+
+        // Act
+
+        val result: ProcessResult = pointerInputEventProcessor.process(pointerInputEvent)
+
+        // Assert
+
+        assertThat(result).isEqualTo(
+            ProcessResult(
+                dispatchedToAPointerInputModifier = false,
+                anyMovementConsumed = false
+            )
+        )
+    }
+
+    @Test
+    fun process_downPointerInputModifierHit_somethingInteractedWithAndNoMovementConsumed() {
+
+        // Arrange
+
+        val pointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            0, 0, 1, 1,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+        addToRoot(layoutNode)
+        val pointerInputEvent =
+            PointerInputEvent(0, 11, Offset(0f, 0f), true)
+
+        // Act
+
+        val result = pointerInputEventProcessor.process(pointerInputEvent)
+
+        // Assert
+
+        assertThat(result).isEqualTo(
+            ProcessResult(
+                dispatchedToAPointerInputModifier = true,
+                anyMovementConsumed = false
+            )
+        )
+    }
+
+    @Test
+    fun process_downHitsPifRemovedPointerMoves_nothingInteractedWithAndNoMovementConsumed() {
+
+        // Arrange
+
+        val pointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            0, 0, 1, 1,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+        addToRoot(layoutNode)
+        val down = PointerInputEvent(0, 11, Offset(0f, 0f), true)
+        pointerInputEventProcessor.process(down)
+        val move = PointerInputEvent(0, 11, Offset(1f, 0f), true)
+
+        // Act
+
+        testOwner.root.removeAt(0, 1)
+        val result = pointerInputEventProcessor.process(move)
+
+        // Assert
+
+        assertThat(result).isEqualTo(
+            ProcessResult(
+                dispatchedToAPointerInputModifier = false,
+                anyMovementConsumed = false
+            )
+        )
+    }
+
+    @Test
+    fun process_downHitsPointerMovesNothingConsumed_somethingInteractedWithAndNoMovementConsumed() {
+
+        // Arrange
+
+        val pointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            0, 0, 1, 1,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+        addToRoot(layoutNode)
+        val down = PointerInputEvent(0, 11, Offset(0f, 0f), true)
+        pointerInputEventProcessor.process(down)
+        val move = PointerInputEvent(0, 11, Offset(1f, 0f), true)
+
+        // Act
+
+        val result = pointerInputEventProcessor.process(move)
+
+        // Assert
+
+        assertThat(result).isEqualTo(
+            ProcessResult(
+                dispatchedToAPointerInputModifier = true,
+                anyMovementConsumed = false
+            )
+        )
+    }
+
+    @Test
+    fun process_downHitsPointerMovementConsumed_somethingInteractedWithAndMovementConsumed() {
+
+        // Arrange
+
+        val pointerInputFilter: PointerInputFilter =
+            PointerInputFilterMock(
+                pointerEventHandler = { pointerEvent, pass, _ ->
+                    if (pass == PointerEventPass.Initial) {
+                        pointerEvent.changes.forEach {
+                            if (it.positionChange() != Offset.Zero) it.consume()
+                        }
+                    }
+                }
+            )
+
+        val layoutNode = LayoutNode(
+            0, 0, 1, 1,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+        addToRoot(layoutNode)
+        val down = PointerInputEvent(0, 11, Offset(0f, 0f), true)
+        pointerInputEventProcessor.process(down)
+        val move = PointerInputEvent(0, 11, Offset(1f, 0f), true)
+
+        // Act
+
+        val result = pointerInputEventProcessor.process(move)
+
+        // Assert
+
+        assertThat(result).isEqualTo(
+            ProcessResult(
+                dispatchedToAPointerInputModifier = true,
+                anyMovementConsumed = true
+            )
+        )
+    }
+
+    @Test
+    fun processResult_trueTrue_propValuesAreCorrect() {
+        val processResult1 = ProcessResult(
+            dispatchedToAPointerInputModifier = true,
+            anyMovementConsumed = true
+        )
+        assertThat(processResult1.dispatchedToAPointerInputModifier).isTrue()
+        assertThat(processResult1.anyMovementConsumed).isTrue()
+    }
+
+    @Test
+    fun processResult_trueFalse_propValuesAreCorrect() {
+        val processResult1 = ProcessResult(
+            dispatchedToAPointerInputModifier = true,
+            anyMovementConsumed = false
+        )
+        assertThat(processResult1.dispatchedToAPointerInputModifier).isTrue()
+        assertThat(processResult1.anyMovementConsumed).isFalse()
+    }
+
+    @Test
+    fun processResult_falseTrue_propValuesAreCorrect() {
+        val processResult1 = ProcessResult(
+            dispatchedToAPointerInputModifier = false,
+            anyMovementConsumed = true
+        )
+        assertThat(processResult1.dispatchedToAPointerInputModifier).isFalse()
+        assertThat(processResult1.anyMovementConsumed).isTrue()
+    }
+
+    @Test
+    fun processResult_falseFalse_propValuesAreCorrect() {
+        val processResult1 = ProcessResult(
+            dispatchedToAPointerInputModifier = false,
+            anyMovementConsumed = false
+        )
+        assertThat(processResult1.dispatchedToAPointerInputModifier).isFalse()
+        assertThat(processResult1.anyMovementConsumed).isFalse()
+    }
+
+    @Test
+    fun buttonsPressed() {
+        // Arrange
+        val pointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            0,
+            0,
+            500,
+            500,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+        addToRoot(layoutNode)
+
+        class ButtonValidation(
+            vararg pressedValues: Int,
+            val primary: Boolean = false,
+            val secondary: Boolean = false,
+            val tertiary: Boolean = false,
+            val back: Boolean = false,
+            val forward: Boolean = false,
+            val anyPressed: Boolean = true,
+        ) {
+            val pressedValues = pressedValues
+        }
+
+        val buttonCheckerMap = mapOf(
+            MotionEvent.BUTTON_PRIMARY to ButtonValidation(0, primary = true),
+            MotionEvent.BUTTON_SECONDARY to ButtonValidation(1, secondary = true),
+            MotionEvent.BUTTON_TERTIARY to ButtonValidation(2, tertiary = true),
+            MotionEvent.BUTTON_STYLUS_PRIMARY to ButtonValidation(0, primary = true),
+            MotionEvent.BUTTON_STYLUS_SECONDARY to ButtonValidation(1, secondary = true),
+            MotionEvent.BUTTON_BACK to ButtonValidation(3, back = true),
+            MotionEvent.BUTTON_FORWARD to ButtonValidation(4, forward = true),
+            MotionEvent.BUTTON_PRIMARY or MotionEvent.BUTTON_TERTIARY to
+                ButtonValidation(0, 2, primary = true, tertiary = true),
+            MotionEvent.BUTTON_BACK or MotionEvent.BUTTON_STYLUS_PRIMARY to
+                ButtonValidation(0, 3, primary = true, back = true),
+            0 to ButtonValidation(anyPressed = false)
+        )
+
+        for (entry in buttonCheckerMap) {
+            val buttonState = entry.key
+            val validator = entry.value
+            val event = PointerInputEvent(
+                0,
+                listOf(PointerInputEventData(0, 0L, Offset.Zero, true)),
+                MotionEvent.obtain(
+                    0L,
+                    0L,
+                    MotionEvent.ACTION_DOWN,
+                    1,
+                    arrayOf(PointerProperties(1, MotionEvent.TOOL_TYPE_MOUSE)),
+                    arrayOf(PointerCoords(0f, 0f)),
+                    0,
+                    buttonState,
+                    0.1f,
+                    0.1f,
+                    0,
+                    0,
+                    InputDevice.SOURCE_MOUSE,
+                    0
+                )
+            )
+            pointerInputEventProcessor.process(event)
+
+            with(
+                (pointerInputFilter.log.last() as OnPointerEventFilterEntry).pointerEvent.buttons
+            ) {
+                assertThat(isPrimaryPressed).isEqualTo(validator.primary)
+                assertThat(isSecondaryPressed).isEqualTo(validator.secondary)
+                assertThat(isTertiaryPressed).isEqualTo(validator.tertiary)
+                assertThat(isBackPressed).isEqualTo(validator.back)
+                assertThat(isForwardPressed).isEqualTo(validator.forward)
+                assertThat(areAnyPressed).isEqualTo(validator.anyPressed)
+                val firstIndex = validator.pressedValues.firstOrNull() ?: -1
+                val lastIndex = validator.pressedValues.lastOrNull() ?: -1
+                assertThat(indexOfFirstPressed()).isEqualTo(firstIndex)
+                assertThat(indexOfLastPressed()).isEqualTo(lastIndex)
+                for (i in 0..10) {
+                    assertThat(isPressed(i)).isEqualTo(validator.pressedValues.contains(i))
+                }
+            }
+        }
+    }
+
+    @Test
+    fun metaState() {
+        // Arrange
+        val pointerInputFilter = PointerInputFilterMock()
+        val layoutNode = LayoutNode(
+            0,
+            0,
+            500,
+            500,
+            PointerInputModifierImpl2(
+                pointerInputFilter
+            )
+        )
+        addToRoot(layoutNode)
+
+        class MetaValidation(
+            val control: Boolean = false,
+            val meta: Boolean = false,
+            val alt: Boolean = false,
+            val shift: Boolean = false,
+            val sym: Boolean = false,
+            val function: Boolean = false,
+            val capsLock: Boolean = false,
+            val scrollLock: Boolean = false,
+            val numLock: Boolean = false
+        )
+
+        val buttonCheckerMap = mapOf(
+            AndroidKeyEvent.META_CTRL_ON to MetaValidation(control = true),
+            AndroidKeyEvent.META_META_ON to MetaValidation(meta = true),
+            AndroidKeyEvent.META_ALT_ON to MetaValidation(alt = true),
+            AndroidKeyEvent.META_SYM_ON to MetaValidation(sym = true),
+            AndroidKeyEvent.META_SHIFT_ON to MetaValidation(shift = true),
+            AndroidKeyEvent.META_FUNCTION_ON to MetaValidation(function = true),
+            AndroidKeyEvent.META_CAPS_LOCK_ON to MetaValidation(capsLock = true),
+            AndroidKeyEvent.META_SCROLL_LOCK_ON to MetaValidation(scrollLock = true),
+            AndroidKeyEvent.META_NUM_LOCK_ON to MetaValidation(numLock = true),
+            AndroidKeyEvent.META_CTRL_ON or AndroidKeyEvent.META_SHIFT_ON or
+                AndroidKeyEvent.META_NUM_LOCK_ON to
+                MetaValidation(control = true, shift = true, numLock = true),
+            0 to MetaValidation(),
+        )
+
+        for (entry in buttonCheckerMap) {
+            val metaState = entry.key
+            val validator = entry.value
+            val event = PointerInputEvent(
+                0,
+                listOf(PointerInputEventData(0, 0L, Offset.Zero, true)),
+                MotionEvent.obtain(
+                    0L,
+                    0L,
+                    MotionEvent.ACTION_DOWN,
+                    1,
+                    arrayOf(PointerProperties(1, MotionEvent.TOOL_TYPE_MOUSE)),
+                    arrayOf(PointerCoords(0f, 0f)),
+                    metaState,
+                    0,
+                    0.1f,
+                    0.1f,
+                    0,
+                    0,
+                    InputDevice.SOURCE_MOUSE,
+                    0
+                )
+            )
+            pointerInputEventProcessor.process(event)
+
+            val keyboardModifiers = (pointerInputFilter.log.last() as OnPointerEventFilterEntry)
+                .pointerEvent.keyboardModifiers
+            with(keyboardModifiers) {
+                assertThat(isCtrlPressed).isEqualTo(validator.control)
+                assertThat(isMetaPressed).isEqualTo(validator.meta)
+                assertThat(isAltPressed).isEqualTo(validator.alt)
+                assertThat(isAltGraphPressed).isFalse()
+                assertThat(isSymPressed).isEqualTo(validator.sym)
+                assertThat(isShiftPressed).isEqualTo(validator.shift)
+                assertThat(isFunctionPressed).isEqualTo(validator.function)
+                assertThat(isCapsLockOn).isEqualTo(validator.capsLock)
+                assertThat(isScrollLockOn).isEqualTo(validator.scrollLock)
+                assertThat(isNumLockOn).isEqualTo(validator.numLock)
+            }
+        }
+    }
+
+    private fun PointerInputEventProcessor.process(event: PointerInputEvent) =
+        process(event, positionCalculator)
+}
+
+private class PointerInputModifierImpl2(override val pointerInputFilter: PointerInputFilter) :
+    PointerInputModifier
+
+internal fun LayoutNode(x: Int, y: Int, x2: Int, y2: Int, modifier: Modifier = Modifier) =
+    LayoutNode().apply {
+        this.modifier = Modifier
+            .layout { measurable, constraints ->
+                val placeable = measurable.measure(constraints)
+                layout(placeable.width, placeable.height) {
+                    placeable.place(x, y)
+                }
+            }
+            .then(modifier)
+        measurePolicy = object : LayoutNode.NoIntrinsicsMeasurePolicy("not supported") {
+            override fun MeasureScope.measure(
+                measurables: List<Measurable>,
+                constraints: Constraints
+            ): MeasureResult =
+                innerCoordinator.layout(x2 - x, y2 - y) {
+                    measurables.forEach { it.measure(constraints).place(0, 0) }
+                }
+        }
+    }
+
+@OptIn(ExperimentalComposeUiApi::class, InternalCoreApi::class)
+private class TestOwner : Owner {
+    val onEndListeners = mutableListOf<() -> Unit>()
+    var position: IntOffset = IntOffset.Zero
+    override val root = LayoutNode(0, 0, 500, 500)
+
+    private val delegate = MeasureAndLayoutDelegate(root)
+
+    init {
+        root.attach(this)
+        delegate.updateRootConstraints(Constraints(maxWidth = 500, maxHeight = 500))
+    }
+
+    override fun requestFocus(): Boolean = false
+    override val rootForTest: RootForTest
+        get() = TODO("Not yet implemented")
+    override val hapticFeedBack: HapticFeedback
+        get() = TODO("Not yet implemented")
+    override val inputModeManager: InputModeManager
+        get() = TODO("Not yet implemented")
+    override val clipboardManager: ClipboardManager
+        get() = TODO("Not yet implemented")
+    override val accessibilityManager: AccessibilityManager
+        get() = TODO("Not yet implemented")
+    override val textToolbar: TextToolbar
+        get() = TODO("Not yet implemented")
+    override val autofillTree: AutofillTree
+        get() = TODO("Not yet implemented")
+    override val autofill: Autofill?
+        get() = null
+    override val density: Density
+        get() = Density(1f)
+    override val textInputService: TextInputService
+        get() = TODO("Not yet implemented")
+
+    override suspend fun textInputSession(
+        session: suspend PlatformTextInputSessionScope.() -> Nothing
+    ): Nothing {
+        TODO("Not yet implemented")
+    }
+
+    override val pointerIconService: PointerIconService
+        get() = TODO("Not yet implemented")
+    override val focusOwner: FocusOwner
+        get() = TODO("Not yet implemented")
+    override val windowInfo: WindowInfo
+        get() = TODO("Not yet implemented")
+
+    @Deprecated(
+        "fontLoader is deprecated, use fontFamilyResolver",
+        replaceWith = ReplaceWith("fontFamilyResolver")
+    )
+    @Suppress("OverridingDeprecatedMember", "DEPRECATION")
+    override val fontLoader: Font.ResourceLoader
+        get() = TODO("Not yet implemented")
+    override val fontFamilyResolver: FontFamily.Resolver
+        get() = TODO("Not yet implemented")
+    override val layoutDirection: LayoutDirection
+        get() = LayoutDirection.Ltr
+    override var showLayoutBounds: Boolean
+        get() = false
+        set(@Suppress("UNUSED_PARAMETER") value) {}
+
+    override fun onRequestMeasure(
+        layoutNode: LayoutNode,
+        affectsLookahead: Boolean,
+        forceRequest: Boolean,
+        scheduleMeasureAndLayout: Boolean
+    ) {
+        if (affectsLookahead) {
+            delegate.requestLookaheadRemeasure(layoutNode)
+        } else {
+            delegate.requestRemeasure(layoutNode)
+        }
+    }
+
+    override fun onRequestRelayout(
+        layoutNode: LayoutNode,
+        affectsLookahead: Boolean,
+        forceRequest: Boolean
+    ) {
+        if (affectsLookahead) {
+            delegate.requestLookaheadRelayout(layoutNode)
+        } else {
+            delegate.requestRelayout(layoutNode)
+        }
+    }
+
+    override fun requestOnPositionedCallback(layoutNode: LayoutNode) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onAttach(node: LayoutNode) {
+    }
+
+    override fun onDetach(node: LayoutNode) {
+    }
+
+    override fun calculatePositionInWindow(localPosition: Offset): Offset =
+        localPosition + position.toOffset()
+
+    override fun calculateLocalPosition(positionInWindow: Offset): Offset =
+        positionInWindow - position.toOffset()
+
+    override fun measureAndLayout(sendPointerUpdate: Boolean) {
+        delegate.measureAndLayout()
+    }
+
+    override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
+        delegate.measureAndLayout(layoutNode, constraints)
+    }
+
+    override fun forceMeasureTheSubtree(layoutNode: LayoutNode, affectsLookahead: Boolean) {
+        delegate.forceMeasureTheSubtree(layoutNode, affectsLookahead)
+    }
+
+    override fun createLayer(
+        drawBlock: (Canvas) -> Unit,
+        invalidateParentLayer: () -> Unit
+    ): OwnedLayer {
+        TODO("Not yet implemented")
+    }
+
+    override fun onSemanticsChange() {
+    }
+
+    override fun onLayoutChange(layoutNode: LayoutNode) {
+    }
+
+    override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
+        TODO("Not yet implemented")
+    }
+
+    override val measureIteration: Long
+        get() = 0
+
+    override val viewConfiguration: ViewConfiguration
+        get() = TODO("Not yet implemented")
+    override val snapshotObserver = OwnerSnapshotObserver { it.invoke() }
+    override val modifierLocalManager: ModifierLocalManager = ModifierLocalManager(this)
+
+    override val coroutineContext: CoroutineContext =
+        Executors.newFixedThreadPool(3).asCoroutineDispatcher()
+    override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
+        onEndListeners += listener
+    }
+
+    override fun onEndApplyChanges() {
+        while (onEndListeners.isNotEmpty()) {
+            onEndListeners.removeAt(0).invoke()
+        }
+    }
+
+    override fun drag(dragAndDropInfo: DragAndDropInfo): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
+        TODO("Not yet implemented")
+    }
+
+    override val sharedDrawScope = LayoutNodeDrawScope()
+}
+
+private fun List<LogEntry>.verifyOnPointerEventCall(
+    index: Int,
+    expectedPif: PointerInputFilter? = null,
+    expectedEvent: PointerEvent,
+    expectedPass: PointerEventPass,
+    expectedBounds: IntSize? = null
+) {
+    val logEntry = this[index]
+    assertThat(logEntry).isInstanceOf(OnPointerEventFilterEntry::class.java)
+    val entry = logEntry as OnPointerEventFilterEntry
+    if (expectedPif != null) {
+        assertThat(entry.pointerInputFilter).isSameInstanceAs(expectedPif)
+    }
+    PointerEventSubject
+        .assertThat(entry.pointerEvent)
+        .isStructurallyEqualTo(expectedEvent)
+    assertThat(entry.pass).isEqualTo(expectedPass)
+    if (expectedBounds != null) {
+        assertThat(entry.bounds).isEqualTo(expectedBounds)
+    }
+}
+
+private fun List<LogEntry>.verifyOnCancelCall(
+    index: Int
+) {
+    val logEntry = this[index]
+    assertThat(logEntry).isInstanceOf(OnCancelFilterEntry::class.java)
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputViewConfigurationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputViewConfigurationTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputViewConfigurationTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputViewConfigurationTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterAndroidViewHookupTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterAndroidViewHookupTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterAndroidViewHookupTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterAndroidViewHookupTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterAndroidViewOffsetsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterAndroidViewOffsetsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterAndroidViewOffsetsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterAndroidViewOffsetsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterComposeHookupTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterComposeHookupTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterComposeHookupTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterComposeHookupTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropUtilsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropUtilsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropUtilsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropUtilsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/RestrictedSizeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/RestrictedSizeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/RestrictedSizeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/RestrictedSizeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterCoroutineJobTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterCoroutineJobTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterCoroutineJobTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterCoroutineJobTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEventTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEventTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEventTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEventTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/BeyondBoundsLayoutTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/BeyondBoundsLayoutTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/BeyondBoundsLayoutTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/BeyondBoundsLayoutTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/IntrinsicsMeasurementTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/IntrinsicsMeasurementTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/IntrinsicsMeasurementTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/IntrinsicsMeasurementTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LayoutCooperationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LayoutCooperationTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LayoutCooperationTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LayoutCooperationTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LayoutNodeDensityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LayoutNodeDensityTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LayoutNodeDensityTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LayoutNodeDensityTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LayoutNodeLayoutDirectionTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LayoutNodeLayoutDirectionTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LayoutNodeLayoutDirectionTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LayoutNodeLayoutDirectionTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureInPlacementTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MeasureInPlacementTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureInPlacementTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MeasureInPlacementTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureOnlyTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MeasureOnlyTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureOnlyTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MeasureOnlyTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasuringPlacingTwiceIsNotAllowedTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MeasuringPlacingTwiceIsNotAllowedTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasuringPlacingTwiceIsNotAllowedTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MeasuringPlacingTwiceIsNotAllowedTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MultiContentLayoutTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MultiContentLayoutTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MultiContentLayoutTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MultiContentLayoutTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacedChildTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacedChildTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacedChildTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacedChildTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsRealClockTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsRealClockTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsRealClockTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsRealClockTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasurementModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RemeasurementModifierTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasurementModifierTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RemeasurementModifierTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/ResizingComposeViewTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ResizingComposeViewTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/ResizingComposeViewTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ResizingComposeViewTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RootNodeLayoutTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RootNodeLayoutTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RootNodeLayoutTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RootNodeLayoutTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RtlLayoutTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RtlLayoutTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RtlLayoutTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RtlLayoutTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/TestRuleExecutesLayoutPassesWhenWaitingForIdleTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/TestRuleExecutesLayoutPassesWhenWaitingForIdleTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/TestRuleExecutesLayoutPassesWhenWaitingForIdleTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/TestRuleExecutesLayoutPassesWhenWaitingForIdleTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/CompositionLocalMapInjectionTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/modifier/CompositionLocalMapInjectionTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/CompositionLocalMapInjectionTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/modifier/CompositionLocalMapInjectionTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierLocalMultiLayoutNodeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/modifier/ModifierLocalMultiLayoutNodeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierLocalMultiLayoutNodeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/modifier/ModifierLocalMultiLayoutNodeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierLocalProviderConsumerOrderTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/modifier/ModifierLocalProviderConsumerOrderTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierLocalProviderConsumerOrderTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/modifier/ModifierLocalProviderConsumerOrderTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierLocalSameLayoutNodeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/modifier/ModifierLocalSameLayoutNodeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierLocalSameLayoutNodeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/modifier/ModifierLocalSameLayoutNodeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/CompositeKeyHashTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/CompositeKeyHashTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/CompositeKeyHashTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/CompositeKeyHashTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/CompositionLocalConsumerModifierNodeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/CompositionLocalConsumerModifierNodeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/CompositionLocalConsumerModifierNodeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/CompositionLocalConsumerModifierNodeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/HotReloadTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/HotReloadTests.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/HotReloadTests.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/HotReloadTests.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModelReadsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModelReadsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModelReadsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModelReadsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeAncestorsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeAncestorsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeAncestorsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeAncestorsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeAttachOrderTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeAttachOrderTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeAttachOrderTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeAttachOrderTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeChildTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeChildTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeChildTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeChildTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeCoroutineScopeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeCoroutineScopeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeCoroutineScopeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeCoroutineScopeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeNearestAncestorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeNearestAncestorTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeNearestAncestorTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeNearestAncestorTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitAncestorsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitAncestorsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitAncestorsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitAncestorsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitLocalAncestorsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitLocalAncestorsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitLocalAncestorsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitLocalAncestorsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitLocalDescendantsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitLocalDescendantsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitLocalDescendantsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitLocalDescendantsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSelfAndChildrenTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSelfAndChildrenTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSelfAndChildrenTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSelfAndChildrenTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/MyersDiffTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/MyersDiffTests.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/MyersDiffTests.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/MyersDiffTests.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeChainOwnerTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainOwnerTests.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeChainOwnerTests.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainOwnerTests.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeChainTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTests.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeChainTests.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTests.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeCoordinatorInitializationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeCoordinatorInitializationTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeCoordinatorInitializationTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeCoordinatorInitializationTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverModifierNodeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ObserverModifierNodeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverModifierNodeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ObserverModifierNodeTest.kt
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/TraversableModifierNodeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/TraversableModifierNodeTest.kt
new file mode 100644
index 0000000..0fb4c18
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/TraversableModifierNodeTest.kt
@@ -0,0 +1,1706 @@
+/*
+ * 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.ui.node
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TraversableModifierNodeTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    private lateinit var parentNode: ClassOneWithSharedKeyTraversalNode
+
+    private lateinit var childA: ClassOneWithSharedKeyTraversalNode
+    private lateinit var childB: ClassTwoWithSharedKeyTraversalNode
+    private lateinit var childC: ClassThreeWithOtherKeyTraversalNode
+
+    private lateinit var grandChildNodeA: ClassOneWithSharedKeyTraversalNode
+    private lateinit var grandChildNodeB: ClassTwoWithSharedKeyTraversalNode
+    private lateinit var grandChildNodeC: ClassThreeWithOtherKeyTraversalNode
+
+    private lateinit var grandChildNodeD: ClassOneWithSharedKeyTraversalNode
+    private lateinit var grandChildNodeF: ClassThreeWithOtherKeyTraversalNode
+
+    private lateinit var grandChildNodeG: ClassOneWithSharedKeyTraversalNode
+
+    /**
+     * The UI hierarchy for this test is setup as:
+     *
+     *  Parent Column (ClassOneWithSharedKeyTraversalNode)
+     *    ⤷ ChildA Row (ClassOneWithSharedKeyTraversalNode)
+     *        ⤷ GrandchildA Box (ClassOneWithSharedKeyTraversalNode)
+     *        ⤷ GrandchildB Box (ClassTwoWithSharedKeyTraversalNode)
+     *        ⤷ GrandchildC Box (ClassThreeWithOtherKeyTraversalNode)
+     *
+     *    ⤷ ChildB Row (ClassTwoWithSharedKeyTraversalNode)
+     *         ⤷ GrandchildD Box (ClassOneWithSharedKeyTraversalNode)
+     *         ⤷ GrandchildE Box (ClassTwoWithSharedKeyTraversalNode)
+     *         ⤷ GrandchildF Box (ClassThreeWithOtherKeyTraversalNode)
+     *
+     *    ⤷ ChildC Row (ClassThreeWithOtherKeyTraversalNode)
+     *         ⤷ GrandchildG Box (ClassOneWithSharedKeyTraversalNode)
+     *         ⤷ GrandchildH Box (ClassTwoWithSharedKeyTraversalNode)
+     *         ⤷ GrandchildI Box (ClassThreeWithOtherKeyTraversalNode)
+     *
+     *    ⤷ ChildD Row (ClassTwoWithSharedKeyTraversalNode)
+     *         ⤷ GrandchildJ Box (ClassOneWithSharedKeyTraversalNode)
+     *
+     */
+    @Composable
+    private fun createUi() {
+        Column(
+            modifier = Modifier
+                .fillMaxSize()
+                .background(Color.Red)
+                .testTraversalNodeClassOneWithSharedKey("Parent") {
+                    parentNode = this
+                },
+        ) {
+            // Child A
+            Row(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .background(Color.Green)
+                    .testTraversalNodeClassOneWithSharedKey("Child_A") {
+                        childA = this
+                    }
+            ) {
+                Box(
+                    modifier = Modifier
+                        .size(30.dp)
+                        .background(Color.Blue)
+                        .testTraversalNodeClassOneWithSharedKey("Grandchild_A") {
+                            grandChildNodeA = this
+                        }
+                ) { }
+                Box(
+                    modifier = Modifier
+                        .size(30.dp)
+                        .background(Color.White)
+                        .testTraversalNodeClassTwoWithSharedKey("Grandchild_B") {
+                            grandChildNodeB = this
+                        }
+                ) { }
+                Box(
+                    modifier = Modifier
+                        .size(30.dp)
+                        .background(Color.Black)
+                        .testTraversalNodeClassThreeWithOtherKey("Grandchild_C") {
+                            grandChildNodeC = this
+                        }
+                ) { }
+            }
+            // Child B
+            Row(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .background(Color.Magenta)
+                    .testTraversalNodeClassTwoWithSharedKey("Child_B") {
+                        childB = this
+                    }
+            ) {
+                Box(
+                    modifier = Modifier
+                        .size(30.dp)
+                        .background(Color.Yellow)
+                        .testTraversalNodeClassOneWithSharedKey("Grandchild_D") {
+                            grandChildNodeD = this
+                        }
+                ) { }
+                Box(
+                    modifier = Modifier
+                        .size(30.dp)
+                        .background(Color.Blue)
+                        .testTraversalNodeClassTwoWithSharedKey("Grandchild_E")
+                ) { }
+                Box(
+                    modifier = Modifier
+                        .size(30.dp)
+                        .background(Color.Gray)
+                        .testTraversalNodeClassThreeWithOtherKey("Grandchild_F") {
+                            grandChildNodeF = this
+                        }
+                ) { }
+            }
+            // Child C
+            Row(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .background(Color.Cyan)
+                    .testTraversalNodeClassThreeWithOtherKey("Child_C") {
+                        childC = this
+                    }
+            ) {
+                Box(
+                    modifier = Modifier
+                        .size(30.dp)
+                        .background(Color.Blue)
+                        .testTraversalNodeClassOneWithSharedKey("Grandchild_G") {
+                            grandChildNodeG = this
+                        }
+                ) { }
+                Box(
+                    modifier = Modifier
+                        .size(30.dp)
+                        .background(Color.Magenta)
+                        .testTraversalNodeClassTwoWithSharedKey("Grandchild_H")
+                ) { }
+                Box(
+                    modifier = Modifier
+                        .size(30.dp)
+                        .background(Color.Black)
+                        .testTraversalNodeClassThreeWithOtherKey("Grandchild_I")
+                ) { }
+            }
+
+            // Child D
+            Row(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .background(Color.Green)
+                    .testTraversalNodeClassTwoWithSharedKey("Child_D")
+            ) {
+                Box(
+                    modifier = Modifier
+                        .size(30.dp)
+                        .background(Color.Black)
+                        .testTraversalNodeClassOneWithSharedKey("Grandchild_J")
+                ) { }
+            }
+        }
+    }
+
+    @Before
+    fun setup() {
+        rule.setContent {
+            createUi()
+        }
+    }
+
+    // *********** Nearest Traversable Ancestor Tests ***********
+    @Test
+    fun nearestTraversableAncestor_ancestorsWithTheSameClass() {
+        var nearestAncestorNode: TraversableNode? = null
+
+        // Starts at grandchild A (which has a parent and grandparent of the same class)
+        rule.runOnIdle {
+            nearestAncestorNode = grandChildNodeA.nearestTraversableAncestor()
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(childA)
+        }
+
+        // Starts at grandchild D (which has a parent of a different class + same key and
+        // grandparent of the same class).
+        nearestAncestorNode = null
+
+        rule.runOnIdle {
+            nearestAncestorNode = grandChildNodeD.nearestTraversableAncestor()
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(parentNode)
+        }
+
+        // Starts at grandchild G (which has a parent of a different class + different key and
+        // a grandparent of the same class).
+        nearestAncestorNode = null
+
+        rule.runOnIdle {
+            nearestAncestorNode = grandChildNodeG.nearestTraversableAncestor()
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(parentNode)
+        }
+    }
+
+    @Test
+    fun nearestTraversableAncestor_ancestorsWithOutTheSameClass() {
+        var nearestAncestorNode: TraversableNode? = null
+
+        // Starts at grandchild B (which has a parent and grandparent of different class but the
+        // same key). Neither should match.
+        rule.runOnIdle {
+            nearestAncestorNode = grandChildNodeB.nearestTraversableAncestor()
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(null)
+        }
+
+        nearestAncestorNode = null
+
+        // Starts at grandchild C (which has a parent and grandparent of different class and a
+        // different key). Neither should match.
+        rule.runOnIdle {
+            nearestAncestorNode = grandChildNodeC.nearestTraversableAncestor()
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(null)
+        }
+    }
+
+    @Test
+    fun nearestTraversableAncestorWithKey_ancestorsWithTheSameKey() {
+        var nearestAncestorNode: TraversableNode? = null
+
+        // Starts from grandchild A with SHARED_TRAVERSAL_NODE_KEY.
+        rule.runOnIdle {
+            nearestAncestorNode =
+                grandChildNodeA.nearestTraversableAncestorWithKey(SHARED_TRAVERSAL_NODE_KEY)
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(childA)
+        }
+
+        nearestAncestorNode = null
+
+        // Starts from grandchild D with SHARED_TRAVERSAL_NODE_KEY.
+        rule.runOnIdle {
+            nearestAncestorNode =
+                grandChildNodeD.nearestTraversableAncestorWithKey(SHARED_TRAVERSAL_NODE_KEY)
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(childB)
+        }
+
+        nearestAncestorNode = null
+
+        // Starts from grandchild G with SHARED_TRAVERSAL_NODE_KEY.
+        rule.runOnIdle {
+            nearestAncestorNode =
+                grandChildNodeG.nearestTraversableAncestorWithKey(SHARED_TRAVERSAL_NODE_KEY)
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(parentNode)
+        }
+
+        nearestAncestorNode = null
+
+        // Starts from grandchild G with OTHER_TRAVERSAL_NODE_KEY.
+        rule.runOnIdle {
+            nearestAncestorNode =
+                grandChildNodeG.nearestTraversableAncestorWithKey(OTHER_TRAVERSAL_NODE_KEY)
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(childC)
+        }
+    }
+
+    @Test
+    fun nearestTraversableAncestorWithKey_ancestorsWithoutTheSameKey() {
+        var nearestAncestorNode: TraversableNode? = null
+
+        // Starts from grandchild A with OTHER_TRAVERSAL_NODE_KEY.
+        rule.runOnIdle {
+            nearestAncestorNode =
+                grandChildNodeA.nearestTraversableAncestorWithKey(OTHER_TRAVERSAL_NODE_KEY)
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(null)
+        }
+
+        nearestAncestorNode = null
+
+        // Starts from grandchild B with OTHER_TRAVERSAL_NODE_KEY.
+        rule.runOnIdle {
+            nearestAncestorNode =
+                grandChildNodeB.nearestTraversableAncestorWithKey(OTHER_TRAVERSAL_NODE_KEY)
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(null)
+        }
+
+        nearestAncestorNode = null
+
+        // Starts from grandchild C with OTHER_TRAVERSAL_NODE_KEY.
+        rule.runOnIdle {
+            nearestAncestorNode =
+                grandChildNodeC.nearestTraversableAncestorWithKey(OTHER_TRAVERSAL_NODE_KEY)
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(null)
+        }
+
+        nearestAncestorNode = null
+
+        // Starts from grandchild F with OTHER_TRAVERSAL_NODE_KEY.
+        rule.runOnIdle {
+            nearestAncestorNode =
+                grandChildNodeF.nearestTraversableAncestorWithKey(OTHER_TRAVERSAL_NODE_KEY)
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(null)
+        }
+    }
+
+    @Test
+    fun nearestTraversableAncestorWithKey_nullKey() {
+        var nearestAncestorNode: TraversableNode? = null
+
+        // Starts from grandchild A with null key.
+        rule.runOnIdle {
+            nearestAncestorNode = grandChildNodeA.nearestTraversableAncestorWithKey(null)
+        }
+
+        rule.runOnIdle {
+            // No ancestors have a key of null
+            Truth.assertThat(nearestAncestorNode).isEqualTo(null)
+        }
+
+        // Starts from grandchild D with null key.
+        nearestAncestorNode = null
+
+        rule.runOnIdle {
+            nearestAncestorNode = grandChildNodeD.nearestTraversableAncestorWithKey(null)
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(null)
+        }
+
+        // Starts from grandchild F with null key.
+        nearestAncestorNode = null
+
+        rule.runOnIdle {
+            nearestAncestorNode = grandChildNodeF.nearestTraversableAncestorWithKey(null)
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(nearestAncestorNode).isEqualTo(null)
+        }
+    }
+
+    // *********** Traverse Ancestors Tests ***********
+    @Test
+    fun traverseAncestors_sameClass() {
+        var sameClassAncestors = 0
+
+        // Starts from grandchild (which has a parent and grandparent of the same class).
+        rule.runOnIdle {
+            grandChildNodeA.traverseAncestors {
+                sameClassAncestors++
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(sameClassAncestors).isEqualTo(2)
+        }
+
+        // Starts at grandchild D (which has a parent of a different class + same key and
+        // grandparent of the same class).
+        sameClassAncestors = 0
+
+        rule.runOnIdle {
+            grandChildNodeD.traverseAncestors {
+                sameClassAncestors++
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(sameClassAncestors).isEqualTo(1)
+        }
+
+        // Starts at grandchild G (which has a parent of a different class + different key and
+        // a grandparent of the same class).
+        sameClassAncestors = 0
+
+        rule.runOnIdle {
+            grandChildNodeG.traverseAncestors {
+                sameClassAncestors++
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(sameClassAncestors).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun traverseAncestors_sameClassWithCancellation() {
+        var sameClassAncestors = 0
+
+        // Starts at grandchild A (which has a parent and grandparent of the same class).
+        rule.runOnIdle {
+            grandChildNodeA.traverseAncestors {
+                sameClassAncestors++
+                // Cancel traversal
+                false
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(sameClassAncestors).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun traverseAncestorsWithKey_sameKey() {
+        var totalMatchingAncestors = 0
+        var classOneWithSharedKeyTraversalNodeAncestors = 0
+        var classTwoWithSharedKeyTraversalNodeAncestors = 0
+        var classThreeWithOtherKeyTraversalNodeAncestors = 0
+
+        // Starts at grandchild A (which has a parent and grandparent of the same class).
+        rule.runOnIdle {
+            grandChildNodeA.traverseAncestorsWithKey(SHARED_TRAVERSAL_NODE_KEY) {
+                totalMatchingAncestors++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        classOneWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        classTwoWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        classThreeWithOtherKeyTraversalNodeAncestors++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingAncestors).isEqualTo(2)
+            Truth.assertThat(classOneWithSharedKeyTraversalNodeAncestors).isEqualTo(2)
+            Truth.assertThat(classTwoWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classThreeWithOtherKeyTraversalNodeAncestors).isEqualTo(0)
+        }
+
+        // Starts at grandchild D (which has a parent of a different class + same key and
+        // grandparent of the same class).
+        totalMatchingAncestors = 0
+        classOneWithSharedKeyTraversalNodeAncestors = 0
+        classTwoWithSharedKeyTraversalNodeAncestors = 0
+        classThreeWithOtherKeyTraversalNodeAncestors = 0
+
+        rule.runOnIdle {
+            grandChildNodeD.traverseAncestorsWithKey(SHARED_TRAVERSAL_NODE_KEY) {
+                totalMatchingAncestors++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        classOneWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        classTwoWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        classThreeWithOtherKeyTraversalNodeAncestors++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingAncestors).isEqualTo(2)
+            Truth.assertThat(classOneWithSharedKeyTraversalNodeAncestors).isEqualTo(1)
+            Truth.assertThat(classTwoWithSharedKeyTraversalNodeAncestors).isEqualTo(1)
+            Truth.assertThat(classThreeWithOtherKeyTraversalNodeAncestors).isEqualTo(0)
+        }
+
+        // Starts at grandchild G (which has a parent of a different class + different key and
+        // a grandparent of the same class).
+        totalMatchingAncestors = 0
+        classOneWithSharedKeyTraversalNodeAncestors = 0
+        classTwoWithSharedKeyTraversalNodeAncestors = 0
+        classThreeWithOtherKeyTraversalNodeAncestors = 0
+
+        rule.runOnIdle {
+            grandChildNodeG.traverseAncestorsWithKey(SHARED_TRAVERSAL_NODE_KEY) {
+                totalMatchingAncestors++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        classOneWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        classTwoWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        classThreeWithOtherKeyTraversalNodeAncestors++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingAncestors).isEqualTo(1)
+            Truth.assertThat(classOneWithSharedKeyTraversalNodeAncestors).isEqualTo(1)
+            Truth.assertThat(classTwoWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classThreeWithOtherKeyTraversalNodeAncestors).isEqualTo(0)
+        }
+
+        // Starts at grandchild G (which has a parent of OTHER_TRAVERSAL_NODE_KEY and
+        // a grandparent without OTHER_TRAVERSAL_NODE_KEY.).
+        totalMatchingAncestors = 0
+        classOneWithSharedKeyTraversalNodeAncestors = 0
+        classTwoWithSharedKeyTraversalNodeAncestors = 0
+        classThreeWithOtherKeyTraversalNodeAncestors = 0
+
+        rule.runOnIdle {
+            grandChildNodeG.traverseAncestorsWithKey(OTHER_TRAVERSAL_NODE_KEY) {
+                totalMatchingAncestors++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        classOneWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        classTwoWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        classThreeWithOtherKeyTraversalNodeAncestors++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingAncestors).isEqualTo(1)
+            Truth.assertThat(classOneWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classTwoWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classThreeWithOtherKeyTraversalNodeAncestors).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun traverseAncestorsWithKey_differentKeyFromCallingNode() {
+        var totalMatchingAncestors = 0
+        var classOneWithSharedKeyTraversalNodeAncestors = 0
+        var classTwoWithSharedKeyTraversalNodeAncestors = 0
+        var classThreeWithOtherKeyTraversalNodeAncestors = 0
+
+        // Starts at grandchild A (which has a parent and grandparent with keys other than
+        // OTHER_TRAVERSAL_NODE_KEY.
+        rule.runOnIdle {
+            grandChildNodeA.traverseAncestorsWithKey(OTHER_TRAVERSAL_NODE_KEY) {
+                totalMatchingAncestors++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        classOneWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        classTwoWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        classThreeWithOtherKeyTraversalNodeAncestors++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingAncestors).isEqualTo(0)
+            Truth.assertThat(classOneWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classTwoWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classThreeWithOtherKeyTraversalNodeAncestors).isEqualTo(0)
+        }
+
+        // Starts at grandchild D (which has a parent and grandparent with keys other than
+        // OTHER_TRAVERSAL_NODE_KEY.
+        totalMatchingAncestors = 0
+        classOneWithSharedKeyTraversalNodeAncestors = 0
+        classTwoWithSharedKeyTraversalNodeAncestors = 0
+        classThreeWithOtherKeyTraversalNodeAncestors = 0
+
+        rule.runOnIdle {
+            grandChildNodeD.traverseAncestorsWithKey(OTHER_TRAVERSAL_NODE_KEY) {
+                totalMatchingAncestors++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        classOneWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        classTwoWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        classThreeWithOtherKeyTraversalNodeAncestors++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingAncestors).isEqualTo(0)
+            Truth.assertThat(classOneWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classTwoWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classThreeWithOtherKeyTraversalNodeAncestors).isEqualTo(0)
+        }
+    }
+
+    // Matches only keys that are set to null (of which there are none).
+    @Test
+    fun traverseAncestorsWithKey_nullKey() {
+        var totalMatchingAncestors = 0
+        var sameClassAncestors = 0
+        var sameKeyDifferentClassAncestors = 0
+        var differentKeyDifferentClassAncestors = 0
+
+        // Starts at grandchild A (which has a parent and grandparent of the same class).
+        rule.runOnIdle {
+            grandChildNodeA.traverseAncestorsWithKey(null) {
+                totalMatchingAncestors++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        differentKeyDifferentClassAncestors++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingAncestors).isEqualTo(0)
+            Truth.assertThat(sameClassAncestors).isEqualTo(0)
+            Truth.assertThat(sameKeyDifferentClassAncestors).isEqualTo(0)
+            Truth.assertThat(differentKeyDifferentClassAncestors).isEqualTo(0)
+        }
+
+        // Starts at grandchild D (which has a parent of a different class + same key and
+        // grandparent of the same class).
+        totalMatchingAncestors = 0
+        sameClassAncestors = 0
+        sameKeyDifferentClassAncestors = 0
+        differentKeyDifferentClassAncestors = 0
+
+        rule.runOnIdle {
+            grandChildNodeD.traverseAncestorsWithKey(null) {
+                totalMatchingAncestors++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        differentKeyDifferentClassAncestors++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingAncestors).isEqualTo(0)
+            Truth.assertThat(sameClassAncestors).isEqualTo(0)
+            Truth.assertThat(sameKeyDifferentClassAncestors).isEqualTo(0)
+            Truth.assertThat(differentKeyDifferentClassAncestors).isEqualTo(0)
+        }
+
+        // Starts at grandchild G (which has a parent of a different class + different key and
+        // a grandparent of the same class).
+        totalMatchingAncestors = 0
+        sameClassAncestors = 0
+        sameKeyDifferentClassAncestors = 0
+        differentKeyDifferentClassAncestors = 0
+
+        rule.runOnIdle {
+            grandChildNodeG.traverseAncestorsWithKey(null) {
+                totalMatchingAncestors++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        differentKeyDifferentClassAncestors++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingAncestors).isEqualTo(0)
+            Truth.assertThat(sameClassAncestors).isEqualTo(0)
+            Truth.assertThat(sameKeyDifferentClassAncestors).isEqualTo(0)
+            Truth.assertThat(differentKeyDifferentClassAncestors).isEqualTo(0)
+        }
+    }
+
+    // *********** Traverse Children Tests ***********
+    @Test
+    fun traverseChildren_sameClass() {
+        var sameClassChildren = 0
+
+        rule.runOnIdle {
+            parentNode.traverseChildren {
+                sameClassChildren++
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(sameClassChildren).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun traverseChildrenWithKey_sameKey() {
+        var totalMatchingChildren = 0
+        // All these are in relation to the parent class where we run the traversal.
+        var sameClassChildren = 0
+        var sameKeyDifferentClassChildren = 0
+
+        rule.runOnIdle {
+            parentNode.traverseChildrenWithKey(SHARED_TRAVERSAL_NODE_KEY) {
+                totalMatchingChildren++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassChildren++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassChildren++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingChildren).isEqualTo(3)
+            Truth.assertThat(sameClassChildren).isEqualTo(1)
+            Truth.assertThat(sameKeyDifferentClassChildren).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun traverseChildrenWithKey_differentKeyFromCallingNode() {
+        var totalMatchingChildren = 0
+        var classOneWithSharedKeyTraversalNodeAncestors = 0
+        var classTwoWithSharedKeyTraversalNodeAncestors = 0
+        // Only class with key = OTHER_TRAVERSAL_NODE_KEY.
+        var classThreeWithOtherKeyTraversalNodeAncestors = 0
+
+        rule.runOnIdle {
+            parentNode.traverseChildrenWithKey(OTHER_TRAVERSAL_NODE_KEY) {
+                totalMatchingChildren++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        classOneWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        classTwoWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        classThreeWithOtherKeyTraversalNodeAncestors++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingChildren).isEqualTo(1)
+            Truth.assertThat(classOneWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classTwoWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classThreeWithOtherKeyTraversalNodeAncestors).isEqualTo(1)
+        }
+    }
+
+    // Matches only keys that are set to null (of which there are none).
+    @Test
+    fun traverseChildrenWithKey_nullKey() {
+        var totalMatchingChildren = 0
+        var classOneWithSharedKeyTraversalNodeAncestors = 0
+        var classTwoWithSharedKeyTraversalNodeAncestors = 0
+        var classThreeWithOtherKeyTraversalNodeAncestors = 0
+
+        rule.runOnIdle {
+            // parentNode is of type ClassOneWithSharedKeyTraversalNode
+            parentNode.traverseChildrenWithKey(null) {
+                totalMatchingChildren++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        classOneWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        classTwoWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        classThreeWithOtherKeyTraversalNodeAncestors++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingChildren).isEqualTo(0)
+            Truth.assertThat(classOneWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classTwoWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classThreeWithOtherKeyTraversalNodeAncestors).isEqualTo(0)
+        }
+    }
+
+    // *********** Traverse Subtree Tests ***********
+    @Test
+    fun traverseSubtree_sameClass() {
+        var sameClassNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtree {
+                sameClassNodes++
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(sameClassNodes).isEqualTo(5)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeWithKey_fromParentWithSameKey() {
+        var totalMatchingNodes = 0
+        var sameClassNodes = 0
+        var sameKeyDifferentClassNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeWithKey(SHARED_TRAVERSAL_NODE_KEY) {
+                totalMatchingNodes++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassNodes++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassNodes++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(10)
+            Truth.assertThat(sameClassNodes).isEqualTo(5)
+            Truth.assertThat(sameKeyDifferentClassNodes).isEqualTo(5)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeWithKey_differentKeyFromCallingNode() {
+        var totalMatchingNodes = 0
+        var classOneWithSharedKeyTraversalNodeAncestors = 0
+        var classTwoWithSharedKeyTraversalNodeAncestors = 0
+        // Only class with key = OTHER_TRAVERSAL_NODE_KEY.
+        var classThreeWithOtherKeyTraversalNodeAncestors = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeWithKey(OTHER_TRAVERSAL_NODE_KEY) {
+                totalMatchingNodes++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        classOneWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        classTwoWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        classThreeWithOtherKeyTraversalNodeAncestors++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(4)
+            // Should be zero because it won't match the shared key
+            Truth.assertThat(classOneWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            // Should be zero because it won't match the shared key
+            Truth.assertThat(classTwoWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classThreeWithOtherKeyTraversalNodeAncestors).isEqualTo(4)
+        }
+    }
+
+    // Matches only keys that are set to null (of which there are none).
+    @Test
+    fun traverseSubtreeWithKey_nullKey() {
+        var totalMatchingNodes = 0
+        // All these are in relation to the parent class where we run the traversal.
+        var sameClassNodes = 0
+        var sameKeyDifferentClassNodes = 0
+        var differentKeyDifferentClassNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeWithKey(null) {
+                totalMatchingNodes++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassNodes++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassNodes++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        differentKeyDifferentClassNodes++
+                    }
+                }
+                // Continue traversal
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(0)
+            Truth.assertThat(sameClassNodes).isEqualTo(0)
+            Truth.assertThat(sameKeyDifferentClassNodes).isEqualTo(0)
+            Truth.assertThat(differentKeyDifferentClassNodes).isEqualTo(0)
+        }
+    }
+
+    // *********** Traverse Subtree If Tests ***********
+    @Test
+    fun traverseSubtreeIf_alwaysContinueTraversal() {
+        var sameClassNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIf {
+                sameClassNodes++
+                VisitSubtreeIfAction.VisitSubtree
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(sameClassNodes).isEqualTo(5)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeIf_alwaysSkipSubtree() {
+        var sameClassNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIf {
+                sameClassNodes++
+                VisitSubtreeIfAction.SkipSubtree
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(sameClassNodes).isEqualTo(4)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeIf_skipOneSubtree() {
+        var sameClassNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIf {
+                sameClassNodes++
+
+                // This will skip the subtree under childA, thus remove the grandchildA of
+                // ClassOneWithSharedKeyTraversalNode from the count
+                if (it == childA) {
+                    VisitSubtreeIfAction.SkipSubtree
+                } else {
+                    VisitSubtreeIfAction.VisitSubtree
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(sameClassNodes).isEqualTo(4)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeWithKey_sameKeyFromCallingNode_alwaysContinueTraversal() {
+        var totalMatchingNodes = 0
+        // All these are in relation to the parent class where we run the traversal.
+        var sameClassNodes = 0
+        var sameKeyDifferentClassNodes = 0
+        var otherNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIfWithKey(SHARED_TRAVERSAL_NODE_KEY) {
+                totalMatchingNodes++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassNodes++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassNodes++
+                    }
+
+                    else -> {
+                        otherNodes++
+                    }
+                }
+                VisitSubtreeIfAction.VisitSubtree
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(10)
+            Truth.assertThat(sameClassNodes).isEqualTo(5)
+            Truth.assertThat(sameKeyDifferentClassNodes).isEqualTo(5)
+            Truth.assertThat(otherNodes).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeWithKey_sameKeyFromCallingNode_alwaysCancelTraversal() {
+        var totalMatchingNodes = 0
+        // All these are in relation to the parent class where we run the traversal.
+        var sameClassNodes = 0
+        var sameKeyDifferentClassNodes = 0
+        var otherNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIfWithKey(SHARED_TRAVERSAL_NODE_KEY) {
+                totalMatchingNodes++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassNodes++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassNodes++
+                    }
+
+                    else -> {
+                        otherNodes++
+                    }
+                }
+                VisitSubtreeIfAction.CancelTraversal
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(1)
+            Truth.assertThat(sameClassNodes).isEqualTo(1)
+            Truth.assertThat(sameKeyDifferentClassNodes).isEqualTo(0)
+            Truth.assertThat(otherNodes).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeWithKey_sameKeyFromCallingNode_alwaysSkipSubtree() {
+        var totalMatchingNodes = 0
+        // All these are in relation to the parent class where we run the traversal.
+        var sameClassNodes = 0
+        var sameKeyDifferentClassNodes = 0
+        var otherNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIfWithKey(SHARED_TRAVERSAL_NODE_KEY) {
+                totalMatchingNodes++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassNodes++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassNodes++
+                    }
+
+                    else -> {
+                        otherNodes++
+                    }
+                }
+                VisitSubtreeIfAction.SkipSubtree
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(5)
+            Truth.assertThat(sameClassNodes).isEqualTo(2)
+            Truth.assertThat(sameKeyDifferentClassNodes).isEqualTo(3)
+            Truth.assertThat(otherNodes).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeWithKey_sameKeyFromCallingNode_skipSubtreeOfSameClass() {
+        var totalMatchingNodes = 0
+        // All these are in relation to the parent class where we run the traversal.
+        var sameClassNodes = 0
+        var sameKeyDifferentClassNodes = 0
+        var otherNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIfWithKey(SHARED_TRAVERSAL_NODE_KEY) {
+                totalMatchingNodes++
+
+                val action = when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassNodes++
+                        VisitSubtreeIfAction.SkipSubtree
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassNodes++
+                        VisitSubtreeIfAction.VisitSubtree
+                    }
+
+                    else -> {
+                        otherNodes++
+                        VisitSubtreeIfAction.VisitSubtree
+                    }
+                }
+                action
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(8)
+            Truth.assertThat(sameClassNodes).isEqualTo(4)
+            Truth.assertThat(sameKeyDifferentClassNodes).isEqualTo(4)
+            Truth.assertThat(otherNodes).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeWithKey_sameKeyFromCallingNode_cancelTraversalOfSameClass() {
+        var totalMatchingNodes = 0
+        // All these are in relation to the parent class where we run the traversal.
+        var sameClassNodes = 0
+        var sameKeyDifferentClassNodes = 0
+        var otherNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIfWithKey(SHARED_TRAVERSAL_NODE_KEY) {
+                totalMatchingNodes++
+
+                val action = when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassNodes++
+                        VisitSubtreeIfAction.CancelTraversal
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassNodes++
+                        VisitSubtreeIfAction.VisitSubtree
+                    }
+
+                    else -> {
+                        otherNodes++
+                        VisitSubtreeIfAction.VisitSubtree
+                    }
+                }
+                action
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(1)
+            Truth.assertThat(sameClassNodes).isEqualTo(1)
+            Truth.assertThat(sameKeyDifferentClassNodes).isEqualTo(0)
+            Truth.assertThat(otherNodes).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeWithKey_sameKeyFromCallingNode_skipSubtreeOfDifferentClassSameKey() {
+        var totalMatchingNodes = 0
+        // All these are in relation to the parent class where we run the traversal.
+        var sameClassNodes = 0
+        var sameKeyDifferentClassNodes = 0
+        var otherNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIfWithKey(SHARED_TRAVERSAL_NODE_KEY) {
+                totalMatchingNodes++
+
+                val action = when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassNodes++
+                        VisitSubtreeIfAction.VisitSubtree
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassNodes++
+                        VisitSubtreeIfAction.SkipSubtree
+                    }
+
+                    else -> {
+                        otherNodes++
+                        VisitSubtreeIfAction.VisitSubtree
+                    }
+                }
+                action
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(7)
+            Truth.assertThat(sameClassNodes).isEqualTo(3)
+            Truth.assertThat(sameKeyDifferentClassNodes).isEqualTo(4)
+            Truth.assertThat(otherNodes).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeWithKey_sameKeyFromCallingNode_cancelTraversalOfDifferentClassSameKey() {
+        var totalMatchingNodes = 0
+        // All these are in relation to the parent class where we run the traversal.
+        var sameClassNodes = 0
+        var sameKeyDifferentClassNodes = 0
+        var otherNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIfWithKey(SHARED_TRAVERSAL_NODE_KEY) {
+                totalMatchingNodes++
+
+                val action = when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassNodes++
+                        VisitSubtreeIfAction.VisitSubtree
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassNodes++
+                        VisitSubtreeIfAction.CancelTraversal
+                    }
+
+                    else -> {
+                        otherNodes++
+                        VisitSubtreeIfAction.VisitSubtree
+                    }
+                }
+                action
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(3)
+            Truth.assertThat(sameClassNodes).isEqualTo(2)
+            Truth.assertThat(sameKeyDifferentClassNodes).isEqualTo(1)
+            Truth.assertThat(otherNodes).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeWithKeyIf_differentKeyFromCallingNode_alwaysContinueTraversal() {
+        var totalMatchingNodes = 0
+        var classOneWithSharedKeyTraversalNodeAncestors = 0
+        var classTwoWithSharedKeyTraversalNodeAncestors = 0
+        // Only class with key = OTHER_TRAVERSAL_NODE_KEY.
+        var classThreeWithOtherKeyTraversalNodeAncestors = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIfWithKey(OTHER_TRAVERSAL_NODE_KEY) {
+                totalMatchingNodes++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        classOneWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        classTwoWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        classThreeWithOtherKeyTraversalNodeAncestors++
+                    }
+                }
+                VisitSubtreeIfAction.VisitSubtree
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(4)
+            // Should be zero because it won't match the shared key
+            Truth.assertThat(classOneWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            // Should be zero because it won't match the shared key
+            Truth.assertThat(classTwoWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classThreeWithOtherKeyTraversalNodeAncestors).isEqualTo(4)
+        }
+    }
+
+    @Test
+    fun traverseSubtreeWithKeyIf_differentKeyFromCallingNode_alwaysSkipSubtree() {
+        var totalMatchingNodes = 0
+        var classOneWithSharedKeyTraversalNodeAncestors = 0
+        var classTwoWithSharedKeyTraversalNodeAncestors = 0
+        // Only class with key = OTHER_TRAVERSAL_NODE_KEY.
+        var classThreeWithOtherKeyTraversalNodeAncestors = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIfWithKey(OTHER_TRAVERSAL_NODE_KEY) {
+                totalMatchingNodes++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        classOneWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        classTwoWithSharedKeyTraversalNodeAncestors++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        classThreeWithOtherKeyTraversalNodeAncestors++
+                    }
+                }
+                VisitSubtreeIfAction.SkipSubtree
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(3)
+            // Should be zero because it won't match the shared key
+            Truth.assertThat(classOneWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            // Should be zero because it won't match the shared key
+            Truth.assertThat(classTwoWithSharedKeyTraversalNodeAncestors).isEqualTo(0)
+            Truth.assertThat(classThreeWithOtherKeyTraversalNodeAncestors).isEqualTo(3)
+        }
+    }
+
+    // Matches only keys that are set to null (of which there are none).
+    @Test
+    fun traverseSubtreeWithKeyIf_nullKey_alwaysContinueTraversal() {
+        var totalMatchingNodes = 0
+        var sameClassNodes = 0
+        var sameKeyDifferentClassNodes = 0
+        var differentKeyDifferentClassNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIfWithKey(null) {
+                totalMatchingNodes++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassNodes++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassNodes++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        differentKeyDifferentClassNodes++
+                    }
+                }
+                VisitSubtreeIfAction.VisitSubtree
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(0)
+            Truth.assertThat(sameClassNodes).isEqualTo(0)
+            Truth.assertThat(sameKeyDifferentClassNodes).isEqualTo(0)
+            Truth.assertThat(differentKeyDifferentClassNodes).isEqualTo(0)
+        }
+    }
+
+    // Matches only keys that are set to null (of which there are none).
+    @Test
+    fun traverseSubtreeWithKeyIf_nullKey_alwaysSkipSubtree() {
+        var totalMatchingNodes = 0
+        // All these are in relation to the parent class where we run the traversal.
+        var sameClassNodes = 0
+        var sameKeyDifferentClassNodes = 0
+        var differentKeyDifferentClassNodes = 0
+
+        rule.runOnIdle {
+            parentNode.traverseSubtreeIfWithKey(null) {
+                totalMatchingNodes++
+
+                when (it) {
+                    is ClassOneWithSharedKeyTraversalNode -> {
+                        sameClassNodes++
+                    }
+
+                    is ClassTwoWithSharedKeyTraversalNode -> {
+                        sameKeyDifferentClassNodes++
+                    }
+
+                    is ClassThreeWithOtherKeyTraversalNode -> {
+                        differentKeyDifferentClassNodes++
+                    }
+                }
+                VisitSubtreeIfAction.SkipSubtree
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(totalMatchingNodes).isEqualTo(0)
+            Truth.assertThat(sameClassNodes).isEqualTo(0)
+            Truth.assertThat(sameKeyDifferentClassNodes).isEqualTo(0)
+            Truth.assertThat(differentKeyDifferentClassNodes).isEqualTo(0)
+        }
+    }
+}
+
+// Keys used across all test classes for testing [TraversalNode].
+private const val SHARED_TRAVERSAL_NODE_KEY = "SHARED_TRAVERSAL_NODE_KEY"
+private const val OTHER_TRAVERSAL_NODE_KEY = "OTHER_TRAVERSAL_NODE_KEY"
+
+// *********** Class One code (uses shared key in tests and contains funs for testing). ***********
+private fun Modifier.testTraversalNodeClassOneWithSharedKey(
+    label: String,
+    block: (ClassOneWithSharedKeyTraversalNode.() -> Unit)? = null
+) = this then TestTraversalModifierElementClassOneWithSharedKey(
+    label = label,
+    block = block
+)
+
+private data class TestTraversalModifierElementClassOneWithSharedKey(
+    val label: String,
+    val block: (ClassOneWithSharedKeyTraversalNode.() -> Unit)?
+) : ModifierNodeElement<ClassOneWithSharedKeyTraversalNode>() {
+    override fun create() =
+        ClassOneWithSharedKeyTraversalNode(label = label, block = block)
+
+    override fun update(node: ClassOneWithSharedKeyTraversalNode) {
+        node.label = label
+        node.block = block
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "testTraversalNodeClassOneWithSharedKey"
+        properties["label"] = label
+        properties["block"] = block
+    }
+}
+
+/*
+ * Main class for testing all the [TraversableNode] functions. The [block] parameter is for setting
+ * variable in the test to this instance so those publicly available [TraversableNode] functions
+ * can be called directly for testing.
+ *
+ * This isn't an example of how to use [TraversableNode]. Instead, you should call all the
+ * traversal methods from within your class when you need to do some operation on nodes of the same
+ * kind/key in the tree.
+ */
+private class ClassOneWithSharedKeyTraversalNode(
+    var label: String,
+    var block: (ClassOneWithSharedKeyTraversalNode.() -> Unit)?
+) : Modifier.Node(), TraversableNode {
+    override val traverseKey = SHARED_TRAVERSAL_NODE_KEY
+
+    init {
+        block?.let {
+            it()
+        }
+    }
+
+    override fun toString() =
+        "ClassOneWithSharedKeyTraversalNode($label) of $SHARED_TRAVERSAL_NODE_KEY"
+}
+
+// *********** Test Class Two code (uses shared key in tests, simple test class). ***********
+private fun Modifier.testTraversalNodeClassTwoWithSharedKey(
+    label: String,
+    block: (ClassTwoWithSharedKeyTraversalNode.() -> Unit)? = null
+) = this then TestTraversalModifierElementClassTwoWithSharedKey(
+    label = label,
+    block = block
+)
+
+private data class TestTraversalModifierElementClassTwoWithSharedKey(
+    val label: String,
+    val block: (ClassTwoWithSharedKeyTraversalNode.() -> Unit)?
+) : ModifierNodeElement<ClassTwoWithSharedKeyTraversalNode>() {
+    override fun create() = ClassTwoWithSharedKeyTraversalNode(
+        label = label,
+        block = block
+    )
+
+    override fun update(node: ClassTwoWithSharedKeyTraversalNode) {
+        node.label = label
+        node.block = block
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "testTraversalNodeClassTwoWithSharedKey"
+        properties["label"] = label
+        properties["block"] = block
+    }
+}
+
+private class ClassTwoWithSharedKeyTraversalNode(
+    var label: String,
+    var block: (ClassTwoWithSharedKeyTraversalNode.() -> Unit)?
+) :
+    Modifier.Node(), TraversableNode {
+
+    override val traverseKey = SHARED_TRAVERSAL_NODE_KEY
+
+    init {
+        block?.let {
+            it()
+        }
+    }
+
+    override fun toString() =
+        "ClassTwoWithSharedKeyTraversalNode($label) of $SHARED_TRAVERSAL_NODE_KEY"
+}
+
+// *********** Test Class Three code (uses other key in tests, simple test class). ***********
+private fun Modifier.testTraversalNodeClassThreeWithOtherKey(
+    label: String,
+    block: (ClassThreeWithOtherKeyTraversalNode.() -> Unit)? = null
+) = this then TestTraversalModifierElementClassThreeWithOtherKey(
+    label = label,
+    block
+)
+
+private data class TestTraversalModifierElementClassThreeWithOtherKey(
+    val label: String,
+    val block: (ClassThreeWithOtherKeyTraversalNode.() -> Unit)?
+) : ModifierNodeElement<ClassThreeWithOtherKeyTraversalNode>() {
+
+    override fun create() =
+        ClassThreeWithOtherKeyTraversalNode(label = label, block = block)
+
+    override fun update(node: ClassThreeWithOtherKeyTraversalNode) {
+        node.label = label
+        node.block = block
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "testTraversalNodeOtherKey"
+        properties["label"] = label
+        properties["block"] = block
+    }
+}
+
+private class ClassThreeWithOtherKeyTraversalNode(
+    var label: String,
+    var block: (ClassThreeWithOtherKeyTraversalNode.() -> Unit)?
+) : Modifier.Node(), TraversableNode {
+    override val traverseKey = OTHER_TRAVERSAL_NODE_KEY
+
+    init {
+        block?.let {
+            it()
+        }
+    }
+
+    override fun toString() =
+        "ClassThreeWithOtherKeyTraversalNode($label) of $OTHER_TRAVERSAL_NODE_KEY"
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInAppCompatActivityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInAppCompatActivityTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInAppCompatActivityTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInAppCompatActivityTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInComponentActivityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInComponentActivityTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInComponentActivityTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInComponentActivityTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInFragmentTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInFragmentTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInFragmentTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInFragmentTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/owners/SavedStateRegistryOwnerInAppCompatActivityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/SavedStateRegistryOwnerInAppCompatActivityTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/owners/SavedStateRegistryOwnerInAppCompatActivityTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/SavedStateRegistryOwnerInAppCompatActivityTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/owners/SavedStateRegistryOwnerInComponentActivityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/SavedStateRegistryOwnerInComponentActivityTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/owners/SavedStateRegistryOwnerInComponentActivityTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/SavedStateRegistryOwnerInComponentActivityTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/owners/SavedStateRegistryOwnerInFragmentTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/SavedStateRegistryOwnerInFragmentTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/owners/SavedStateRegistryOwnerInFragmentTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/SavedStateRegistryOwnerInFragmentTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidComposeViewsInRecyclerViewTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidComposeViewsInRecyclerViewTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidComposeViewsInRecyclerViewTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidComposeViewsInRecyclerViewTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidCompositionLocalTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidCompositionLocalTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidCompositionLocalTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidCompositionLocalTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidFontResourceLoaderTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidFontResourceLoaderTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidFontResourceLoaderTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidFontResourceLoaderTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidOwnerExtraAssertionsRule.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidOwnerExtraAssertionsRule.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidOwnerExtraAssertionsRule.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidOwnerExtraAssertionsRule.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewOverlayTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewOverlayTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewOverlayTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewOverlayTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewSavedStateSizeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewSavedStateSizeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewSavedStateSizeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewSavedStateSizeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/DepthSortedSetTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/DepthSortedSetTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/DepthSortedSetTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/DepthSortedSetTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/DisposableSaveableStateRegistryTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/DisposableSaveableStateRegistryTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/DisposableSaveableStateRegistryTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/DisposableSaveableStateRegistryTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/InspectableValueTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/InspectableValueTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/InspectableValueTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/InspectableValueTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LayoutIdTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/LayoutIdTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LayoutIdTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/LayoutIdTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardControllerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardControllerTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardControllerTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardControllerTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowInfoCompositionLocalTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoCompositionLocalTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowInfoCompositionLocalTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoCompositionLocalTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WrapperTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WrapperTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WrapperTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WrapperTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestActivity.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/TestActivity.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestActivity.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/TestActivity.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextMeasurerHelperTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerHelperTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextMeasurerHelperTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerHelperTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/AndroidPlatformTextInputSessionTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/AndroidPlatformTextInputSessionTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/AndroidPlatformTextInputSessionTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/AndroidPlatformTextInputSessionTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/NullableInputConnectionWrapperTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/NullableInputConnectionWrapperTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/NullableInputConnectionWrapperTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/NullableInputConnectionWrapperTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/TestInputMethodRequest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/TestInputMethodRequest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/TestInputMethodRequest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/TestInputMethodRequest.kt
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
new file mode 100644
index 0000000..3225799
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -0,0 +1,1778 @@
+/*
+ * Copyright 2020 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.viewinterop
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import android.util.DisplayMetrics
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.SurfaceView
+import android.view.View
+import android.view.View.OnAttachStateChangeListener
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.EditText
+import android.widget.FrameLayout
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisallowComposableCalls
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.ReusableContent
+import androidx.compose.runtime.ReusableContentHost
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.AbsoluteAlignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.platform.findViewTreeCompositionContext
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.TestActivity
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.tests.R
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnCreate
+import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnRelease
+import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnReset
+import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnUpdate
+import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnViewAttach
+import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnViewDetach
+import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.ViewLifecycleEvent
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.Lifecycle.Event.ON_CREATE
+import androidx.lifecycle.Lifecycle.Event.ON_PAUSE
+import androidx.lifecycle.Lifecycle.Event.ON_RESUME
+import androidx.lifecycle.Lifecycle.Event.ON_START
+import androidx.lifecycle.Lifecycle.Event.ON_STOP
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.savedstate.SavedStateRegistry
+import androidx.savedstate.SavedStateRegistryOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.typeText
+import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withClassName
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
+import kotlin.test.assertIs
+import org.hamcrest.CoreMatchers.endsWith
+import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.CoreMatchers.instanceOf
+import org.junit.Assert.assertEquals
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalComposeUiApi::class)
+class AndroidViewTest {
+    @get:Rule
+    val rule = createAndroidComposeRule<TestActivity>()
+
+    @Test
+    fun androidViewWithConstructor() {
+        rule.setContent {
+            AndroidView({ TextView(it).apply { text = "Test" } })
+        }
+        Espresso
+            .onView(instanceOf(TextView::class.java))
+            .check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun androidViewWithResourceTest() {
+        rule.setContent {
+            AndroidView({ LayoutInflater.from(it).inflate(R.layout.test_layout, null) })
+        }
+        Espresso
+            .onView(instanceOf(RelativeLayout::class.java))
+            .check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun androidViewInvalidatingDuringDrawTest() {
+        var drawCount = 0
+        val timesToInvalidate = 10
+        var customView: InvalidatedTextView? = null
+        rule.setContent {
+            AndroidView(
+                factory = {
+                    val view: View = LayoutInflater.from(it)
+                        .inflate(R.layout.test_multiple_invalidation_layout, null)
+                    customView = view.findViewById<InvalidatedTextView>(R.id.custom_draw_view)
+                    customView!!.timesToInvalidate = timesToInvalidate
+                    view.viewTreeObserver?.addOnPreDrawListener {
+                        ++drawCount
+                        true
+                    }
+                    view
+                })
+        }
+        // the first drawn was not caused by invalidation, thus add it to expected draw count.
+        var expectedDraws = timesToInvalidate + 1
+        repeat(expectedDraws) {
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Ensure we wait until the time advancement actually happened as sometimes we can race if
+        // we use runOnIdle directly making the test fail, so providing a big enough timeout to
+        // give plenty of time for the frame advancement to happen.
+        rule.waitUntil(3000) {
+            drawCount == expectedDraws
+        }
+
+        rule.runOnIdle {
+            // Verify that we only drew once per invalidation
+            assertThat(drawCount).isEqualTo(expectedDraws)
+            assertThat(drawCount).isEqualTo(customView!!.timesDrawn)
+        }
+    }
+
+    @Test
+    fun androidViewWithViewTest() {
+        lateinit var frameLayout: FrameLayout
+        rule.activityRule.scenario.onActivity { activity ->
+            frameLayout = FrameLayout(activity).apply {
+                layoutParams = ViewGroup.LayoutParams(300, 300)
+            }
+        }
+        rule.setContent {
+            AndroidView({ frameLayout })
+        }
+        Espresso
+            .onView(equalTo(frameLayout))
+            .check(matches(isDisplayed()))
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    fun androidViewAccessibilityDelegate() {
+        rule.setContent {
+             AndroidView({ TextView(it).apply { text = "Test"; setScreenReaderFocusable(true) } })
+        }
+        Espresso
+            .onView(instanceOf(TextView::class.java))
+            .check(matches(isDisplayed()))
+            .check { view, exception ->
+                val viewParent = view.getParent()
+                if (viewParent !is View) {
+                    throw exception
+                }
+                val delegate = viewParent.getAccessibilityDelegate()
+                if (viewParent.getAccessibilityDelegate() == null) {
+                    throw exception
+                }
+                val info: AccessibilityNodeInfo = AccessibilityNodeInfo()
+                delegate.onInitializeAccessibilityNodeInfo(view, info)
+                if (info.isVisibleToUser()) {
+                    throw exception
+                }
+                if (!info.isScreenReaderFocusable()) {
+                    throw exception
+                }
+            }
+    }
+
+    @Test
+    fun androidViewWithResourceTest_preservesLayoutParams() {
+        rule.setContent {
+            AndroidView({
+                LayoutInflater.from(it).inflate(R.layout.test_layout, FrameLayout(it), false)
+            })
+        }
+        Espresso
+            .onView(withClassName(endsWith("RelativeLayout")))
+            .check(matches(isDisplayed()))
+            .check { view, exception ->
+                if (view.layoutParams.width != 300.dp.toPx(view.context.resources.displayMetrics)) {
+                    throw exception
+                }
+                if (view.layoutParams.height != WRAP_CONTENT) {
+                    throw exception
+                }
+            }
+    }
+
+    @Test
+    fun androidViewProperlyDetached() {
+        lateinit var frameLayout: FrameLayout
+        rule.activityRule.scenario.onActivity { activity ->
+            frameLayout = FrameLayout(activity).apply {
+                layoutParams = ViewGroup.LayoutParams(300, 300)
+            }
+        }
+        var emit by mutableStateOf(true)
+        rule.setContent {
+            if (emit) {
+                AndroidView({ frameLayout })
+            }
+        }
+
+        // Assert view initially attached
+        rule.runOnUiThread {
+            assertThat(frameLayout.parent).isNotNull()
+            emit = false
+        }
+
+        // Assert view detached when removed from composition hierarchy
+        rule.runOnIdle {
+            assertThat(frameLayout.parent).isNull()
+            emit = true
+        }
+
+        // Assert view reattached when added back to the composition hierarchy
+        rule.runOnIdle {
+            assertThat(frameLayout.parent).isNotNull()
+        }
+    }
+
+    @Test
+    @LargeTest
+    fun androidView_attachedAfterDetached_addsViewBack() {
+        lateinit var root: FrameLayout
+        lateinit var composeView: ComposeView
+        lateinit var viewInsideCompose: View
+        rule.activityRule.scenario.onActivity { activity ->
+            root = FrameLayout(activity)
+            composeView = ComposeView(activity)
+            composeView.setViewCompositionStrategy(
+                ViewCompositionStrategy.DisposeOnLifecycleDestroyed(activity)
+            )
+            viewInsideCompose = View(activity)
+
+            activity.setContentView(root)
+            root.addView(composeView)
+            composeView.setContent {
+                AndroidView({ viewInsideCompose })
+            }
+        }
+
+        var viewInsideComposeHolder: ViewGroup? = null
+        rule.runOnUiThread {
+            assertThat(viewInsideCompose.parent).isNotNull()
+            viewInsideComposeHolder = viewInsideCompose.parent as ViewGroup
+            root.removeView(composeView)
+        }
+
+        rule.runOnIdle {
+            // Views don't detach from the parent when the parent is detached
+            assertThat(viewInsideCompose.parent).isNotNull()
+            assertThat(viewInsideComposeHolder?.childCount).isEqualTo(1)
+            root.addView(composeView)
+        }
+
+        rule.runOnIdle {
+            assertThat(viewInsideCompose.parent).isEqualTo(viewInsideComposeHolder)
+            assertThat(viewInsideComposeHolder?.childCount).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun androidViewWithResource_modifierIsApplied() {
+        val size = 20.dp
+        rule.setContent {
+            AndroidView(
+                { LayoutInflater.from(it).inflate(R.layout.test_layout, null) },
+                Modifier.requiredSize(size)
+            )
+        }
+        Espresso
+            .onView(instanceOf(RelativeLayout::class.java))
+            .check(matches(isDisplayed()))
+            .check { view, exception ->
+                val expectedSize = size.toPx(view.context.resources.displayMetrics)
+                if (view.width != expectedSize || view.height != expectedSize) {
+                    throw exception
+                }
+            }
+    }
+
+    @Test
+    fun androidViewWithView_modifierIsApplied() {
+        val size = 20.dp
+        lateinit var frameLayout: FrameLayout
+        rule.activityRule.scenario.onActivity { activity ->
+            frameLayout = FrameLayout(activity)
+        }
+        rule.setContent {
+            AndroidView({ frameLayout }, Modifier.requiredSize(size))
+        }
+
+        Espresso
+            .onView(equalTo(frameLayout))
+            .check(matches(isDisplayed()))
+            .check { view, exception ->
+                val expectedSize = size.toPx(view.context.resources.displayMetrics)
+                if (view.width != expectedSize || view.height != expectedSize) {
+                    throw exception
+                }
+            }
+    }
+
+    @Test
+    @LargeTest
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    fun androidViewWithView_drawModifierIsApplied() {
+        val size = 300
+        lateinit var frameLayout: FrameLayout
+        rule.activityRule.scenario.onActivity { activity ->
+            frameLayout = FrameLayout(activity).apply {
+                layoutParams = ViewGroup.LayoutParams(size, size)
+            }
+        }
+        rule.setContent {
+            AndroidView({ frameLayout },
+                Modifier
+                    .testTag("view")
+                    .background(color = Color.Blue))
+        }
+
+        rule.onNodeWithTag("view").captureToImage().assertPixels(IntSize(size, size)) {
+            Color.Blue
+        }
+    }
+
+    @Test
+    fun androidViewWithResource_modifierIsCorrectlyChanged() {
+        val size = mutableStateOf(20.dp)
+        rule.setContent {
+            AndroidView(
+                { LayoutInflater.from(it).inflate(R.layout.test_layout, null) },
+                Modifier.requiredSize(size.value)
+            )
+        }
+        Espresso
+            .onView(instanceOf(RelativeLayout::class.java))
+            .check(matches(isDisplayed()))
+            .check { view, exception ->
+                val expectedSize = size.value.toPx(view.context.resources.displayMetrics)
+                if (view.width != expectedSize || view.height != expectedSize) {
+                    throw exception
+                }
+            }
+        rule.runOnIdle { size.value = 30.dp }
+        Espresso
+            .onView(instanceOf(RelativeLayout::class.java))
+            .check(matches(isDisplayed()))
+            .check { view, exception ->
+                val expectedSize = size.value.toPx(view.context.resources.displayMetrics)
+                if (view.width != expectedSize || view.height != expectedSize) {
+                    throw exception
+                }
+            }
+    }
+
+    @Test
+    fun androidView_notDetachedFromWindowTwice() {
+        // Should not crash.
+        rule.setContent {
+            Box {
+                AndroidView(::ComposeView) {
+                    it.setContent {
+                        Box(Modifier)
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun androidView_updateIsRanInitially() {
+        rule.setContent {
+            Box {
+                AndroidView(::UpdateTestView) { view ->
+                    view.counter = 1
+                }
+            }
+        }
+
+        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
+            assertIs<UpdateTestView>(view)
+            assertThat(view.counter).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun androidView_updateObservesMultipleStateChanges() {
+        var counter by mutableStateOf(1)
+
+        rule.setContent {
+            Box {
+                AndroidView(::UpdateTestView) { view ->
+                    view.counter = counter
+                }
+            }
+        }
+
+        counter = 2
+        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
+            assertIs<UpdateTestView>(view)
+            assertThat(view.counter).isEqualTo(counter)
+        }
+
+        counter = 3
+        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
+            assertIs<UpdateTestView>(view)
+            assertThat(view.counter).isEqualTo(counter)
+        }
+
+        counter = 4
+        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
+            assertIs<UpdateTestView>(view)
+            assertThat(view.counter).isEqualTo(counter)
+        }
+    }
+
+    @Test
+    fun androidView_updateObservesStateChanges_fromDisposableEffect() {
+        var counter by mutableStateOf(1)
+
+        rule.setContent {
+            DisposableEffect(Unit) {
+                counter = 2
+                onDispose {}
+            }
+
+            Box {
+                AndroidView(::UpdateTestView) { view ->
+                    view.counter = counter
+                }
+            }
+        }
+
+        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
+            assertIs<UpdateTestView>(view)
+            assertThat(view.counter).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun androidView_updateObservesStateChanges_fromLaunchedEffect() {
+        var counter by mutableStateOf(1)
+
+        rule.setContent {
+            LaunchedEffect(Unit) {
+                counter = 2
+            }
+
+            Box {
+                AndroidView(::UpdateTestView) { view ->
+                    view.counter = counter
+                }
+            }
+        }
+
+        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
+            assertIs<UpdateTestView>(view)
+            assertThat(view.counter).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun androidView_updateObservesMultipleStateChanges_fromEffect() {
+        var counter by mutableStateOf(1)
+
+        rule.setContent {
+            LaunchedEffect(Unit) {
+                counter = 2
+                withFrameNanos {
+                    counter = 3
+                }
+            }
+
+            Box {
+                AndroidView(::UpdateTestView) { view ->
+                    view.counter = counter
+                }
+            }
+        }
+
+        onView(instanceOf(UpdateTestView::class.java)).check { view, _ ->
+            assertIs<UpdateTestView>(view)
+            assertThat(view.counter).isEqualTo(3)
+        }
+    }
+
+    @Test
+    fun androidView_updateObservesLayoutStateChanges() {
+        var size by mutableStateOf(20)
+        var obtainedSize: IntSize = IntSize.Zero
+        rule.setContent {
+            Box {
+                AndroidView(
+                    ::View,
+                    Modifier.onGloballyPositioned { obtainedSize = it.size }
+                ) { view ->
+                    view.layoutParams = ViewGroup.LayoutParams(size, size)
+                }
+            }
+        }
+        rule.runOnIdle {
+            assertThat(obtainedSize).isEqualTo(IntSize(size, size))
+            size = 40
+        }
+        rule.runOnIdle {
+            assertThat(obtainedSize).isEqualTo(IntSize(size, size))
+        }
+    }
+
+    @Test
+    fun androidView_propagatesDensity() {
+        rule.setContent {
+            val size = 50.dp
+            val density = Density(3f)
+            val sizeIpx = with(density) { size.roundToPx() }
+            CompositionLocalProvider(LocalDensity provides density) {
+                AndroidView(
+                    { FrameLayout(it) },
+                    Modifier
+                        .requiredSize(size)
+                        .onGloballyPositioned {
+                            assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
+                        }
+                )
+            }
+        }
+        rule.waitForIdle()
+    }
+
+    @Test
+    fun androidView_propagatesViewTreeCompositionContext() {
+        lateinit var parentComposeView: ComposeView
+        lateinit var compositionChildView: View
+        rule.activityRule.scenario.onActivity { activity ->
+            parentComposeView = ComposeView(activity).apply {
+                setContent {
+                    AndroidView(::View) {
+                        compositionChildView = it
+                    }
+                }
+                activity.setContentView(this)
+            }
+        }
+        rule.runOnIdle {
+            assertThat(compositionChildView.findViewTreeCompositionContext())
+                .isNotEqualTo(parentComposeView.findViewTreeCompositionContext())
+        }
+    }
+
+    @Test
+    fun androidView_propagatesLocalsToComposeViewChildren() {
+        val ambient = compositionLocalOf { "unset" }
+        var childComposedAmbientValue = "uncomposed"
+        rule.setContent {
+            CompositionLocalProvider(ambient provides "setByParent") {
+                AndroidView(
+                    factory = {
+                        ComposeView(it).apply {
+                            setContent {
+                                childComposedAmbientValue = ambient.current
+                            }
+                        }
+                    }
+                )
+            }
+        }
+        rule.runOnIdle {
+            assertThat(childComposedAmbientValue).isEqualTo("setByParent")
+        }
+    }
+
+    @Test
+    fun androidView_propagatesLayoutDirectionToComposeViewChildren() {
+        var childViewLayoutDirection: Int = Int.MIN_VALUE
+        var childCompositionLayoutDirection: LayoutDirection? = null
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                AndroidView(
+                    factory = {
+                        FrameLayout(it).apply {
+                            addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+                                childViewLayoutDirection = layoutDirection
+                            }
+                            addView(
+                                ComposeView(it).apply {
+                                    // The view hierarchy's layout direction should always override
+                                    // the ambient layout direction from the parent composition.
+                                    layoutDirection = android.util.LayoutDirection.LTR
+                                    setContent {
+                                        childCompositionLayoutDirection =
+                                            LocalLayoutDirection.current
+                                    }
+                                },
+                                ViewGroup.LayoutParams(
+                                    ViewGroup.LayoutParams.MATCH_PARENT,
+                                    ViewGroup.LayoutParams.MATCH_PARENT
+                                )
+                            )
+                        }
+                    }
+                )
+            }
+        }
+        rule.runOnIdle {
+            assertThat(childViewLayoutDirection).isEqualTo(android.util.LayoutDirection.RTL)
+            assertThat(childCompositionLayoutDirection).isEqualTo(LayoutDirection.Ltr)
+        }
+    }
+
+    @Test
+    fun androidView_propagatesLocalLifecycleOwnerAsViewTreeOwner() {
+        lateinit var parentLifecycleOwner: LifecycleOwner
+        val compositionLifecycleOwner = TestLifecycleOwner()
+        var childViewTreeLifecycleOwner: LifecycleOwner? = null
+
+        rule.setContent {
+            LocalLifecycleOwner.current.also {
+                SideEffect {
+                    parentLifecycleOwner = it
+                }
+            }
+
+            CompositionLocalProvider(LocalLifecycleOwner provides compositionLifecycleOwner) {
+                AndroidView(
+                    factory = {
+                        object : FrameLayout(it) {
+                            override fun onAttachedToWindow() {
+                                super.onAttachedToWindow()
+                                childViewTreeLifecycleOwner = findViewTreeLifecycleOwner()
+                            }
+                        }
+                    }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(childViewTreeLifecycleOwner).isSameInstanceAs(compositionLifecycleOwner)
+            assertThat(childViewTreeLifecycleOwner).isNotSameInstanceAs(parentLifecycleOwner)
+        }
+    }
+
+    @Test
+    fun androidView_propagatesLocalSavedStateRegistryOwnerAsViewTreeOwner() {
+        lateinit var parentSavedStateRegistryOwner: SavedStateRegistryOwner
+        val compositionSavedStateRegistryOwner =
+            object : SavedStateRegistryOwner, LifecycleOwner by TestLifecycleOwner() {
+                // We don't actually need to ever get actual instance.
+                override val savedStateRegistry: SavedStateRegistry
+                    get() = throw UnsupportedOperationException()
+            }
+        var childViewTreeSavedStateRegistryOwner: SavedStateRegistryOwner? = null
+
+        rule.setContent {
+            LocalSavedStateRegistryOwner.current.also {
+                SideEffect {
+                    parentSavedStateRegistryOwner = it
+                }
+            }
+
+            CompositionLocalProvider(
+                LocalSavedStateRegistryOwner provides compositionSavedStateRegistryOwner
+            ) {
+                AndroidView(
+                    factory = {
+                        object : FrameLayout(it) {
+                            override fun onAttachedToWindow() {
+                                super.onAttachedToWindow()
+                                childViewTreeSavedStateRegistryOwner =
+                                    findViewTreeSavedStateRegistryOwner()
+                            }
+                        }
+                    }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(childViewTreeSavedStateRegistryOwner)
+                .isSameInstanceAs(compositionSavedStateRegistryOwner)
+            assertThat(childViewTreeSavedStateRegistryOwner)
+                .isNotSameInstanceAs(parentSavedStateRegistryOwner)
+        }
+    }
+
+    @Test
+    fun androidView_runsFactoryExactlyOnce_afterFirstComposition() {
+        var factoryRunCount = 0
+        rule.setContent {
+            val view = remember { View(rule.activity) }
+            AndroidView({ ++factoryRunCount; view })
+        }
+        rule.runOnIdle {
+            assertThat(factoryRunCount).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun androidView_runsFactoryExactlyOnce_evenWhenFactoryIsChanged() {
+        var factoryRunCount = 0
+        var first by mutableStateOf(true)
+        rule.setContent {
+            val view = remember { View(rule.activity) }
+            AndroidView(
+                if (first) {
+                    { ++factoryRunCount; view }
+                } else {
+                    { ++factoryRunCount; view }
+                }
+            )
+        }
+        rule.runOnIdle {
+            assertThat(factoryRunCount).isEqualTo(1)
+            first = false
+        }
+        rule.runOnIdle {
+            assertThat(factoryRunCount).isEqualTo(1)
+        }
+    }
+
+    @Ignore
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    fun androidView_clipsToBounds() {
+        val size = 20
+        val sizeDp = with(rule.density) { size.toDp() }
+        rule.setContent {
+            Column {
+                Box(
+                    Modifier
+                        .size(sizeDp)
+                        .background(Color.Blue)
+                        .testTag("box"))
+                AndroidView(factory = { SurfaceView(it) })
+            }
+        }
+
+        rule.onNodeWithTag("box").captureToImage().assertPixels(IntSize(size, size)) {
+            Color.Blue
+        }
+    }
+
+    @Test
+    fun androidView_callsOnRelease() {
+        var releaseCount = 0
+        var showContent by mutableStateOf(true)
+        rule.setContent {
+            if (showContent) {
+                AndroidView(
+                    factory = { TextView(it) },
+                    update = { it.text = "onRelease test" },
+                    onRelease = { releaseCount++ }
+                )
+            }
+        }
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(isDisplayed()))
+
+        assertEquals("onRelease() was called unexpectedly", 0, releaseCount)
+
+        showContent = false
+
+        onView(instanceOf(TextView::class.java))
+            .check(doesNotExist())
+
+        assertEquals(
+            "onRelease() should be called exactly once after " +
+                "removing the view from the composition hierarchy",
+            1, releaseCount
+        )
+    }
+
+    @Test
+    fun androidView_restoresState() {
+        var result = ""
+
+        @Composable
+        fun <T : Any> Navigation(
+            currentScreen: T,
+            modifier: Modifier = Modifier,
+            content: @Composable (T) -> Unit
+        ) {
+            val saveableStateHolder = rememberSaveableStateHolder()
+            Box(modifier) {
+                saveableStateHolder.SaveableStateProvider(currentScreen) {
+                    content(currentScreen)
+                }
+            }
+        }
+
+        var screen by mutableStateOf("screen1")
+        rule.setContent {
+            Navigation(screen) { currentScreen ->
+                if (currentScreen == "screen1") {
+                    AndroidView({
+                        StateSavingView(
+                            "testKey",
+                            "testValue",
+                            { restoredValue -> result = restoredValue },
+                            it
+                        )
+                    })
+                } else {
+                    Box(Modifier)
+                }
+            }
+        }
+
+        rule.runOnIdle { screen = "screen2" }
+        rule.runOnIdle { screen = "screen1" }
+        rule.runOnIdle {
+            assertThat(result).isEqualTo("testValue")
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    fun androidView_noClip() {
+        rule.setContent {
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .background(Color.White)) {
+                with(LocalDensity.current) {
+                    Box(
+                        Modifier
+                            .requiredSize(150.toDp())
+                            .testTag("box")) {
+                        Box(
+                            Modifier
+                                .size(100.toDp(), 100.toDp())
+                                .align(AbsoluteAlignment.TopLeft)
+                        ) {
+                            AndroidView(factory = { context ->
+                                object : View(context) {
+                                    init {
+                                        clipToOutline = false
+                                    }
+
+                                    override fun onDraw(canvas: Canvas) {
+                                        val paint = Paint()
+                                        paint.color = Color.Blue.toArgb()
+                                        paint.style = Paint.Style.FILL
+                                        canvas.drawRect(0f, 0f, 150f, 150f, paint)
+                                    }
+                                }
+                            })
+                        }
+                    }
+                }
+            }
+        }
+        rule.onNodeWithTag("box").captureToImage().assertPixels(IntSize(150, 150)) {
+            Color.Blue
+        }
+    }
+
+    @Test
+    fun testInitialComposition_causesViewToBecomeActive() {
+        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
+        rule.setContent {
+            ReusableContent("never-changes") {
+                ReusableAndroidViewWithLifecycleTracking(
+                    factory = { TextView(it).apply { text = "Test" } },
+                    onLifecycleEvent = lifecycleEvents::add
+                )
+            }
+        }
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(isDisplayed()))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "added to the composition hierarchy",
+            listOf(
+                OnCreate,
+                OnUpdate,
+                OnViewAttach,
+                ViewLifecycleEvent(ON_CREATE),
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+    }
+
+    @Test
+    fun testViewRecomposition_onlyInvokesUpdate() {
+        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
+        var state by mutableStateOf(0)
+        rule.setContent {
+            ReusableContent("never-changes") {
+                ReusableAndroidViewWithLifecycleTracking(
+                    factory = { TextView(it) },
+                    update = { it.text = "Text $state" },
+                    onLifecycleEvent = lifecycleEvents::add
+                )
+            }
+        }
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(isDisplayed()))
+            .check(matches(withText("Text 0")))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "added to the composition hierarchy",
+            listOf(
+                OnCreate,
+                OnUpdate,
+                OnViewAttach,
+                ViewLifecycleEvent(ON_CREATE),
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        state++
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(isDisplayed()))
+            .check(matches(withText("Text 1")))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when recomposed",
+            listOf(OnUpdate),
+            lifecycleEvents
+        )
+    }
+
+    @Test
+    fun testViewDeactivation_causesViewResetAndDetach() {
+        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
+        var attached by mutableStateOf(true)
+        rule.setContent {
+            ReusableContentHost(attached) {
+                ReusableAndroidViewWithLifecycleTracking(
+                    factory = { TextView(it).apply { text = "Test" } },
+                    onLifecycleEvent = lifecycleEvents::add
+                )
+            }
+        }
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "added to the composition hierarchy",
+            listOf(
+                OnCreate,
+                OnUpdate,
+                OnViewAttach,
+                ViewLifecycleEvent(ON_CREATE),
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        attached = false
+
+        onView(instanceOf(TextView::class.java))
+            .check(doesNotExist())
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "removed from the composition hierarchy and retained by Compose",
+            listOf(OnReset, OnViewDetach),
+            lifecycleEvents
+        )
+    }
+
+    @Test
+    fun testViewReattachment_causesViewToBecomeReusedAndReactivated() {
+        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
+        var attached by mutableStateOf(true)
+        rule.setContent {
+            ReusableContentHost(attached) {
+                ReusableAndroidViewWithLifecycleTracking(
+                    factory = { TextView(it).apply { text = "Test" } },
+                    onLifecycleEvent = lifecycleEvents::add
+                )
+            }
+        }
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(isDisplayed()))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "added to the composition hierarchy",
+            listOf(
+                OnCreate,
+                OnUpdate,
+                OnViewAttach,
+                ViewLifecycleEvent(ON_CREATE),
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        attached = false
+
+        onView(instanceOf(TextView::class.java))
+            .check(doesNotExist())
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "removed from the composition hierarchy and retained by Compose",
+            listOf(OnReset, OnViewDetach),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        attached = true
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(isDisplayed()))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "reattached to the composition hierarchy",
+            listOf(OnViewAttach, OnUpdate),
+            lifecycleEvents
+        )
+    }
+
+    @Test
+    fun testViewDisposalWhenDetached_causesViewToBeReleased() {
+        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
+        var active by mutableStateOf(true)
+        var emit by mutableStateOf(true)
+        rule.setContent {
+            if (emit) {
+                ReusableContentHost(active) {
+                    ReusableAndroidViewWithLifecycleTracking(
+                        factory = { TextView(it).apply { text = "Test" } },
+                        onLifecycleEvent = lifecycleEvents::add
+                    )
+                }
+            }
+        }
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "added to the composition hierarchy",
+            listOf(
+                OnCreate,
+                OnUpdate,
+                OnViewAttach,
+                ViewLifecycleEvent(ON_CREATE),
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        active = false
+
+        onView(instanceOf(TextView::class.java))
+            .check(doesNotExist())
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "removed from the composition hierarchy and retained by Compose",
+            listOf(OnReset, OnViewDetach),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        emit = false
+
+        onView(instanceOf(TextView::class.java))
+            .check(doesNotExist())
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "removed from the composition hierarchy while deactivated",
+            listOf(OnRelease),
+            lifecycleEvents
+        )
+    }
+
+    @Test
+    fun testViewRemovedFromComposition_causesViewToBeReleased() {
+        var includeViewInComposition by mutableStateOf(true)
+        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
+        rule.setContent {
+            if (includeViewInComposition) {
+                ReusableAndroidViewWithLifecycleTracking(
+                    factory = { TextView(it).apply { text = "Test" } },
+                    onLifecycleEvent = lifecycleEvents::add
+                )
+            }
+        }
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(isDisplayed()))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "added to the composition hierarchy",
+            listOf(
+                OnCreate,
+                OnUpdate,
+                OnViewAttach,
+                ViewLifecycleEvent(ON_CREATE),
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        includeViewInComposition = false
+
+        onView(instanceOf(TextView::class.java))
+            .check(doesNotExist())
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "removed from composition while visible",
+            listOf(OnViewDetach, OnRelease),
+            lifecycleEvents
+        )
+    }
+
+    @Test
+    fun testViewReusedInComposition_invokesReuseCallbackSequence() {
+        var key by mutableStateOf(0)
+        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
+        rule.setContent {
+            ReusableContent(key) {
+                ReusableAndroidViewWithLifecycleTracking(
+                    factory = { TextView(it) },
+                    update = { it.text = "Test" },
+                    onLifecycleEvent = lifecycleEvents::add
+                )
+            }
+        }
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(isDisplayed()))
+            .check(matches(withText("Test")))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "added to the composition hierarchy",
+            listOf(
+                OnCreate,
+                OnUpdate,
+                OnViewAttach,
+                ViewLifecycleEvent(ON_CREATE),
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        key++
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(isDisplayed()))
+            .check(matches(withText("Test")))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "reused in composition",
+            listOf(OnReset, OnUpdate),
+            lifecycleEvents
+        )
+    }
+
+    @Test
+    fun testViewInComposition_experiencesHostLifecycle_andDoesNotRecreateView() {
+        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
+        rule.setContent {
+            ReusableContentHost(active = true) {
+                ReusableAndroidViewWithLifecycleTracking(
+                    factory = { TextView(it).apply { text = "Test" } },
+                    onLifecycleEvent = lifecycleEvents::add
+                )
+            }
+        }
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "added to the composition hierarchy",
+            listOf(
+                OnCreate,
+                OnUpdate,
+                OnViewAttach,
+                ViewLifecycleEvent(ON_CREATE),
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+
+        rule.activityRule.scenario.moveToState(Lifecycle.State.CREATED)
+        rule.runOnIdle { /* Ensure lifecycle callbacks propagate */ }
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "its host transitioned from RESUMED to CREATED while the view was attached",
+            listOf(
+                ViewLifecycleEvent(ON_PAUSE),
+                ViewLifecycleEvent(ON_STOP)
+            ),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        rule.activityRule.scenario.moveToState(Lifecycle.State.RESUMED)
+        rule.runOnIdle { /* Ensure lifecycle callbacks propagate */ }
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "its host transitioned from CREATED to RESUMED while the view was attached",
+            listOf(
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+    }
+
+    @Test
+    fun testReactivationWithChangingKey_onlyResetsOnce() {
+        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
+        var attach by mutableStateOf(true)
+        var key by mutableStateOf(1)
+        rule.setContent {
+            ReusableContentHost(active = attach) {
+                ReusableContent(key = key) {
+                    ReusableAndroidViewWithLifecycleTracking(
+                        factory = { TextView(it).apply { text = "Test" } },
+                        onLifecycleEvent = lifecycleEvents::add
+                    )
+                }
+            }
+        }
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "added to the composition hierarchy",
+            listOf(
+                OnCreate,
+                OnUpdate,
+                OnViewAttach,
+                ViewLifecycleEvent(ON_CREATE),
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        attach = false
+
+        onView(instanceOf(TextView::class.java))
+            .check(doesNotExist())
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "detached from the composition hierarchy",
+            listOf(OnReset, OnViewDetach),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        rule.runOnUiThread {
+            // Make sure both changes are applied in the same composition.
+            attach = true
+            key++
+        }
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "simultaneously reactivating and changing reuse keys",
+            listOf(OnViewAttach, OnUpdate),
+            lifecycleEvents
+        )
+    }
+
+    @Test
+    fun testViewDetachedFromComposition_stillExperiencesHostLifecycle() {
+        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
+        var attached by mutableStateOf(true)
+        rule.setContent {
+            ReusableContentHost(attached) {
+                ReusableAndroidViewWithLifecycleTracking(
+                    factory = { TextView(it).apply { text = "Test" } },
+                    onLifecycleEvent = lifecycleEvents::add
+                )
+            }
+        }
+
+        onView(instanceOf(TextView::class.java))
+            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "added to the composition hierarchy",
+            listOf(
+                OnCreate,
+                OnUpdate,
+                OnViewAttach,
+                ViewLifecycleEvent(ON_CREATE),
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        attached = false
+
+        onView(instanceOf(TextView::class.java))
+            .check(doesNotExist())
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "removed from the composition hierarchy and retained by Compose",
+            listOf(OnReset, OnViewDetach),
+            lifecycleEvents
+        )
+        lifecycleEvents.clear()
+
+        rule.activityRule.scenario.moveToState(Lifecycle.State.CREATED)
+        rule.runOnIdle { /* Ensure lifecycle callbacks propagate */ }
+
+        assertEquals(
+            "AndroidView did not receive callbacks when its host transitioned from " +
+                "RESUMED to CREATED while the view was detached",
+            listOf(
+                ViewLifecycleEvent(ON_PAUSE),
+                ViewLifecycleEvent(ON_STOP)
+            ),
+            lifecycleEvents
+        )
+
+        lifecycleEvents.clear()
+        rule.activityRule.scenario.moveToState(Lifecycle.State.RESUMED)
+        rule.runOnIdle { /* Wait for UI to settle */ }
+
+        assertEquals(
+            "AndroidView did not receive callbacks when its host transitioned from " +
+                "CREATED to RESUMED while the view was detached",
+            listOf(
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+    }
+
+    @Test
+    fun testViewIsReused_whenMoved() {
+        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
+        var slotWithContent by mutableStateOf(0)
+
+        rule.setContent {
+            val movableContext = remember {
+                movableContentOf {
+                    ReusableAndroidViewWithLifecycleTracking(
+                        factory = {
+                            EditText(it).apply { id = R.id.testContentViewId }
+                        },
+                        onLifecycleEvent = lifecycleEvents::add
+                    )
+                }
+            }
+
+            Column {
+                repeat(10) { slot ->
+                    if (slot == slotWithContent) {
+                        ReusableContent(Unit) {
+                            movableContext()
+                        }
+                    } else {
+                        Text("Slot $slot")
+                    }
+                }
+            }
+        }
+
+        onView(instanceOf(EditText::class.java))
+            .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+            .perform(typeText("Input"))
+
+        assertEquals(
+            "AndroidView did not experience the expected lifecycle when " +
+                "added to the composition hierarchy",
+            listOf(
+                OnCreate,
+                OnUpdate,
+                OnViewAttach,
+                ViewLifecycleEvent(ON_CREATE),
+                ViewLifecycleEvent(ON_START),
+                ViewLifecycleEvent(ON_RESUME)
+            ),
+            lifecycleEvents
+        )
+        lifecycleEvents.clear()
+        slotWithContent++
+
+        rule.runOnIdle { /* Wait for UI to settle */ }
+
+        assertEquals(
+            "AndroidView experienced unexpected lifecycle events when " +
+                "moved in the composition",
+            emptyList<AndroidViewLifecycleEvent>(),
+            lifecycleEvents
+        )
+
+        // Check that the state of the view is retained
+        onView(instanceOf(EditText::class.java))
+            .check(matches(isDisplayed()))
+            .check(matches(withText("Input")))
+    }
+
+    @Test
+    fun testViewRestoresState_whenRemovedAndRecreatedWithNoReuse() {
+        val lifecycleEvents = mutableListOf<AndroidViewLifecycleEvent>()
+        var screen by mutableStateOf("screen1")
+        rule.setContent {
+            with(rememberSaveableStateHolder()) {
+                if (screen == "screen1") {
+                    SaveableStateProvider("screen1") {
+                        ReusableAndroidViewWithLifecycleTracking(
+                            factory = {
+                                EditText(it).apply { id = R.id.testContentViewId }
+                            },
+                            onLifecycleEvent = lifecycleEvents::add
+                        )
+                    }
+                }
+            }
+        }
+
+        onView(instanceOf(EditText::class.java))
+            .check(matches(isDisplayed()))
+            .perform(typeText("User Input"))
+
+        rule.runOnIdle { screen = "screen2" }
+
+        onView(instanceOf(EditText::class.java))
+            .check(doesNotExist())
+
+        rule.runOnIdle { screen = "screen1" }
+
+        onView(instanceOf(EditText::class.java))
+            .check(matches(isDisplayed()))
+            .check(matches(withText("User Input")))
+    }
+
+    @Test
+    fun androidView_withParentDataModifier() {
+        val columnHeight = 100
+        val columnHeightDp = with(rule.density) { columnHeight.toDp() }
+        var viewSize = IntSize.Zero
+        rule.setContent {
+            Column(
+                Modifier
+                    .height(columnHeightDp)
+                    .fillMaxWidth()) {
+                AndroidView(
+                    factory = { View(it) },
+                    modifier = Modifier
+                        .weight(1f)
+                        .onGloballyPositioned { viewSize = it.size }
+                )
+
+                Box(Modifier.height(columnHeightDp / 4))
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(columnHeight * 3 / 4, viewSize.height)
+        }
+    }
+
+    @Test
+    fun androidView_visibilityGone() {
+        var view: View? = null
+        var drawCount = 0
+        val viewSizeDp = 50.dp
+        val viewSize = with(rule.density) { viewSizeDp.roundToPx() }
+        rule.setContent {
+            AndroidView(
+                modifier = Modifier
+                    .testTag("wrapper")
+                    .heightIn(max = viewSizeDp),
+                factory = {
+                    object : View(it) {
+                        override fun dispatchDraw(canvas: Canvas) {
+                            drawCount++
+                            super.dispatchDraw(canvas)
+                        }
+                    }
+                },
+                update = {
+                    view = it
+                    it.layoutParams = ViewGroup.LayoutParams(viewSize, WRAP_CONTENT)
+                },
+            )
+        }
+
+        rule.onNodeWithTag("wrapper")
+            .assertHeightIsEqualTo(viewSizeDp)
+
+        rule.runOnUiThread {
+            drawCount = 0
+            view?.visibility = View.GONE
+        }
+
+        rule.onNodeWithTag("wrapper")
+            .assertHeightIsEqualTo(0.dp)
+        assertEquals(0, drawCount)
+    }
+
+    @Test
+    fun androidView_visibilityGone_column() {
+        var view: View? = null
+        val viewSizeDp = 50.dp
+        val viewSize = with(rule.density) { viewSizeDp.roundToPx() }
+        rule.setContent {
+            Column {
+                AndroidView(
+                    modifier = Modifier
+                        .testTag("wrapper")
+                        .heightIn(max = viewSizeDp),
+                    factory = {
+                        View(it)
+                    },
+                    update = {
+                        view = it
+                        it.layoutParams = ViewGroup.LayoutParams(viewSize, WRAP_CONTENT)
+                    },
+                )
+
+                Box(
+                    Modifier
+                        .size(viewSizeDp)
+                        .testTag("box")
+                )
+            }
+        }
+
+        rule.onNodeWithTag("box")
+            .assertTopPositionInRootIsEqualTo(viewSizeDp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+
+        rule.runOnUiThread {
+            view?.visibility = View.GONE
+        }
+
+        rule.onNodeWithTag("box")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+    }
+
+    @ExperimentalComposeUiApi
+    @Composable
+    private inline fun <T : View> ReusableAndroidViewWithLifecycleTracking(
+        crossinline factory: (Context) -> T,
+        noinline onLifecycleEvent: @DisallowComposableCalls (AndroidViewLifecycleEvent) -> Unit,
+        modifier: Modifier = Modifier,
+        crossinline update: (T) -> Unit = { },
+        crossinline reuse: (T) -> Unit = { },
+        crossinline release: (T) -> Unit = { }
+    ) {
+        AndroidView(
+            factory = {
+                onLifecycleEvent(OnCreate)
+                factory(it).apply {
+                    addOnAttachStateChangeListener(
+                        object : OnAttachStateChangeListener, LifecycleEventObserver {
+                            override fun onViewAttachedToWindow(v: View) {
+                                onLifecycleEvent(OnViewAttach)
+                                findViewTreeLifecycleOwner()!!.lifecycle.addObserver(this)
+                            }
+
+                            override fun onViewDetachedFromWindow(v: View) {
+                                onLifecycleEvent(OnViewDetach)
+                            }
+
+                            override fun onStateChanged(
+                                source: LifecycleOwner,
+                                event: Lifecycle.Event
+                            ) {
+                                onLifecycleEvent(ViewLifecycleEvent(event))
+                            }
+                        }
+                    )
+                }
+            },
+            modifier = modifier,
+            update = {
+                onLifecycleEvent(OnUpdate)
+                update(it)
+            },
+            onReset = {
+                onLifecycleEvent(OnReset)
+                reuse(it)
+            },
+            onRelease = {
+                onLifecycleEvent(OnRelease)
+                release(it)
+            }
+        )
+    }
+
+    private sealed class AndroidViewLifecycleEvent {
+        override fun toString(): String {
+            return javaClass.simpleName
+        }
+
+        // Sent when the factory lambda is invoked
+        object OnCreate : AndroidViewLifecycleEvent()
+
+        object OnUpdate : AndroidViewLifecycleEvent()
+        object OnReset : AndroidViewLifecycleEvent()
+        object OnRelease : AndroidViewLifecycleEvent()
+
+        object OnViewAttach : AndroidViewLifecycleEvent()
+        object OnViewDetach : AndroidViewLifecycleEvent()
+
+        data class ViewLifecycleEvent(
+            val event: Lifecycle.Event
+        ) : AndroidViewLifecycleEvent() {
+            override fun toString() = "ViewLifecycleEvent($event)"
+        }
+    }
+
+    private class StateSavingView(
+        private val key: String,
+        private val value: String,
+        private val onRestoredValue: (String) -> Unit,
+        context: Context
+    ) : View(context) {
+        init {
+            id = 73
+        }
+
+        override fun onSaveInstanceState(): Parcelable {
+            val superState = super.onSaveInstanceState()
+            val bundle = Bundle()
+            bundle.putParcelable("superState", superState)
+            bundle.putString(key, value)
+            return bundle
+        }
+
+        @Suppress("DEPRECATION")
+        override fun onRestoreInstanceState(state: Parcelable?) {
+            super.onRestoreInstanceState((state as Bundle).getParcelable("superState"))
+            onRestoredValue(state.getString(key)!!)
+        }
+    }
+
+    private class UpdateTestView(context: Context) : View(context) {
+        var counter = 0
+    }
+
+    private fun Dp.toPx(displayMetrics: DisplayMetrics) =
+        TypedValue.applyDimension(
+            TypedValue.COMPLEX_UNIT_DIP,
+            value,
+            displayMetrics
+        ).roundToInt()
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/InvalidatedTextView.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/InvalidatedTextView.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/InvalidatedTextView.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/InvalidatedTextView.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/ActivityWithInsets.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/ActivityWithInsets.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/ActivityWithInsets.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/ActivityWithInsets.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogScreenshotTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogScreenshotTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogScreenshotTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogScreenshotTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupAlignmentTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupAlignmentTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupAlignmentTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupAlignmentTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupDismissTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupDismissTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupDismissTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupDismissTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupLayoutTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupLayoutTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupLayoutTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupLayoutTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupSecureFlagTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupSecureFlagTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupSecureFlagTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupSecureFlagTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PositionInWindowTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PositionInWindowTest.kt
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PositionInWindowTest.kt
rename to compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PositionInWindowTest.kt
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/failed_image.png b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/failed_image.png
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/failed_image.png
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/failed_image.png
Binary files differ
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle2.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle2.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle2.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle2.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_color_resource_tint.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_color_resource_tint.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_color_resource_tint.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_color_resource_tint.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_color_theme_tint.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_color_theme_tint.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_color_theme_tint.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_color_theme_tint.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_config.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_config.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_config.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_config.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_csl_tint.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_csl_tint.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_csl_tint.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_csl_tint.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_csl_tint_theme.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_csl_tint_theme.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_csl_tint_theme.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_csl_tint_theme.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_modulate.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_modulate.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_modulate.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_modulate.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_plus.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_plus.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_plus.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_plus.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_screen.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_screen.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_screen.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_screen.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_src_atop.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_src_atop.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_src_atop.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_src_atop.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_src_in.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_src_in.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_src_in.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_src_in.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_src_over.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_src_over.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/ic_triangle_src_over.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/ic_triangle_src_over.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/loaded_image.png b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/loaded_image.png
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/loaded_image.png
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/loaded_image.png
Binary files differ
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/pending_image.png b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/pending_image.png
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/pending_image.png
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/pending_image.png
Binary files differ
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/test_compose_vector.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/test_compose_vector.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/test_compose_vector.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/test_compose_vector.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/test_compose_vector2.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/test_compose_vector2.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/test_compose_vector2.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/test_compose_vector2.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/drawable/test_compose_vector3.xml b/compose/ui/ui/src/androidInstrumentedTest/res/drawable/test_compose_vector3.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/drawable/test_compose_vector3.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/drawable/test_compose_vector3.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/layout/android_compose_lists_fling.xml b/compose/ui/ui/src/androidInstrumentedTest/res/layout/android_compose_lists_fling.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/layout/android_compose_lists_fling.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/layout/android_compose_lists_fling.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/layout/android_compose_lists_fling_item.xml b/compose/ui/ui/src/androidInstrumentedTest/res/layout/android_compose_lists_fling_item.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/layout/android_compose_lists_fling_item.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/layout/android_compose_lists_fling_item.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/layout/android_in_compose_nested_scroll_interop.xml b/compose/ui/ui/src/androidInstrumentedTest/res/layout/android_in_compose_nested_scroll_interop.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/layout/android_in_compose_nested_scroll_interop.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/layout/android_in_compose_nested_scroll_interop.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/layout/android_in_compose_nested_scroll_interop_list_item.xml b/compose/ui/ui/src/androidInstrumentedTest/res/layout/android_in_compose_nested_scroll_interop_list_item.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/layout/android_in_compose_nested_scroll_interop_list_item.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/layout/android_in_compose_nested_scroll_interop_list_item.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/layout/composeview_transition_group_false.xml b/compose/ui/ui/src/androidInstrumentedTest/res/layout/composeview_transition_group_false.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/layout/composeview_transition_group_false.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/layout/composeview_transition_group_false.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/layout/pooling_container_compose_test.xml b/compose/ui/ui/src/androidInstrumentedTest/res/layout/pooling_container_compose_test.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/layout/pooling_container_compose_test.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/layout/pooling_container_compose_test.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/layout/test_layout.xml b/compose/ui/ui/src/androidInstrumentedTest/res/layout/test_layout.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/layout/test_layout.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/layout/test_layout.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/layout/test_multiple_invalidation_layout.xml b/compose/ui/ui/src/androidInstrumentedTest/res/layout/test_multiple_invalidation_layout.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/layout/test_multiple_invalidation_layout.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/layout/test_multiple_invalidation_layout.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/layout/test_nested_scroll_coordinator_layout.xml b/compose/ui/ui/src/androidInstrumentedTest/res/layout/test_nested_scroll_coordinator_layout.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/layout/test_nested_scroll_coordinator_layout.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/layout/test_nested_scroll_coordinator_layout.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/layout/test_nested_scroll_coordinator_layout_without_toolbar.xml b/compose/ui/ui/src/androidInstrumentedTest/res/layout/test_nested_scroll_coordinator_layout_without_toolbar.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/layout/test_nested_scroll_coordinator_layout_without_toolbar.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/layout/test_nested_scroll_coordinator_layout_without_toolbar.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/layout/velocity_tracker_compose_vs_view.xml b/compose/ui/ui/src/androidInstrumentedTest/res/layout/velocity_tracker_compose_vs_view.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/layout/velocity_tracker_compose_vs_view.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/layout/velocity_tracker_compose_vs_view.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/values-es/strings.xml b/compose/ui/ui/src/androidInstrumentedTest/res/values-es/strings.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/values-es/strings.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/values-es/strings.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/values-land/resources.xml b/compose/ui/ui/src/androidInstrumentedTest/res/values-land/resources.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/values-land/resources.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/values-land/resources.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/values/donottranslate-strings.xml b/compose/ui/ui/src/androidInstrumentedTest/res/values/donottranslate-strings.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/values/donottranslate-strings.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/values/donottranslate-strings.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/values/ids.xml b/compose/ui/ui/src/androidInstrumentedTest/res/values/ids.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/values/ids.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/values/ids.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/values/resources.xml b/compose/ui/ui/src/androidInstrumentedTest/res/values/resources.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/values/resources.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/values/resources.xml
diff --git a/compose/ui/ui/src/androidAndroidTest/res/values/themes.xml b/compose/ui/ui/src/androidInstrumentedTest/res/values/themes.xml
similarity index 100%
rename from compose/ui/ui/src/androidAndroidTest/res/values/themes.xml
rename to compose/ui/ui/src/androidInstrumentedTest/res/values/themes.xml
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
index e0846ac..cf0b7e1 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
@@ -262,6 +262,7 @@
         val pressure = motionEvent.getPressure(index)
 
         var position = Offset(motionEvent.getX(index), motionEvent.getY(index))
+        val originalPositionEventPosition = position.copy()
         val rawPosition: Offset
         if (index == 0) {
             rawPosition = Offset(motionEvent.rawX, motionEvent.rawY)
@@ -287,9 +288,11 @@
                 val x = getHistoricalX(index, pos)
                 val y = getHistoricalY(index, pos)
                 if (x.isFinite() && y.isFinite()) {
+                    val originalEventPosition = Offset(x, y) // hit path will convert to local
                     val historicalChange = HistoricalChange(
                         getHistoricalEventTime(pos),
-                        Offset(x, y)
+                        originalEventPosition,
+                        originalEventPosition
                     )
                     historical.add(historicalChange)
                 }
@@ -330,7 +333,8 @@
             toolType,
             issuesEnterExit,
             historical,
-            scrollDelta
+            scrollDelta,
+            originalPositionEventPosition,
         )
     }
 }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index ef354a4..5859ede 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -60,6 +60,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.referentialEqualityPolicy
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.SessionMutex
@@ -101,6 +102,8 @@
 import androidx.compose.ui.input.key.Key.Companion.Enter
 import androidx.compose.ui.input.key.Key.Companion.Escape
 import androidx.compose.ui.input.key.Key.Companion.NumPadEnter
+import androidx.compose.ui.input.key.Key.Companion.PageDown
+import androidx.compose.ui.input.key.Key.Companion.PageUp
 import androidx.compose.ui.input.key.Key.Companion.Tab
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
@@ -815,6 +818,10 @@
                     info: AccessibilityNodeInfoCompat
                 ) {
                     super.onInitializeAccessibilityNodeInfo(host, info)
+
+                    // Prevent TalkBack from trying to focus the AndroidViewHolder
+                    info.setVisibleToUser(false)
+
                     var parentId = layoutNode
                         .findClosestParentNode { it.nodes.has(Nodes.Semantics) }
                         ?.semanticsId
@@ -1171,8 +1178,12 @@
             Tab -> if (keyEvent.isShiftPressed) Previous else Next
             DirectionRight -> Right
             DirectionLeft -> Left
-            DirectionUp -> Up
-            DirectionDown -> Down
+            // For the initial key input of a new composable, both up/down and page up/down will
+            // trigger the composable to get focus (so the composable can handle key events to
+            // move focus or scroll content). Remember, composables can't receive key events without
+            // focus.
+            DirectionUp, PageUp -> Up
+            DirectionDown, PageDown -> Down
             DirectionCenter, Enter, NumPadEnter -> FocusDirection.Enter
             Back, Escape -> Exit
             else -> null
@@ -1184,6 +1195,7 @@
             invalidateLayers(root)
         }
         measureAndLayout()
+        Snapshot.sendApplyNotifications()
 
         isDrawingContent = true
         // we don't have to observe here because the root has a layer modifier
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 047c75e..ee90886 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -50,6 +50,9 @@
 import androidx.collection.ArrayMap
 import androidx.collection.ArraySet
 import androidx.collection.SparseArrayCompat
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.R
 import androidx.compose.ui.geometry.Offset
@@ -262,13 +265,14 @@
 
     /**
      * True if any content capture service enabled in the system.
-     *
-     * TODO(b/272068594): follow up on improving the performance and actually enabling the content
-     * capture feature in production later.
      */
+    @OptIn(ExperimentalComposeUiApi::class)
     private val isEnabledForContentCapture: Boolean
         get() {
-            return contentCaptureForceEnabledForTesting
+            if (DisableContentCapture) {
+                return false
+            }
+            return contentCaptureSession != null || contentCaptureForceEnabledForTesting
         }
 
     /**
@@ -3642,3 +3646,16 @@
  */
 internal fun AndroidViewsHandler.semanticsIdToView(id: Int): View? =
     layoutNodeToHolder.entries.firstOrNull { it.key.semanticsId == id }?.value
+
+/**
+ * A flag to force disable the content capture feature.
+ *
+ * If you find any issues with the new feature, flip this flag to true to confirm they are newly
+ * introduced then file a bug.
+ */
+@Suppress("GetterSetterNames", "OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:Suppress("GetterSetterNames")
+@get:ExperimentalComposeUiApi
+@set:ExperimentalComposeUiApi
+@ExperimentalComposeUiApi
+var DisableContentCapture: Boolean by mutableStateOf(false)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.android.kt
index 32dcbe67..3f330b4 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.android.kt
@@ -26,12 +26,15 @@
 import androidx.compose.ui.graphics.nativeCanvas
 import androidx.compose.ui.graphics.painter.BitmapPainter
 import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.GroupComponent
 import androidx.compose.ui.graphics.vector.VectorPainter
 import androidx.compose.ui.graphics.vector.compat.seekToStartTag
-import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.graphics.vector.createGroupComponent
+import androidx.compose.ui.graphics.vector.createVectorPainterFromImageVector
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalImageVectorCache
+import androidx.compose.ui.res.ImageVectorCache.ImageVectorEntry
 
 /**
  * Create a [Painter] from an Android resource id. This can load either an instance of
@@ -62,8 +65,7 @@
     val path = value.string
     // Assume .xml suffix implies loading a VectorDrawable resource
     return if (path?.endsWith(".xml") == true) {
-        val imageVector = loadVectorResource(context.theme, res, id, value.changingConfigurations)
-        rememberVectorPainter(imageVector)
+        obtainVectorPainter(context.theme, res, id, value.changingConfigurations)
     } else {
         // Otherwise load the bitmap resource
         val imageBitmap = remember(path, id, context.theme) {
@@ -74,29 +76,42 @@
 }
 
 /**
- * Helper method to validate that the xml resource is a vector drawable then load
- * the ImageVector. Because this throws exceptions we cannot have this implementation as part of
- * the composable implementation it is invoked in.
+ * Helper method to load the previously cached VectorPainter instance if it exists, otherwise
+ * this parses the xml into an ImageVector and creates a new VectorPainter inserting it into the
+ * cache for reuse
  */
 @Composable
-private fun loadVectorResource(
+private fun obtainVectorPainter(
     theme: Resources.Theme,
     res: Resources,
     id: Int,
     changingConfigurations: Int
-): ImageVector {
+): VectorPainter {
     val imageVectorCache = LocalImageVectorCache.current
-    val key = ImageVectorCache.Key(theme, id)
-    var imageVectorEntry = imageVectorCache[key]
-    if (imageVectorEntry == null) {
+    val density = LocalDensity.current
+    val key = remember(theme, id, density) {
+        ImageVectorCache.Key(theme, id, density)
+    }
+    val imageVectorEntry = imageVectorCache[key]
+    var imageVector = imageVectorEntry?.imageVector
+    if (imageVector == null) {
         @Suppress("ResourceType") val parser = res.getXml(id)
         if (parser.seekToStartTag().name != "vector") {
             throw IllegalArgumentException(errorMessage)
         }
-        imageVectorEntry = loadVectorResourceInner(theme, res, parser, changingConfigurations)
-        imageVectorCache[key] = imageVectorEntry
+        imageVector = loadVectorResourceInner(theme, res, parser)
     }
-    return imageVectorEntry.imageVector
+
+    var rootGroup = imageVectorEntry?.rootGroup
+    if (rootGroup == null) {
+        rootGroup = GroupComponent().apply {
+            createGroupComponent(imageVector.root)
+        }
+        imageVectorCache[key] = ImageVectorEntry(imageVector, changingConfigurations, rootGroup)
+    }
+    return remember(key) {
+        createVectorPainterFromImageVector(density, imageVector, rootGroup)
+    }
 }
 
 /**
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.android.kt
index eeb02ee..9c5cfdd 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.android.kt
@@ -24,6 +24,7 @@
 import androidx.annotation.DrawableRes
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.vector.GroupComponent
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.graphics.vector.compat.AndroidVectorParser
 import androidx.compose.ui.graphics.vector.compat.createVectorImageBuilder
@@ -31,6 +32,9 @@
 import androidx.compose.ui.graphics.vector.compat.parseCurrentVectorNode
 import androidx.compose.ui.graphics.vector.compat.seekToStartTag
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalImageVectorCache
+import androidx.compose.ui.unit.Density
 import java.lang.ref.WeakReference
 import org.xmlpull.v1.XmlPullParserException
 
@@ -46,13 +50,26 @@
  */
 @Composable
 fun ImageVector.Companion.vectorResource(@DrawableRes id: Int): ImageVector {
+    val imageCache = LocalImageVectorCache.current
     val context = LocalContext.current
+    val density = LocalDensity.current
     val res = resources()
     val theme = context.theme
-
-    return remember(id, res, theme, res.configuration) {
-        vectorResource(theme, res, id)
+    val key = remember(theme, id, density) {
+        ImageVectorCache.Key(theme, id, density)
     }
+    var imageVector = imageCache[key]?.imageVector
+    if (imageVector == null) {
+        val value = remember { TypedValue() }
+        res.getValue(id, value, true)
+        imageVector = vectorResource(theme, res, id)
+        imageCache[key] = ImageVectorCache.ImageVectorEntry(
+            imageVector,
+            value.changingConfigurations,
+            null
+        )
+    }
+    return imageVector
 }
 
 @Throws(XmlPullParserException::class)
@@ -61,15 +78,11 @@
     res: Resources,
     resId: Int
 ): ImageVector {
-    val value = TypedValue()
-    res.getValue(resId, value, true)
-
     return loadVectorResourceInner(
         theme,
         res,
         res.getXml(resId).apply { seekToStartTag() },
-        value.changingConfigurations
-    ).imageVector
+    )
 }
 
 /**
@@ -81,9 +94,8 @@
 internal fun loadVectorResourceInner(
     theme: Resources.Theme? = null,
     res: Resources,
-    parser: XmlResourceParser,
-    changingConfigurations: Int
-): ImageVectorCache.ImageVectorEntry {
+    parser: XmlResourceParser
+): ImageVector {
     val attrs = Xml.asAttributeSet(parser)
     val resourceParser = AndroidVectorParser(parser)
     val builder = resourceParser.createVectorImageBuilder(res, theme, attrs)
@@ -99,7 +111,7 @@
         )
         parser.next()
     }
-    return ImageVectorCache.ImageVectorEntry(builder.build(), changingConfigurations)
+    return builder.build()
 }
 
 /**
@@ -113,7 +125,8 @@
      */
     data class Key(
         val theme: Resources.Theme,
-        val id: Int
+        val id: Int,
+        val density: Density
     )
 
     /**
@@ -123,7 +136,8 @@
      */
     data class ImageVectorEntry(
         val imageVector: ImageVector,
-        val configFlags: Int
+        val configFlags: Int,
+        val rootGroup: GroupComponent?,
     )
 
     private val map = HashMap<Key, WeakReference<ImageVectorEntry>>()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputEventCallback2.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputEventCallback2.android.kt
index 11c09cf..2c2ffa5 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputEventCallback2.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputEventCallback2.android.kt
@@ -58,7 +58,7 @@
     /**
      * Called when IME closed the input connection.
      *
-     * @param ic a closed input connection
+     * @param inputConnection a closed input connection
      */
-    fun onConnectionClosed(ic: RecordingInputConnection)
+    fun onConnectionClosed(inputConnection: RecordingInputConnection)
 }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index 62f0a4a..b3f0880 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -170,9 +170,9 @@
                     )
                 }
 
-                override fun onConnectionClosed(ic: RecordingInputConnection) {
+                override fun onConnectionClosed(inputConnection: RecordingInputConnection) {
                     for (i in 0 until ics.size) {
-                        if (ics[i].get() == ic) {
+                        if (ics[i].get() == inputConnection) {
                             ics.removeAt(i)
                             return // No duplicated instances should be in the list.
                         }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
index bef25e7e..12dd53e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
@@ -202,6 +202,10 @@
     override val isValidOwnerScope: Boolean
         get() = isAttachedToWindow
 
+    override fun getAccessibilityClassName(): CharSequence {
+        return javaClass.name
+    }
+
     override fun onReuse() {
         // We reset at the same time we remove the view. So if the view was removed, we can just
         // re-add it and it's ready to go. If it's already attached, we didn't reset it and need
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/ComposedModifierTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/ComposedModifierTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ModifierTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/ModifierTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/ModifierTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/ModifierTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/SessionMutexTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/SessionMutexTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/SessionMutexTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/SessionMutexTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/TransformOriginTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/TransformOriginTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/TransformOriginTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/TransformOriginTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPopulateViewStructureTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidPopulateViewStructureTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPopulateViewStructureTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidPopulateViewStructureTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusChangedModifierTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/focus/FocusChangedModifierTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusChangedModifierTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/focus/FocusChangedModifierTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusEventModifierTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/focus/FocusEventModifierTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusEventModifierTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/focus/FocusEventModifierTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusRequesterModifierTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/focus/FocusRequesterModifierTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusRequesterModifierTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/focus/FocusRequesterModifierTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/TestUtils.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/gesture/TestUtils.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/TestUtils.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/gesture/TestUtils.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/PolyFitLeastSquaresTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/PolyFitLeastSquaresTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/PolyFitLeastSquaresTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/PolyFitLeastSquaresTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/graphics/GraphicsLayerScopeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/graphics/GraphicsLayerScopeTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/graphics/GraphicsLayerScopeTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/graphics/GraphicsLayerScopeTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/graphics/vector/ImageVectorBuilderTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/graphics/vector/ImageVectorBuilderTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/graphics/vector/ImageVectorBuilderTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/graphics/vector/ImageVectorBuilderTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/hapticfeedback/HapticFeedbackTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/hapticfeedback/HapticFeedbackTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/hapticfeedback/HapticFeedbackTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/hapticfeedback/HapticFeedbackTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/RecordingInputConnectionInactiveTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/input/RecordingInputConnectionInactiveTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/RecordingInputConnectionInactiveTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/input/RecordingInputConnectionInactiveTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/RecordingInputConnectionUpdateTextFieldValueTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/input/RecordingInputConnectionUpdateTextFieldValueTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/RecordingInputConnectionUpdateTextFieldValueTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/input/RecordingInputConnectionUpdateTextFieldValueTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/key/KeyInputModifierTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/input/key/KeyInputModifierTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/key/KeyInputModifierTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/input/key/KeyInputModifierTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/PointerInputTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/input/pointer/PointerInputTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/PointerInputTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/input/pointer/PointerInputTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker1DTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker1DTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker1DTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker1DTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/util/VelocityTrackerTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/input/pointer/util/VelocityTrackerTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/util/VelocityTrackerTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/input/pointer/util/VelocityTrackerTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/AlignmentTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/layout/AlignmentTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/AlignmentTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/layout/AlignmentTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ConstraintsTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/layout/ConstraintsTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ConstraintsTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/layout/ConstraintsTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ContentScaleTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/layout/ContentScaleTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ContentScaleTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/layout/ContentScaleTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ScaleFactorTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/layout/ScaleFactorTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ScaleFactorTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/layout/ScaleFactorTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/HitTestResultTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/HitTestResultTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/HitTestResultTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/HitTestResultTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierNodeElementTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierNodeElementTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierNodeElementTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierNodeElementTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/platform/ClipboardManagerTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/platform/ClipboardManagerTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/platform/ClipboardManagerTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/platform/ClipboardManagerTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/platform/WindowInfoTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/platform/WindowInfoTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/platform/WindowInfoTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/platform/WindowInfoTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/text/TextActionModeCallbackTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextActionModeCallbackTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/text/TextActionModeCallbackTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/text/TextActionModeCallbackTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/window/PopupPositionProviderTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/window/PopupPositionProviderTest.kt
similarity index 100%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/window/PopupPositionProviderTest.kt
rename to compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/window/PopupPositionProviderTest.kt
diff --git a/compose/ui/ui/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/compose/ui/ui/src/androidUnitTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
similarity index 100%
rename from compose/ui/ui/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
rename to compose/ui/ui/src/androidUnitTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRestorer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRestorer.kt
index c490f79..bfa2812 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRestorer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRestorer.kt
@@ -19,7 +19,6 @@
 import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRestorerNode.Companion.FocusRestorerElement
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.Nodes
 import androidx.compose.ui.node.currentValueOf
@@ -63,17 +62,25 @@
 
 // TODO: Move focusRestorer to foundation after saveFocusedChild and restoreFocusedChild are stable.
 /**
- * This modifier can be uses to save and restore focus to a focus group.
+ * This modifier can be used to save and restore focus to a focus group.
  * When focus leaves the focus group, it stores a reference to the item that was previously focused.
  * Then when focus re-enters this focus group, it restores focus to the previously focused item.
  *
+ * @param onRestoreFailed callback provides a lambda that is invoked if focus restoration fails.
+ * This lambda can be used to return a custom fallback item by providing a [FocusRequester]
+ * attached to that item. This can be used to customize the initially focused item.
+ *
  * @sample androidx.compose.ui.samples.FocusRestorerSample
+ * @sample androidx.compose.ui.samples.FocusRestorerCustomFallbackSample
  */
 @ExperimentalComposeUiApi
-fun Modifier.focusRestorer(): Modifier = this then FocusRestorerElement
+fun Modifier.focusRestorer(
+    onRestoreFailed: (() -> FocusRequester)? = null
+): Modifier = this then FocusRestorerElement(onRestoreFailed)
 
-internal class FocusRestorerNode :
-    FocusPropertiesModifierNode, FocusRequesterModifierNode, Modifier.Node() {
+internal class FocusRestorerNode(
+    var onRestoreFailed: (() -> FocusRequester)?
+) : FocusPropertiesModifierNode, FocusRequesterModifierNode, Modifier.Node() {
     private val onExit: (FocusDirection) -> FocusRequester = {
         @OptIn(ExperimentalComposeUiApi::class)
         saveFocusedChild()
@@ -83,22 +90,32 @@
     @OptIn(ExperimentalComposeUiApi::class)
     private val onEnter: (FocusDirection) -> FocusRequester = {
         @OptIn(ExperimentalComposeUiApi::class)
-        if (restoreFocusedChild()) FocusRequester.Cancel else FocusRequester.Default
+        if (restoreFocusedChild()) {
+            FocusRequester.Cancel
+        } else {
+            onRestoreFailed?.invoke() ?: FocusRequester.Default
+        }
     }
+
     override fun applyFocusProperties(focusProperties: FocusProperties) {
         @OptIn(ExperimentalComposeUiApi::class)
         focusProperties.enter = onEnter
         @OptIn(ExperimentalComposeUiApi::class)
         focusProperties.exit = onExit
     }
+}
 
-    companion object {
-        val FocusRestorerElement = object : ModifierNodeElement<FocusRestorerNode>() {
-            override fun create() = FocusRestorerNode()
-            override fun update(node: FocusRestorerNode) {}
-            override fun InspectorInfo.inspectableProperties() { name = "focusRestorer" }
-            override fun hashCode(): Int = "focusRestorer".hashCode()
-            override fun equals(other: Any?) = other === this
-        }
+private data class FocusRestorerElement(
+    val onRestoreFailed: (() -> FocusRequester)?
+) : ModifierNodeElement<FocusRestorerNode>() {
+    override fun create() = FocusRestorerNode(onRestoreFailed)
+
+    override fun update(node: FocusRestorerNode) {
+        node.onRestoreFailed = onRestoreFailed
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "focusRestorer"
+        properties["onRestoreFailed"] = onRestoreFailed
     }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
index 972e0ae..fd2d8fb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
@@ -19,6 +19,7 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.geometry.Size.Companion.Unspecified
 import androidx.compose.ui.graphics.BlendMode
@@ -36,6 +37,7 @@
 import androidx.compose.ui.graphics.StrokeJoin
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.drawscope.scale
 import androidx.compose.ui.graphics.drawscope.withTransform
 import androidx.compose.ui.graphics.isSpecified
 import androidx.compose.ui.graphics.isUnspecified
@@ -69,7 +71,7 @@
 
 inline fun PathData(block: PathBuilder.() -> Unit) = with(PathBuilder()) {
     block()
-    getNodes()
+    nodes
 }
 
 fun addPathNodes(pathStr: String?) = if (pathStr == null) {
@@ -92,20 +94,15 @@
     abstract fun DrawScope.draw()
 }
 
-internal class VectorComponent : VNode() {
-    val root = GroupComponent().apply {
-        pivotX = 0.0f
-        pivotY = 0.0f
-        invalidateListener = {
+internal class VectorComponent(val root: GroupComponent) : VNode() {
+
+    init {
+        root.invalidateListener = {
             doInvalidate()
         }
     }
 
-    var name: String
-        get() = root.name
-        set(value) {
-            root.name = value
-        }
+    var name: String = DefaultGroupName
 
     private fun doInvalidate() {
         isDirty = true
@@ -131,11 +128,18 @@
 
     private var previousDrawSize = Unspecified
 
+    private var rootScaleX = 1f
+    private var rootScaleY = 1f
+
     /**
      * Cached lambda used to avoid allocating the lambda on each draw invocation
      */
     private val drawVectorBlock: DrawScope.() -> Unit = {
-        with(root) { draw() }
+        with(root) {
+            scale(rootScaleX, rootScaleY, pivot = Offset.Zero) {
+                draw()
+            }
+        }
     }
 
     fun DrawScope.draw(alpha: Float, colorFilter: ColorFilter?) {
@@ -155,8 +159,8 @@
             } else {
                 null
             }
-            root.scaleX = size.width / viewportSize.width
-            root.scaleY = size.height / viewportSize.height
+            rootScaleX = size.width / viewportSize.width
+            rootScaleY = size.height / viewportSize.height
             cacheDrawScope.drawCachedImage(
                 targetImageConfig,
                 IntSize(ceil(size.width).toInt(), ceil(size.height).toInt()),
@@ -266,29 +270,23 @@
 
     var trimPathStart = DefaultTrimPathStart
         set(value) {
-            if (field != value) {
-                field = value
-                isTrimPathDirty = true
-                invalidate()
-            }
+            field = value
+            isTrimPathDirty = true
+            invalidate()
         }
 
     var trimPathEnd = DefaultTrimPathEnd
         set(value) {
-            if (field != value) {
-                field = value
-                isTrimPathDirty = true
-                invalidate()
-            }
+            field = value
+            isTrimPathDirty = true
+            invalidate()
         }
 
     var trimPathOffset = DefaultTrimPathOffset
         set(value) {
-            if (field != value) {
-                field = value
-                isTrimPathDirty = true
-                invalidate()
-            }
+            field = value
+            isTrimPathDirty = true
+            invalidate()
         }
 
     private var isPathDirty = true
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
index 35225ac..fbc1930 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
@@ -19,8 +19,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.ComposableOpenTarget
 import androidx.compose.runtime.Composition
-import androidx.compose.runtime.CompositionContext
-import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableStateOf
@@ -35,9 +33,11 @@
 import androidx.compose.ui.graphics.ImageBitmapConfig
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.graphics.drawscope.scale
+import androidx.compose.ui.graphics.isSpecified
 import androidx.compose.ui.graphics.painter.Painter
 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.LayoutDirection
 
@@ -127,26 +127,35 @@
     content: @Composable @VectorComposable (viewportWidth: Float, viewportHeight: Float) -> Unit
 ): VectorPainter {
     val density = LocalDensity.current
-    val widthPx = with(density) { defaultWidth.toPx() }
-    val heightPx = with(density) { defaultHeight.toPx() }
-
-    val vpWidth = if (viewportWidth.isNaN()) widthPx else viewportWidth
-    val vpHeight = if (viewportHeight.isNaN()) heightPx else viewportHeight
-
+    val defaultSize = density.obtainSizePx(defaultWidth, defaultHeight)
+    val viewport = obtainViewportSize(defaultSize, viewportWidth, viewportHeight)
     val intrinsicColorFilter = remember(tintColor, tintBlendMode) {
-        if (tintColor != Color.Unspecified) {
-            ColorFilter.tint(tintColor, tintBlendMode)
-        } else {
-            null
-        }
+        createColorFilter(tintColor, tintBlendMode)
     }
-
     return remember { VectorPainter() }.apply {
-        // These assignments are thread safe as parameters are backed by a mutableState object
-        size = Size(widthPx, heightPx)
-        this.autoMirror = autoMirror
-        this.intrinsicColorFilter = intrinsicColorFilter
-        RenderVector(name, vpWidth, vpHeight, content)
+        configureVectorPainter(
+            defaultSize = defaultSize,
+            viewportSize = viewport,
+            name = name,
+            intrinsicColorFilter = intrinsicColorFilter,
+            autoMirror = autoMirror
+        )
+        val compositionContext = rememberCompositionContext()
+        this.composition = remember(viewportWidth, viewportHeight, content) {
+            val curComp = this.composition
+            val next = if (curComp == null || curComp.isDisposed) {
+                Composition(
+                    VectorApplier(this.vector.root),
+                    compositionContext
+                )
+            } else {
+                curComp
+            }
+            next.setContent {
+                content(viewport.width, viewport.height)
+            }
+            next
+        }
     }
 }
 
@@ -175,7 +184,7 @@
  * This can be represented by either a [ImageVector] or a programmatic
  * composition of a vector
  */
-class VectorPainter internal constructor() : Painter() {
+class VectorPainter internal constructor(root: GroupComponent = GroupComponent()) : Painter() {
 
     internal var size by mutableStateOf(Size.Zero)
 
@@ -190,7 +199,19 @@
             vector.intrinsicColorFilter = value
         }
 
-    private val vector = VectorComponent().apply {
+    internal var viewportSize: Size
+        get() = vector.viewportSize
+        set(value) {
+            vector.viewportSize = value
+        }
+
+    internal var name: String
+        get() = vector.name
+        set(value) {
+            vector.name = value
+        }
+
+    internal val vector = VectorComponent(root).apply {
         invalidateCallback = {
             if (drawCount == invalidateCount) {
                 invalidateCount++
@@ -201,56 +222,11 @@
     internal val bitmapConfig: ImageBitmapConfig
         get() = vector.cacheBitmapConfig
 
-    private var composition: Composition? = null
-
-    @Suppress("PrimitiveInLambda")
-    private fun composeVector(
-        parent: CompositionContext,
-        composable: @Composable (viewportWidth: Float, viewportHeight: Float) -> Unit
-    ): Composition {
-        val existing = composition
-        val next = if (existing == null || existing.isDisposed) {
-            Composition(
-                VectorApplier(vector.root),
-                parent
-            )
-        } else {
-            existing
-        }
-        composition = next
-        next.setContent {
-            composable(vector.viewportSize.width, vector.viewportSize.height)
-        }
-        return next
-    }
+    internal var composition: Composition? = null
 
     // TODO replace with mutableStateOf(Unit, neverEqualPolicy()) after b/291647821 is addressed
     private var invalidateCount by mutableIntStateOf(0)
 
-    @Suppress("PrimitiveInLambda")
-    @Composable
-    internal fun RenderVector(
-        name: String,
-        viewportWidth: Float,
-        viewportHeight: Float,
-        content: @Composable (viewportWidth: Float, viewportHeight: Float) -> Unit
-    ) {
-        vector.apply {
-            this.name = name
-            this.viewportSize = Size(viewportWidth, viewportHeight)
-        }
-        val composition = composeVector(
-            rememberCompositionContext(),
-            content
-        )
-
-        DisposableEffect(composition) {
-            onDispose {
-                composition.dispose()
-            }
-        }
-    }
-
     private var currentAlpha: Float = 1.0f
     private var currentColorFilter: ColorFilter? = null
 
@@ -326,6 +302,117 @@
     }
 }
 
+private fun Density.obtainSizePx(defaultWidth: Dp, defaultHeight: Dp) =
+        Size(defaultWidth.toPx(), defaultHeight.toPx())
+
+/**
+ * Helper method to calculate the viewport size. If the viewport width/height are not specified
+ * this falls back on the default size provided
+ */
+private fun obtainViewportSize(
+    defaultSize: Size,
+    viewportWidth: Float,
+    viewportHeight: Float
+) = Size(
+        if (viewportWidth.isNaN()) defaultSize.width else viewportWidth,
+        if (viewportHeight.isNaN()) defaultSize.height else viewportHeight
+    )
+
+/**
+ * Helper method to conditionally create a ColorFilter to tint contents if [tintColor] is
+ * specified, that is [Color.isSpecified] returns true
+ */
+private fun createColorFilter(tintColor: Color, tintBlendMode: BlendMode): ColorFilter? =
+    if (tintColor.isSpecified) {
+        ColorFilter.tint(tintColor, tintBlendMode)
+    } else {
+        null
+    }
+
+/**
+ * Helper method to configure the properties of a VectorPainter that maybe re-used
+ */
+internal fun VectorPainter.configureVectorPainter(
+    defaultSize: Size,
+    viewportSize: Size,
+    name: String = RootGroupName,
+    intrinsicColorFilter: ColorFilter?,
+    autoMirror: Boolean = false,
+): VectorPainter = apply {
+        this.size = defaultSize
+        this.autoMirror = autoMirror
+        this.intrinsicColorFilter = intrinsicColorFilter
+        this.viewportSize = viewportSize
+        this.name = name
+    }
+
+/**
+ * Helper method to create a VectorPainter instance from an ImageVector
+ */
+internal fun createVectorPainterFromImageVector(
+    density: Density,
+    imageVector: ImageVector,
+    root: GroupComponent
+): VectorPainter {
+    val defaultSize = density.obtainSizePx(imageVector.defaultWidth, imageVector.defaultHeight)
+    val viewport = obtainViewportSize(
+        defaultSize,
+        imageVector.viewportWidth,
+        imageVector.viewportHeight
+    )
+    return VectorPainter(root).configureVectorPainter(
+        defaultSize = defaultSize,
+        viewportSize = viewport,
+        name = imageVector.name,
+        intrinsicColorFilter = createColorFilter(imageVector.tintColor, imageVector.tintBlendMode),
+        autoMirror = imageVector.autoMirror
+    )
+}
+
+/**
+ * statically create a a GroupComponent from the VectorGroup representation provided from
+ * an [ImageVector] instance
+ */
+internal fun GroupComponent.createGroupComponent(currentGroup: VectorGroup): GroupComponent {
+    for (index in 0 until currentGroup.size) {
+        val vectorNode = currentGroup[index]
+        if (vectorNode is VectorPath) {
+            val pathComponent = PathComponent().apply {
+                pathData = vectorNode.pathData
+                pathFillType = vectorNode.pathFillType
+                name = vectorNode.name
+                fill = vectorNode.fill
+                fillAlpha = vectorNode.fillAlpha
+                stroke = vectorNode.stroke
+                strokeAlpha = vectorNode.strokeAlpha
+                strokeLineWidth = vectorNode.strokeLineWidth
+                strokeLineCap = vectorNode.strokeLineCap
+                strokeLineJoin = vectorNode.strokeLineJoin
+                strokeLineMiter = vectorNode.strokeLineMiter
+                trimPathStart = vectorNode.trimPathStart
+                trimPathEnd = vectorNode.trimPathEnd
+                trimPathOffset = vectorNode.trimPathOffset
+            }
+            insertAt(index, pathComponent)
+        } else if (vectorNode is VectorGroup) {
+            val groupComponent = GroupComponent().apply {
+                name = vectorNode.name
+                rotation = vectorNode.rotation
+                scaleX = vectorNode.scaleX
+                scaleY = vectorNode.scaleY
+                translationX = vectorNode.translationX
+                translationY = vectorNode.translationY
+                pivotX = vectorNode.pivotX
+                pivotY = vectorNode.pivotY
+                clipPathData = vectorNode.clipPathData
+                createGroupComponent(vectorNode)
+            }
+            insertAt(index, groupComponent)
+        }
+    }
+    return this
+}
+
 /**
  * Recursively creates the vector graphic composition by traversing the tree structure.
  *
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
index 1e6d25b..1b0994a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
@@ -387,7 +387,8 @@
                     historical.add(
                         HistoricalChange(
                             it.uptimeMillis,
-                            coordinates!!.localPositionOf(parentCoordinates, it.position)
+                            coordinates!!.localPositionOf(parentCoordinates, it.position),
+                            it.originalEventPosition
                         )
                     )
                 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
index ed60c9a..a7a1a96 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
@@ -50,7 +50,8 @@
     val type: PointerType,
     val issuesEnterExit: Boolean = false,
     val historical: List<HistoricalChange> = mutableListOf(),
-    val scrollDelta: Offset = Offset.Zero
+    val scrollDelta: Offset = Offset.Zero,
+    val originalEventPosition: Offset = Offset.Zero,
 )
 
 /**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
index 7a6010a..35aedef 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
@@ -499,6 +499,7 @@
         type: PointerType,
         historical: List<HistoricalChange>,
         scrollDelta: Offset,
+        originalEventPosition: Offset,
     ) : this(
         id,
         uptimeMillis,
@@ -513,6 +514,7 @@
         scrollDelta
     ) {
         _historical = historical
+        this.originalEventPosition = originalEventPosition
     }
 
     /**
@@ -527,9 +529,12 @@
     @get:ExperimentalComposeUiApi
     val historical: List<HistoricalChange>
         get() = _historical ?: listOf()
+
     @OptIn(ExperimentalComposeUiApi::class)
     private var _historical: List<HistoricalChange>? = null
 
+    internal var originalEventPosition: Offset = Offset.Zero
+
     /**
      * Indicates whether the change was consumed or not. Note that the change must be consumed in
      * full as there's no partial consumption system provided.
@@ -590,7 +595,8 @@
         consumed.downChange || consumed.positionChange,
         type,
         this.historical,
-        this.scrollDelta
+        this.scrollDelta,
+        this.originalEventPosition,
     ).also {
         this.consumed = consumed
     }
@@ -663,7 +669,8 @@
         consumed.downChange || consumed.positionChange,
         type,
         this.historical,
-        scrollDelta
+        scrollDelta,
+        this.originalEventPosition,
     ).also {
         this.consumed = consumed
     }
@@ -701,7 +708,8 @@
         isInitiallyConsumed = false, // doesn't matter, we will pass a holder anyway
         type,
         historical = this.historical,
-        scrollDelta
+        scrollDelta,
+        this.originalEventPosition,
     ).also {
         it.consumed = this.consumed
     }
@@ -755,6 +763,7 @@
         id: PointerId = this.id,
         currentTime: Long = this.uptimeMillis,
         currentPosition: Offset = this.position,
+        originalEventPosition: Offset = this.originalEventPosition,
         currentPressed: Boolean = this.pressed,
         pressure: Float = this.pressure,
         previousTime: Long = this.previousUptimeMillis,
@@ -763,7 +772,7 @@
         type: PointerType = this.type,
         historical: List<HistoricalChange> = this.historical,
         scrollDelta: Offset = this.scrollDelta
-        ): PointerInputChange = PointerInputChange(
+    ): PointerInputChange = PointerInputChange(
         id,
         currentTime,
         currentPosition,
@@ -775,7 +784,8 @@
         isInitiallyConsumed = false, // doesn't matter, we will pass a holder anyway
         type,
         historical,
-        scrollDelta
+        scrollDelta,
+        originalEventPosition,
     ).also {
         it.consumed = this.consumed
     }
@@ -814,6 +824,17 @@
     val uptimeMillis: Long,
     val position: Offset
 ) {
+    internal var originalEventPosition: Offset = Offset.Zero
+        private set
+
+    internal constructor(
+        uptimeMillis: Long,
+        position: Offset,
+        originalEventPosition: Offset
+    ) : this(uptimeMillis, position) {
+        this.originalEventPosition = originalEventPosition
+    }
+
     override fun toString(): String {
         return "HistoricalChange(uptimeMillis=$uptimeMillis, " +
             "position=$position)"
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
index a1ac20b..f55c989 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
@@ -16,22 +16,21 @@
 
 package androidx.compose.ui.input.pointer
 
-import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
 import androidx.compose.ui.input.pointer.PointerEventPass.Main
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ModifierLocalReadScope
-import androidx.compose.ui.modifier.ProvidableModifierLocal
-import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.PointerInputModifierNode
+import androidx.compose.ui.node.TraversableNode
+import androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction
+import androidx.compose.ui.node.currentValueOf
+import androidx.compose.ui.node.traverseAncestors
+import androidx.compose.ui.node.traverseSubtree
+import androidx.compose.ui.node.traverseSubtreeIf
+import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.LocalPointerIconService
-import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.IntSize
 
 /**
  * Represents a pointer icon to use in [Modifier.pointerHoverIcon]
@@ -71,172 +70,251 @@
 
 /**
  * Modifier that lets a developer define a pointer icon to display when the cursor is hovered over
- * the element. When [overrideDescendants] is set to true, children cannot override the pointer icon
- * using this modifier.
+ * the element. When [overrideDescendants] is set to true, descendants cannot override the
+ * pointer icon using this modifier.
  *
  * @sample androidx.compose.ui.samples.PointerIconSample
  *
  * @param icon The icon to set
  * @param overrideDescendants when false (by default) descendants are able to set their own pointer
- * icon. If true, all children under this parent will receive the requested pointer [icon] and are
- * no longer allowed to override their own pointer icon.
+ * icon. If true, no descendants under this parent are eligible to change the icon (it will be set
+ * to the this [the parent's] icon).
  */
 @Stable
 fun Modifier.pointerHoverIcon(icon: PointerIcon, overrideDescendants: Boolean = false) =
-    composed(inspectorInfo = debugInspectorInfo {
+    this then PointerHoverIconModifierElement(
+        icon = icon,
+        overrideDescendants = overrideDescendants
+    )
+
+internal data class PointerHoverIconModifierElement(
+    val icon: PointerIcon,
+    val overrideDescendants: Boolean = false
+) : ModifierNodeElement<PointerHoverIconModifierNode>() {
+    override fun create() = PointerHoverIconModifierNode(icon, overrideDescendants)
+
+    override fun update(node: PointerHoverIconModifierNode) {
+        node.icon = icon
+        node.overrideDescendants = overrideDescendants
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
         name = "pointerHoverIcon"
         properties["icon"] = icon
         properties["overrideDescendants"] = overrideDescendants
-    }) {
-        val pointerIconService = LocalPointerIconService.current
-        if (pointerIconService == null) {
-            Modifier
-        } else {
-            val onSetIcon = { pointerIcon: PointerIcon? ->
-                pointerIconService.setIcon(pointerIcon)
-            }
-            val pointerIconModifierLocal = remember {
-                PointerIconModifierLocal(icon, overrideDescendants, onSetIcon)
-            }
-            SideEffect {
-                pointerIconModifierLocal.updateValues(
-                    icon = icon,
-                    overrideDescendants = overrideDescendants,
-                    onSetIcon = onSetIcon
-                )
-            }
-            val pointerInputModifier = if (pointerIconModifierLocal.shouldUpdatePointerIcon()) {
-                pointerInput(pointerIconModifierLocal) {
-                    awaitPointerEventScope {
-                        while (true) {
-                            val event = awaitPointerEvent(Main)
-
-                            if (event.type == PointerEventType.Enter) {
-                                pointerIconModifierLocal.enter()
-                            } else if (event.type == PointerEventType.Exit) {
-                                pointerIconModifierLocal.exit()
-                            }
-                        }
-                    }
-                }
-            } else {
-                Modifier
-            }
-
-            pointerIconModifierLocal.then(pointerInputModifier)
-        }
-    }
-
-/**
- * Handles storing all pointer icon information that needs to be passed between Modifiers to
- * determine which icon needs to be set in the hierarchy.
- *
- * @property icon the stored current icon we are keeping track of.
- * @property overrideDescendants value indicating whether the stored icon should always be
- * respected by its children. If true, the stored icon will be considered the source of truth for
- * all children. If false, the stored icon can be overwritten by a child.
- * @property onSetIcon is a lambda that will handle the process of physically setting the user
- * facing pointer icon. This allows the [PointerIconModifierLocal] to be solely responsible for
- * determining what the state of the icon should be, but removes the responsibility of needing to
- * actually set the icon for the user.
- */
-private class PointerIconModifierLocal(
-    private var icon: PointerIcon,
-    private var overrideDescendants: Boolean,
-    private var onSetIcon: (PointerIcon?) -> Unit,
-) : PointerIcon, ModifierLocalProvider<PointerIconModifierLocal?>, ModifierLocalConsumer {
-    // TODO: (b/266976920) Remove making this a mutable state once we fully support a dynamic
-    //  overrideDescendants param.
-    private var parentInfo: PointerIconModifierLocal? by mutableStateOf(null)
-
-    // TODO: (b/267170292) Properly reset isPaused upon PointerIconModifierLocal disposal.
-    var isPaused: Boolean = false
-
-    /* True if the cursor is within the surface area of this element's bounds. Otherwise, false. */
-    var isHovered: Boolean = false
-
-    override val key: ProvidableModifierLocal<PointerIconModifierLocal?> = ModifierLocalPointerIcon
-    override val value: PointerIconModifierLocal = this
-
-    override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) {
-        val oldParentInfo = parentInfo
-        parentInfo = ModifierLocalPointerIcon.current
-        if (oldParentInfo != null && parentInfo == null) {
-            // When the old parentInfo for this element is reassigned to null, we assume this
-            // element is being alienated for disposal. Exit out of our pointer icon logic for this
-            // element and then update onSetIcon to null so it will not change the icon any further.
-            exit(oldParentInfo)
-            onSetIcon = {}
-        }
-    }
-
-    fun shouldUpdatePointerIcon(): Boolean {
-        val parentPointerInfo = parentInfo
-        return parentPointerInfo == null || !parentPointerInfo.hasOverride()
-    }
-
-    private fun hasOverride(): Boolean {
-        return overrideDescendants || parentInfo?.hasOverride() == true
-    }
-
-    fun enter() {
-        isHovered = true
-        if (!isPaused) {
-            parentInfo?.pause()
-            onSetIcon(icon)
-        }
-    }
-
-    fun exit() {
-        exit(parentInfo)
-    }
-
-    private fun exit(parent: PointerIconModifierLocal?) {
-        if (isHovered) {
-            if (parent == null) {
-                // Notify that oldest ancestor in hierarchy exited by passing null to onSetIcon().
-                onSetIcon(null)
-            } else {
-                parent.reassignIcon()
-            }
-        }
-        isHovered = false
-    }
-
-    private fun reassignIcon() {
-        isPaused = false
-        if (isHovered) {
-            onSetIcon(icon)
-        } else if (parentInfo == null) {
-            // Reassign the icon back to the default arrow by passing in a null PointerIcon
-            onSetIcon(null)
-        } else {
-            parentInfo?.reassignIcon()
-        }
-    }
-
-    private fun pause() {
-        isPaused = true
-        parentInfo?.pause()
-    }
-
-    fun updateValues(
-        icon: PointerIcon,
-        overrideDescendants: Boolean,
-        onSetIcon: (PointerIcon?) -> Unit
-    ) {
-        if (this.icon != icon && isHovered && !isPaused) {
-            // Hovered element's icon has dynamically changed so we need to set the user facing icon
-            onSetIcon(icon)
-        }
-        this.icon = icon
-        this.overrideDescendants = overrideDescendants
-        this.onSetIcon = onSetIcon
     }
 }
 
-/**
- * The unique identifier used as the key for the custom [ModifierLocalProvider] created to tell us
- * the current [PointerIcon].
+/*
+ * Changes the pointer hover icon if the node is in bounds and if the node is not overridden
+ * by a parent pointer hover icon node. This node implements [PointerInputModifierNode] so it can
+ * listen to pointer input events and determine if the pointer has entered or exited the bounds of
+ * the modifier itself.
+ *
+ * If the icon or overrideDescendants values are changed, this node will determine if it needs to
+ * walk down and/or up the modifier chain to update those pointer hover icon modifier nodes as well.
  */
-private val ModifierLocalPointerIcon = modifierLocalOf<PointerIconModifierLocal?> { null }
+internal class PointerHoverIconModifierNode(
+    icon: PointerIcon,
+    overrideDescendants: Boolean = false
+) : Modifier.Node(),
+    TraversableNode,
+    PointerInputModifierNode,
+    CompositionLocalConsumerModifierNode {
+    /* Traversal key used with the [TraversableNode] interface to enable all the traversing
+     * functions (ancestor, child, subtree, and subtreeIf).
+     */
+    override val traverseKey = "androidx.compose.ui.input.pointer.PointerHoverIcon"
+
+    var icon = icon
+        set(value) {
+            if (field != value) {
+                field = value
+                if (cursorInBoundsOfNode) {
+                    displayIconIfDescendantsDoNotHavePriority()
+                }
+            }
+        }
+
+    var overrideDescendants = overrideDescendants
+        set(value) {
+            if (field != value) {
+                field = value
+
+                if (overrideDescendants) { // overrideDescendants changed from false -> true
+                    // If this node or any descendants have the cursor in bounds, change the icon.
+                    if (cursorInBoundsOfNode) {
+                        displayIcon()
+                    }
+                } else { // overrideDescendants changed from true -> false
+                    if (cursorInBoundsOfNode) {
+                        displayIconFromCurrentNodeOrDescendantsWithCursorInBounds()
+                    }
+                }
+            }
+        }
+
+    // Service used to actually update the icon with the system when needed.
+    private val pointerIconService: PointerIconService?
+        get() = currentValueOf(LocalPointerIconService)
+
+    private var cursorInBoundsOfNode = false
+
+    // Pointer Input callback for determining if a Pointer has Entered or Exited this node.
+    override fun onPointerEvent(
+        pointerEvent: PointerEvent,
+        pass: PointerEventPass,
+        bounds: IntSize
+    ) {
+        if (pass == Main) {
+            // Cursor within the surface area of this node's bounds
+            if (pointerEvent.type == PointerEventType.Enter) {
+                cursorInBoundsOfNode = true
+                displayIconIfDescendantsDoNotHavePriority()
+            } else if (pointerEvent.type == PointerEventType.Exit) {
+                cursorInBoundsOfNode = false
+                displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
+            }
+        }
+    }
+
+    override fun onCancelPointerInput() {
+        // We aren't processing the event (only listening for enter/exit), so we don't need to
+        // do anything.
+    }
+
+    override fun onDetach() {
+        cursorInBoundsOfNode = false
+        displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
+
+        super.onDetach()
+    }
+
+    private fun displayIcon() {
+        // If there are any ancestor that override this node, we must use that icon. Otherwise, we
+        // use the current node's icon
+        val iconToUse = findOverridingAncestorNode()?.icon ?: icon
+        pointerIconService?.setIcon(iconToUse)
+    }
+
+    private fun displayDefaultIcon() {
+        pointerIconService?.setIcon(null)
+    }
+
+    private fun displayIconIfDescendantsDoNotHavePriority() {
+        var hasIconRightsOverDescendants = true
+
+        if (!overrideDescendants) {
+            traverseSubtree {
+                // Descendant in bounds has rights to the icon (and has already set it),
+                // so we ignore.
+                val continueTraversal = if (it.cursorInBoundsOfNode) {
+                    hasIconRightsOverDescendants = false
+                    false
+                } else {
+                    true
+                }
+                continueTraversal
+            }
+        }
+
+        if (hasIconRightsOverDescendants) {
+            displayIcon()
+        }
+    }
+
+    /*
+     * Finds and returns the lowest descendant node with the cursor within its bounds (true node
+     * that gets to decide the icon).
+     *
+     * Note: Multiple descendant nodes may have `cursorInBoundsOfNode` set to true (for when the
+     * cursor enters their bounds). The lowest one is the one that is the correct node for the
+     * mouse (see example for explanation).
+     *
+     * Example: Parent node contains a child node within its visual border (both are pointer icon
+     * nodes).
+     * - Mouse moves over the PARENT node triggers the pointer input handler ENTER event which sets
+     * `cursorInBoundsOfNode` = `true`.
+     * - Mouse moves over CHILD node triggers the pointer input handler ENTER event which sets
+     * `cursorInBoundsOfNode` = `true`.
+     *
+     * They are both true now because the pointer input event's exit is not triggered (which would
+     * set cursorInBoundsOfNode` = `false`) unless the mouse moves outside the parent node. Because
+     * the child node is contained visually within the parent node, it is not triggered. That is why
+     * we need to get the lowest node with `cursorInBoundsOfNode` set to true.
+     */
+    private fun findDescendantNodeWithCursorInBounds(): PointerHoverIconModifierNode? {
+        var descendantNodeWithCursorInBounds: PointerHoverIconModifierNode? = null
+
+        traverseSubtreeIf {
+            var actionForSubtreeOfCurrentNode = VisitSubtreeIfAction.VisitSubtree
+
+            if (it.cursorInBoundsOfNode) {
+                descendantNodeWithCursorInBounds = it
+
+                // No descendant nodes below this one are eligible to set the icon.
+                if (it.overrideDescendants) {
+                    actionForSubtreeOfCurrentNode = VisitSubtreeIfAction.SkipSubtree
+                }
+            }
+            actionForSubtreeOfCurrentNode
+        }
+
+        return descendantNodeWithCursorInBounds
+    }
+
+    private fun displayIconFromCurrentNodeOrDescendantsWithCursorInBounds() {
+        if (!cursorInBoundsOfNode) return
+
+        var pointerHoverIconModifierNode: PointerHoverIconModifierNode = this
+
+        if (!overrideDescendants) {
+            findDescendantNodeWithCursorInBounds()?.let {
+                pointerHoverIconModifierNode = it
+            }
+        }
+
+        pointerHoverIconModifierNode.displayIcon()
+    }
+
+    private fun findOverridingAncestorNode(): PointerHoverIconModifierNode? {
+        var pointerHoverIconModifierNode: PointerHoverIconModifierNode? = null
+
+        traverseAncestors {
+            if (it.overrideDescendants &&
+                it.cursorInBoundsOfNode) {
+                pointerHoverIconModifierNode = it
+            }
+            // continue traversal
+            true
+        }
+
+        return pointerHoverIconModifierNode
+    }
+
+    /*
+     * Sets the icon to either the ancestor where the mouse is in its bounds (or to its
+     * ancestors if one overrides it) or to a default icon.
+     */
+    private fun displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon() {
+        var pointerHoverIconModifierNode: PointerHoverIconModifierNode? = null
+
+        traverseAncestors {
+            if (pointerHoverIconModifierNode == null && it.cursorInBoundsOfNode) {
+                pointerHoverIconModifierNode = it
+
+            // We should only assign a node that override its descendants if there was a node
+            // below it where the mouse was in bounds meaning the pointerHoverIconModifierNode
+            // will not be null.
+            } else if (pointerHoverIconModifierNode != null &&
+                it.overrideDescendants &&
+                it.cursorInBoundsOfNode) {
+                pointerHoverIconModifierNode = it
+            }
+
+            // continue traversal
+            true
+        }
+        pointerHoverIconModifierNode?.displayIcon() ?: displayDefaultIcon()
+    }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
index 052066b..d578e14 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
@@ -196,7 +196,8 @@
                     false,
                     it.type,
                     it.historical,
-                    it.scrollDelta
+                    it.scrollDelta,
+                    it.originalEventPosition
                 )
             )
             if (it.down) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
index 6a35182..4517319 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
@@ -109,6 +109,7 @@
     fun resetTracking() {
         xVelocityTracker.resetTracking()
         yVelocityTracker.resetTracking()
+        lastMoveEventTimeStamp = 0L
     }
 }
 
@@ -405,38 +406,22 @@
 }
 
 private fun VelocityTracker.addPointerInputChangeWithFix(event: PointerInputChange) {
-    // If this is ACTION_DOWN: Register down event as the starting point for the accumulator
-    // Since compose uses relative positions, for a more accurate velocity calculation we'll need
-    // to transform all events positions. We use the start of the movement signaled by the DOWN
-    // event as the start point. Any subsequent event will be accumulated into
-    // [currentPointerPositionAccumulator] and used to update the tracker.
-    // We also use this to reset [lastMoveEventTimeStamp].
+    // If this is ACTION_DOWN: Reset the tracking.
     if (event.changedToDownIgnoreConsumed()) {
-        lastMoveEventTimeStamp = 0L
-        currentPointerPositionAccumulator = event.position
         resetTracking()
-        return
     }
 
-    // If this is a ACTION_MOVE event: Add events to the tracker as per the platform implementation.
-    // ACTION_MOVE may or may not have a historical array. If they do have a historical array, use
-    // the data provided by the array only, if they do not have historical data, use the data
-    // provided by the event itself. This is in line with the platform implementation.
+    // If this is not ACTION_UP event: Add events to the tracker as per the platform implementation.
+    // In the platform implementation the historical events array is used, they store the current
+    // event data in the position HistoricalArray.Size. Our historical array doesn't have access
+    // to the final position, but we can get that information from the original event data X and Y
+    // coordinates.
     @OptIn(ExperimentalComposeUiApi::class)
-    if (!event.changedToUpIgnoreConsumed() && !event.changedToDownIgnoreConsumed()) {
-        lastMoveEventTimeStamp = event.uptimeMillis
-        if (event.historical.isEmpty()) {
-            val delta = event.position - currentPointerPositionAccumulator
-            currentPointerPositionAccumulator += delta
-            addPosition(event.uptimeMillis, currentPointerPositionAccumulator)
-        } else {
-            event.historical.fastForEach {
-                val historicalDelta = it.position - currentPointerPositionAccumulator
-                // Update the current position with the historical delta and add it to the tracker
-                currentPointerPositionAccumulator += historicalDelta
-                addPosition(it.uptimeMillis, currentPointerPositionAccumulator)
-            }
+    if (!event.changedToUpIgnoreConsumed()) {
+        event.historical.fastForEach {
+            addPosition(it.uptimeMillis, it.originalEventPosition)
         }
+        addPosition(event.uptimeMillis, event.originalEventPosition)
     }
 
     // If this is ACTION_UP. Fix for b/238654963. If there's been enough time after the last MOVE
@@ -444,6 +429,7 @@
     if (event.changedToUpIgnoreConsumed() && (event.uptimeMillis - lastMoveEventTimeStamp) > 40L) {
         resetTracking()
     }
+    lastMoveEventTimeStamp = event.uptimeMillis
 }
 
 internal data class DataPointAtTime(var time: Long, var dataPoint: Float)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index 2e4eb7a..7e16660 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -244,6 +244,13 @@
     internal var lookaheadPassDelegate: LookaheadPassDelegate? = null
         private set
 
+    // Used by performMeasureBlock so that we don't have to allocate a lambda on every call
+    private var performMeasureConstraints = Constraints()
+
+    private val performMeasureBlock: () -> Unit = {
+        outerCoordinator.measure(performMeasureConstraints)
+    }
+
     fun onCoordinatesUsed() {
         val state = layoutNode.layoutState
         if (state == LayoutState.LayingOut || state == LayoutState.LookaheadLayingOut) {
@@ -349,6 +356,20 @@
         var layingOutChildren = false
             private set
 
+        private val layoutChildrenBlock: () -> Unit = {
+            clearPlaceOrder()
+            forEachChildAlignmentLinesOwner {
+                it.alignmentLines.usedDuringParentLayout = false
+            }
+            innerCoordinator.measureResult.placeChildren()
+
+            checkChildrenPlaceOrderForUpdates()
+            forEachChildAlignmentLinesOwner {
+                it.alignmentLines.previousUsedDuringParentLayout =
+                    it.alignmentLines.usedDuringParentLayout
+            }
+        }
+
         override fun layoutChildren() {
             layingOutChildren = true
             alignmentLines.recalculateQueryOwner()
@@ -370,20 +391,9 @@
                     val owner = requireOwner()
                     owner.snapshotObserver.observeLayoutSnapshotReads(
                         this,
-                        affectsLookahead = false
-                    ) {
-                        clearPlaceOrder()
-                        forEachChildAlignmentLinesOwner {
-                            it.alignmentLines.usedDuringParentLayout = false
-                        }
-                        innerCoordinator.measureResult.placeChildren()
-
-                        checkChildrenPlaceOrderForUpdates()
-                        forEachChildAlignmentLinesOwner {
-                            it.alignmentLines.previousUsedDuringParentLayout =
-                                it.alignmentLines.usedDuringParentLayout
-                        }
-                    }
+                        affectsLookahead = false,
+                        block = layoutChildrenBlock
+                    )
                 }
                 layoutState = oldLayoutState
 
@@ -464,6 +474,29 @@
 
         private var onNodePlacedCalled = false
 
+        // Used by placeOuterBlock to avoid allocating the lambda on every call
+        private var placeOuterCoordinatorLayerBlock: (GraphicsLayerScope.() -> Unit)? = null
+        private var placeOuterCoordinatorPosition = IntOffset.Zero
+        private var placeOuterCoordinatorZIndex = 0f
+
+        private val placeOuterCoordinatorBlock: () -> Unit = {
+            with(PlacementScope) {
+                val layerBlock = placeOuterCoordinatorLayerBlock
+                if (layerBlock == null) {
+                    outerCoordinator.place(
+                        placeOuterCoordinatorPosition,
+                        placeOuterCoordinatorZIndex
+                    )
+                } else {
+                    outerCoordinator.placeWithLayer(
+                        placeOuterCoordinatorPosition,
+                        placeOuterCoordinatorZIndex,
+                        layerBlock
+                    )
+                }
+            }
+        }
+
         /**
          * Invoked when the parent placed the node. It will trigger the layout.
          */
@@ -701,17 +734,13 @@
             } else {
                 alignmentLines.usedByModifierLayout = false
                 coordinatesAccessedDuringModifierPlacement = false
+                placeOuterCoordinatorLayerBlock = layerBlock
+                placeOuterCoordinatorPosition = position
+                placeOuterCoordinatorZIndex = zIndex
                 owner.snapshotObserver.observeLayoutModifierSnapshotReads(
-                    layoutNode, affectsLookahead = false
-                ) {
-                    with(PlacementScope) {
-                        if (layerBlock == null) {
-                            outerCoordinator.place(position, zIndex)
-                        } else {
-                            outerCoordinator.placeWithLayer(position, zIndex, layerBlock)
-                        }
-                    }
-                }
+                    layoutNode, affectsLookahead = false, block = placeOuterCoordinatorBlock
+                )
+                placeOuterCoordinatorLayerBlock = null
             }
 
             layoutState = LayoutState.Idle
@@ -1548,12 +1577,12 @@
         }
         layoutState = LayoutState.Measuring
         measurePending = false
+        performMeasureConstraints = constraints
         layoutNode.requireOwner().snapshotObserver.observeMeasureSnapshotReads(
             layoutNode,
-            affectsLookahead = false
-        ) {
-            outerCoordinator.measure(constraints)
-        }
+            affectsLookahead = false,
+            performMeasureBlock
+        )
         // The resulting layout state might be Ready. This can happen when the layout node's
         // own modifier is querying an alignment line during measurement, therefore we
         // need to also layout the layout node.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index afe3564..4abf36f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -102,6 +102,8 @@
     @JvmStatic
     inline val SoftKeyboardKeyInput
         get() = NodeKind<SoftKeyboardInterceptionModifierNode>(0b1 shl 17)
+    @JvmStatic
+    inline val Traversable get() = NodeKind<TraversableNode>(0b1 shl 18)
     // ...
 }
 
@@ -203,6 +205,9 @@
     if (node is SoftKeyboardInterceptionModifierNode) {
         mask = mask or Nodes.SoftKeyboardKeyInput
     }
+    if (node is TraversableNode) {
+        mask = mask or Nodes.Traversable
+    }
     return mask
 }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/TraversableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/TraversableNode.kt
new file mode 100644
index 0000000..778e634
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/TraversableNode.kt
@@ -0,0 +1,256 @@
+/*
+ * 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.ui.node
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.areObjectsOfSameType
+import androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction
+
+/**
+ * Allows [Modifier.Node] classes to traverse up/down the Node tree for classes of the same type or
+ * for a particular key (traverseKey).
+ *
+ * Note: The actual traversals are done in extension functions (see bottom of file).
+ */
+interface TraversableNode : DelegatableNode {
+    val traverseKey: Any
+
+    companion object {
+        /**
+         * Tree traversal actions for the traverseSubtreeWithKeyIf related functions:
+         *  - VisitSubtree - visit the subtree of current match
+         *  - SkipSubtree - do NOT visit the subtree of the current match
+         *  - CancelTraversal - cancels the traversal (returns from function call)
+         *
+         * To see examples of all the actions, see TraversableModifierNodeTest. For a return/cancel
+         * example specifically, see
+         * traverseSubtreeWithSameKeyIf_cancelTraversalOfDifferentClassSameKey().
+         */
+        enum class VisitSubtreeIfAction {
+            VisitSubtree,
+            SkipSubtree,
+            CancelTraversal
+        }
+    }
+}
+
+// *********** Nearest Traversable Ancestor methods ***********
+/**
+ * Finds the nearest ancestor with a matching [key].
+ */
+fun DelegatableNode.nearestTraversableAncestorWithKey(
+    key: Any?
+): TraversableNode? {
+    visitAncestors(Nodes.Traversable) {
+        if (key == it.traverseKey) {
+            return it
+        }
+    }
+    return null
+}
+
+/**
+ * Finds the nearest ancestor of the same class and key.
+ */
+fun <T> T.nearestTraversableAncestor(): T? where T : TraversableNode {
+    visitAncestors(Nodes.Traversable) {
+        if (this.traverseKey == it.traverseKey && areObjectsOfSameType(this, it)) {
+            @Suppress("UNCHECKED_CAST")
+            return it as T
+        }
+    }
+    return null
+}
+
+// *********** Traverse Ancestors methods ***********
+/**
+ * Executes [block] for all ancestors with a matching [key].
+ *
+ * Note: The parameter [block]'s return boolean value will determine if the traversal will
+ * continue (true = continue, false = cancel).
+ */
+fun DelegatableNode.traverseAncestorsWithKey(
+    key: Any?,
+    block: (TraversableNode) -> Boolean
+) {
+    visitAncestors(Nodes.Traversable) {
+        val continueTraversal = if (key == it.traverseKey) {
+            block(it)
+        } else {
+            true
+        }
+        if (!continueTraversal) return
+    }
+}
+
+/**
+ * Executes [block] for all ancestors of the same class and key.
+ *
+ * Note: The parameter [block]'s return boolean value will determine if the traversal will
+ * continue (true = continue, false = cancel).
+ */
+fun <T> T.traverseAncestors(block: (T) -> Boolean) where T : TraversableNode {
+    visitAncestors(Nodes.Traversable) {
+        val continueTraversal =
+            if (this.traverseKey == it.traverseKey && areObjectsOfSameType(this, it)) {
+                @Suppress("UNCHECKED_CAST")
+                block(it as T)
+            } else {
+                true
+            }
+        if (!continueTraversal) return
+    }
+}
+
+// *********** Traverse Children methods ***********
+/**
+ * Executes [block] for all direct children of the node with a matching [key].
+ *
+ * Note 1: This stops at the children and does not include grandchildren and so on down the tree.
+ *
+ * Note 2: The parameter [block]'s return boolean value will determine if the traversal will
+ * continue (true = continue, false = cancel).
+ */
+fun DelegatableNode.traverseChildrenWithKey(
+    key: Any?,
+    block: (TraversableNode) -> Boolean
+) {
+    visitChildren(Nodes.Traversable) {
+        val continueTraversal = if (key == it.traverseKey) {
+            block(it)
+        } else {
+            true
+        }
+        if (!continueTraversal) return
+    }
+}
+
+/**
+ * Executes [block] for all direct children of the node that are of the same class.
+ *
+ * Note 1: This stops at the children and does not include grandchildren and so on down the tree.
+ *
+ * Note 2: The parameter [block]'s return boolean value will determine if the traversal will
+ * continue (true = continue, false = cancel).
+ */
+fun <T> T.traverseChildren(block: (T) -> Boolean) where T : TraversableNode {
+    visitChildren(Nodes.Traversable) {
+        val continueTraversal =
+            if (this.traverseKey == it.traverseKey && areObjectsOfSameType(this, it)) {
+                @Suppress("UNCHECKED_CAST")
+                block(it as T)
+            } else {
+                true
+            }
+        if (!continueTraversal) return
+    }
+}
+
+// *********** Traverse Subtree methods ***********
+/**
+ * Executes [block] for all nodes in the subtree with a matching [key].
+ *
+ * Note: The parameter [block]'s return boolean value will determine if the traversal will
+ * continue (true = continue, false = cancel).
+ */
+fun DelegatableNode.traverseSubtreeWithKey(
+    key: Any?,
+    block: (TraversableNode) -> Boolean
+) {
+    visitSubtree(Nodes.Traversable) {
+        val continueTraversal = if (key == it.traverseKey) {
+            block(it)
+        } else {
+            true
+        }
+        if (!continueTraversal) return
+    }
+}
+
+/**
+ * Executes [block] for all nodes of the same class in the subtree.
+ *
+ * Note: The parameter [block]'s return boolean value will determine if the traversal will
+ * continue (true = continue, false = cancel).
+ */
+fun <T> T.traverseSubtree(block: (T) -> Boolean) where T : TraversableNode {
+    visitSubtree(Nodes.Traversable) {
+        val continueTraversal =
+            if (this.traverseKey == it.traverseKey && areObjectsOfSameType(this, it)) {
+                @Suppress("UNCHECKED_CAST")
+                block(it as T)
+            } else {
+                true
+            }
+        if (!continueTraversal) return
+    }
+}
+
+// *********** Traverse Subtree If methods ***********
+/**
+ * Conditionally executes [block] for each node with a matching [key] in the subtree.
+ *
+ * Note 1: For nodes that do not have the same key, it will continue to execute the [block] for
+ * the subtree below that non-matching node (where there may be a node that matches).
+ *
+ * Note 2: The parameter [block]'s return value [VisitSubtreeIfAction] will determine the next step
+ * in the traversal.
+ */
+fun DelegatableNode.traverseSubtreeIfWithKey(
+    key: Any?,
+    block: (TraversableNode) -> VisitSubtreeIfAction
+) {
+    visitSubtreeIf(Nodes.Traversable) {
+        val action = if (key == it.traverseKey) {
+            block(it)
+        } else {
+            VisitSubtreeIfAction.VisitSubtree
+        }
+        if (action == VisitSubtreeIfAction.CancelTraversal) return
+
+        // visitSubtreeIf() requires a true to continue down the subtree and a false if you
+        // want to skip the subtree, so we check if the action is NOT EQUAL to the subtree
+        // to trigger false if the action is Skip subtree and true otherwise.
+        action != VisitSubtreeIfAction.SkipSubtree
+    }
+}
+
+/**
+ * Conditionally executes [block] for each node of the same class in the subtree.
+ *
+ * Note 1: For nodes that do not have the same key, it will continue to execute the [block] for
+ * the subtree below that non-matching node (where there may be a node that matches).
+ *
+ * Note 2: The parameter [block]'s return value [VisitSubtreeIfAction] will determine the next step
+ * in the traversal.
+ */
+fun <T> T.traverseSubtreeIf(block: (T) -> VisitSubtreeIfAction) where T : TraversableNode {
+    visitSubtreeIf(Nodes.Traversable) {
+        val action =
+            if (this.traverseKey == it.traverseKey && areObjectsOfSameType(this, it)) {
+                @Suppress("UNCHECKED_CAST")
+                block(it as T)
+            } else {
+                VisitSubtreeIfAction.VisitSubtree
+            }
+        if (action == VisitSubtreeIfAction.CancelTraversal) return
+
+        // visitSubtreeIf() requires a true to continue down the subtree and a false if you
+        // want to skip the subtree, so we check if the action is NOT EQUAL to the subtree
+        // to trigger false if the action is Skip subtree and true otherwise.
+        action != VisitSubtreeIfAction.SkipSubtree
+    }
+}
diff --git a/constraintlayout/constraintlayout-compose/build.gradle b/constraintlayout/constraintlayout-compose/build.gradle
index 5a3d7f55..52093a5 100644
--- a/constraintlayout/constraintlayout-compose/build.gradle
+++ b/constraintlayout/constraintlayout-compose/build.gradle
@@ -63,7 +63,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.kotlinTest)
@@ -84,9 +84,10 @@
         //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
         //  level dependencies block instead:
         //  `dependencies { testImplementation(libs.robolectric) }`
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
             dependencies {
+                implementation(libs.kotlinTest)
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
                 implementation(libs.junit)
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/AndroidManifest.xml b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/AndroidManifest.xml
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ChainsTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ChainsTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ChainsTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ChainsTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintSetParserKtTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ConstraintSetParserKtTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintSetParserKtTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ConstraintSetParserKtTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/DesignInfoProviderTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/DesignInfoProviderTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/DesignInfoProviderTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/DesignInfoProviderTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/FlowTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/FlowTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/FlowTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/FlowTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/GridDslTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/GridDslTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/GridDslTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/GridDslTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/GridTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/GridTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/GridTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/GridTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionFlowTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/MotionFlowTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionFlowTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/MotionFlowTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionGridTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/MotionGridTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionGridTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/MotionGridTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionParserTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/MotionParserTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionParserTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/MotionParserTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MultiMeasureCompositionTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/MultiMeasureCompositionTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MultiMeasureCompositionTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/MultiMeasureCompositionTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/OnSwipeTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/OnSwipeTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/OnSwipeTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/OnSwipeTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/RowColumnDslTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/RowColumnDslTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/RowColumnDslTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/RowColumnDslTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/RowColumnTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/RowColumnTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/RowColumnTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/RowColumnTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/Utils.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/Utils.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/Utils.kt
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/Utils.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/res/raw/custom_text_size_scene.json5 b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/res/raw/custom_text_size_scene.json5
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidAndroidTest/res/raw/custom_text_size_scene.json5
rename to constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/res/raw/custom_text_size_scene.json5
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
index 337d63a..04f1c6d 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
@@ -20,7 +20,7 @@
 import android.os.Handler
 import android.os.Looper
 import android.util.Log
-import androidx.collection.PairIntInt
+import androidx.collection.IntIntPair
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.tween
@@ -2019,7 +2019,7 @@
     private fun measureWidget(
         constraintWidget: ConstraintWidget,
         constraints: Constraints
-    ): PairIntInt {
+    ): IntIntPair {
         val measurable = constraintWidget.companionWidget
         val widgetId = constraintWidget.stringId
         return when {
@@ -2042,15 +2042,15 @@
                     heightMode,
                     constraints.maxHeight
                 )
-                PairIntInt(constraintWidget.measuredWidth, constraintWidget.measuredHeight)
+                IntIntPair(constraintWidget.measuredWidth, constraintWidget.measuredHeight)
             }
             measurable is Measurable -> {
                 val result = measurable.measure(constraints).also { placeables[measurable] = it }
-                PairIntInt(result.width, result.height)
+                IntIntPair(result.width, result.height)
             }
             else -> {
                 Log.w("CCL", "Nothing to measure for widget: $widgetId")
-                PairIntInt(0, 0)
+                IntIntPair(0, 0)
             }
         }
     }
diff --git a/constraintlayout/constraintlayout-compose/src/androidTest/kotlin/androidx/constraintlayout/compose/ConstraintSetStabilityTest.kt b/constraintlayout/constraintlayout-compose/src/androidUnitTest/kotlin/androidx/constraintlayout/compose/ConstraintSetStabilityTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidTest/kotlin/androidx/constraintlayout/compose/ConstraintSetStabilityTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidUnitTest/kotlin/androidx/constraintlayout/compose/ConstraintSetStabilityTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidTest/kotlin/androidx/constraintlayout/compose/DebugFlagsTest.kt b/constraintlayout/constraintlayout-compose/src/androidUnitTest/kotlin/androidx/constraintlayout/compose/DebugFlagsTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidTest/kotlin/androidx/constraintlayout/compose/DebugFlagsTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidUnitTest/kotlin/androidx/constraintlayout/compose/DebugFlagsTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidTest/kotlin/androidx/constraintlayout/compose/MotionSceneStabilityTest.kt b/constraintlayout/constraintlayout-compose/src/androidUnitTest/kotlin/androidx/constraintlayout/compose/MotionSceneStabilityTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidTest/kotlin/androidx/constraintlayout/compose/MotionSceneStabilityTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidUnitTest/kotlin/androidx/constraintlayout/compose/MotionSceneStabilityTest.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidTest/kotlin/androidx/constraintlayout/compose/TransitionStabilityTest.kt b/constraintlayout/constraintlayout-compose/src/androidUnitTest/kotlin/androidx/constraintlayout/compose/TransitionStabilityTest.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/src/androidTest/kotlin/androidx/constraintlayout/compose/TransitionStabilityTest.kt
rename to constraintlayout/constraintlayout-compose/src/androidUnitTest/kotlin/androidx/constraintlayout/compose/TransitionStabilityTest.kt
diff --git a/core/core-performance-play-services/api/1.0.0-beta01.txt b/core/core-performance-play-services/api/1.0.0-beta01.txt
new file mode 100644
index 0000000..7e46ef7
--- /dev/null
+++ b/core/core-performance-play-services/api/1.0.0-beta01.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.core.performance.play.services {
+
+  public final class PlayServicesDevicePerformance implements androidx.core.performance.DevicePerformance {
+    ctor public PlayServicesDevicePerformance(android.content.Context context);
+    method public int getMediaPerformanceClass();
+    property public int mediaPerformanceClass;
+  }
+
+}
+
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt b/core/core-performance-play-services/api/res-1.0.0-beta01.txt
similarity index 100%
copy from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
copy to core/core-performance-play-services/api/res-1.0.0-beta01.txt
diff --git a/core/core-performance-play-services/api/restricted_1.0.0-beta01.txt b/core/core-performance-play-services/api/restricted_1.0.0-beta01.txt
new file mode 100644
index 0000000..7e46ef7
--- /dev/null
+++ b/core/core-performance-play-services/api/restricted_1.0.0-beta01.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.core.performance.play.services {
+
+  public final class PlayServicesDevicePerformance implements androidx.core.performance.DevicePerformance {
+    ctor public PlayServicesDevicePerformance(android.content.Context context);
+    method public int getMediaPerformanceClass();
+    property public int mediaPerformanceClass;
+  }
+
+}
+
diff --git a/core/core-performance-play-services/build.gradle b/core/core-performance-play-services/build.gradle
index c7a44dd..5ae6276 100644
--- a/core/core-performance-play-services/build.gradle
+++ b/core/core-performance-play-services/build.gradle
@@ -29,23 +29,13 @@
     implementation(libs.kotlinCoroutinesCore)
     implementation(project(":core:core-performance"))
     implementation("androidx.datastore:datastore-preferences:1.0.0")
-    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
 
-    testImplementation(libs.robolectric)
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.truth)
-    androidTestImplementation(libs.espressoCore, excludes.espresso)
-    androidTestImplementation(libs.mockitoAndroid)
 
 
-    testImplementation(libs.testCore)
-    testImplementation(libs.kotlinStdlib)
-    testImplementation(libs.kotlinCoroutinesTest)
-    testImplementation(libs.junit)
-    testImplementation(libs.truth)
-
 }
 
 android {
diff --git a/core/core-performance-play-services/src/androidTest/java/androidx/core/performance/play/services/PlayServiceDevicePerformanceAndroidTest.kt b/core/core-performance-play-services/src/androidTest/java/androidx/core/performance/play/services/PlayServiceDevicePerformanceAndroidTest.kt
index 033e68a..26ed265 100644
--- a/core/core-performance-play-services/src/androidTest/java/androidx/core/performance/play/services/PlayServiceDevicePerformanceAndroidTest.kt
+++ b/core/core-performance-play-services/src/androidTest/java/androidx/core/performance/play/services/PlayServiceDevicePerformanceAndroidTest.kt
@@ -28,27 +28,26 @@
 import com.google.android.gms.common.api.internal.ApiKey
 import com.google.android.gms.deviceperformance.DevicePerformanceClient
 import com.google.android.gms.tasks.Task
-import com.google.android.gms.tasks.Tasks
+import com.google.android.gms.tasks.TaskCompletionSource
 import com.google.common.truth.Truth
 import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.runBlocking
 import org.junit.After
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.`when`
 
 /** Android Unit tests for [PlayServicesDevicePerformance]. */
 @RunWith(AndroidJUnit4::class)
 class PlayServicesDevicePerformanceTest {
-    open class DevicePerformanceClientTest : DevicePerformanceClient {
+    class FakeDevicePerformanceClient() : DevicePerformanceClient {
+        val taskSource: TaskCompletionSource<Int> = TaskCompletionSource()
         override fun getApiKey(): ApiKey<Api.ApiOptions.NoOptions> {
             // method for testing purpose
             return this.apiKey
         }
 
         override fun mediaPerformanceClass(): Task<Int> {
-            return Tasks.forResult(0)
+            return taskSource.task
         }
     }
 
@@ -62,45 +61,26 @@
 
     @Test
     @MediumTest
-    fun basePlayServiceDevicePerformanceClassTest() {
-        val playServicesDevicePerformance = PlayServicesDevicePerformance(
-            context
-        )
-        val pcScore = playServicesDevicePerformance.mediaPerformanceClass
-        Truth.assertThat(pcScore).isEqualTo(defaultMediaPerformanceClass)
-    }
+    fun mediaPerformanceClass_EmptyStore_33Client() {
+        val fakeDevicePerformanceClient = FakeDevicePerformanceClient()
 
-    @Test
-    @MediumTest
-    fun mockPlayServiceDevicePerformanceClassTest() {
-        val mockClient: DevicePerformanceClient = mock(DevicePerformanceClientTest::class.java)
-        val mediaPerformanceClass = 33
-        `when`(mockClient.mediaPerformanceClass()).thenAnswer {
-            Tasks.forResult(mediaPerformanceClass)
-        }
-        val playServicesDevicePerformance = PlayServicesDevicePerformance(
+        val playServicesDevicePerformance = PlayServicesDevicePerformance.create(
             context,
-            mockClient
+            fakeDevicePerformanceClient
         )
+        fakeDevicePerformanceClient.taskSource.setResult(33)
         delayRead()
         val pcScore = playServicesDevicePerformance.mediaPerformanceClass
-        Truth.assertThat(pcScore).isEqualTo(mediaPerformanceClass)
+        Truth.assertThat(pcScore).isEqualTo(33)
     }
 
     @Test
     @MediumTest
-    fun delayMockPlayServiceDevicePerformanceClassTest() {
-        val mockClient: DevicePerformanceClient = mock(DevicePerformanceClientTest::class.java)
-
-        // Delay the response from mockClient.mediaPerformanceClass() so
-        // response will be different that provided.
-        `when`(mockClient.mediaPerformanceClass()).thenAnswer {
-            TimeUnit.SECONDS.sleep(5)
-            Tasks.forResult(defaultMediaPerformanceClass + 100)
-        }
-        val playServicesDevicePerformance = PlayServicesDevicePerformance(
+    fun mediaPerformanceClass_EmptyStore() {
+        val fakeDevicePerformanceClient = FakeDevicePerformanceClient()
+        val playServicesDevicePerformance = PlayServicesDevicePerformance.create(
             context,
-            mockClient
+            fakeDevicePerformanceClient
         )
         val pcScore = playServicesDevicePerformance.mediaPerformanceClass
         Truth.assertThat(pcScore).isEqualTo(defaultMediaPerformanceClass)
@@ -108,32 +88,30 @@
 
     @Test
     @MediumTest
-    fun playServiceCrashPerformanceClassTest() {
-        val mockClient: DevicePerformanceClient = mock(DevicePerformanceClientTest::class.java)
-        `when`(mockClient.mediaPerformanceClass()).thenReturn( // Throw an exception here.
-            Tasks.forException(IllegalStateException())
-        )
-        val pc = PlayServicesDevicePerformance(
+    fun mediaPerformanceClass_EmptyStore_IllegalStateException() {
+        val fakeDevicePerformanceClient = FakeDevicePerformanceClient()
+        fakeDevicePerformanceClient.taskSource.setException(IllegalStateException())
+        val playServicesDevicePerformance = PlayServicesDevicePerformance.create(
             context,
-            mockClient
+            fakeDevicePerformanceClient
         )
         // Since the gms service has crashed, the library should still return default value.
-        Truth.assertThat(pc.mediaPerformanceClass).isEqualTo(defaultMediaPerformanceClass)
+        Truth.assertThat(playServicesDevicePerformance.mediaPerformanceClass)
+            .isEqualTo(defaultMediaPerformanceClass)
     }
 
     @Test
     @MediumTest
-    fun playServiceNotStartPerformanceClassTest() {
-        val mockClient: DevicePerformanceClient = mock(DevicePerformanceClientTest::class.java)
-        `when`(mockClient.mediaPerformanceClass()).thenReturn( // Throw an exception here.
-            Tasks.forException(ApiException(Status.RESULT_TIMEOUT))
-        )
-        val pc = PlayServicesDevicePerformance(
+    fun mediaPerformanceClass_EmptyStore_TimeOut() {
+        val fakeDevicePerformanceClient = FakeDevicePerformanceClient()
+        fakeDevicePerformanceClient.taskSource.setException(ApiException(Status.RESULT_TIMEOUT))
+        val playServicesDevicePerformance = PlayServicesDevicePerformance.create(
             context,
-            mockClient
+            fakeDevicePerformanceClient
         )
         // Since the gms service not started, the library should still return default value.
-        Truth.assertThat(pc.mediaPerformanceClass).isEqualTo(defaultMediaPerformanceClass)
+        Truth.assertThat(playServicesDevicePerformance.mediaPerformanceClass)
+            .isEqualTo(defaultMediaPerformanceClass)
     }
 
     /* Add delay to make sure that value is written in Preference datastore before reading it */
diff --git a/core/core-performance-play-services/src/main/java/androidx/core/performance/play/services/PlayServicesDevicePerformance.kt b/core/core-performance-play-services/src/main/java/androidx/core/performance/play/services/PlayServicesDevicePerformance.kt
index 2fa0a804..03f2d6a 100644
--- a/core/core-performance-play-services/src/main/java/androidx/core/performance/play/services/PlayServicesDevicePerformance.kt
+++ b/core/core-performance-play-services/src/main/java/androidx/core/performance/play/services/PlayServicesDevicePerformance.kt
@@ -39,7 +39,9 @@
  *
  * @param context The application context value to use.
  */
-class PlayServicesDevicePerformance(private val context: Context) : DevicePerformance {
+class PlayServicesDevicePerformance
+private constructor(private val context: Context, client: DevicePerformanceClient) :
+    DevicePerformance {
     private val tag = "PlayServicesDevicePerformance"
 
     private val defaultMpc = DefaultDevicePerformance()
@@ -64,24 +66,32 @@
             "Getting mediaPerformanceClass from " +
                 "com.google.android.gms.deviceperformance.DevicePerformanceClient"
         )
-        updatePerformanceStore(
-            com.google.android.gms.deviceperformance.DevicePerformance.getClient(context)
-        )
+        updatePerformanceStore(client)
     }
 
-    @VisibleForTesting
-    internal constructor(context: Context, client: DevicePerformanceClient) : this(context) {
-        // mock client should wait for the playServices client to finish,
-        // so the test results are determined by the mock client.
-        runBlocking {
-            playServicesValueStoredDeferred.await()
-        }
-        updatePerformanceStore(client)
+    /**
+     * A DevicePerformance that uses Google Play Services to retrieve media performance class data.
+     *
+     * @param context The application context value to use.
+     */
+      constructor(context: Context) : this(
+        context,
+        com.google.android.gms.deviceperformance.DevicePerformance.getClient(context)
+    ) {
     }
 
     private val mpcKey = intPreferencesKey("mpc_value")
 
     internal companion object {
+
+        @VisibleForTesting
+        fun create(
+            context: Context,
+            client: DevicePerformanceClient
+        ): PlayServicesDevicePerformance {
+            return PlayServicesDevicePerformance(context, client)
+        }
+
         // To avoid creating multiple instance of datastore
         private val Context.performanceStore by
         preferencesDataStore(name = "media_performance_class")
@@ -115,16 +125,14 @@
                 launch {
                     savePerformanceClass(storedVal)
                     Log.v(tag, "Saved mediaPerformanceClass $storedVal")
-                    playServicesValueStoredDeferred.complete(true)
                 }
             }
         }.addOnFailureListener { e: Exception ->
             if (e is ApiException) {
-                Log.e(tag, "Error saving mediaPerformanceClass: $e")
+                Log.e(tag, "Error saving mediaPerformanceClass", e)
             } else if (e is IllegalStateException) {
-                Log.e(tag, "Error saving mediaPerformanceClass: $e")
+                Log.e(tag, "Error saving mediaPerformanceClass", e)
             }
-            playServicesValueStoredDeferred.complete(true)
         }
     }
 }
diff --git a/core/core-performance-testing/api/1.0.0-beta01.txt b/core/core-performance-testing/api/1.0.0-beta01.txt
new file mode 100644
index 0000000..6e565c7
--- /dev/null
+++ b/core/core-performance-testing/api/1.0.0-beta01.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.core.performance.testing {
+
+  public final class FakeDevicePerformance implements androidx.core.performance.DevicePerformance {
+    ctor public FakeDevicePerformance(int mediaPerformanceClass);
+    method public int getMediaPerformanceClass();
+    property public int mediaPerformanceClass;
+  }
+
+}
+
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt b/core/core-performance-testing/api/res-1.0.0-beta01.txt
similarity index 100%
copy from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
copy to core/core-performance-testing/api/res-1.0.0-beta01.txt
diff --git a/core/core-performance-testing/api/restricted_1.0.0-beta01.txt b/core/core-performance-testing/api/restricted_1.0.0-beta01.txt
new file mode 100644
index 0000000..6e565c7
--- /dev/null
+++ b/core/core-performance-testing/api/restricted_1.0.0-beta01.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.core.performance.testing {
+
+  public final class FakeDevicePerformance implements androidx.core.performance.DevicePerformance {
+    ctor public FakeDevicePerformance(int mediaPerformanceClass);
+    method public int getMediaPerformanceClass();
+    property public int mediaPerformanceClass;
+  }
+
+}
+
diff --git a/core/core-performance/api/1.0.0-beta01.txt b/core/core-performance/api/1.0.0-beta01.txt
new file mode 100644
index 0000000..05ff76d2
--- /dev/null
+++ b/core/core-performance/api/1.0.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.core.performance {
+
+  public final class DefaultDevicePerformance implements androidx.core.performance.DevicePerformance {
+    ctor public DefaultDevicePerformance();
+    method public int getMediaPerformanceClass();
+    property public int mediaPerformanceClass;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface DevicePerformance {
+    method public int getMediaPerformanceClass();
+    property public abstract int mediaPerformanceClass;
+  }
+
+}
+
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt b/core/core-performance/api/res-1.0.0-beta01.txt
similarity index 100%
copy from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
copy to core/core-performance/api/res-1.0.0-beta01.txt
diff --git a/core/core-performance/api/restricted_1.0.0-beta01.txt b/core/core-performance/api/restricted_1.0.0-beta01.txt
new file mode 100644
index 0000000..05ff76d2
--- /dev/null
+++ b/core/core-performance/api/restricted_1.0.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.core.performance {
+
+  public final class DefaultDevicePerformance implements androidx.core.performance.DevicePerformance {
+    ctor public DefaultDevicePerformance();
+    method public int getMediaPerformanceClass();
+    property public int mediaPerformanceClass;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface DevicePerformance {
+    method public int getMediaPerformanceClass();
+    property public abstract int mediaPerformanceClass;
+  }
+
+}
+
diff --git a/core/core-telecom/api/current.txt b/core/core-telecom/api/current.txt
index b01e13c..4bc7ae5 100644
--- a/core/core-telecom/api/current.txt
+++ b/core/core-telecom/api/current.txt
@@ -88,3 +88,10 @@
 
 }
 
+package androidx.core.telecom.util {
+
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAppActions {
+  }
+
+}
+
diff --git a/core/core-telecom/api/restricted_current.txt b/core/core-telecom/api/restricted_current.txt
index b01e13c..4bc7ae5 100644
--- a/core/core-telecom/api/restricted_current.txt
+++ b/core/core-telecom/api/restricted_current.txt
@@ -88,3 +88,10 @@
 
 }
 
+package androidx.core.telecom.util {
+
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAppActions {
+  }
+
+}
+
diff --git a/core/core-telecom/build.gradle b/core/core-telecom/build.gradle
index c186a3b..047deba 100644
--- a/core/core-telecom/build.gradle
+++ b/core/core-telecom/build.gradle
@@ -27,6 +27,8 @@
     api(libs.kotlinStdlib)
     api(libs.guavaListenableFuture)
     implementation("androidx.annotation:annotation:1.4.0")
+    // @OptIn annotations
+    api("androidx.annotation:annotation-experimental:1.3.0")
     implementation("androidx.core:core:1.9.0")
     implementation(libs.kotlinCoroutinesCore)
     implementation(libs.kotlinCoroutinesGuava)
@@ -45,6 +47,9 @@
 
 android {
     namespace "androidx.core.telecom"
+    buildFeatures {
+        aidl = true
+    }
 }
 
 androidx {
diff --git a/core/core-telecom/src/androidTest/AndroidManifest.xml b/core/core-telecom/src/androidTest/AndroidManifest.xml
index 6e24bd3..4d94ecb2 100644
--- a/core/core-telecom/src/androidTest/AndroidManifest.xml
+++ b/core/core-telecom/src/androidTest/AndroidManifest.xml
@@ -16,6 +16,7 @@
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
     <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
+    <uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
 
     <application>
         <service
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2ECallExtensionExtrasTests.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2ECallExtensionExtrasTests.kt
new file mode 100644
index 0000000..7fa9e16
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2ECallExtensionExtrasTests.kt
@@ -0,0 +1,205 @@
+/*
+ * 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.core.telecom.test
+
+import android.Manifest
+import android.os.Build
+import android.telecom.Call
+import android.telecom.DisconnectCause
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallsManager
+import androidx.core.telecom.internal.InCallServiceCompat
+import androidx.core.telecom.internal.utils.Utils
+import androidx.core.telecom.test.utils.BaseTelecomTest
+import androidx.core.telecom.test.utils.TestUtils
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.rule.GrantPermissionRule
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * This test class helps verify the E2E behavior for calls added via Jetpack to ensure that the
+ * call details contain the appropriate extension extras that define the support for capability
+ * exchange between the VOIP app and ICS.
+ *
+ * Note: Currently, this test only verifies the presence of [CallsManager.PROPERTY_IS_TRANSACTIONAL]
+ * (only in V) in the call properties, if the phone account supports transactional ops (U+ devices),
+ * or if the [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] key is present in the call
+ * extras (pre-U devices). In the future, this will be expanded to be provide more robust testing
+ * to verify binder functionality as well as supporting the case for auto
+ * ([CallsManager.EXTRA_VOIP_API_VERSION]).
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@RequiresApi(Build.VERSION_CODES.O)
+@RunWith(AndroidJUnit4::class)
+class E2ECallExtensionExtrasTests : BaseTelecomTest() {
+    private lateinit var inCallServiceCompat: InCallServiceCompat
+
+    /**
+     * Grant READ_PHONE_NUMBERS permission as part of testing
+     * [InCallServiceCompat#resolveCallExtensionsType].
+     */
+    @get:Rule
+    val readPhoneNumbersRule: GrantPermissionRule =
+        GrantPermissionRule.grant(Manifest.permission.READ_PHONE_NUMBERS)!!
+
+    @Before
+    fun setUp() {
+        Utils.resetUtils()
+        inCallServiceCompat = InCallServiceCompat(mContext)
+    }
+
+    @After
+    fun onDestroy() {
+        Utils.resetUtils()
+    }
+
+    /***********************************************************************************************
+     *                           V2 APIs (Android U and above) tests
+     *********************************************************************************************/
+
+    /**
+     * For U+ devices using the v2 APIs, assert that the incoming call details either support
+     * the [CallsManager.PROPERTY_IS_TRANSACTIONAL] property (V) or the phone account supports
+     * transactional operations (U+).
+     */
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testCapabilityExchangeIncoming_V2() {
+        setUpV2Test()
+        addAndVerifyCallExtensionTypeE2E(TestUtils.INCOMING_CALL_ATTRIBUTES)
+    }
+
+    /**
+     * For U+ devices using the v2 APIs, assert that the outgoing call details either support
+     * the [CallsManager.PROPERTY_IS_TRANSACTIONAL] property (V) or the phone account supports
+     * transactional operations (U+).
+     */
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testCapabilityExchangeOutgoing_V2() {
+        setUpV2Test()
+        addAndVerifyCallExtensionTypeE2E(TestUtils.OUTGOING_CALL_ATTRIBUTES)
+    }
+
+    /***********************************************************************************************
+     *                           Backwards Compatibility Layer tests
+     *********************************************************************************************/
+
+    /**
+     * For pre-U devices using the backwards compatibility library, assert that the incoming call
+     * details contain the [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] key
+     */
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testCapabilityExchangeIncoming_BackwardsCompat() {
+        setUpBackwardsCompatTest()
+        addAndVerifyCallExtensionTypeE2E(
+            TestUtils.INCOMING_CALL_ATTRIBUTES,
+            waitForCallDetailExtras = true
+        )
+    }
+
+    /**
+     * For pre-U devices using the backwards compatibility library, assert that the outgoing call
+     * details contain the [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] key
+     */
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testCapabilityExchangeOutgoing_BackwardsCompat() {
+        setUpBackwardsCompatTest()
+        addAndVerifyCallExtensionTypeE2E(
+            TestUtils.OUTGOING_CALL_ATTRIBUTES,
+            waitForCallDetailExtras = true
+        )
+    }
+
+    /***********************************************************************************************
+     *                           Helpers
+     *********************************************************************************************/
+
+    /**
+     * Helper to add a call via CallsManager#addCall and block (if needed) until the connection
+     * extras are propagated into the call details.
+     *
+     * @param callAttributesCompat for the call.
+     * @param waitForCallDetailExtras used for waiting on the call details extras to be non-empty.
+     */
+    private fun addAndVerifyCallExtensionTypeE2E(
+        callAttributesCompat: CallAttributesCompat,
+        waitForCallDetailExtras: Boolean = false
+    ) {
+        runBlocking {
+            assertWithinTimeout_addCall(callAttributesCompat) {
+                launch {
+                    try {
+                        val call = TestUtils.waitOnInCallServiceToReachXCalls(1)
+                        Assert.assertNotNull("The returned Call object is <NULL>", call!!)
+
+                        // Enforce waiting logic to ensure that the call details extras are populated.
+                        if (waitForCallDetailExtras) {
+                            TestUtils.waitOnCallExtras(call)
+                        }
+
+                        // Assert the call extra or call property from the details
+                        assertCallExtraOrProperty(call)
+                    } finally {
+                        // Always send disconnect signal if possible.
+                        assertTrue(disconnect(DisconnectCause(DisconnectCause.LOCAL)))
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Helper to assert the call extra or property set on the call coming from Telecom.
+     */
+    private fun assertCallExtraOrProperty(call: Call) {
+        // Call details should be present at this point
+        val callDetails = call.details!!
+        if (TestUtils.buildIsAtLeastU()) {
+            if (TestUtils.buildIsAtLeastV()) {
+                assertTrue(callDetails.hasProperty(CallsManager.PROPERTY_IS_TRANSACTIONAL))
+            } else if (Utils.hasPlatformV2Apis()) {
+                // We need to check the phone account, which requires accessing TelecomManager.
+                // Directly resolving the extension type via resolveCallExtensionsType() will
+                // provide that functionality so no need to rewrite it here.
+                assertEquals(
+                    inCallServiceCompat.resolveCallExtensionsType(call),
+                    InCallServiceCompat.CAPABILITY_EXCHANGE)
+            }
+        } else {
+            val containsBackwardsCompatKey = callDetails.extras != null && callDetails.extras
+                .containsKey(CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED)
+            assertTrue(containsBackwardsCompatKey)
+        }
+    }
+}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/ExtensionAidlTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/ExtensionAidlTest.kt
new file mode 100644
index 0000000..d2afbba
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/ExtensionAidlTest.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.core.telecom.test
+
+import androidx.core.telecom.extensions.Capability
+import androidx.core.telecom.extensions.ICapabilityExchange
+import androidx.core.telecom.extensions.ICapabilityExchangeListener
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Not very useful for now, but tests the visibility of the AIDL files and ensures that they can
+ * be used as described.
+ */
+@RunWith(AndroidJUnit4::class)
+class ExtensionAidlTest {
+
+    class CapabilityExchangeImpl(
+        val onSetListener: (ICapabilityExchangeListener?) -> Unit = {},
+        val onNegotiateCapabilities: (MutableList<Capability>) -> Unit = {},
+        val onFeatureSetupComplete: () -> Unit = {}
+    ) : ICapabilityExchange.Stub() {
+        override fun setListener(l: ICapabilityExchangeListener?) {
+            onSetListener(l)
+        }
+
+        override fun negotiateCapabilities(capabilities: MutableList<Capability>?) {
+            capabilities?.let {
+                onNegotiateCapabilities(capabilities)
+            }
+        }
+
+        override fun featureSetupComplete() {
+            onFeatureSetupComplete()
+        }
+    }
+
+    class CapabilityExchangeListenerImpl(
+        val capabilitiesNegotiated: (MutableList<Capability>) -> Unit = {}
+    ) : ICapabilityExchangeListener.Stub() {
+        override fun onCapabilitiesNegotiated(filteredCapabilities: MutableList<Capability>?) {
+            filteredCapabilities?.let {
+                capabilitiesNegotiated(filteredCapabilities)
+            }
+        }
+    }
+
+    @SmallTest
+    @Test
+    fun testSetListener() {
+        // setup interfaces
+        var listener: ICapabilityExchangeListener? = null
+        val capExchange = CapabilityExchangeImpl(onSetListener = {
+            listener = it
+        })
+        val capExchangeListener = CapabilityExchangeListenerImpl()
+
+        // set the listener to non-null value
+        capExchange.setListener(capExchangeListener)
+        assertEquals(capExchangeListener, listener)
+
+        // set back to null value
+        capExchange.setListener(null)
+        assertNull(listener)
+    }
+
+    @SmallTest
+    @Test
+    fun testNegotiateCapabilities() {
+        // setup
+        var listener: ICapabilityExchangeListener? = null
+        var capabilities: MutableList<Capability>? = null
+        var filteredCapabilities: MutableList<Capability>? = null
+        val capExchange = CapabilityExchangeImpl(onSetListener = {
+             listener = it
+        }, onNegotiateCapabilities = {
+            capabilities = it
+        })
+        capExchange.onSetListener(CapabilityExchangeListenerImpl {
+            filteredCapabilities = it
+        })
+        val testCapability = Capability()
+        testCapability.featureVersion = 2
+        testCapability.featureId = 1
+        testCapability.supportedActions = intArrayOf(1, 2)
+        val testCapabilities = mutableListOf(testCapability)
+
+        // Send caps
+        capExchange.negotiateCapabilities(testCapabilities)
+        assertEquals(testCapabilities, capabilities)
+        assertNotNull(listener)
+
+        // Receive filtered caps
+        listener?.onCapabilitiesNegotiated(testCapabilities)
+        assertEquals(testCapabilities, filteredCapabilities)
+    }
+
+    @SmallTest
+    @Test
+    fun testSetupComplete() {
+        // setup
+        var isComplete = false
+        val capExchange = CapabilityExchangeImpl(onFeatureSetupComplete = {
+            isComplete = true
+        })
+
+        // ensure feature setup complete is called properly
+        capExchange.featureSetupComplete()
+        assertTrue(isComplete)
+    }
+}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/InCallServiceCompatTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/InCallServiceCompatTest.kt
new file mode 100644
index 0000000..94f3a2e
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/InCallServiceCompatTest.kt
@@ -0,0 +1,224 @@
+/*
+ * 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.core.telecom.test
+
+import android.Manifest
+import android.os.Build
+import android.telecom.Call
+import android.telecom.DisconnectCause
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallsManager
+import androidx.core.telecom.internal.InCallServiceCompat
+import androidx.core.telecom.internal.utils.Utils
+import androidx.core.telecom.test.utils.BaseTelecomTest
+import androidx.core.telecom.test.utils.TestUtils
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.rule.GrantPermissionRule
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * This test class verifies the [InCallServiceCompat] functionality around resolving the call
+ * extension type in order to determine the supported extensions between the VOIP app and the
+ * associated InCallServices. This test constructs calls via TelecomManager and modifies the call
+ * details (if required) to test each scenario. This is explained in more detail at the test level
+ * for each of the applicable cases below.
+ *
+ * Note: [Call] is package-private so we still need to leverage Telecom to create calls on our
+ * behalf for testing. The call properties and extras fields aren't mutable so we need to ensure
+ * that we wait for them to become available before accessing them.
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@RequiresApi(Build.VERSION_CODES.O)
+@RunWith(AndroidJUnit4::class)
+class InCallServiceCompatTest : BaseTelecomTest() {
+    private lateinit var inCallServiceCompat: InCallServiceCompat
+
+    /**
+     * Grant READ_PHONE_NUMBERS permission as part of testing
+     * [InCallServiceCompat#resolveCallExtensionsType].
+     */
+    @get:Rule
+    val readPhoneNumbersRule: GrantPermissionRule =
+        GrantPermissionRule.grant(Manifest.permission.READ_PHONE_NUMBERS)!!
+
+    companion object {
+        /**
+         * Logging for within the test class.
+         */
+        internal val TAG = InCallServiceCompatTest::class.simpleName
+    }
+
+    @Before
+    fun setUp() {
+        Utils.resetUtils()
+        inCallServiceCompat = InCallServiceCompat(mContext)
+    }
+
+    @After
+    fun onDestroy() {
+        Utils.resetUtils()
+    }
+
+    /**
+     * Assert that EXTRAS is the extension type for calls made using the V1.5 ConnectionService +
+     * Extensions Library (Auto). The call should have the [CallsManager.EXTRA_VOIP_API_VERSION]
+     * defined in the extras.
+     *
+     * The contents of the call detail extras need to be modified to test calls using the V1.5
+     * ConnectionService + Extensions library (until E2E testing can be supported for it). This
+     * requires us to manually insert the [CallsManager.EXTRA_VOIP_API_VERSION] key into the bundle.
+     */
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testResolveCallExtension_Extra() {
+        setUpBackwardsCompatTest()
+        val voipApiExtra = Pair(CallsManager.EXTRA_VOIP_API_VERSION, true)
+        addAndVerifyCallExtensionType(
+            TestUtils.OUTGOING_CALL_ATTRIBUTES,
+            InCallServiceCompat.EXTRAS,
+            extraToInclude = voipApiExtra)
+    }
+
+    /**
+     * Assert that CAPABILITY_EXCHANGE is the extension type for calls that either have the
+     * [CallsManager.PROPERTY_IS_TRANSACTIONAL] (V) defined as a property or the phone account
+     * supports transactional ops (U+). For pre-U devices, the call extras would define the
+     * [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] key.
+     *
+     * Note: The version codes for V is not available so we need to enforce a strict manual check
+     * to ensure the V test path is not executed by incompatible devices.
+     */
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testResolveCallExtension_CapabilityExchange() {
+        if (TestUtils.buildIsAtLeastU()) {
+            Log.w(TAG, "Setting up v2 tests for U+ device")
+            setUpV2Test()
+        } else {
+            Log.w(TAG, "Setting up backwards compatibility tests for pre-U device")
+            setUpBackwardsCompatTest()
+        }
+
+        // Add EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED for pre-U testing
+        val backwardsCompatExtra = if (!TestUtils.buildIsAtLeastU())
+            Pair(CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED, true)
+        else null
+        addAndVerifyCallExtensionType(
+            TestUtils.OUTGOING_CALL_ATTRIBUTES,
+            InCallServiceCompat.CAPABILITY_EXCHANGE,
+            // Waiting is not required for U+ testing
+            waitForCallDetailExtras = !TestUtils.buildIsAtLeastU(),
+            extraToInclude = backwardsCompatExtra
+        )
+    }
+
+    /**
+     * Assert that NONE is the extension type for calls with phone accounts that do not support
+     * transactional ops. Note that the caller must have had the read phone numbers permission.
+     *
+     * Note: Ensure that all extras are cleared before asserting extension type so that the phone
+     * account can be checked. For backwards compatibility tests, calls define the
+     * [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] key in the details extras so this
+     * needs to be disregarded.
+     *
+     * We need to ensure that all extras/properties are ignored for testing so that the phone
+     * account can be checked to see if it supports transactional ops. In jetpack, this can only be
+     * verified on pre-U devices as those phone accounts are registered in Telecom without
+     * transactional ops. Keep in mind that because these calls are set up for backwards
+     * compatibility, they will have the [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED]
+     * extra in the details (which will need to be ignored during testing).
+     */
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testResolveCallExtension_TransactionalOpsNotSupported() {
+        // Phone accounts that don't use the v2 APIs don't support transactional ops.
+        setUpBackwardsCompatTest()
+        addAndVerifyCallExtensionType(
+            TestUtils.OUTGOING_CALL_ATTRIBUTES,
+            InCallServiceCompat.NONE,
+            waitForCallDetailExtras = false
+        )
+    }
+
+    /***********************************************************************************************
+     *                           Helpers
+     *********************************************************************************************/
+
+    /**
+     * Helper to add a call via CallsManager#addCall and verify the extension type depending on
+     * the APIs that are leveraged.
+     *
+     * Note: The connection extras are not added into the call until the connection is successfully
+     * created. This is usually the case when the call moves from the CONNECTING state into either
+     * the DIALING/RINGING state. This would be the case for [CallsManager.EXTRA_VOIP_API_VERSION]
+     * (handled by auto) as well as for [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED]
+     * (see JetpackConnectionService#createSelfManagedConnection). Keep in mind that these extras
+     * would not be available in [InCalLService#onCallAdded], but after
+     * [Call#handleCreateConnectionSuccess] is invoked and the connection service extras are
+     * propagated into the call details via [Call#putConnectionServiceExtras].
+     *
+     * @param callAttributesCompat for the call.
+     * @param expectedType for call extension type.
+     * @param waitForCallDetailExtras used for waiting on the call details extras to be non-null.
+     * @param extraToInclude as part of the call extras.
+     */
+    private fun addAndVerifyCallExtensionType(
+        callAttributesCompat: CallAttributesCompat,
+        @InCallServiceCompat.Companion.CapabilityExchangeType expectedType: Int,
+        waitForCallDetailExtras: Boolean = true,
+        extraToInclude: Pair<String, Boolean>? = null
+    ) {
+        runBlocking {
+            assertWithinTimeout_addCall(callAttributesCompat) {
+                launch {
+                    val call = TestUtils.waitOnInCallServiceToReachXCalls(1)
+                    Assert.assertNotNull("The returned Call object is <NULL>", call!!)
+
+                    // Enforce waiting logic to ensure that the call details extras are populated.
+                    if (waitForCallDetailExtras) {
+                        TestUtils.waitOnCallExtras(call)
+                    }
+
+                    val callDetails = call.details
+                    // Clear out extras to isolate the testing scenarios.
+                    call.details.extras?.clear()
+                    // Add extraToInclude for testing.
+                    if (extraToInclude != null) {
+                        callDetails.extras?.putBoolean(extraToInclude.first, extraToInclude.second)
+                    }
+
+                    // Assert call extension type.
+                    assertEquals(expectedType, inCallServiceCompat.resolveCallExtensionsType(call))
+                    // Always send disconnect signal if possible.
+                    Assert.assertTrue(disconnect(DisconnectCause(DisconnectCause.LOCAL)))
+                }
+            }
+        }
+    }
+}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/ManagedConnectionService.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/ManagedConnectionService.kt
index 1d7e7d0..c5d9e8f 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/ManagedConnectionService.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/ManagedConnectionService.kt
@@ -23,6 +23,7 @@
 import android.telecom.PhoneAccountHandle
 import android.telecom.TelecomManager
 import android.telecom.VideoProfile
+import android.util.Log
 import androidx.annotation.RequiresApi
 import androidx.core.telecom.CallAttributesCompat
 import androidx.core.telecom.internal.utils.Utils
@@ -39,6 +40,7 @@
  */
 @RequiresApi(Build.VERSION_CODES.Q)
 class ManagedConnectionService : ConnectionService() {
+    val TAG = ManagedConnectionService::class.simpleName;
     data class PendingConnectionRequest(
         val callAttributes: CallAttributesCompat,
         val completableDeferred: CompletableDeferred<ManagedConnection>?
@@ -53,6 +55,8 @@
         phoneAccountHandle: PhoneAccountHandle,
         pendingConnectionRequest: PendingConnectionRequest,
     ) {
+        Log.i(TAG, "createConnectionRequest: request=[$pendingConnectionRequest]," +
+            " handle=[$phoneAccountHandle]")
         pendingConnectionRequest.callAttributes.mHandle = phoneAccountHandle
 
         // add request to list
@@ -95,6 +99,7 @@
         connectionManagerPhoneAccount: PhoneAccountHandle,
         request: ConnectionRequest
     ) {
+        Log.i(TAG, "onCreateOutgoingConnectionFailed: request=[$request]")
         val pendingRequest: PendingConnectionRequest? = findTargetPendingConnectionRequest(
             request, CallAttributesCompat.DIRECTION_OUTGOING
         )
@@ -119,6 +124,7 @@
         connectionManagerPhoneAccount: PhoneAccountHandle,
         request: ConnectionRequest
     ) {
+        Log.i(TAG, "onCreateIncomingConnectionFailed: request=[$request]")
         val pendingRequest: PendingConnectionRequest? = findTargetPendingConnectionRequest(
             request, CallAttributesCompat.DIRECTION_INCOMING
         )
@@ -130,6 +136,7 @@
         request: ConnectionRequest,
         direction: Int
     ): Connection? {
+        Log.i(TAG, "createSelfManagedConnection: request=[$request], direction=[$direction]")
         val targetRequest: PendingConnectionRequest =
             findTargetPendingConnectionRequest(request, direction) ?: return null
 
@@ -164,7 +171,7 @@
             jetpackConnection.connectionCapabilities =
                 Connection.CAPABILITY_HOLD or Connection.CAPABILITY_SUPPORT_HOLD
         }
-
+        Log.i(TAG, "createSelfManagedConnection: targetRequest=[$targetRequest]")
         targetRequest.completableDeferred?.complete(jetpackConnection)
         mPendingConnectionRequests.remove(targetRequest)
 
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
index 5aba448..e879ead 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.media.AudioManager
 import android.net.Uri
+import android.os.Build
 import android.os.Build.VERSION_CODES
 import android.os.UserHandle
 import android.os.UserManager
@@ -310,4 +311,40 @@
             )
         }
     }
+
+    /**
+     * Helper to wait on the call detail extras to be populated from the connection service
+     */
+    suspend fun waitOnCallExtras(call: Call) {
+        try {
+            withTimeout(TestUtils.WAIT_ON_CALL_STATE_TIMEOUT) {
+                while (isActive /* aka  within timeout window */ && (
+                        call.details?.extras == null || call.details.extras.isEmpty)) {
+                    yield() // another mechanism to stop the while loop if the coroutine is dead
+                    delay(1) // sleep x millisecond(s) instead of spamming check
+                }
+            }
+        } catch (e: TimeoutCancellationException) {
+            Log.i(TestUtils.LOG_TAG, "waitOnCallExtras: timeout reached")
+            TestUtils.dumpTelecom()
+            MockInCallService.destroyAllCalls()
+            throw AssertionError("Expected call detail extras to be non-null.")
+        }
+    }
+
+    /**
+     * Used for testing in V. The build version is not available for referencing so this helper
+     * performs a manual check instead.
+     */
+    fun buildIsAtLeastV(): Boolean {
+        // V is not referencable as a valid build version yet. Enforce strict manual check instead.
+        return Build.VERSION.SDK_INT > 34
+    }
+
+    /**
+     * Determine if the current build supports at least U.
+     */
+    fun buildIsAtLeastU(): Boolean {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+    }
 }
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/Capability.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/Capability.aidl
new file mode 100644
index 0000000..f922042
--- /dev/null
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/Capability.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 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.core.telecom.extensions;
+
+@JavaPassthrough(annotation="@androidx.core.telecom.util.ExperimentalAppActions")
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+parcelable Capability {
+    // ID of the feature (must be unique for each feature)
+    int featureId;
+    // version of the feature (used to ensure compatibility between applications using different
+    // versions of the telecom jetpack library)
+    int featureVersion;
+    // Array of ints which represent the actions associated with the feature that this application
+    // supports.
+    int[] supportedActions;
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ICapabilityExchange.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ICapabilityExchange.aidl
new file mode 100644
index 0000000..13f19d5
--- /dev/null
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ICapabilityExchange.aidl
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 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.core.telecom.extensions;
+
+import androidx.core.telecom.extensions.Capability;
+import androidx.core.telecom.extensions.ICapabilityExchangeListener;
+
+@JavaPassthrough(annotation="@androidx.core.telecom.util.ExperimentalAppActions")
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+oneway interface ICapabilityExchange {
+    const int VERSION = 1;
+
+    // Notify the remote of the singleton listener interface that must be used to perform return
+    // communication.
+    void setListener(ICapabilityExchangeListener l) = 0;
+    // Provide the capabilities of the service and request that capabilities of the remote are
+    // calculated. The response will be signalled back via
+    // ICapabilityExchangeListener#onCapabilitiesNegotiated
+    void negotiateCapabilities(in List<Capability> capabilities) = 1;
+    // All associated extension feature state has been synchronized and the user can now use the
+    // extensions.
+    void featureSetupComplete() = 2;
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ICapabilityExchangeListener.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ICapabilityExchangeListener.aidl
new file mode 100644
index 0000000..df9dc27
--- /dev/null
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ICapabilityExchangeListener.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 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.core.telecom.extensions;
+
+import androidx.core.telecom.extensions.Capability;
+
+@JavaPassthrough(annotation="@androidx.core.telecom.util.ExperimentalAppActions")
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+oneway interface ICapabilityExchangeListener {
+    // Called to complete the ICapabilityExchange#negotiateCapabilities request with the
+    // capabilities that both the VOIP service and InCallService support.
+    void onCapabilitiesNegotiated(in List<Capability> filteredCapabilities) = 0;
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
index fd67da4..88798fc 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
@@ -82,6 +82,11 @@
         annotation class Capability
 
         /**
+         * Set on Connections that are using ConnectionService+AUTO specific extension layer.
+         */
+        internal const val EXTRA_VOIP_API_VERSION = "android.telecom.extra.VOIP_API_VERSION"
+
+        /**
          * Set on Jetpack Connections that are emulating the transactional APIs using
          * ConnectionService.
          */
@@ -89,6 +94,15 @@
             "android.telecom.extra.VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED"
 
         /**
+         * The connection is using transactional call APIs.
+         *
+         *
+         * The underlying connection was added as a transactional call via the
+         * [TelecomManager.addCall] API.
+         */
+        internal const val PROPERTY_IS_TRANSACTIONAL = 0x00008000
+
+        /**
          * If your VoIP application does not want support any of the capabilities below, then your
          * application can register with [CAPABILITY_BASELINE].
          *
@@ -180,44 +194,42 @@
      * priority will prevent the [android.app.ActivityManager] from killing your application when
      * it is placed the background. Foreground execution priority is removed from your app when all
      * of your app's calls terminate or your app no longer posts a valid notification.
-     *
-     * Note: For outgoing calls, your application should either immediately post a
-     * [android.app.Notification.CallStyle] notification or delay adding the call via this
-     * addCall method until the remote side is ready.
+     * - Other things that should be noted:
+     *     - For outgoing calls, your application should either immediately post a
+     *       [android.app.Notification.CallStyle] notification or delay adding the call via this
+     *       addCall method until the remote side is ready.
+     *     - Each lambda function (onAnswer, onDisconnect, onSetActive, onSetInactive) has a
+     *       timeout of 5000 milliseconds. Failing to complete the suspend fun before the timeout
+     *       will result in a failed transaction.
      *
      * @param callAttributes     attributes of the new call (incoming or outgoing, address, etc. )
-     * @param block              DSL interface block that will run when the call is ready
-     * @param onAnswer           Telecom is informing your VoIP application to answer an incoming
-     *                           call and  set it to active. Telecom is requesting this on behalf
-     *                           of an system service (e.g. Automotive service) or a device (e.g.
-     *                           Wearable).
+     * @param onAnswer           where callType is the audio/video state the call should be
+     *                           answered as.  Telecom is informing your VoIP application to answer
+     *                           an incoming call and  set it to active. Telecom is requesting this
+     *                           on behalf of an system service (e.g. Automotive service) or a
+     *                           device (e.g. Wearable).  Return true to indicate your VoIP
+     *                           application can answer the call with the given
+     *                           [CallAttributesCompat.Companion.CallType]. Otherwise, return false
+     *                           to indicate your application is unable to process the request and
+     *                           telecom will cancel the external request.
      *
-     *                           @param callType that call is requesting to be answered as.
-     *
-     *                           @return true to indicate your VoIP application can answer the
-     *                           call with the given [CallAttributesCompat.Companion.CallType].
+     * @param onDisconnect       where disconnectCause represents the cause for disconnecting the
+     *                           call. Telecom is informing your VoIP application to disconnect the
+     *                           incoming call. Telecom is requesting this on behalf of an system
+     *                           service (e.g. Automotive service) or a device (e.g. Wearable).
+     *                           Return true when your VoIP  application has disconnected the call.
      *                           Otherwise, return false to indicate your application is unable to
-     *                           process the request and telecom will cancel the external request.
-     *
-     * @param onDisconnect       Telecom is informing your VoIP application to disconnect the
-     *                           incoming  call and set it to active. Telecom is requesting this on
-     *                           behalf of an system service (e.g. Automotive service) or a device
-     *                           (e.g. Wearable).
-     *
-     *                           @param disconnectCause represents the cause for disconnecting the
+     *                           process the request. However, Telecom will still disconnect the
+     *                           call from the platform and other services will no longer see the
      *                           call.
      *
-     *                           @return true when your VoIP application has disconnected the call.
-     *                           Otherwise, return false to indicate your application is unable to
-     *                           process the request. However, telecom will still
      * @param onSetActive        Telecom is informing your VoIP application to set the call active.
      *                           Telecom is requesting this on behalf of an system service (e.g.
-     *                           Automotive service) or a device (e.g. Wearable).
-     *
-     *                           @return true to indicate your VoIP application can set the call
-     *                           (that corresponds to this lambda function) to active.
-     *                           Otherwise, return false to indicate your application is unable to
-     *                           process the request and telecom will cancel the external request.
+     *                           Automotive service) or a device (e.g. Wearable). Return true to
+     *                           indicate your VoIP application can set the call (that corresponds
+     *                           to this lambda function) to active. Otherwise, return false to
+     *                           indicate your application is unable to process the request and
+     *                           telecom will cancel the external request.
      *
      * @param onSetInactive      Telecom is informing your VoIP application to set the call
      *                           inactive. This is the same as holding a call for two endpoints but
@@ -225,18 +237,15 @@
      *                           requesting this on behalf of an system service (e.g. Automotive
      *                           service) or a device (e.g.Wearable). Note: Your app must stop
      *                           using the microphone and playing incoming media when returning.
+     *                           Return true to indicate your application can set the call (that
+     *                           corresponds to this lambda function) to inactive. Otherwise, return
+     *                           false to indicate your application is unable to process the request
+     *                           and telecom will cancel the external request.
      *
-     *                           @return true to indicate your VoIP application can set the call
-     *                           (that corresponds to this lambda function) to inactive.
-     *                           Otherwise, return false to indicate your application is unable to
-     *                           process the request and telecom will cancel the external request.
+     * @param block              DSL interface block that will run when the call is ready
      *
-     * Note: Each lambda function (onAnswer, onDisconnect, onSetActive, onSetInactive) has a
-     * timeout of 5000 milliseconds. Failing to complete the suspend fun before the timeout will
-     * result in a failed transaction.
-     *
-     * @Throws UnsupportedOperationException if the device is on an invalid build
-     * @Throws CancellationException if the call failed to be added within 5000 milliseconds
+     * @throws UnsupportedOperationException if the device is on an invalid build
+     * @throws CancellationException if the call failed to be added within 5000 milliseconds
      */
     @RequiresPermission(value = "android.permission.MANAGE_OWN_CALLS")
     @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallCompat.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallCompat.kt
new file mode 100644
index 0000000..890707d
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallCompat.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.core.telecom.internal
+
+import android.telecom.Call
+import kotlinx.coroutines.CoroutineScope
+
+internal class CallCompat(call: Call, block: CoroutineScope.() -> Unit) {
+    private val mCall: Call = call
+    private val mBlock: CoroutineScope.() -> Unit = block
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/InCallServiceCompat.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/InCallServiceCompat.kt
new file mode 100644
index 0000000..f3063c0
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/InCallServiceCompat.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.core.telecom.internal
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.telecom.Call
+import android.telecom.InCallService
+import android.telecom.PhoneAccount
+import android.telecom.TelecomManager
+import android.util.Log
+import androidx.annotation.IntDef
+import androidx.annotation.RequiresApi
+import androidx.core.content.ContextCompat
+import androidx.core.telecom.CallsManager
+
+/**
+ * This class defines the Jetpack ICS layer which will be leveraged as part of supporting VOIP app
+ * actions.
+ */
+@RequiresApi(Build.VERSION_CODES.M)
+internal class InCallServiceCompat(context: Context) : InCallService() {
+    private val mContext: Context = context
+
+    companion object {
+        /**
+         * Constants used to denote the extension level supported by the VOIP app.
+         */
+        @Retention(AnnotationRetention.SOURCE)
+        @IntDef(NONE, EXTRAS, CAPABILITY_EXCHANGE, UNKNOWN)
+        internal annotation class CapabilityExchangeType
+
+        internal const val NONE = 0
+        internal const val EXTRAS = 1
+        internal const val CAPABILITY_EXCHANGE = 2
+        internal const val UNKNOWN = 3
+
+        private val TAG = InCallServiceCompat::class.simpleName
+    }
+
+    fun onCreateCall(call: Call): CallCompat {
+        Log.d(TAG, "onCreateCall: call = $call")
+        return with(this) {
+            CallCompat(call) {
+            }
+        }
+    }
+
+    fun onRemoveCall(call: CallCompat) {
+        Log.d(TAG, "onRemoveCall: call = $call")
+    }
+
+    /**
+     * Internal helper used by the [InCallService] to help resolve the call extension type. This
+     * is invoked before capability exchange between the [InCallService] and VOIP app starts to
+     * ensure the necessary features are enabled to support it.
+     *
+     * If the call is placed using the V1.5 ConnectionService + Extensions Library (Auto Case), the
+     * call will have the [CallsManager.EXTRA_VOIP_API_VERSION] defined in the extras. The call
+     * extension would be resolved as [InCallServiceCompat.EXTRAS].
+     *
+     * If the call is using the v2 APIs and the phone account associated with the call supports
+     * transactional ops (U+) or the call has the [CallsManager.PROPERTY_IS_TRANSACTIONAL] property
+     * defined (on V devices), then the extension type is [InCallServiceCompat.CAPABILITY_EXCHANGE].
+     *
+     * If the call is added via CallsManager#addCall on pre-U devices and the
+     * [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] is present in the call extras,
+     * the extension type also resolves to [InCallServiceCompat.CAPABILITY_EXCHANGE].
+     *
+     * In the case that none of the cases above apply and the phone account is found not to support
+     * transactional ops (assumes that caller has [android.Manifest.permission.READ_PHONE_NUMBERS]
+     * permission), then the extension type is [InCallServiceCompat.NONE].
+     *
+     * If the caller does not have the required permission to retrieve the phone account, then
+     * the extension type will be [InCallServiceCompat.UNKNOWN], until it can be resolved.
+     *
+     * @param call to resolve the extension type for.
+     * @return the extension type [InCallServiceCompat.CapabilityExchangeType] resolved for the
+     * call.
+     */
+    @RequiresApi(Build.VERSION_CODES.O)
+    @CapabilityExchangeType
+    internal fun resolveCallExtensionsType(call: Call): Int {
+        var callDetails = call.details
+        val callExtras = callDetails?.extras ?: Bundle()
+
+        if (callExtras.containsKey(CallsManager.EXTRA_VOIP_API_VERSION)) {
+            return EXTRAS
+        }
+        if (callDetails?.hasProperty(CallsManager.PROPERTY_IS_TRANSACTIONAL) == true || callExtras
+            .containsKey(CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED)) {
+            return CAPABILITY_EXCHANGE
+        }
+        // Verify read phone numbers permission to see if phone account supports transactional ops.
+        if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_PHONE_NUMBERS)
+            == PackageManager.PERMISSION_GRANTED) {
+            var telecomManager = mContext.getSystemService(Context.TELECOM_SERVICE)
+                as TelecomManager
+            var phoneAccount = telecomManager.getPhoneAccount(callDetails?.accountHandle)
+            if (phoneAccount?.hasCapabilities(
+                    PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS) == true) {
+                return CAPABILITY_EXCHANGE
+            } else {
+                return NONE
+            }
+        }
+
+        Log.i(TAG, "Unable to resolve call extension type. Returning $UNKNOWN.")
+        return UNKNOWN
+    }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/util/ExperimentalAppActions.java b/core/core-telecom/src/main/java/androidx/core/telecom/util/ExperimentalAppActions.java
new file mode 100644
index 0000000..65af69e
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/util/ExperimentalAppActions.java
@@ -0,0 +1,26 @@
+/*
+ * 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.core.telecom.util;
+
+import androidx.annotation.RequiresOptIn;
+
+/**
+ * This API is still experimental. Any features associated with this annotation are unstable and
+ * should not be used in production.
+ */
+@RequiresOptIn
+public @interface ExperimentalAppActions {}
diff --git a/core/haptics/haptics/api/current.txt b/core/haptics/haptics/api/current.txt
index 87cfe87..b4b7e54 100644
--- a/core/haptics/haptics/api/current.txt
+++ b/core/haptics/haptics/api/current.txt
@@ -3,7 +3,7 @@
 
   public interface HapticManager {
     method public static androidx.core.haptics.HapticManager create(android.content.Context context);
-    method @RequiresPermission(android.Manifest.permission.VIBRATE) public void play(androidx.core.haptics.signal.PredefinedEffect effect);
+    method @RequiresPermission(android.Manifest.permission.VIBRATE) public void play(androidx.core.haptics.signal.HapticSignal signal);
     field public static final androidx.core.haptics.HapticManager.Companion Companion;
   }
 
@@ -15,15 +15,155 @@
 
 package androidx.core.haptics.signal {
 
-  public final class PredefinedEffect {
-    field public static final androidx.core.haptics.signal.PredefinedEffect.Companion Companion;
-    field public static final androidx.core.haptics.signal.PredefinedEffect PredefinedClick;
-    field public static final androidx.core.haptics.signal.PredefinedEffect PredefinedDoubleClick;
-    field public static final androidx.core.haptics.signal.PredefinedEffect PredefinedHeavyClick;
-    field public static final androidx.core.haptics.signal.PredefinedEffect PredefinedTick;
+  public final class CompositionSignal extends androidx.core.haptics.signal.FiniteSignal {
+    ctor public CompositionSignal(java.util.List<? extends androidx.core.haptics.signal.CompositionSignal.Atom> atoms);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom click();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom click(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public static androidx.core.haptics.signal.CompositionSignal compositionOf(androidx.core.haptics.signal.CompositionSignal.Atom... atoms);
+    method public java.util.List<androidx.core.haptics.signal.CompositionSignal.Atom> getAtoms();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom lowTick();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom lowTick(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.core.haptics.signal.CompositionSignal.OffAtom off(java.time.Duration duration);
+    method public static androidx.core.haptics.signal.CompositionSignal.OffAtom off(long durationMillis);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickFall();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickFall(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickRise();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickRise(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom slowRise();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom slowRise(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom spin();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom spin(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom thud();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom thud(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom tick();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom tick(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    property public final java.util.List<androidx.core.haptics.signal.CompositionSignal.Atom> atoms;
+    field public static final androidx.core.haptics.signal.CompositionSignal.Companion Companion;
   }
 
-  public static final class PredefinedEffect.Companion {
+  public abstract static class CompositionSignal.Atom {
+  }
+
+  public static final class CompositionSignal.Companion {
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom click();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom click(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public androidx.core.haptics.signal.CompositionSignal compositionOf(androidx.core.haptics.signal.CompositionSignal.Atom... atoms);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom lowTick();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom lowTick(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.core.haptics.signal.CompositionSignal.OffAtom off(java.time.Duration duration);
+    method public androidx.core.haptics.signal.CompositionSignal.OffAtom off(long durationMillis);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickFall();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickFall(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickRise();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickRise(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom slowRise();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom slowRise(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom spin();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom spin(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom thud();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom thud(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom tick();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom tick(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+  }
+
+  public static final class CompositionSignal.OffAtom extends androidx.core.haptics.signal.CompositionSignal.Atom {
+    method public long getDurationMillis();
+    property public final long durationMillis;
+  }
+
+  public static final class CompositionSignal.PrimitiveAtom extends androidx.core.haptics.signal.CompositionSignal.Atom {
+    method public float getAmplitudeScale();
+    method public int getType();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom withAmplitudeScale(@FloatRange(from=0.0, to=1.0) float newAmplitudeScale);
+    property public final float amplitudeScale;
+    property public final int type;
+    field public static final int CLICK = 1; // 0x1
+    field public static final androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom.Companion Companion;
+    field public static final int LOW_TICK = 8; // 0x8
+    field public static final int QUICK_FALL = 6; // 0x6
+    field public static final int QUICK_RISE = 4; // 0x4
+    field public static final int SLOW_RISE = 5; // 0x5
+    field public static final int SPIN = 3; // 0x3
+    field public static final int THUD = 2; // 0x2
+    field public static final int TICK = 7; // 0x7
+  }
+
+  public static final class CompositionSignal.PrimitiveAtom.Companion {
+  }
+
+  public abstract class FiniteSignal extends androidx.core.haptics.signal.HapticSignal {
+  }
+
+  public abstract class HapticSignal {
+  }
+
+  public abstract class InfiniteSignal extends androidx.core.haptics.signal.HapticSignal {
+  }
+
+  public final class PredefinedEffectSignal extends androidx.core.haptics.signal.FiniteSignal {
+    method public static androidx.core.haptics.signal.PredefinedEffectSignal predefinedClick();
+    method public static androidx.core.haptics.signal.PredefinedEffectSignal predefinedDoubleClick();
+    method public static androidx.core.haptics.signal.PredefinedEffectSignal predefinedHeavyClick();
+    method public static androidx.core.haptics.signal.PredefinedEffectSignal predefinedTick();
+    field public static final androidx.core.haptics.signal.PredefinedEffectSignal.Companion Companion;
+  }
+
+  public static final class PredefinedEffectSignal.Companion {
+    method public androidx.core.haptics.signal.PredefinedEffectSignal predefinedClick();
+    method public androidx.core.haptics.signal.PredefinedEffectSignal predefinedDoubleClick();
+    method public androidx.core.haptics.signal.PredefinedEffectSignal predefinedHeavyClick();
+    method public androidx.core.haptics.signal.PredefinedEffectSignal predefinedTick();
+  }
+
+  public final class RepeatingWaveformSignal extends androidx.core.haptics.signal.InfiniteSignal {
+    method public androidx.core.haptics.signal.WaveformSignal? getInitialWaveform();
+    method public androidx.core.haptics.signal.WaveformSignal getRepeatingWaveform();
+    property public final androidx.core.haptics.signal.WaveformSignal? initialWaveform;
+    property public final androidx.core.haptics.signal.WaveformSignal repeatingWaveform;
+  }
+
+  public final class WaveformSignal extends androidx.core.haptics.signal.FiniteSignal {
+    ctor public WaveformSignal(java.util.List<? extends androidx.core.haptics.signal.WaveformSignal.Atom> atoms);
+    method public java.util.List<androidx.core.haptics.signal.WaveformSignal.Atom> getAtoms();
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom off(java.time.Duration duration);
+    method public static androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom off(long durationMillis);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(java.time.Duration duration);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(java.time.Duration duration, @FloatRange(from=0.0, to=1.0) float amplitude);
+    method public static androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(long durationMillis);
+    method public static androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(long durationMillis, @FloatRange(from=0.0, to=1.0) float amplitude);
+    method public androidx.core.haptics.signal.RepeatingWaveformSignal repeat();
+    method public static androidx.core.haptics.signal.RepeatingWaveformSignal repeatingWaveformOf(androidx.core.haptics.signal.WaveformSignal.Atom... atoms);
+    method public androidx.core.haptics.signal.RepeatingWaveformSignal thenRepeat(androidx.core.haptics.signal.WaveformSignal waveformToRepeat);
+    method public androidx.core.haptics.signal.RepeatingWaveformSignal thenRepeat(androidx.core.haptics.signal.WaveformSignal.Atom... atoms);
+    method public static androidx.core.haptics.signal.WaveformSignal waveformOf(androidx.core.haptics.signal.WaveformSignal.Atom... atoms);
+    property public final java.util.List<androidx.core.haptics.signal.WaveformSignal.Atom> atoms;
+    field public static final androidx.core.haptics.signal.WaveformSignal.Companion Companion;
+  }
+
+  public abstract static class WaveformSignal.Atom {
+  }
+
+  public static final class WaveformSignal.Companion {
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom off(java.time.Duration duration);
+    method public androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom off(long durationMillis);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(java.time.Duration duration);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(java.time.Duration duration, @FloatRange(from=0.0, to=1.0) float amplitude);
+    method public androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(long durationMillis);
+    method public androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(long durationMillis, @FloatRange(from=0.0, to=1.0) float amplitude);
+    method public androidx.core.haptics.signal.RepeatingWaveformSignal repeatingWaveformOf(androidx.core.haptics.signal.WaveformSignal.Atom... atoms);
+    method public androidx.core.haptics.signal.WaveformSignal waveformOf(androidx.core.haptics.signal.WaveformSignal.Atom... atoms);
+  }
+
+  public static final class WaveformSignal.ConstantVibrationAtom extends androidx.core.haptics.signal.WaveformSignal.Atom {
+    method public float getAmplitude();
+    method public long getDurationMillis();
+    property public final float amplitude;
+    property public final long durationMillis;
+    field public static final androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom.Companion Companion;
+    field public static final float DEFAULT_AMPLITUDE = -1.0f;
+  }
+
+  public static final class WaveformSignal.ConstantVibrationAtom.Companion {
   }
 
 }
diff --git a/core/haptics/haptics/api/restricted_current.txt b/core/haptics/haptics/api/restricted_current.txt
index 87cfe87..b4b7e54 100644
--- a/core/haptics/haptics/api/restricted_current.txt
+++ b/core/haptics/haptics/api/restricted_current.txt
@@ -3,7 +3,7 @@
 
   public interface HapticManager {
     method public static androidx.core.haptics.HapticManager create(android.content.Context context);
-    method @RequiresPermission(android.Manifest.permission.VIBRATE) public void play(androidx.core.haptics.signal.PredefinedEffect effect);
+    method @RequiresPermission(android.Manifest.permission.VIBRATE) public void play(androidx.core.haptics.signal.HapticSignal signal);
     field public static final androidx.core.haptics.HapticManager.Companion Companion;
   }
 
@@ -15,15 +15,155 @@
 
 package androidx.core.haptics.signal {
 
-  public final class PredefinedEffect {
-    field public static final androidx.core.haptics.signal.PredefinedEffect.Companion Companion;
-    field public static final androidx.core.haptics.signal.PredefinedEffect PredefinedClick;
-    field public static final androidx.core.haptics.signal.PredefinedEffect PredefinedDoubleClick;
-    field public static final androidx.core.haptics.signal.PredefinedEffect PredefinedHeavyClick;
-    field public static final androidx.core.haptics.signal.PredefinedEffect PredefinedTick;
+  public final class CompositionSignal extends androidx.core.haptics.signal.FiniteSignal {
+    ctor public CompositionSignal(java.util.List<? extends androidx.core.haptics.signal.CompositionSignal.Atom> atoms);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom click();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom click(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public static androidx.core.haptics.signal.CompositionSignal compositionOf(androidx.core.haptics.signal.CompositionSignal.Atom... atoms);
+    method public java.util.List<androidx.core.haptics.signal.CompositionSignal.Atom> getAtoms();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom lowTick();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom lowTick(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.core.haptics.signal.CompositionSignal.OffAtom off(java.time.Duration duration);
+    method public static androidx.core.haptics.signal.CompositionSignal.OffAtom off(long durationMillis);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickFall();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickFall(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickRise();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickRise(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom slowRise();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom slowRise(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom spin();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom spin(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom thud();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom thud(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom tick();
+    method public static androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom tick(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    property public final java.util.List<androidx.core.haptics.signal.CompositionSignal.Atom> atoms;
+    field public static final androidx.core.haptics.signal.CompositionSignal.Companion Companion;
   }
 
-  public static final class PredefinedEffect.Companion {
+  public abstract static class CompositionSignal.Atom {
+  }
+
+  public static final class CompositionSignal.Companion {
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom click();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom click(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public androidx.core.haptics.signal.CompositionSignal compositionOf(androidx.core.haptics.signal.CompositionSignal.Atom... atoms);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom lowTick();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom lowTick(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.core.haptics.signal.CompositionSignal.OffAtom off(java.time.Duration duration);
+    method public androidx.core.haptics.signal.CompositionSignal.OffAtom off(long durationMillis);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickFall();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickFall(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickRise();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom quickRise(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom slowRise();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom slowRise(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom spin();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom spin(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom thud();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom thud(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom tick();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom tick(optional @FloatRange(from=0.0, to=1.0) float amplitudeScale);
+  }
+
+  public static final class CompositionSignal.OffAtom extends androidx.core.haptics.signal.CompositionSignal.Atom {
+    method public long getDurationMillis();
+    property public final long durationMillis;
+  }
+
+  public static final class CompositionSignal.PrimitiveAtom extends androidx.core.haptics.signal.CompositionSignal.Atom {
+    method public float getAmplitudeScale();
+    method public int getType();
+    method public androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom withAmplitudeScale(@FloatRange(from=0.0, to=1.0) float newAmplitudeScale);
+    property public final float amplitudeScale;
+    property public final int type;
+    field public static final int CLICK = 1; // 0x1
+    field public static final androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom.Companion Companion;
+    field public static final int LOW_TICK = 8; // 0x8
+    field public static final int QUICK_FALL = 6; // 0x6
+    field public static final int QUICK_RISE = 4; // 0x4
+    field public static final int SLOW_RISE = 5; // 0x5
+    field public static final int SPIN = 3; // 0x3
+    field public static final int THUD = 2; // 0x2
+    field public static final int TICK = 7; // 0x7
+  }
+
+  public static final class CompositionSignal.PrimitiveAtom.Companion {
+  }
+
+  public abstract class FiniteSignal extends androidx.core.haptics.signal.HapticSignal {
+  }
+
+  public abstract class HapticSignal {
+  }
+
+  public abstract class InfiniteSignal extends androidx.core.haptics.signal.HapticSignal {
+  }
+
+  public final class PredefinedEffectSignal extends androidx.core.haptics.signal.FiniteSignal {
+    method public static androidx.core.haptics.signal.PredefinedEffectSignal predefinedClick();
+    method public static androidx.core.haptics.signal.PredefinedEffectSignal predefinedDoubleClick();
+    method public static androidx.core.haptics.signal.PredefinedEffectSignal predefinedHeavyClick();
+    method public static androidx.core.haptics.signal.PredefinedEffectSignal predefinedTick();
+    field public static final androidx.core.haptics.signal.PredefinedEffectSignal.Companion Companion;
+  }
+
+  public static final class PredefinedEffectSignal.Companion {
+    method public androidx.core.haptics.signal.PredefinedEffectSignal predefinedClick();
+    method public androidx.core.haptics.signal.PredefinedEffectSignal predefinedDoubleClick();
+    method public androidx.core.haptics.signal.PredefinedEffectSignal predefinedHeavyClick();
+    method public androidx.core.haptics.signal.PredefinedEffectSignal predefinedTick();
+  }
+
+  public final class RepeatingWaveformSignal extends androidx.core.haptics.signal.InfiniteSignal {
+    method public androidx.core.haptics.signal.WaveformSignal? getInitialWaveform();
+    method public androidx.core.haptics.signal.WaveformSignal getRepeatingWaveform();
+    property public final androidx.core.haptics.signal.WaveformSignal? initialWaveform;
+    property public final androidx.core.haptics.signal.WaveformSignal repeatingWaveform;
+  }
+
+  public final class WaveformSignal extends androidx.core.haptics.signal.FiniteSignal {
+    ctor public WaveformSignal(java.util.List<? extends androidx.core.haptics.signal.WaveformSignal.Atom> atoms);
+    method public java.util.List<androidx.core.haptics.signal.WaveformSignal.Atom> getAtoms();
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom off(java.time.Duration duration);
+    method public static androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom off(long durationMillis);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(java.time.Duration duration);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(java.time.Duration duration, @FloatRange(from=0.0, to=1.0) float amplitude);
+    method public static androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(long durationMillis);
+    method public static androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(long durationMillis, @FloatRange(from=0.0, to=1.0) float amplitude);
+    method public androidx.core.haptics.signal.RepeatingWaveformSignal repeat();
+    method public static androidx.core.haptics.signal.RepeatingWaveformSignal repeatingWaveformOf(androidx.core.haptics.signal.WaveformSignal.Atom... atoms);
+    method public androidx.core.haptics.signal.RepeatingWaveformSignal thenRepeat(androidx.core.haptics.signal.WaveformSignal waveformToRepeat);
+    method public androidx.core.haptics.signal.RepeatingWaveformSignal thenRepeat(androidx.core.haptics.signal.WaveformSignal.Atom... atoms);
+    method public static androidx.core.haptics.signal.WaveformSignal waveformOf(androidx.core.haptics.signal.WaveformSignal.Atom... atoms);
+    property public final java.util.List<androidx.core.haptics.signal.WaveformSignal.Atom> atoms;
+    field public static final androidx.core.haptics.signal.WaveformSignal.Companion Companion;
+  }
+
+  public abstract static class WaveformSignal.Atom {
+  }
+
+  public static final class WaveformSignal.Companion {
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom off(java.time.Duration duration);
+    method public androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom off(long durationMillis);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(java.time.Duration duration);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(java.time.Duration duration, @FloatRange(from=0.0, to=1.0) float amplitude);
+    method public androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(long durationMillis);
+    method public androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom on(long durationMillis, @FloatRange(from=0.0, to=1.0) float amplitude);
+    method public androidx.core.haptics.signal.RepeatingWaveformSignal repeatingWaveformOf(androidx.core.haptics.signal.WaveformSignal.Atom... atoms);
+    method public androidx.core.haptics.signal.WaveformSignal waveformOf(androidx.core.haptics.signal.WaveformSignal.Atom... atoms);
+  }
+
+  public static final class WaveformSignal.ConstantVibrationAtom extends androidx.core.haptics.signal.WaveformSignal.Atom {
+    method public float getAmplitude();
+    method public long getDurationMillis();
+    property public final float amplitude;
+    property public final long durationMillis;
+    field public static final androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom.Companion Companion;
+    field public static final float DEFAULT_AMPLITUDE = -1.0f;
+  }
+
+  public static final class WaveformSignal.ConstantVibrationAtom.Companion {
   }
 
 }
diff --git a/core/haptics/haptics/build.gradle b/core/haptics/haptics/build.gradle
index 203d4db..7dc493a 100644
--- a/core/haptics/haptics/build.gradle
+++ b/core/haptics/haptics/build.gradle
@@ -15,7 +15,6 @@
  */
 
 import androidx.build.LibraryType
-import androidx.build.RunApiTasks
 
 plugins {
     id("AndroidXPlugin")
@@ -33,8 +32,7 @@
 
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRunner)
-    androidTestImplementation(libs.mockitoCore4)
-    androidTestImplementation(libs.dexmakerMockitoInline)
+    implementation(libs.truth)
 }
 
 android {
diff --git a/core/haptics/haptics/integration-tests/demos/src/main/AndroidManifest.xml b/core/haptics/haptics/integration-tests/demos/src/main/AndroidManifest.xml
index 4c19b57..ea32d19 100644
--- a/core/haptics/haptics/integration-tests/demos/src/main/AndroidManifest.xml
+++ b/core/haptics/haptics/integration-tests/demos/src/main/AndroidManifest.xml
@@ -22,7 +22,7 @@
         android:theme="@style/AppTheme">
         <activity
             android:allowBackup="false"
-            android:name=".HapticSamplesActivity"
+            android:name=".HapticDemosActivity"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/core/haptics/haptics/integration-tests/demos/src/main/java/androidx/core/haptics/demos/HapticDemosActivity.kt b/core/haptics/haptics/integration-tests/demos/src/main/java/androidx/core/haptics/demos/HapticDemosActivity.kt
new file mode 100644
index 0000000..b147e5c
--- /dev/null
+++ b/core/haptics/haptics/integration-tests/demos/src/main/java/androidx/core/haptics/demos/HapticDemosActivity.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.core.haptics.demos
+
+import android.os.Bundle
+import android.widget.Button
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.haptics.HapticManager
+import androidx.core.haptics.signal.CompositionSignal.Companion.click
+import androidx.core.haptics.signal.CompositionSignal.Companion.compositionOf
+import androidx.core.haptics.signal.PredefinedEffectSignal.Companion.predefinedClick
+import androidx.core.haptics.signal.WaveformSignal.Companion.off
+import androidx.core.haptics.signal.WaveformSignal.Companion.on
+import androidx.core.haptics.signal.WaveformSignal.Companion.waveformOf
+
+/**
+ * Demonstrations of multiple haptic signal samples.
+ */
+class HapticDemosActivity : AppCompatActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.haptics_demos_activity)
+
+        val hapticManager = HapticManager.create(this)
+        findViewById<Button>(R.id.standard_click_btn).setOnClickListener {
+            hapticManager.play(predefinedClick())
+        }
+        findViewById<Button>(R.id.scaled_click_btn).setOnClickListener {
+            hapticManager.play(compositionOf(click().withAmplitudeScale(0.8f)))
+        }
+        findViewById<Button>(R.id.on_off_pattern_btn).setOnClickListener {
+            hapticManager.play(
+                waveformOf(
+                    on(durationMillis = 350),
+                    off(durationMillis = 250),
+                    on(durationMillis = 350),
+                )
+            )
+        }
+        findViewById<Button>(R.id.repeating_waveform_btn).setOnClickListener {
+            hapticManager.play(
+                waveformOf(
+                    on(durationMillis = 20),
+                    off(durationMillis = 50),
+                    on(durationMillis = 20),
+                ).thenRepeat(
+                    // 500ms off
+                    off(durationMillis = 500),
+                    // 600ms ramp up with 50% increments
+                    on(durationMillis = 100, amplitude = 0.1f),
+                    on(durationMillis = 100, amplitude = 0.15f),
+                    on(durationMillis = 100, amplitude = 0.22f),
+                    on(durationMillis = 100, amplitude = 0.34f),
+                    on(durationMillis = 100, amplitude = 0.51f),
+                    on(durationMillis = 100, amplitude = 0.76f),
+                    // 400ms at max amplitude
+                    on(durationMillis = 400, amplitude = 1f),
+                )
+            )
+        }
+    }
+}
diff --git a/core/haptics/haptics/integration-tests/demos/src/main/java/androidx/core/haptics/demos/HapticSamplesActivity.kt b/core/haptics/haptics/integration-tests/demos/src/main/java/androidx/core/haptics/demos/HapticSamplesActivity.kt
deleted file mode 100644
index 5fb1076..0000000
--- a/core/haptics/haptics/integration-tests/demos/src/main/java/androidx/core/haptics/demos/HapticSamplesActivity.kt
+++ /dev/null
@@ -1,39 +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.core.haptics.demos
-
-import android.os.Bundle
-import android.widget.Button
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.haptics.HapticManager
-import androidx.core.haptics.signal.PredefinedEffect.Companion.PredefinedClick
-
-/**
- * Demo with multiple selection of haptic effect samples.
- */
-class HapticSamplesActivity : AppCompatActivity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.haptic_samples_activity)
-
-        val hapticManager = HapticManager.create(this)
-        findViewById<Button>(R.id.standard_click_btn).setOnClickListener {
-            hapticManager.play(PredefinedClick)
-        }
-    }
-}
diff --git a/core/haptics/haptics/integration-tests/demos/src/main/res/layout/haptic_samples_activity.xml b/core/haptics/haptics/integration-tests/demos/src/main/res/layout/haptic_samples_activity.xml
deleted file mode 100644
index 24002ee..0000000
--- a/core/haptics/haptics/integration-tests/demos/src/main/res/layout/haptic_samples_activity.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  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.
-  -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:orientation="vertical"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:gravity="center"
-    tools:context="androidx.core.haptics.demos.HapticSamplesActivity">
-
-    <Button
-        android:id="@+id/standard_click_btn"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="@string/standard_click" />
-
-</LinearLayout>
diff --git a/core/haptics/haptics/integration-tests/demos/src/main/res/layout/haptics_demos_activity.xml b/core/haptics/haptics/integration-tests/demos/src/main/res/layout/haptics_demos_activity.xml
new file mode 100644
index 0000000..8b48bab
--- /dev/null
+++ b/core/haptics/haptics/integration-tests/demos/src/main/res/layout/haptics_demos_activity.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
+    tools:context=".HapticDemosActivity">
+
+    <Button
+        android:id="@+id/standard_click_btn"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/standard_click" />
+
+    <Button
+        android:id="@+id/scaled_click_btn"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/scaled_click" />
+
+    <Button
+        android:id="@+id/on_off_pattern_btn"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/on_off_pattern" />
+
+    <Button
+        android:id="@+id/repeating_waveform_btn"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/repeating_waveform" />
+
+</LinearLayout>
diff --git a/core/haptics/haptics/integration-tests/demos/src/main/res/values/donottranslate-strings.xml b/core/haptics/haptics/integration-tests/demos/src/main/res/values/donottranslate-strings.xml
index 903a0be..e79e071 100644
--- a/core/haptics/haptics/integration-tests/demos/src/main/res/values/donottranslate-strings.xml
+++ b/core/haptics/haptics/integration-tests/demos/src/main/res/values/donottranslate-strings.xml
@@ -17,4 +17,7 @@
 <resources>
     <string name="app_name">Haptic Demos</string>
     <string name="standard_click">Standard Click</string>
+    <string name="scaled_click">Scaled Click</string>
+    <string name="on_off_pattern">On/Off Pattern</string>
+    <string name="repeating_waveform">Repeating Waveform</string>
 </resources>
diff --git a/core/haptics/haptics/samples/src/main/java/androidx/core/haptics/samples/CompositionSignalSamples.kt b/core/haptics/haptics/samples/src/main/java/androidx/core/haptics/samples/CompositionSignalSamples.kt
new file mode 100644
index 0000000..856b999
--- /dev/null
+++ b/core/haptics/haptics/samples/src/main/java/androidx/core/haptics/samples/CompositionSignalSamples.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.core.haptics.samples
+
+import androidx.annotation.Sampled
+import androidx.core.haptics.signal.CompositionSignal.Companion.compositionOf
+import androidx.core.haptics.signal.CompositionSignal.Companion.off
+import androidx.core.haptics.signal.CompositionSignal.Companion.quickFall
+import androidx.core.haptics.signal.CompositionSignal.Companion.slowRise
+import androidx.core.haptics.signal.CompositionSignal.Companion.thud
+
+/**
+ * Sample showing how to create a composition signal with scaled effects and off atoms.
+ */
+@Sampled
+fun CompositionSignalOfScaledEffectsAndOff() {
+    compositionOf(
+        slowRise().withAmplitudeScale(0.7f),
+        quickFall().withAmplitudeScale(0.7f),
+        off(durationMillis = 50),
+        thud(),
+    )
+}
diff --git a/core/haptics/haptics/samples/src/main/java/androidx/core/haptics/samples/HapticManagerSamples.kt b/core/haptics/haptics/samples/src/main/java/androidx/core/haptics/samples/HapticManagerSamples.kt
index e1d3df4..83ce76d 100644
--- a/core/haptics/haptics/samples/src/main/java/androidx/core/haptics/samples/HapticManagerSamples.kt
+++ b/core/haptics/haptics/samples/src/main/java/androidx/core/haptics/samples/HapticManagerSamples.kt
@@ -19,7 +19,12 @@
 import android.content.Context
 import androidx.annotation.Sampled
 import androidx.core.haptics.HapticManager
-import androidx.core.haptics.signal.PredefinedEffect.Companion.PredefinedClick
+import androidx.core.haptics.signal.CompositionSignal.Companion.compositionOf
+import androidx.core.haptics.signal.CompositionSignal.Companion.off
+import androidx.core.haptics.signal.CompositionSignal.Companion.quickFall
+import androidx.core.haptics.signal.CompositionSignal.Companion.slowRise
+import androidx.core.haptics.signal.CompositionSignal.Companion.thud
+import androidx.core.haptics.signal.PredefinedEffectSignal.Companion.predefinedClick
 
 /**
  * Sample showing how to play a standard click haptic effect on the system vibrator.
@@ -27,5 +32,20 @@
 @Sampled
 fun PlaySystemStandardClick(context: Context) {
     val hapticManager = HapticManager.create(context)
-    hapticManager.play(PredefinedClick)
+    hapticManager.play(predefinedClick())
+}
+
+/**
+ * Sample showing how to play a haptic signal on a vibrator.
+ */
+@Sampled
+fun PlayHapticSignal(hapticManager: HapticManager) {
+    hapticManager.play(
+        compositionOf(
+            slowRise(),
+            quickFall(),
+            off(durationMillis = 50),
+            thud(),
+        )
+    )
 }
diff --git a/core/haptics/haptics/samples/src/main/java/androidx/core/haptics/samples/WaveformSignalSamples.kt b/core/haptics/haptics/samples/src/main/java/androidx/core/haptics/samples/WaveformSignalSamples.kt
new file mode 100644
index 0000000..3b7b597
--- /dev/null
+++ b/core/haptics/haptics/samples/src/main/java/androidx/core/haptics/samples/WaveformSignalSamples.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.core.haptics.samples
+
+import androidx.annotation.Sampled
+import androidx.core.haptics.signal.WaveformSignal
+import androidx.core.haptics.signal.WaveformSignal.Companion.off
+import androidx.core.haptics.signal.WaveformSignal.Companion.on
+import androidx.core.haptics.signal.WaveformSignal.Companion.repeatingWaveformOf
+import androidx.core.haptics.signal.WaveformSignal.Companion.waveformOf
+
+/**
+ * Sample showing how to create an on-off pattern.
+ */
+@Sampled
+fun PatternWaveform() {
+    waveformOf(
+        on(durationMillis = 250),
+        off(durationMillis = 350),
+        on(durationMillis = 250),
+    )
+}
+
+/**
+ * Sample showing how to create an infinite haptic signal a repeating step waveform.
+ */
+@Sampled
+fun PatternWaveformRepeat() {
+    waveformOf(
+        on(durationMillis = 100),
+        off(durationMillis = 50),
+        on(durationMillis = 100),
+        off(durationMillis = 50),
+    ).repeat()
+}
+
+/**
+ * Sample showing how to create an amplitude step waveform.
+ */
+@Sampled
+fun AmplitudeWaveform() {
+    waveformOf(
+        on(durationMillis = 10, amplitude = 0.2f),
+        on(durationMillis = 20, amplitude = 0.4f),
+        on(durationMillis = 30, amplitude = 0.8f),
+        on(durationMillis = 40, amplitude = 1f),
+        off(durationMillis = 50),
+        on(durationMillis = 50),
+    )
+}
+
+/**
+ * Sample showing how to create an amplitude step waveform.
+ */
+@Sampled
+fun RepeatingAmplitudeWaveform() {
+    repeatingWaveformOf(
+        on(durationMillis = 100),
+        off(durationMillis = 50),
+    )
+}
+
+/**
+ * Sample showing how to create an infinite haptic signal as repeating step waveform.
+ */
+@Sampled
+fun PatternThenRepeatExistingWaveform(waveformSignal: WaveformSignal) {
+    waveformOf(
+        on(durationMillis = 100),
+        off(durationMillis = 50),
+        on(durationMillis = 100),
+        off(durationMillis = 500),
+    ).thenRepeat(waveformSignal)
+}
+
+/**
+ * Sample showing how to create an infinite haptic signal as repeating step waveform.
+ */
+@Sampled
+fun PatternThenRepeatAmplitudeWaveform() {
+    waveformOf(
+        on(durationMillis = 100),
+        off(durationMillis = 50),
+        on(durationMillis = 100),
+    ).thenRepeat(
+        on(durationMillis = 500, amplitude = 0f),
+        on(durationMillis = 100, amplitude = 0.2f),
+        on(durationMillis = 200, amplitude = 0.4f),
+        on(durationMillis = 300, amplitude = 0.8f),
+        on(durationMillis = 400, amplitude = 1f),
+    )
+}
diff --git a/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/PlayCompositionSignalTest.kt b/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/PlayCompositionSignalTest.kt
new file mode 100644
index 0000000..584ffc637
--- /dev/null
+++ b/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/PlayCompositionSignalTest.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.core.haptics
+
+import android.os.Build
+import androidx.core.haptics.signal.CompositionSignal.Companion.click
+import androidx.core.haptics.signal.CompositionSignal.Companion.compositionOf
+import androidx.core.haptics.signal.CompositionSignal.Companion.lowTick
+import androidx.core.haptics.signal.CompositionSignal.Companion.off
+import androidx.core.haptics.signal.CompositionSignal.Companion.quickFall
+import androidx.core.haptics.signal.CompositionSignal.Companion.quickRise
+import androidx.core.haptics.signal.CompositionSignal.Companion.slowRise
+import androidx.core.haptics.signal.CompositionSignal.Companion.spin
+import androidx.core.haptics.signal.CompositionSignal.Companion.thud
+import androidx.core.haptics.signal.CompositionSignal.Companion.tick
+import androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom
+import androidx.core.haptics.testing.CompositionPrimitive
+import androidx.core.haptics.testing.FakeVibratorSubject.Companion.assertThat
+import androidx.core.haptics.testing.FullVibrator
+import androidx.core.haptics.testing.vibration
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import kotlin.time.Duration.Companion.milliseconds
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.Parameterized
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+@RunWith(Parameterized::class)
+@SmallTest
+class PlayCompositionSignalSdk30AndAboveTest(
+    private val primitive: PrimitiveAtom,
+) {
+    private val fakeVibrator = FullVibrator()
+    private val hapticManager = HapticManager.createForVibrator(fakeVibrator)
+
+    @Test
+    fun play_vibratesWithSupportedPrimitives() {
+        hapticManager.play(
+            compositionOf(
+                primitive,
+                off(durationMillis = 50),
+                primitive.withAmplitudeScale(0.5f),
+                off(durationMillis = 100),
+                primitive.withAmplitudeScale(0.8f),
+                off(durationMillis = 200),
+            )
+        )
+        assertThat(fakeVibrator).vibratedExactly(
+            vibration(
+                CompositionPrimitive(primitive),
+                CompositionPrimitive(primitive, scale = 0.5f, delay = 50.milliseconds),
+                CompositionPrimitive(primitive, scale = 0.8f, delay = 100.milliseconds),
+                // Skips trailing 200ms delay from vibrate call
+            )
+        )
+    }
+
+    companion object {
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "primitive:{0}")
+        fun data(): Collection<Any> {
+            val primitives = mutableListOf(
+                tick(),
+                click(),
+                slowRise(),
+                quickRise(),
+                quickFall(),
+            )
+            if (Build.VERSION.SDK_INT >= 31) {
+                primitives.apply {
+                    add(lowTick())
+                    add(spin())
+                    add(thud())
+                }
+            }
+            return primitives
+        }
+    }
+}
+
+@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+@RunWith(Parameterized::class)
+@SmallTest
+class PlayCompositionSignalBelowSdk30Test(
+    private val primitive: PrimitiveAtom,
+) {
+    private val fakeVibrator = FullVibrator()
+    private val hapticManager = HapticManager.createForVibrator(fakeVibrator)
+
+    @Test
+    fun play_doesNotVibrate() {
+        hapticManager.play(compositionOf(primitive))
+        assertThat(fakeVibrator).neverVibrated()
+    }
+
+    companion object {
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "primitive:{0}")
+        fun data(): Collection<Any> = mutableListOf(
+            tick(),
+            click(),
+            slowRise(),
+            quickRise(),
+            quickFall(),
+            lowTick(),
+            spin(),
+            thud(),
+        )
+    }
+}
+
+@RunWith(JUnit4::class)
+@SmallTest
+class PlayCompositionSignalPartialPrimitiveSdkSupportTest {
+    private val fakeVibrator = FullVibrator()
+    private val hapticManager = HapticManager.createForVibrator(fakeVibrator)
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R, maxSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun play_api30AndPrimitiveFromApi31AndAbove_doesNotVibrate() {
+        hapticManager.play(compositionOf(lowTick()))
+        hapticManager.play(compositionOf(thud()))
+        hapticManager.play(compositionOf(spin()))
+        // Mix supported/unsupported primitives
+        hapticManager.play(compositionOf(tick(), lowTick()))
+        assertThat(fakeVibrator).neverVibrated()
+    }
+}
+
+@RunWith(JUnit4::class)
+@SmallTest
+class PlayCompositionSignalAllSdksTest {
+
+    @Test
+    fun compositionOf_withNoAtom_throwsException() {
+        assertThrows(IllegalArgumentException::class.java) {
+            compositionOf()
+        }
+    }
+
+    @Test
+    fun off_withNegativeDuration_throwsException() {
+        assertThrows(IllegalArgumentException::class.java) {
+            off(durationMillis = -10)
+        }
+    }
+
+    @Test
+    fun withAmplitudeScale_withAmplitudeLargerThanOne_throwsException() {
+        assertThrows(IllegalArgumentException::class.java) {
+            click().withAmplitudeScale(2f)
+        }
+    }
+
+    @Test
+    fun withAmplitudeScale_withNegativeAmplitude_throwsException() {
+        assertThrows(IllegalArgumentException::class.java) {
+            click().withAmplitudeScale(-1f)
+        }
+    }
+}
diff --git a/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/PlayPredefinedEffectSignalTest.kt b/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/PlayPredefinedEffectSignalTest.kt
new file mode 100644
index 0000000..f5572c1
--- /dev/null
+++ b/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/PlayPredefinedEffectSignalTest.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.core.haptics
+
+import android.os.Build
+import androidx.core.haptics.signal.PredefinedEffectSignal
+import androidx.core.haptics.signal.PredefinedEffectSignal.Companion.predefinedClick
+import androidx.core.haptics.signal.PredefinedEffectSignal.Companion.predefinedDoubleClick
+import androidx.core.haptics.signal.PredefinedEffectSignal.Companion.predefinedHeavyClick
+import androidx.core.haptics.signal.PredefinedEffectSignal.Companion.predefinedTick
+import androidx.core.haptics.testing.FakeVibratorSubject.Companion.assertThat
+import androidx.core.haptics.testing.PredefinedEffectsAndAmplitudeVibrator
+import androidx.core.haptics.testing.vibration
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@SmallTest
+class PlayPredefinedEffectSignalTest(
+    private val effect: PredefinedEffectSignal,
+    private val expectedFallbackPattern: LongArray,
+) {
+    private val fakeVibrator = PredefinedEffectsAndAmplitudeVibrator()
+    private val hapticManager = HapticManager.createForVibrator(fakeVibrator)
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun play_api29AndAbove_vibratesWithPredefinedEffect() {
+        hapticManager.play(effect)
+        assertThat(fakeVibrator).vibratedExactly(vibration(effect))
+    }
+
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.P)
+    @Test
+    fun play_belowApi29_vibratesWithFallbackPattern() {
+        hapticManager.play(effect)
+        assertThat(fakeVibrator).vibratedExactly(vibration(expectedFallbackPattern))
+    }
+
+    companion object {
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "effect:{0}, expectedFallbackPattern:{1}")
+        fun data(): Collection<Array<Any>> = listOf(
+            arrayOf(predefinedTick(), longArrayOf(0, 10)),
+            arrayOf(predefinedClick(), longArrayOf(0, 20)),
+            arrayOf(predefinedHeavyClick(), longArrayOf(0, 30)),
+            arrayOf(predefinedDoubleClick(), longArrayOf(0, 30, 100, 30)),
+        )
+    }
+}
diff --git a/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/PlayPredefinedEffectTest.kt b/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/PlayPredefinedEffectTest.kt
deleted file mode 100644
index fab916e..0000000
--- a/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/PlayPredefinedEffectTest.kt
+++ /dev/null
@@ -1,76 +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.core.haptics
-
-import android.os.Build
-import android.os.VibrationEffect
-import android.os.Vibrator
-import androidx.core.haptics.signal.PredefinedEffect
-import androidx.core.haptics.signal.PredefinedEffect.Companion.PredefinedClick
-import androidx.core.haptics.signal.PredefinedEffect.Companion.PredefinedDoubleClick
-import androidx.core.haptics.signal.PredefinedEffect.Companion.PredefinedHeavyClick
-import androidx.core.haptics.signal.PredefinedEffect.Companion.PredefinedTick
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-
-@RunWith(Parameterized::class)
-@SmallTest
-class PlayPredefinedEffectTest(
-    private val effect: PredefinedEffect,
-    private val expectedFallbackPattern: LongArray,
-) {
-    // Vibrator has package-protected constructor and cannot be extended by a FakeVibrator
-    // TODO(b/275084444): replace with a testable interface to allow all SDK levels
-    private val vibrator = mock(Vibrator::class.java)
-    private val hapticManager = HapticManager.createForVibrator(vibrator)
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
-    @Test
-    fun perform_api29AndAbove() {
-        hapticManager.play(effect)
-        verify(vibrator).vibrate(eq(VibrationEffect.createPredefined(effect.effectId)))
-    }
-
-    @Suppress("DEPRECATION") // Verifying deprecated APIs are triggered by this test
-    @SdkSuppress(
-        minSdkVersion = 28, // TODO(b/275084444): remove this once we introduce fake vibrator
-        maxSdkVersion = Build.VERSION_CODES.P
-    )
-    @Test
-    fun perform_belowApi29() {
-        hapticManager.play(effect)
-        verify(vibrator).vibrate(eq(expectedFallbackPattern), eq(-1))
-    }
-
-    companion object {
-
-        @JvmStatic
-        @Parameterized.Parameters(name = "effect:{0}, expectedFallbackPattern:{1}")
-        fun data(): Collection<Array<Any>> = listOf(
-            arrayOf(PredefinedTick, longArrayOf(0, 10)),
-            arrayOf(PredefinedClick, longArrayOf(0, 20)),
-            arrayOf(PredefinedHeavyClick, longArrayOf(0, 30)),
-            arrayOf(PredefinedDoubleClick, longArrayOf(0, 30, 100, 30)),
-        )
-    }
-}
diff --git a/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/PlayWaveformSignalTest.kt b/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/PlayWaveformSignalTest.kt
new file mode 100644
index 0000000..cd7a70d
--- /dev/null
+++ b/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/PlayWaveformSignalTest.kt
@@ -0,0 +1,279 @@
+/*
+ * 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.core.haptics
+
+import android.os.Build
+import androidx.core.haptics.signal.WaveformSignal.Companion.off
+import androidx.core.haptics.signal.WaveformSignal.Companion.on
+import androidx.core.haptics.signal.WaveformSignal.Companion.repeatingWaveformOf
+import androidx.core.haptics.signal.WaveformSignal.Companion.waveformOf
+import androidx.core.haptics.testing.AmplitudeVibrator
+import androidx.core.haptics.testing.FakeVibratorSubject.Companion.assertThat
+import androidx.core.haptics.testing.PatternVibrator
+import androidx.core.haptics.testing.vibration
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@RunWith(JUnit4::class)
+@SmallTest
+class PlayWaveformSignalSdk26AndAboveTest {
+    private val fakeVibrator = AmplitudeVibrator()
+    private val hapticManager = HapticManager.createForVibrator(fakeVibrator)
+
+    @Test
+    fun play_withOneShot_vibratesWithOneShotEffect() {
+        hapticManager.play(waveformOf(on(durationMillis = 10)))
+        hapticManager.play(waveformOf(on(durationMillis = 20, amplitude = 0.2f)))
+        assertThat(fakeVibrator).vibratedExactly(
+            vibration(timings = longArrayOf(10), amplitudes = intArrayOf(-1)),
+            vibration(timings = longArrayOf(20), amplitudes = intArrayOf(51)),
+        ).inOrder()
+    }
+
+    @Test
+    fun play_withAmplitudes_vibratesWithAmplitudes() {
+        hapticManager.play(
+            waveformOf(
+                on(durationMillis = 10, amplitude = 0.2f),
+                on(durationMillis = 20, amplitude = 0.8f),
+                on(durationMillis = 30, amplitude = 0f),
+                on(durationMillis = 40, amplitude = 1f),
+            )
+        )
+        assertThat(fakeVibrator).vibratedExactly(
+            vibration(
+                timings = longArrayOf(10, 20, 30, 40),
+                amplitudes = intArrayOf(51, 204, 0, 255),
+            )
+        )
+    }
+
+    @Test
+    fun play_withOnOffPattern_vibratesWithAmplitudes() {
+        hapticManager.play(
+            waveformOf(
+                on(durationMillis = 10),
+                off(durationMillis = 20),
+                on(durationMillis = 30),
+                off(durationMillis = 40),
+            )
+        )
+        assertThat(fakeVibrator).vibratedExactly(
+            vibration(
+                timings = longArrayOf(10, 20, 30, 40),
+                amplitudes = intArrayOf(-1, 0, -1, 0),
+            )
+        )
+    }
+
+    @Test
+    fun play_withRepeatingAmplitudes_vibratesWithRepeatIndex() {
+        hapticManager.play(
+            waveformOf(
+                on(durationMillis = 10, amplitude = 0.2f),
+                on(durationMillis = 20, amplitude = 0.4f),
+            ).thenRepeat(
+                on(durationMillis = 30, amplitude = 0.6f),
+                on(durationMillis = 40, amplitude = 0.8f),
+            )
+        )
+        assertThat(fakeVibrator).vibratedExactly(
+            vibration(
+                timings = longArrayOf(10, 20, 30, 40),
+                amplitudes = intArrayOf(51, 102, 153, 204),
+                repeat = 2,
+            )
+        )
+    }
+}
+
+@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.N_MR1)
+@RunWith(JUnit4::class)
+@SmallTest
+class PlayWaveformSignalBelowSdk26Test {
+    private val fakeVibrator = PatternVibrator()
+    private val hapticManager = HapticManager.createForVibrator(fakeVibrator)
+
+    @Test
+    fun play_withOneShot_vibratesWithPatternForDefaultAndMaxAmplitudes() {
+        hapticManager.play(waveformOf(on(durationMillis = 10)))
+        hapticManager.play(waveformOf(on(durationMillis = 20, amplitude = 1f)))
+        hapticManager.play(waveformOf(on(durationMillis = 30, amplitude = 0.2f)))
+        assertThat(fakeVibrator).vibratedExactly(
+            vibration(pattern = longArrayOf(0, 10)),
+            vibration(pattern = longArrayOf(0, 20)),
+            // Ignores last request with non-default amplitude
+        ).inOrder()
+    }
+
+    @Test
+    fun play_withAmplitudes_doesNotVibrate() {
+        hapticManager.play(
+            waveformOf(
+                on(durationMillis = 10, amplitude = 0.2f),
+                on(durationMillis = 20, amplitude = 0.8f),
+                on(durationMillis = 30, amplitude = 0f),
+                on(durationMillis = 40, amplitude = 1f),
+            )
+        )
+        assertThat(fakeVibrator).neverVibrated()
+    }
+
+    @Test
+    fun play_withOnOffPattern_vibratesWithFallbackPattern() {
+        hapticManager.play(
+            waveformOf(
+                on(durationMillis = 10),
+                off(durationMillis = 20),
+                on(durationMillis = 30),
+                on(durationMillis = 40),
+                off(durationMillis = 50),
+                off(durationMillis = 60),
+            )
+        )
+        assertThat(fakeVibrator).vibratedExactly(
+            // OFF(0ms), ON(10ms), OFF(20ms), ON(30+40ms), OFF(50+60ms)
+            vibration(pattern = longArrayOf(0, 10, 20, 70, 110))
+        )
+    }
+
+    @Test
+    fun play_withOnOffMaxAmplitudePattern_vibratesWithFallbackPattern() {
+        hapticManager.play(
+            waveformOf(
+                on(durationMillis = 10),
+                on(durationMillis = 20, amplitude = 1f),
+                off(durationMillis = 30),
+                on(durationMillis = 40, amplitude = 0f),
+                on(durationMillis = 50),
+            )
+        )
+        assertThat(fakeVibrator).vibratedExactly(
+            // OFF(0ms), ON(10+20ms), OFF(30+40ms), ON(50ms)
+            vibration(pattern = longArrayOf(0, 30, 70, 50))
+        )
+    }
+
+    @Test
+    fun play_withRepeatingPattern_vibratesWithRepeatIndex() {
+        hapticManager.play(
+            repeatingWaveformOf(
+                off(durationMillis = 10),
+                on(durationMillis = 20),
+                off(durationMillis = 30),
+                off(durationMillis = 40),
+                on(durationMillis = 50),
+                on(durationMillis = 60),
+            )
+        )
+        assertThat(fakeVibrator).vibratedExactly(
+            vibration(
+                // OFF(10ms), ON(20ms), OFF(30+40ms), ON(50+60ms)
+                pattern = longArrayOf(10, 20, 70, 110),
+                repeat = 0,
+            )
+        )
+    }
+
+    @Test
+    fun play_withInitialAndRepeatingPattern_doesNotMergeInitialWithRepeatingPattern() {
+        hapticManager.play(
+            waveformOf(
+                on(durationMillis = 10),
+                off(durationMillis = 20),
+                off(durationMillis = 30),
+                on(durationMillis = 40),
+            ).thenRepeat(
+                on(durationMillis = 50),
+                on(durationMillis = 60),
+                off(durationMillis = 70),
+                off(durationMillis = 80),
+            )
+        )
+        assertThat(fakeVibrator).vibratedExactly(
+            vibration(
+                // Does not merge consecutive ON steps 40 and 50 because of repeat index.
+                // OFF(0ms), ON(10ms), OFF(20+30ms), ON(40ms), OFF(+0ms), ON(50+60ms), OFF(70+80ms)
+                pattern = longArrayOf(0, 10, 50, 40, 0, 110, 150),
+                repeat = 4,
+            )
+        )
+    }
+}
+
+@RunWith(JUnit4::class)
+@SmallTest
+class PlayWaveformSignalAllSdksTest {
+    private val fakeVibrator = AmplitudeVibrator()
+    private val hapticManager = HapticManager.createForVibrator(fakeVibrator)
+
+    @Test
+    fun play_withZeroDurationSignal_doesNotVibrate() {
+        hapticManager.play(waveformOf(on(durationMillis = 0)))
+        hapticManager.play(waveformOf(on(durationMillis = 0, amplitude = 0.2f)))
+        hapticManager.play(
+            waveformOf(
+                on(durationMillis = 0, amplitude = 0.2f),
+                on(durationMillis = 0, amplitude = 0.8f),
+                on(durationMillis = 0, amplitude = 0f),
+                on(durationMillis = 0, amplitude = 1f),
+                off(durationMillis = 0),
+            )
+        )
+        assertThat(fakeVibrator).neverVibrated()
+    }
+
+    @Test
+    fun waveformOf_withNoAtom_throwsException() {
+        assertThrows(IllegalArgumentException::class.java) {
+            waveformOf()
+        }
+    }
+
+    @Test
+    fun on_withAmplitudeLargerThanOne_throwsException() {
+        assertThrows(IllegalArgumentException::class.java) {
+            on(durationMillis = 0, amplitude = 1.1f)
+        }
+    }
+
+    @Test
+    fun on_withNegativeAmplitude_throwsException() {
+        assertThrows(IllegalArgumentException::class.java) {
+            on(durationMillis = 0, amplitude = -0.5f)
+        }
+    }
+
+    @Test
+    fun on_withNegativeDuration_throwsException() {
+        assertThrows(IllegalArgumentException::class.java) {
+            on(durationMillis = -10)
+        }
+    }
+
+    @Test
+    fun off_withNegativeDuration_throwsException() {
+        assertThrows(IllegalArgumentException::class.java) {
+            off(durationMillis = -10)
+        }
+    }
+}
diff --git a/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/testing/FakeVibrator.kt b/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/testing/FakeVibrator.kt
new file mode 100644
index 0000000..2ff140a
--- /dev/null
+++ b/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/testing/FakeVibrator.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.core.haptics.testing
+
+import android.os.Build
+import android.os.VibrationEffect
+import androidx.annotation.RequiresApi
+import androidx.core.haptics.PatternVibrationWrapper
+import androidx.core.haptics.VibrationEffectWrapper
+import androidx.core.haptics.VibrationWrapper
+import androidx.core.haptics.VibratorWrapper
+import androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom
+import androidx.core.haptics.signal.PredefinedEffectSignal
+import kotlin.time.Duration
+
+/**
+ * Fake [VibratorWrapper] implementation for testing.
+ */
+internal sealed class FakeVibrator(
+    private val amplitudeControlSupported: Boolean,
+    private val effectsSupported: IntArray? = null,
+    private val primitivesSupported: IntArray = intArrayOf(),
+) : VibratorWrapper {
+    private val vibrations: MutableList<VibrationWrapper> = mutableListOf()
+
+    override fun hasVibrator(): Boolean = true
+
+    override fun hasAmplitudeControl(): Boolean = amplitudeControlSupported
+
+    override fun areEffectsSupported(
+        effects: IntArray,
+    ): Array<VibratorWrapper.EffectSupport> =
+        effects.map {
+            when {
+                effectsSupported == null -> VibratorWrapper.EffectSupport.UNKNOWN
+                effectsSupported.contains(it) -> VibratorWrapper.EffectSupport.YES
+                else -> VibratorWrapper.EffectSupport.NO
+            }
+        }.toTypedArray()
+
+    override fun arePrimitivesSupported(primitives: IntArray): BooleanArray =
+        primitives.map { primitivesSupported.contains(it) }.toBooleanArray()
+
+    override fun vibrate(vibration: VibrationWrapper) {
+        vibrations.add(vibration)
+    }
+
+    override fun cancel() {
+        // No-op
+    }
+
+    /** Returns all requests sent to the [android.os.Vibrator], in order. */
+    internal fun vibrations(): List<VibrationWrapper> = vibrations
+}
+
+/**
+ * Vibrator that only supports on-off patterns.
+ */
+internal class PatternVibrator : FakeVibrator(
+    amplitudeControlSupported = false,
+)
+
+/**
+ * Vibrator that only supports amplitude control.
+ */
+internal class AmplitudeVibrator : FakeVibrator(
+    amplitudeControlSupported = true,
+)
+
+/**
+ * Vibrator that supports amplitude control and all predefined effects.
+ */
+internal class PredefinedEffectsAndAmplitudeVibrator : FakeVibrator(
+    amplitudeControlSupported = true,
+    effectsSupported = intArrayOf(
+        PredefinedEffectSignal.TICK,
+        PredefinedEffectSignal.CLICK,
+        PredefinedEffectSignal.HEAVY_CLICK,
+        PredefinedEffectSignal.DOUBLE_CLICK,
+    ),
+)
+
+/**
+ * Vibrator that supports amplitude control and all predefined and primitive effects.
+ */
+internal class FullVibrator : FakeVibrator(
+    amplitudeControlSupported = true,
+    effectsSupported = intArrayOf(
+        PredefinedEffectSignal.TICK,
+        PredefinedEffectSignal.CLICK,
+        PredefinedEffectSignal.HEAVY_CLICK,
+        PredefinedEffectSignal.DOUBLE_CLICK,
+    ),
+    primitivesSupported = intArrayOf(
+        PrimitiveAtom.LOW_TICK,
+        PrimitiveAtom.TICK,
+        PrimitiveAtom.CLICK,
+        PrimitiveAtom.SLOW_RISE,
+        PrimitiveAtom.QUICK_RISE,
+        PrimitiveAtom.QUICK_FALL,
+        PrimitiveAtom.SPIN,
+        PrimitiveAtom.THUD,
+    ),
+)
+
+/** Helper to create [android.os.VibrationEffect.Composition] entries. */
+internal data class CompositionPrimitive(
+    val primitiveId: Int,
+    val scale: Float,
+    val delayMs: Int,
+) {
+    constructor(
+        primitive: PrimitiveAtom,
+        scale: Float = primitive.amplitudeScale,
+        delay: Duration = Duration.ZERO,
+    ) : this(primitive.type, scale, delay.inWholeMilliseconds.toInt())
+}
+
+/** Helper to create [VibrationWrapper] request for a on-off pattern. */
+internal fun vibration(
+    pattern: LongArray,
+    repeat: Int = -1,
+): VibrationWrapper =
+    PatternVibrationWrapper(pattern, repeat)
+
+/** Helper to create [VibrationWrapper] request for a predefined effect. */
+@RequiresApi(Build.VERSION_CODES.Q)
+internal fun vibration(effect: PredefinedEffectSignal): VibrationWrapper =
+    VibrationEffectWrapper(VibrationEffect.createPredefined(effect.type))
+
+/** Helper to create [VibrationWrapper] request for a waveform effect. */
+@RequiresApi(Build.VERSION_CODES.O)
+internal fun vibration(
+    timings: LongArray,
+    amplitudes: IntArray,
+    repeat: Int = -1,
+): VibrationWrapper =
+    VibrationEffectWrapper(VibrationEffect.createWaveform(timings, amplitudes, repeat))
+
+/** Helper to create [VibrationWrapper] request for a primitive composition effect. */
+@RequiresApi(Build.VERSION_CODES.R)
+internal fun vibration(vararg primitives: CompositionPrimitive): VibrationWrapper {
+    return VibrationEffectWrapper(
+        VibrationEffect.startComposition().apply {
+            primitives.forEach { addPrimitive(it.primitiveId, it.scale, it.delayMs) }
+        }.compose()
+    )
+}
diff --git a/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/testing/FakeVibratorSubject.kt b/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/testing/FakeVibratorSubject.kt
new file mode 100644
index 0000000..9ab2ca2
--- /dev/null
+++ b/core/haptics/haptics/src/androidTest/java/androidx/core/haptics/testing/FakeVibratorSubject.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.core.haptics.testing
+
+import androidx.core.haptics.VibrationWrapper
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Ordered
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth.assertAbout
+
+/**
+ * Truth extension for [FakeVibrator].
+ */
+internal class FakeVibratorSubject private constructor(
+    metadata: FailureMetadata?,
+    private val actual: FakeVibrator,
+) : Subject(metadata, actual) {
+
+    companion object {
+        private val SUBJECT_FACTORY: Factory<FakeVibratorSubject?, FakeVibrator> =
+            Factory { failureMetadata, subject -> FakeVibratorSubject(failureMetadata, subject) }
+
+        internal fun assertThat(vibrator: FakeVibrator): FakeVibratorSubject =
+            requireNotNull(assertAbout(SUBJECT_FACTORY).that(vibrator))
+    }
+
+    /**
+     * Checks the subject was requested to vibrate with exactly the provided parameters.
+     *
+     * To also test that the requests appear in the given order, make a call to inOrder() on the
+     * object returned by this method.
+     */
+    fun vibratedExactly(vararg expected: VibrationWrapper): Ordered =
+        check("vibrations()").that(actual.vibrations()).containsExactly(*expected)
+
+    /**
+     * Checks the subject has never requested to vibrate.
+     */
+    fun neverVibrated(): Unit =
+        check("vibrations()").that(actual.vibrations()).isEmpty()
+}
diff --git a/core/haptics/haptics/src/main/java/androidx/core/haptics/HapticManager.kt b/core/haptics/haptics/src/main/java/androidx/core/haptics/HapticManager.kt
index 1ca0bd0..37b74bd 100644
--- a/core/haptics/haptics/src/main/java/androidx/core/haptics/HapticManager.kt
+++ b/core/haptics/haptics/src/main/java/androidx/core/haptics/HapticManager.kt
@@ -19,11 +19,14 @@
 import android.content.Context
 import android.os.Vibrator
 import androidx.annotation.RequiresPermission
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
 import androidx.core.haptics.impl.HapticManagerImpl
-import androidx.core.haptics.signal.PredefinedEffect
+import androidx.core.haptics.impl.VibratorWrapperImpl
+import androidx.core.haptics.signal.HapticSignal
 
 /**
- * Manager for the vibrators of a device.
+ * Manager for interactions with a device vibrator.
  *
  * <p>If your process exits, any vibration you started will stop.
  */
@@ -32,35 +35,38 @@
     companion object {
 
         /**
-         * Creates haptic manager for the system vibrators.
+         * Creates a haptic manager for the system vibrator.
          *
-         * Sample code:
          * @sample androidx.core.haptics.samples.PlaySystemStandardClick
          *
-         * @param context Context to load the device vibrators.
-         * @return a new instance of HapticManager for the system vibrators.
+         * @param context Context to load the device vibrator.
+         * @return a new instance of HapticManager for the system vibrator.
          */
         @JvmStatic
         fun create(context: Context): HapticManager {
-            return HapticManagerImpl(context)
+            return HapticManagerImpl(
+                VibratorWrapperImpl(
+                    requireNotNull(ContextCompat.getSystemService(context, Vibrator::class.java)) {
+                        "Vibrator service not found"
+                    }
+                )
+            )
         }
 
-        /** Creates haptic manager for given vibrator. */
-        internal fun createForVibrator(vibrator: Vibrator): HapticManager {
+        /** Creates a haptic manager for the given vibrator. */
+        @VisibleForTesting
+        internal fun createForVibrator(vibrator: VibratorWrapper): HapticManager {
             return HapticManagerImpl(vibrator)
         }
     }
 
     /**
-     * Play a [PredefinedEffect].
+     * Play a [HapticSignal].
      *
-     * The app should be in the foreground for the vibration to happen.
+     * @sample androidx.core.haptics.samples.PlayHapticSignal
      *
-     * Sample code:
-     * @sample androidx.core.haptics.samples.PlaySystemStandardClick
-     *
-     * @param effect The predefined haptic effect to be played.
+     * @param signal The haptic signal to be played.
      */
     @RequiresPermission(android.Manifest.permission.VIBRATE)
-    fun play(effect: PredefinedEffect)
+    fun play(signal: HapticSignal)
 }
diff --git a/core/haptics/haptics/src/main/java/androidx/core/haptics/VibratorWrapper.kt b/core/haptics/haptics/src/main/java/androidx/core/haptics/VibratorWrapper.kt
new file mode 100644
index 0000000..683c071
--- /dev/null
+++ b/core/haptics/haptics/src/main/java/androidx/core/haptics/VibratorWrapper.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.core.haptics
+
+import androidx.annotation.RequiresPermission
+
+/**
+ * Internal wrapper for [android.os.Vibrator] to enable fake implementations for testing.
+ */
+internal interface VibratorWrapper {
+    /** Check whether the hardware has a vibrator. */
+    fun hasVibrator(): Boolean
+
+    /** Check whether the vibrator has amplitude control. */
+    fun hasAmplitudeControl(): Boolean
+
+    /** Check whether the hardware supports each predefined effect type. */
+    fun areEffectsSupported(effects: IntArray): Array<EffectSupport>?
+
+    /** Check whether the hardware supports each primitive effect type. */
+    fun arePrimitivesSupported(primitives: IntArray): BooleanArray?
+
+    /** Vibrate with a given vibration effect or pattern. */
+    @RequiresPermission(android.Manifest.permission.VIBRATE)
+    fun vibrate(vibration: VibrationWrapper)
+
+    /** Cancel any ongoing vibration from this app and turns the vibrator off. */
+    @RequiresPermission(android.Manifest.permission.VIBRATE)
+    fun cancel()
+
+    /** Represents constants from [android.os.Vibrator.VIBRATION_EFFECT_SUPPORT_*]. */
+    enum class EffectSupport {
+        UNKNOWN, YES, NO
+    }
+}
+
+/**
+ * Represents different API levels of support for [android.os.Vibrator.vibrate] parameters.
+ */
+internal sealed interface VibrationWrapper
+
+/**
+ * Represents vibrations defined by on-off patterns.
+ */
+internal data class PatternVibrationWrapper(
+    val timings: LongArray,
+    val repeatIndex: Int,
+) : VibrationWrapper {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as PatternVibrationWrapper
+
+        if (!timings.contentEquals(other.timings)) return false
+        if (repeatIndex != other.repeatIndex) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = timings.contentHashCode()
+        result = 31 * result + repeatIndex.hashCode()
+        return result
+    }
+}
+
+/**
+ * Represents vibrations defined by an instance of [android.os.VibrationEffect].
+ */
+internal data class VibrationEffectWrapper(
+    val vibrationEffect: Any,
+) : VibrationWrapper
diff --git a/core/haptics/haptics/src/main/java/androidx/core/haptics/impl/HapticManagerImpl.kt b/core/haptics/haptics/src/main/java/androidx/core/haptics/impl/HapticManagerImpl.kt
index a5c518c..bd249c9 100644
--- a/core/haptics/haptics/src/main/java/androidx/core/haptics/impl/HapticManagerImpl.kt
+++ b/core/haptics/haptics/src/main/java/androidx/core/haptics/impl/HapticManagerImpl.kt
@@ -16,72 +16,23 @@
 
 package androidx.core.haptics.impl
 
-import android.content.Context
-import android.os.Build.VERSION
-import android.os.VibrationEffect
 import android.os.Vibrator
-import androidx.annotation.DoNotInline
-import androidx.annotation.RequiresApi
 import androidx.annotation.RequiresPermission
-import androidx.core.content.ContextCompat
 import androidx.core.haptics.HapticManager
-import androidx.core.haptics.signal.PredefinedEffect
-import androidx.core.haptics.signal.PredefinedEffect.Companion.PredefinedClick
-import androidx.core.haptics.signal.PredefinedEffect.Companion.PredefinedDoubleClick
-import androidx.core.haptics.signal.PredefinedEffect.Companion.PredefinedHeavyClick
-import androidx.core.haptics.signal.PredefinedEffect.Companion.PredefinedTick
+import androidx.core.haptics.VibratorWrapper
+import androidx.core.haptics.signal.HapticSignal
 
 /**
  * [HapticManager] implementation for the [Vibrator] service.
  */
 internal class HapticManagerImpl internal constructor(
-    private val vibrator: Vibrator
+    private val vibrator: VibratorWrapper
 ) : HapticManager {
 
-    internal constructor(context: Context) : this(
-        requireNotNull(ContextCompat.getSystemService(context, Vibrator::class.java)) {
-            "Vibrator service not found"
-        }
-    )
-
     @RequiresPermission(android.Manifest.permission.VIBRATE)
-    override fun play(effect: PredefinedEffect) {
-        if (VERSION.SDK_INT >= 29) {
-            Api29Impl.play(vibrator, effect)
-        } else {
-            ApiImpl.play(vibrator, effect)
-        }
-    }
-
-    /** Version-specific static inner class. */
-    @RequiresApi(29)
-    private object Api29Impl {
-
-        @JvmStatic
-        @DoNotInline
-        @RequiresPermission(android.Manifest.permission.VIBRATE)
-        fun play(vibrator: Vibrator, effect: PredefinedEffect) {
-            vibrator.vibrate(VibrationEffect.createPredefined(effect.effectId))
-        }
-    }
-
-    /** Version-specific static inner class. */
-    private object ApiImpl {
-
-        private val predefinedEffectFallbackPatterns = mapOf(
-            PredefinedTick to longArrayOf(0, 10),
-            PredefinedClick to longArrayOf(0, 20),
-            PredefinedHeavyClick to longArrayOf(0, 30),
-            PredefinedDoubleClick to longArrayOf(0, 30, 100, 30)
-        )
-
-        @JvmStatic
-        @Suppress("DEPRECATION") // ApkVariant for compatibility
-        @RequiresPermission(android.Manifest.permission.VIBRATE)
-        fun play(vibrator: Vibrator, effect: PredefinedEffect) {
-            predefinedEffectFallbackPatterns[effect]?.let {
-                vibrator.vibrate(/* pattern= */ it, /* repeat= */ -1)
-            }
+    override fun play(signal: HapticSignal) {
+        signal.toVibration()?.let {
+                vibration -> vibrator.vibrate(vibration)
         }
     }
 }
diff --git a/core/haptics/haptics/src/main/java/androidx/core/haptics/impl/HapticSignalConverter.kt b/core/haptics/haptics/src/main/java/androidx/core/haptics/impl/HapticSignalConverter.kt
new file mode 100644
index 0000000..705c6d1
--- /dev/null
+++ b/core/haptics/haptics/src/main/java/androidx/core/haptics/impl/HapticSignalConverter.kt
@@ -0,0 +1,269 @@
+/*
+ * 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.core.haptics.impl
+
+import android.os.Build
+import android.os.VibrationEffect
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.core.haptics.PatternVibrationWrapper
+import androidx.core.haptics.VibrationEffectWrapper
+import androidx.core.haptics.VibrationWrapper
+import androidx.core.haptics.signal.CompositionSignal
+import androidx.core.haptics.signal.CompositionSignal.OffAtom
+import androidx.core.haptics.signal.CompositionSignal.PrimitiveAtom
+import androidx.core.haptics.signal.PredefinedEffectSignal
+import androidx.core.haptics.signal.WaveformSignal
+import androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom
+import androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom.Companion.DEFAULT_AMPLITUDE
+import kotlin.math.roundToInt
+
+private const val VIBRATION_DEFAULT_AMPLITUDE: Int = -1 // VibrationEffect.DEFAULT_AMPLITUDE
+private const val VIBRATION_MAX_AMPLITUDE: Int = 255 // VibrationEffect.MAX_AMPLITUDE
+
+/** Returns true if amplitude is 0, 1 or [DEFAULT_AMPLITUDE]. */
+internal fun ConstantVibrationAtom.hasPatternAmplitude(): Boolean =
+    (amplitude == 0f) || (amplitude == 1f) || (amplitude == DEFAULT_AMPLITUDE)
+
+/** Returns the amplitude value in [0,255] or [android.os.VibrationEffect.DEFAULT_AMPLITUDE]. */
+internal fun ConstantVibrationAtom.getAmplitudeInt(): Int =
+    if (amplitude == DEFAULT_AMPLITUDE) {
+        VIBRATION_DEFAULT_AMPLITUDE
+    } else {
+        (amplitude * VIBRATION_MAX_AMPLITUDE).roundToInt()
+    }
+
+/**
+ * Helper class to convert haptic signals to platform types based on SDK support available.
+ */
+internal object HapticSignalConverter {
+
+    internal fun toVibration(effect: PredefinedEffectSignal): VibrationWrapper? =
+        if (Build.VERSION.SDK_INT >= 29) {
+            Api29Impl.toVibrationEffect(effect)
+        } else {
+            ApiImpl.toPatternVibration(effect)
+        }
+
+    internal fun toVibration(
+        initialWaveform: WaveformSignal?,
+        repeatingWaveform: WaveformSignal?
+    ): VibrationWrapper? =
+        if (Build.VERSION.SDK_INT >= 26) {
+            Api26Impl.toVibrationEffect(initialWaveform, repeatingWaveform)
+        } else {
+            ApiImpl.toPatternVibration(initialWaveform, repeatingWaveform)
+        }
+
+    internal fun toVibration(composition: CompositionSignal): VibrationWrapper? =
+        if (Build.VERSION.SDK_INT >= 31) {
+            Api31Impl.toVibrationEffect(composition)
+        } else if (Build.VERSION.SDK_INT >= 30) {
+            Api30Impl.toVibrationEffect(composition)
+        } else {
+            null
+        }
+
+    /** Version-specific static inner class. */
+    @RequiresApi(31)
+    private object Api31Impl {
+        @JvmStatic
+        @DoNotInline
+        fun toVibrationEffect(composition: CompositionSignal): VibrationEffectWrapper? =
+            if (composition.minSdk() <= 31) {
+                // Use same API to create composition from API 30, but allow constants from API 31.
+                Api30Impl.createComposition(composition)
+            } else {
+                null
+            }
+    }
+
+    /** Version-specific static inner class. */
+    @RequiresApi(30)
+    private object Api30Impl {
+        @JvmStatic
+        @DoNotInline
+        fun toVibrationEffect(composition: CompositionSignal): VibrationEffectWrapper? =
+            if (composition.minSdk() <= 30) {
+                createComposition(composition)
+            } else {
+                null
+            }
+
+        @JvmStatic
+        @DoNotInline
+        fun createComposition(composition: CompositionSignal): VibrationEffectWrapper? {
+            val platformComposition = VibrationEffect.startComposition()
+            var delayMs = 0
+
+            composition.atoms.forEach { atom ->
+                when (atom) {
+                    is PrimitiveAtom -> {
+                        platformComposition.addPrimitive(atom.type, atom.amplitudeScale, delayMs)
+                        delayMs = 0
+                    }
+
+                    is OffAtom -> {
+                        delayMs += atom.durationMillis.toInt()
+                    }
+
+                    else -> {
+                        // Unsupported composition atom
+                        return@createComposition null
+                    }
+                }
+            }
+
+            return VibrationEffectWrapper(platformComposition.compose())
+        }
+    }
+
+    /** Version-specific static inner class. */
+    @RequiresApi(29)
+    private object Api29Impl {
+        @JvmStatic
+        @DoNotInline
+        fun toVibrationEffect(effect: PredefinedEffectSignal): VibrationEffectWrapper? =
+            if (effect.minSdk() <= 29) {
+                VibrationEffectWrapper(VibrationEffect.createPredefined(effect.type))
+            } else {
+                null
+            }
+    }
+
+    /** Version-specific static inner class. */
+    @RequiresApi(26)
+    private object Api26Impl {
+
+        @JvmStatic
+        @DoNotInline
+        fun toVibrationEffect(
+            initialWaveform: WaveformSignal? = null,
+            repeatingWaveform: WaveformSignal? = null,
+        ): VibrationEffectWrapper? {
+            if (initialWaveform?.atoms?.any { it !is ConstantVibrationAtom } == true ||
+                repeatingWaveform?.atoms?.any { it !is ConstantVibrationAtom } == true) {
+                // Unsupported waveform atoms
+                return null
+            }
+
+            val initialAtoms =
+                initialWaveform?.atoms?.filterIsInstance<ConstantVibrationAtom>().orEmpty()
+            val repeatingAtoms =
+                repeatingWaveform?.atoms?.filterIsInstance<ConstantVibrationAtom>().orEmpty()
+            val allAtoms = initialAtoms + repeatingAtoms
+
+            val timings = allAtoms.map { it.durationMillis }.toLongArray()
+            val amplitudes = allAtoms.map { it.getAmplitudeInt() }.toIntArray()
+            val repeatIndex = if (repeatingAtoms.isNotEmpty()) initialAtoms.size else -1
+
+            if (timings.isEmpty() || timings.sum() == 0L) {
+                // Empty or zero duration waveforms not supported by VibrationEffect.createWaveform
+                return null
+            }
+
+            return VibrationEffectWrapper(
+                VibrationEffect.createWaveform(timings, amplitudes, repeatIndex)
+            )
+        }
+    }
+
+    /** Version-specific static inner class. */
+    private object ApiImpl {
+
+        @JvmStatic
+        fun toPatternVibration(effect: PredefinedEffectSignal): PatternVibrationWrapper? =
+            // Fallback patterns for predefined effects in SDK < 29.
+            when (effect.type) {
+                PredefinedEffectSignal.TICK ->
+                    PatternVibrationWrapper(longArrayOf(0, 10), repeatIndex = -1)
+                PredefinedEffectSignal.CLICK ->
+                    PatternVibrationWrapper(longArrayOf(0, 20), repeatIndex = -1)
+                PredefinedEffectSignal.HEAVY_CLICK ->
+                    PatternVibrationWrapper(longArrayOf(0, 30), repeatIndex = -1)
+                PredefinedEffectSignal.DOUBLE_CLICK ->
+                    PatternVibrationWrapper(longArrayOf(0, 30, 100, 30), repeatIndex = -1)
+                else ->
+                    null
+            }
+
+        @JvmStatic
+        fun toPatternVibration(
+            initialWaveform: WaveformSignal? = null,
+            repeatingWaveform: WaveformSignal? = null,
+        ): PatternVibrationWrapper? {
+            if (initialWaveform?.atoms?.any { it !is ConstantVibrationAtom } == true ||
+                repeatingWaveform?.atoms?.any { it !is ConstantVibrationAtom } == true) {
+                // Unsupported waveform entries
+                return null
+            }
+
+            val initialAtoms =
+                initialWaveform?.atoms?.filterIsInstance<ConstantVibrationAtom>().orEmpty()
+                    .toMutableList()
+            val repeatingAtoms =
+                repeatingWaveform?.atoms?.filterIsInstance<ConstantVibrationAtom>().orEmpty()
+
+            if (!initialAtoms.all { it.hasPatternAmplitude() } ||
+                !repeatingAtoms.all { it.hasPatternAmplitude() }) {
+                // Not possible to represent all amplitudes by an on-off pattern.
+                return null
+            }
+
+            val allAtoms = initialAtoms + repeatingAtoms
+            val timings = mutableListOf<Long>()
+            var currentIsOff = true // Vibration pattern starts with an off entry.
+            var currentTiming = 0L
+            var repeatIndex = -1
+
+            for ((index, atom) in allAtoms.withIndex()) {
+                if (index == initialAtoms.size) { // This is the first repeating atom.
+                    if (currentTiming > 0) {
+                        // Make sure not to merge the last initial atom to the first repeating one.
+                        timings.add(currentTiming)
+                        currentTiming = 0
+                        currentIsOff = !currentIsOff
+                    }
+                    // Mark the start of the repetition before adding the first repeating atom.
+                    repeatIndex = timings.size
+                }
+                val atomIsOff = atom.amplitude == 0f
+                if (currentIsOff == atomIsOff) {
+                    // Merge timings of same on/off state.
+                    currentTiming += atom.durationMillis
+                } else {
+                    // Start new timing with different on/off state.
+                    timings.add(currentTiming)
+                    currentTiming = atom.durationMillis
+                    currentIsOff = atomIsOff
+                }
+            }
+
+            if (currentTiming > 0) {
+                // Add last timing entry to the pattern.
+                timings.add(currentTiming)
+            }
+
+            if (timings.isEmpty() || timings.sum() == 0L) {
+                // Empty or zero duration waveforms are not supported by pattern vibrations.
+                return null
+            }
+
+            return PatternVibrationWrapper(timings.toLongArray(), repeatIndex)
+        }
+    }
+}
diff --git a/core/haptics/haptics/src/main/java/androidx/core/haptics/impl/VibratorWrapperImpl.kt b/core/haptics/haptics/src/main/java/androidx/core/haptics/impl/VibratorWrapperImpl.kt
new file mode 100644
index 0000000..decc9e0
--- /dev/null
+++ b/core/haptics/haptics/src/main/java/androidx/core/haptics/impl/VibratorWrapperImpl.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.core.haptics.impl
+
+import android.annotation.SuppressLint
+import android.os.Build
+import android.os.VibrationEffect
+import android.os.Vibrator
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.annotation.RequiresPermission
+import androidx.core.haptics.PatternVibrationWrapper
+import androidx.core.haptics.VibrationEffectWrapper
+import androidx.core.haptics.VibrationWrapper
+import androidx.core.haptics.VibratorWrapper
+
+/**
+ * [VibratorWrapper] implementation backed by a real [Vibrator] service.
+ */
+internal class VibratorWrapperImpl(
+    private val vibrator: Vibrator
+) : VibratorWrapper {
+
+    override fun hasVibrator(): Boolean = ApiImpl.hasVibrator(vibrator)
+
+    override fun hasAmplitudeControl(): Boolean =
+        if (Build.VERSION.SDK_INT >= 26) {
+            Api26Impl.hasAmplitudeControl(vibrator)
+        } else {
+            false
+        }
+
+    override fun areEffectsSupported(effects: IntArray): Array<VibratorWrapper.EffectSupport>? =
+        if (Build.VERSION.SDK_INT >= 30) {
+            Api30Impl.areEffectsSupported(vibrator, effects)
+        } else {
+            null
+        }
+
+    override fun arePrimitivesSupported(primitives: IntArray): BooleanArray? =
+        if (Build.VERSION.SDK_INT >= 30) {
+            Api30Impl.arePrimitivesSupported(vibrator, primitives)
+        } else {
+            null
+        }
+
+    @RequiresPermission(android.Manifest.permission.VIBRATE)
+    override fun vibrate(vibration: VibrationWrapper) {
+        when (vibration) {
+            is VibrationEffectWrapper -> {
+                check(Build.VERSION.SDK_INT >= 26) {
+                    "Attempting to vibrate with VibrationEffect before Android O is not supported"
+                }
+                if (Build.VERSION.SDK_INT >= 26) {
+                    Api26Impl.vibrate(vibrator, vibration)
+                }
+            }
+            is PatternVibrationWrapper ->
+                ApiImpl.vibrate(vibrator, vibration)
+        }
+    }
+
+    @RequiresPermission(android.Manifest.permission.VIBRATE)
+    override fun cancel() {
+        ApiImpl.cancel(vibrator)
+    }
+
+    /** Version-specific static inner class. */
+    @RequiresApi(30)
+    private object Api30Impl {
+
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        @DoNotInline
+        fun areEffectsSupported(
+            vibrator: Vibrator,
+            effects: IntArray,
+        ): Array<VibratorWrapper.EffectSupport> {
+            return vibrator.areEffectsSupported(*effects).map {
+                when (it) {
+                    Vibrator.VIBRATION_EFFECT_SUPPORT_YES -> VibratorWrapper.EffectSupport.YES
+                    Vibrator.VIBRATION_EFFECT_SUPPORT_NO -> VibratorWrapper.EffectSupport.NO
+                    else -> VibratorWrapper.EffectSupport.UNKNOWN
+                }
+            }.toTypedArray()
+        }
+
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        @DoNotInline
+        fun arePrimitivesSupported(
+            vibrator: Vibrator,
+            primitives: IntArray,
+        ): BooleanArray {
+            return vibrator.arePrimitivesSupported(*primitives).toTypedArray().toBooleanArray()
+        }
+    }
+
+    /** Version-specific static inner class. */
+    @RequiresApi(26)
+    private object Api26Impl {
+
+        @JvmStatic
+        @DoNotInline
+        fun hasAmplitudeControl(vibrator: Vibrator) = vibrator.hasAmplitudeControl()
+
+        @JvmStatic
+        @DoNotInline
+        @RequiresPermission(android.Manifest.permission.VIBRATE)
+        fun vibrate(vibrator: Vibrator, effect: VibrationEffectWrapper) {
+            check(effect.vibrationEffect is VibrationEffect) {
+                "Attempting to vibrate with unexpected vibration effect ${effect.vibrationEffect}"
+            }
+            vibrator.vibrate(effect.vibrationEffect)
+        }
+    }
+
+    /** Version-specific static inner class. */
+    private object ApiImpl {
+
+        @JvmStatic
+        fun hasVibrator(vibrator: Vibrator) = vibrator.hasVibrator()
+
+        @JvmStatic
+        @Suppress("DEPRECATION") // ApkVariant for compatibility
+        @RequiresPermission(android.Manifest.permission.VIBRATE)
+        fun vibrate(vibrator: Vibrator, pattern: PatternVibrationWrapper) =
+            vibrator.vibrate(pattern.timings, pattern.repeatIndex)
+
+        @JvmStatic
+        @RequiresPermission(android.Manifest.permission.VIBRATE)
+        fun cancel(vibrator: Vibrator) = vibrator.cancel()
+    }
+}
diff --git a/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/CompositionSignal.kt b/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/CompositionSignal.kt
new file mode 100644
index 0000000..0197b93
--- /dev/null
+++ b/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/CompositionSignal.kt
@@ -0,0 +1,444 @@
+/*
+ * 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.core.haptics.signal
+
+import android.os.Build
+import androidx.annotation.FloatRange
+import androidx.annotation.IntDef
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.core.haptics.VibrationWrapper
+import androidx.core.haptics.impl.HapticSignalConverter
+import java.util.Objects
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.toKotlinDuration
+
+/**
+ * A composition of haptic elements that can be played as one single haptic effect.
+ *
+ * Composition signals may be defined as a composition of scalable primitive effects, which are
+ * tailored to the device hardware. The composition signal is based on the
+ * [android.os.VibrationEffect.Composition] platform API.
+ *
+ * @sample androidx.core.haptics.samples.CompositionSignalOfScaledEffectsAndOff
+ */
+class CompositionSignal(
+
+    /**
+     * The composition signal atoms that describes the haptic elements to be played in sequence.
+     */
+    val atoms: List<Atom>,
+
+) : FiniteSignal() {
+    init {
+        require(atoms.isNotEmpty()) { "Haptic signals cannot be empty" }
+    }
+
+    companion object {
+
+        /**
+         * Returns a [CompositionSignal] with given atoms.
+         *
+         * @sample androidx.core.haptics.samples.CompositionSignalOfScaledEffectsAndOff
+         *
+         * @param atoms The [CompositionSignal.Atom] instances that define the [CompositionSignal].
+         */
+        @JvmStatic
+        fun compositionOf(vararg atoms: Atom): CompositionSignal =
+            CompositionSignal(atoms.toList())
+
+        /**
+         * Returns a [CompositionSignal.Atom] for a very short low frequency tick effect.
+         *
+         * This effect should produce a light crisp sensation intended to be used repetitively
+         * for dynamic feedback.
+         *
+         * @param amplitudeScale The amplitude scale for the new [PrimitiveAtom]
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun lowTick(@FloatRange(from = 0.0, to = 1.0) amplitudeScale: Float = 1f) =
+            PrimitiveAtom.LowTick.withAmplitudeScale(amplitudeScale)
+
+        /**
+         * Returns a [CompositionSignal.Atom] for a very short light tick effect.
+         *
+         * This effect should produce a light crisp sensation stronger than the [lowTick()], and is
+         * also intended to be used repetitively for dynamic feedback.
+         *
+         * @param amplitudeScale The amplitude scale for the new [PrimitiveAtom]
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun tick(@FloatRange(from = 0.0, to = 1.0) amplitudeScale: Float = 1f) =
+            PrimitiveAtom.Tick.withAmplitudeScale(amplitudeScale)
+
+        /**
+         * Returns a [CompositionSignal.Atom] for a click effect.
+         *
+         * This effect should produce a sharp, crisp click sensation.
+         *
+         * @param amplitudeScale The amplitude scale for the new [PrimitiveAtom]
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun click(@FloatRange(from = 0.0, to = 1.0) amplitudeScale: Float = 1f) =
+            PrimitiveAtom.Click.withAmplitudeScale(amplitudeScale)
+
+        /**
+         * Returns a [CompositionSignal.Atom] for an effect with increasing strength.
+         *
+         * This effect simulates quick upward movement against gravity.
+         *
+         * @param amplitudeScale The amplitude scale for the new [PrimitiveAtom]
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun quickRise(@FloatRange(from = 0.0, to = 1.0) amplitudeScale: Float = 1f) =
+            PrimitiveAtom.QuickRise.withAmplitudeScale(amplitudeScale)
+
+        /**
+         * Returns a [CompositionSignal.Atom] for a longer effect with increasing strength.
+         *
+         * This effect simulates slow upward movement against gravity and is longer than the
+         * [quickRise()].
+         *
+         * @param amplitudeScale The amplitude scale for the new [PrimitiveAtom]
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun slowRise(@FloatRange(from = 0.0, to = 1.0) amplitudeScale: Float = 1f) =
+            PrimitiveAtom.SlowRise.withAmplitudeScale(amplitudeScale)
+
+        /**
+         * Returns a [CompositionSignal.Atom] for an effect with decreasing strength.
+         *
+         * This effect simulates quick downwards movement against gravity.
+         *
+         * @param amplitudeScale The amplitude scale for the new [PrimitiveAtom]
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun quickFall(@FloatRange(from = 0.0, to = 1.0) amplitudeScale: Float = 1f) =
+            PrimitiveAtom.QuickFall.withAmplitudeScale(amplitudeScale)
+
+        /**
+         * Returns a [CompositionSignal.Atom] for a spin effect.
+         *
+         * This effect simulates spinning momentum.
+         *
+         * @param amplitudeScale The amplitude scale for the new [PrimitiveAtom]
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun spin(@FloatRange(from = 0.0, to = 1.0) amplitudeScale: Float = 1f) =
+            PrimitiveAtom.Spin.withAmplitudeScale(amplitudeScale)
+
+        /**
+         * Returns a [CompositionSignal.Atom] for a thud effect.
+         *
+         * This effect simulates downwards movement with gravity, often followed by extra energy
+         * of hitting and reverberation to augment physicality.
+         *
+         * @param amplitudeScale The amplitude scale for the new [PrimitiveAtom]
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun thud(
+            @FloatRange(from = 0.0, to = 1.0) amplitudeScale: Float = 1f,
+        ) =
+            PrimitiveAtom.Thud.withAmplitudeScale(amplitudeScale)
+
+        /**
+         * Returns a [CompositionSignal.Atom] to turn the vibrator off for the specified duration.
+         *
+         * @sample androidx.core.haptics.samples.CompositionSignalOfScaledEffectsAndOff
+         *
+         * @param duration The duration the vibrator should be turned off.
+         */
+        @RequiresApi(Build.VERSION_CODES.O)
+        @JvmStatic
+        fun off(duration: java.time.Duration) =
+            OffAtom(duration.toKotlinDuration())
+
+        /**
+         * Returns a [CompositionSignal.Atom] to turn the vibrator off for the specified duration.
+         *
+         * @sample androidx.core.haptics.samples.CompositionSignalOfScaledEffectsAndOff
+         *
+         * @param durationMillis The duration the vibrator should be turned off, in milliseconds.
+         */
+        @JvmStatic
+        fun off(durationMillis: Long) =
+            OffAtom(durationMillis.milliseconds)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is CompositionSignal) return false
+        if (atoms != other.atoms) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return atoms.hashCode()
+    }
+
+    override fun toString(): String {
+        return "CompositionSignal(${atoms.joinToString()})"
+    }
+
+    /**
+     * Returns the minimum SDK level required by the atoms of this signal.
+     */
+    internal fun minSdk(): Int = atoms.maxOf { it.minSdk() }
+
+    override fun toVibration(): VibrationWrapper? = HapticSignalConverter.toVibration(this)
+
+    /**
+     * A [CompositionSignal.Atom] is a building block for creating a [CompositionSignal].
+     *
+     * Composition signal atoms describe basic haptic elements to be played in sequence as a single
+     * haptic effect. They can describe haptic effects tailored to the device hardware, like click
+     * and tick effects, or then can represent pauses in the effect composition.
+     *
+     * @sample androidx.core.haptics.samples.CompositionSignalOfScaledEffectsAndOff
+     */
+    abstract class Atom internal constructor() {
+
+        /**
+         * The minimum SDK level where this atom is available in the platform.
+         */
+        internal abstract fun minSdk(): Int
+    }
+
+    /**
+     * A [PrimitiveAtom] plays a haptic effect with the specified vibration strength scale.
+     *
+     * Composition primitives are haptic effects tailored to the device hardware with configurable
+     * vibration strength. They can be used as building blocks to create more complex haptic
+     * effects. The primitive atoms are based on the
+     * [android.os.VibrationEffect.Composition.addPrimitive] platform API.
+     *
+     * A primitive effect will always be played with a non-zero vibration amplitude, but the actual
+     * vibration strength can be scaled by values in the range [0f..1f]. Zero [amplitudeScale]
+     * implies the vibrator will play it at the minimum strength required for the effect to be
+     * perceived on the device. The maximum [amplitudeScale] value of 1 implies the vibrator will
+     * play it at the maximum strength that preserves the effect's intended design. For instance, a
+     * [Click] effect with [amplitudeScale] of 1 will usually feel stronger than a [Tick] with same
+     * amplitude scale.
+     *
+     * @sample androidx.core.haptics.samples.CompositionSignalOfScaledEffectsAndOff
+     */
+    class PrimitiveAtom private constructor(
+
+        /**
+         * The type of haptic effect to be played.
+         */
+        @Type val type: Int,
+
+        /**
+         * The minimum SDK level where this effect type is available in the platform.
+         */
+        private val minSdk: Int,
+
+        /**
+         * The scale for the vibration strength.
+         *
+         * A primitive effect will always be played with a non-zero vibration strength. Zero values
+         * here represent minimum effect strength that can still be perceived on the device, and
+         * maximum values represent the maximum strength the effect can be played.
+         */
+        @FloatRange(from = 0.0, to = 1.0) val amplitudeScale: Float = 1f,
+
+    ) : Atom() {
+        init {
+            require(amplitudeScale in 0.0..1.0) {
+                "Primitive amplitude scale must be in [0,1]: $amplitudeScale"
+            }
+        }
+
+        /** Typedef for the [type] attribute. */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @Retention(AnnotationRetention.SOURCE)
+        @IntDef(
+            LOW_TICK,
+            TICK,
+            CLICK,
+            SLOW_RISE,
+            QUICK_RISE,
+            QUICK_FALL,
+            SPIN,
+            THUD,
+        )
+        annotation class Type
+
+        companion object {
+
+            /**
+             * A very short low frequency tick effect.
+             *
+             * This effect should produce a light crisp sensation intended to be used repetitively
+             * for dynamic feedback.
+             */
+            const val LOW_TICK = 8 // VibrationEffect.Composition.PRIMITIVE_LOW_TICK
+
+            /**
+             * A very short light tick effect.
+             *
+             * This effect should produce a light crisp sensation stronger than the [LowTick], and is
+             * also intended to be used repetitively for dynamic feedback.
+             */
+            const val TICK = 7 // VibrationEffect.Composition.PRIMITIVE_TICK
+
+            /**
+             * A click effect.
+             *
+             * This effect should produce a sharp, crisp click sensation.
+             */
+            const val CLICK = 1 // VibrationEffect.Composition.PRIMITIVE_CLICK
+
+            /**
+             * An effect with increasing strength.
+             *
+             * This effect simulates quick upward movement against gravity.
+             */
+            const val QUICK_RISE = 4 // VibrationEffect.Composition.PRIMITIVE_QUICK_RISE
+
+            /**
+             * A longer effect with increasing strength.
+             *
+             * This effect simulates slow upward movement against gravity and is longer than the
+             * [QuickRise].
+             */
+            const val SLOW_RISE = 5 // VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
+
+            /**
+             * An effect with decreasing strength.
+             *
+             * This effect simulates quick downwards movement against gravity.
+             */
+            const val QUICK_FALL = 6 // VibrationEffect.Composition.PRIMITIVE_QUICK_FALL
+
+            /**
+             * A spin effect.
+             *
+             * This effect simulates spinning momentum.
+             */
+            const val SPIN = 3 // VibrationEffect.Composition.PRIMITIVE_SPIN
+
+            /**
+             * A thud effect.
+             *
+             * This effect simulates downwards movement with gravity, often followed by extra energy
+             * of hitting and reverberation to augment physicality.
+             */
+            const val THUD = 2 // VibrationEffect.Composition.PRIMITIVE_THUD
+
+            internal val LowTick = PrimitiveAtom(LOW_TICK, Build.VERSION_CODES.S)
+            internal val Tick = PrimitiveAtom(TICK, Build.VERSION_CODES.R)
+            internal val Click = PrimitiveAtom(CLICK, Build.VERSION_CODES.R)
+            internal val QuickRise = PrimitiveAtom(QUICK_RISE, Build.VERSION_CODES.R)
+            internal val SlowRise = PrimitiveAtom(SLOW_RISE, Build.VERSION_CODES.R)
+            internal val QuickFall = PrimitiveAtom(QUICK_FALL, Build.VERSION_CODES.R)
+            internal val Spin = PrimitiveAtom(SPIN, Build.VERSION_CODES.S)
+            internal val Thud = PrimitiveAtom(THUD, Build.VERSION_CODES.S)
+        }
+
+        /**
+         * Returns a [PrimitiveAtom] with same effect type and new [amplitudeScale].
+         *
+         * @sample androidx.core.haptics.samples.CompositionSignalOfScaledEffectsAndOff
+         *
+         * @param newAmplitudeScale The amplitude scale for the new [PrimitiveAtom]
+         * @return A new [PrimitiveAtom] with the same effect type and the new amplitude scale.
+         */
+        fun withAmplitudeScale(
+            @FloatRange(from = 0.0, to = 1.0) newAmplitudeScale: Float,
+        ): PrimitiveAtom =
+            if (amplitudeScale == newAmplitudeScale) {
+                this
+            } else {
+                PrimitiveAtom(type, minSdk, newAmplitudeScale)
+            }
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is PrimitiveAtom) return false
+            if (type != other.type) return false
+            if (minSdk != other.minSdk) return false
+            if (amplitudeScale != other.amplitudeScale) return false
+            return true
+        }
+
+        override fun hashCode(): Int = Objects.hash(type, amplitudeScale)
+
+        override fun toString(): String {
+            val typeStr = when (type) {
+                LOW_TICK -> "LowTick"
+                TICK -> "Tick"
+                CLICK -> "Click"
+                SLOW_RISE -> "SlowRise"
+                QUICK_RISE -> "QuickRise"
+                QUICK_FALL -> "QuickFall"
+                SPIN -> "Spin"
+                THUD -> "Thud"
+                else -> type.toString()
+            }
+            return "PrimitiveAtom(type=$typeStr, amplitude=$amplitudeScale)"
+        }
+
+        override fun minSdk(): Int = minSdk
+    }
+
+    /**
+     * A [OffAtom] turns off the vibrator for the specified duration.
+     *
+     * @sample androidx.core.haptics.samples.CompositionSignalOfScaledEffectsAndOff
+     */
+    class OffAtom internal constructor(duration: Duration) : Atom() {
+        /**
+         * The duration for the vibrator to be turned off, in milliseconds.
+         */
+        val durationMillis: Long
+
+        init {
+            require(duration.isFinite() && !duration.isNegative()) {
+                "Composition signal off atom duration must be finite and non-negative: $duration"
+            }
+            durationMillis = duration.inWholeMilliseconds
+        }
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is OffAtom) return false
+            if (durationMillis != other.durationMillis) return false
+            return true
+        }
+
+        override fun hashCode(): Int {
+            return durationMillis.hashCode()
+        }
+
+        override fun toString(): String {
+            return "OffAtom(durationMillis=$durationMillis)"
+        }
+
+        override fun minSdk(): Int = Build.VERSION_CODES.R
+    }
+}
diff --git a/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/HapticSignal.kt b/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/HapticSignal.kt
new file mode 100644
index 0000000..69ae2c5
--- /dev/null
+++ b/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/HapticSignal.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.core.haptics.signal
+
+import androidx.core.haptics.VibrationWrapper
+
+/**
+ * A [HapticSignal] describes a generic vibration to be played by a vibrator.
+ *
+ * These signals may represent any number of things, from single shot vibrations to complex
+ * waveforms, device-specific predefined effects or custom vibration patterns.
+ *
+ * A haptic signal can be defined as a [FiniteSignal] or [InfiniteSignal]. Infinite signals will be
+ * played by the vibrator until canceled, while finite signals will stop playing once completed.
+ *
+ * Note: This is a library-restricted representation of a haptic signal that will be mapped to a
+ * platform representation available, like [android.os.VibrationEffect]. This class cannot be
+ * extended or supplemented outside the library, but they can be instantiated from custom extensions
+ * via factory methods.
+ */
+abstract class HapticSignal internal constructor() {
+
+    /**
+     * Returns a [VibrationWrapper] representing this signal, or null if not supported in this SDK
+     * level.
+     */
+    internal abstract fun toVibration(): VibrationWrapper?
+}
+
+/**
+ * A [FiniteSignal] describes a non-infinite haptic signal to be played by a vibrator.
+ */
+abstract class FiniteSignal internal constructor() : HapticSignal()
+
+/**
+ * A [InfiniteSignal] describes a haptic signal that will be played by a vibrator until canceled.
+ */
+abstract class InfiniteSignal internal constructor() : HapticSignal()
diff --git a/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/PredefinedEffect.kt b/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/PredefinedEffect.kt
deleted file mode 100644
index abe1b89..0000000
--- a/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/PredefinedEffect.kt
+++ /dev/null
@@ -1,81 +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.core.haptics.signal
-
-/**
- * A [PredefinedEffect] describes a haptic effect to be played by a vibrator.
- *
- * Predefined effects represent common vibration effects that should be identical, regardless of
- * the app they come from, in order to provide a cohesive experience for users across the entire
- * device.
- *
- * They also may be custom tailored to the device hardware in order to provide a better
- * experience than you could otherwise build using the generic building blocks.
- *
- * This will fallback to a generic pattern if one exists and there is no hardware-specific
- * implementation of the effect available.
- */
-class PredefinedEffect private constructor(
-
-    /** The id of the effect to be played. */
-    internal val effectId: Int
-) {
-
-    companion object {
-
-        /**
-         * A standard tick effect.
-         *
-         * This effect is less strong than the [PredefinedClick].
-         */
-        @JvmField
-        val PredefinedTick = PredefinedEffect(2) // VibrationEffect.EFFECT_TICK
-
-        /**
-         * A standard click effect.
-         *
-         * Use this effect as a baseline, as it's the most common type of click effect.
-         */
-        @JvmField
-        val PredefinedClick = PredefinedEffect(0) // VibrationEffect.EFFECT_CLICK
-
-        /**
-         * A heavy click effect.
-         *
-         * This effect is stronger than the [PredefinedClick].
-         */
-        @JvmField
-        val PredefinedHeavyClick = PredefinedEffect(5) // VibrationEffect.EFFECT_HEAVY_CLICK
-
-        /**
-         * A double-click effect.
-         */
-        @JvmField
-        val PredefinedDoubleClick = PredefinedEffect(1) // VibrationEffect.EFFECT_DOUBLE_CLICK
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is PredefinedEffect) return false
-        if (effectId != other.effectId) return false
-        return true
-    }
-
-    override fun hashCode(): Int {
-        return effectId.hashCode()
-    }
-}
diff --git a/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/PredefinedEffectSignal.kt b/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/PredefinedEffectSignal.kt
new file mode 100644
index 0000000..a81419a
--- /dev/null
+++ b/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/PredefinedEffectSignal.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.core.haptics.signal
+
+import android.os.Build
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.core.haptics.VibrationWrapper
+import androidx.core.haptics.impl.HapticSignalConverter
+
+/**
+ * A predefined haptic effect that represents common vibration effects, like clicks and ticks.
+ *
+ * Predefined haptic effects should be identical, regardless of the app they come from, in order to
+ * provide a cohesive experience for users across the entire device. The predefined effects are
+ * based on the [android.os.VibrationEffect.createPredefined] platform API.
+ *
+ * @sample androidx.core.haptics.samples.PlaySystemStandardClick
+ */
+class PredefinedEffectSignal private constructor(
+
+    /**
+     * The type of haptic effect to be played.
+     */
+    @Type internal val type: Int,
+
+    /**
+     * The minimum SDK level where this effect type is available in the platform.
+     */
+    private val minSdk: Int,
+
+) : FiniteSignal() {
+
+    /** Typedef for the [type] attribute. */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(AnnotationRetention.SOURCE)
+    @IntDef(
+        TICK,
+        CLICK,
+        HEAVY_CLICK,
+        DOUBLE_CLICK,
+    )
+    annotation class Type
+
+    companion object {
+        internal const val TICK = 2 // VibrationEffect.EFFECT_TICK
+        internal const val CLICK = 0 // VibrationEffect.EFFECT_CLICK
+        internal const val HEAVY_CLICK = 5 // VibrationEffect.EFFECT_HEAVY_CLICK
+        internal const val DOUBLE_CLICK = 1 // VibrationEffect.EFFECT_DOUBLE_CLICK
+
+        private val Tick = PredefinedEffectSignal(TICK, Build.VERSION_CODES.Q)
+        private val Click = PredefinedEffectSignal(CLICK, Build.VERSION_CODES.Q)
+        private val HeavyClick = PredefinedEffectSignal(HEAVY_CLICK, Build.VERSION_CODES.Q)
+        private val DoubleClick = PredefinedEffectSignal(DOUBLE_CLICK, Build.VERSION_CODES.Q)
+
+        /**
+         * A standard tick effect.
+         *
+         * This effect is less strong than the [predefinedClick()].
+         */
+        @JvmStatic
+        fun predefinedTick() = Tick
+
+        /**
+         * A standard click effect.
+         *
+         * Use this effect as a baseline, as it's the most common type of click effect.
+         */
+        @JvmStatic
+        fun predefinedClick() = Click
+
+        /**
+         * A heavy click effect.
+         *
+         * This effect is stronger than the [predefinedClick()].
+         */
+        @JvmStatic
+        fun predefinedHeavyClick() = HeavyClick
+
+        /**
+         * A double-click effect.
+         */
+        @JvmStatic
+        fun predefinedDoubleClick() = DoubleClick
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is PredefinedEffectSignal) return false
+        if (type != other.type) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return type.hashCode()
+    }
+
+    override fun toString(): String {
+        val typeStr = when (type) {
+            TICK -> "Tick"
+            CLICK -> "Click"
+            HEAVY_CLICK -> "HeavyClick"
+            DOUBLE_CLICK -> "DoubleClick"
+            else -> type.toString()
+        }
+        return "PredefinedEffectSignal(type=$typeStr)"
+    }
+
+    /**
+     * Returns the minimum SDK level required by the effect type.
+     */
+    internal fun minSdk(): Int = minSdk
+
+    override fun toVibration(): VibrationWrapper? = HapticSignalConverter.toVibration(this)
+}
diff --git a/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/WaveformSignal.kt b/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/WaveformSignal.kt
new file mode 100644
index 0000000..3f1b87f
--- /dev/null
+++ b/core/haptics/haptics/src/main/java/androidx/core/haptics/signal/WaveformSignal.kt
@@ -0,0 +1,333 @@
+/*
+ * 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.core.haptics.signal
+
+import android.os.Build
+import androidx.annotation.FloatRange
+import androidx.annotation.RequiresApi
+import androidx.core.haptics.VibrationWrapper
+import androidx.core.haptics.impl.HapticSignalConverter
+import androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom.Companion.DEFAULT_AMPLITUDE
+import java.util.Objects
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.toKotlinDuration
+
+/**
+ * A haptic signal where the vibration parameters change over time.
+ *
+ * Waveform signals may be used to describe step waveforms, defined by a sequence of constant
+ * vibrations played at different strengths. They can also be combined to define a
+ * [RepeatingWaveformSignal], which is an [InfiniteSignal] that repeats a waveform until the
+ * vibration is canceled.
+ *
+ * @sample androidx.core.haptics.samples.AmplitudeWaveform
+ * @sample androidx.core.haptics.samples.PatternThenRepeatAmplitudeWaveform
+ */
+class WaveformSignal(
+
+    /**
+     * The waveform signal atoms that describes the vibration parameters over time.
+     */
+    val atoms: List<Atom>,
+
+) : FiniteSignal() {
+    init {
+        require(atoms.isNotEmpty()) { "Haptic signals cannot be empty" }
+    }
+
+    companion object {
+
+        /**
+         * Returns a [WaveformSignal] created with given waveform atoms.
+         *
+         * Use [on] and [off] to create atoms.
+         *
+         * @sample androidx.core.haptics.samples.AmplitudeWaveform
+         *
+         * @param atoms The [WaveformSignal.Atom] instances that define the [WaveformSignal].
+         */
+        @JvmStatic
+        fun waveformOf(vararg atoms: Atom): WaveformSignal =
+            WaveformSignal(atoms.toList())
+
+        /**
+         * Returns a [RepeatingWaveformSignal] created with given waveform atoms.
+         *
+         * Repeating waveforms should include any desired loop delay as an [off] atom at the end of
+         * the atom list.
+         *
+         * @sample androidx.core.haptics.samples.RepeatingAmplitudeWaveform
+         *
+         * @param atoms The [WaveformSignal.Atom] instances that define the
+         *   [RepeatingWaveformSignal].
+         */
+        @JvmStatic
+        fun repeatingWaveformOf(vararg atoms: Atom): RepeatingWaveformSignal =
+            waveformOf(*atoms).repeat()
+
+        /**
+         * Returns a [WaveformSignal.Atom] that turns off the vibrator for the specified duration.
+         *
+         * @sample androidx.core.haptics.samples.PatternWaveform
+         *
+         * @param duration The duration the vibrator should be turned off.
+         */
+        @RequiresApi(Build.VERSION_CODES.O)
+        @JvmStatic
+        fun off(duration: java.time.Duration) =
+            ConstantVibrationAtom(duration.toKotlinDuration(), amplitude = 0f)
+
+        /**
+         * Returns a [WaveformSignal.Atom] that turns off the vibrator for the specified duration.
+         *
+         * @sample androidx.core.haptics.samples.PatternWaveform
+         *
+         * @param durationMillis The duration the vibrator should be turned off, in milliseconds.
+         */
+        @JvmStatic
+        fun off(durationMillis: Long) =
+            ConstantVibrationAtom(durationMillis.milliseconds, amplitude = 0f)
+
+        /**
+         * Returns a [WaveformSignal.Atom] that turns on the vibrator for the specified duration at
+         * a device-specific default amplitude.
+         *
+         * @sample androidx.core.haptics.samples.PatternWaveform
+         *
+         * @param duration The duration for the vibration.
+         */
+        @RequiresApi(Build.VERSION_CODES.O)
+        @JvmStatic
+        fun on(duration: java.time.Duration) =
+            ConstantVibrationAtom(duration.toKotlinDuration(), DEFAULT_AMPLITUDE)
+
+        /**
+         * Returns a [WaveformSignal.Atom] that turns on the vibrator for the specified duration at
+         * a device-specific default amplitude.
+         *
+         * @sample androidx.core.haptics.samples.PatternWaveform
+         *
+         * @param durationMillis The duration for the vibration, in milliseconds.
+         */
+        @JvmStatic
+        fun on(durationMillis: Long) =
+            ConstantVibrationAtom(durationMillis.milliseconds, DEFAULT_AMPLITUDE)
+
+        /**
+         * Returns a [WaveformSignal.Atom] that turns on the vibrator for the specified duration at
+         * the specified amplitude.
+         *
+         * @sample androidx.core.haptics.samples.AmplitudeWaveform
+         *
+         * @param duration The duration for the vibration.
+         * @param amplitude The vibration strength, with 1 representing maximum amplitude, and 0
+         *   representing off - equivalent to calling [off].
+         */
+        @RequiresApi(Build.VERSION_CODES.O)
+        @JvmStatic
+        fun on(duration: java.time.Duration, @FloatRange(from = 0.0, to = 1.0) amplitude: Float) =
+            ConstantVibrationAtom(duration.toKotlinDuration(), amplitude)
+
+        /**
+         * Returns a [WaveformSignal.Atom] that turns on the vibrator for the specified duration at
+         * the specified amplitude.
+         *
+         * @sample androidx.core.haptics.samples.AmplitudeWaveform
+         *
+         * @param durationMillis The duration for the vibration, in milliseconds.
+         * @param amplitude The vibration strength, with 1 representing maximum amplitude, and 0
+         *   representing off - equivalent to calling [off].
+         */
+        @JvmStatic
+        fun on(durationMillis: Long, @FloatRange(from = 0.0, to = 1.0) amplitude: Float) =
+            ConstantVibrationAtom(durationMillis.milliseconds, amplitude)
+    }
+
+    /**
+     * Returns a [RepeatingWaveformSignal] to play this waveform on repeat until it's canceled.
+     *
+     * @sample androidx.core.haptics.samples.PatternWaveformRepeat
+     */
+    fun repeat(): RepeatingWaveformSignal =
+        RepeatingWaveformSignal(initialWaveform = null, repeatingWaveform = this)
+
+    /**
+     * Returns a [RepeatingWaveformSignal] that starts with this waveform signal then plays the
+     * given waveform signal on repeat until the vibration is canceled.
+     *
+     * @sample androidx.core.haptics.samples.PatternThenRepeatExistingWaveform
+     *
+     * @param waveformToRepeat The waveform to be played on repeat after this waveform.
+     */
+    fun thenRepeat(waveformToRepeat: WaveformSignal): RepeatingWaveformSignal =
+        RepeatingWaveformSignal(initialWaveform = this, repeatingWaveform = waveformToRepeat)
+
+    /**
+     * Returns a [RepeatingWaveformSignal] that starts with this waveform signal then plays the
+     * given waveform atoms on repeat until the vibration is canceled.
+     *
+     * @sample androidx.core.haptics.samples.PatternThenRepeatAmplitudeWaveform
+     *
+     * @param atoms The [WaveformSignal.Atom] instances that define the repeating [WaveformSignal]
+     *   to be played after this waveform.
+     */
+    fun thenRepeat(vararg atoms: Atom): RepeatingWaveformSignal =
+        thenRepeat(waveformOf(*atoms))
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WaveformSignal) return false
+        if (atoms != other.atoms) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return atoms.hashCode()
+    }
+
+    override fun toString(): String {
+        return "WaveformSignal(${atoms.joinToString()})"
+    }
+
+    override fun toVibration(): VibrationWrapper? =
+        HapticSignalConverter.toVibration(initialWaveform = this, repeatingWaveform = null)
+
+    /**
+     * A [WaveformSignal.Atom] is a building block for creating a [WaveformSignal].
+     *
+     * Waveform signal atoms describe how vibration parameters change over time. They can describe
+     * a constant vibration sustained for a fixed duration, for example, which can be used to create
+     * a step waveform. They can also be used to describe simpler on-off vibration patterns.
+     *
+     * @sample androidx.core.haptics.samples.PatternWaveform
+     * @sample androidx.core.haptics.samples.AmplitudeWaveform
+     */
+    abstract class Atom internal constructor()
+
+    /**
+     * A [ConstantVibrationAtom] plays a constant vibration for the specified period of time.
+     *
+     * Constant vibrations can be played in sequence to create custom waveform signals.
+     *
+     * The amplitude determines the strength of the vibration, defined as a value in the range
+     * [0f..1f]. Zero amplitude implies the vibrator motor should be off. The amplitude can also be
+     * defined by [DEFAULT_AMPLITUDE], which will vibrate constantly at a hardware-specific default
+     * vibration strength.
+     *
+     * @sample androidx.core.haptics.samples.PatternWaveform
+     * @sample androidx.core.haptics.samples.AmplitudeWaveform
+     */
+    class ConstantVibrationAtom internal constructor(
+
+        duration: Duration,
+
+        /**
+         * The vibration strength.
+         *
+         * Zero amplitude turns the vibrator off for the specified duration, and [DEFAULT_AMPLITUDE]
+         * uses a hardware-specific default vibration strength.
+         */
+        val amplitude: Float,
+
+    ) : Atom() {
+        /**
+         * The duration to sustain the constant vibration, in milliseconds.
+         */
+        val durationMillis: Long
+
+        init {
+            require(duration.isFinite() && !duration.isNegative()) {
+                "Constant vibration duration must be finite and non-negative: $duration"
+            }
+            require(amplitude in (0.0..1.0) || amplitude == DEFAULT_AMPLITUDE) {
+                "Constant vibration amplitude must be in [0,1]: $amplitude"
+            }
+            durationMillis = duration.inWholeMilliseconds
+        }
+
+        companion object {
+            /**
+             * The [amplitude] value that represents a hardware-specific default vibration strength.
+             */
+            const val DEFAULT_AMPLITUDE: Float = -1f
+        }
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is ConstantVibrationAtom) return false
+            if (durationMillis != other.durationMillis) return false
+            if (amplitude != other.amplitude) return false
+            return true
+        }
+
+        override fun hashCode(): Int {
+            return Objects.hash(durationMillis, amplitude)
+        }
+
+        override fun toString(): String {
+            return "ConstantVibrationAtom(durationMillis=$durationMillis" +
+                ", amplitude=${if (amplitude == DEFAULT_AMPLITUDE) "default" else amplitude})"
+        }
+    }
+}
+
+/**
+ * A [RepeatingWaveformSignal] describes an infinite haptic signal where a waveform signal is played
+ * on repeat until canceled.
+ *
+ * A repeating waveform signal has an optional initial [WaveformSignal] that plays once before the
+ * repeating waveform signal is played on repeat until the vibration is canceled.
+ *
+ * @sample androidx.core.haptics.samples.RepeatingAmplitudeWaveform
+ */
+class RepeatingWaveformSignal internal constructor(
+
+    /**
+     * The optional initial waveform signal to be played once at the beginning of the vibration.
+     */
+    val initialWaveform: WaveformSignal?,
+
+    /**
+     * The waveform signal to be repeated after the initial waveform.
+     */
+    val repeatingWaveform: WaveformSignal,
+
+) : InfiniteSignal() {
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is RepeatingWaveformSignal) return false
+        if (initialWaveform != other.initialWaveform) return false
+        if (repeatingWaveform != other.repeatingWaveform) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return Objects.hash(initialWaveform, repeatingWaveform)
+    }
+
+    override fun toString(): String {
+        return "RepeatingWaveformSignal(initial=$initialWaveform, repeating=$repeatingWaveform)"
+    }
+
+    override fun toVibration(): VibrationWrapper? =
+        HapticSignalConverter.toVibration(
+            initialWaveform = initialWaveform,
+            repeatingWaveform = repeatingWaveform,
+        )
+}
diff --git a/credentials/credentials-fido/credentials-fido/api/current.txt b/credentials/credentials-fido/credentials-fido/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/credentials/credentials-fido/credentials-fido/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt b/credentials/credentials-fido/credentials-fido/api/res-current.txt
similarity index 100%
copy from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
copy to credentials/credentials-fido/credentials-fido/api/res-current.txt
diff --git a/credentials/credentials-fido/credentials-fido/api/restricted_current.txt b/credentials/credentials-fido/credentials-fido/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/credentials/credentials-fido/credentials-fido/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/credentials/credentials-fido/credentials-fido/build.gradle b/credentials/credentials-fido/credentials-fido/build.gradle
new file mode 100644
index 0000000..0fa93f9
--- /dev/null
+++ b/credentials/credentials-fido/credentials-fido/build.gradle
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    api project(":credentials:credentials")
+
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.mockitoAndroid)
+    androidTestImplementation(libs.truth)
+    androidTestImplementation(libs.multidex)
+    androidTestImplementation(project(":internal-testutils-truth"))
+    androidTestImplementation(libs.kotlinCoroutinesAndroid)
+    androidTestImplementation("androidx.core:core-ktx:1.10.0")
+}
+
+android {
+    namespace "androidx.credentials.fido"
+
+    defaultConfig {
+        minSdkVersion 19
+        multiDexEnabled = true
+    }
+}
+
+androidx {
+    name = "credentials-fido"
+    type = LibraryType.PUBLISHED_LIBRARY
+    inceptionYear = "2023"
+    description = "Util library for apps using FIDO"
+    publish = Publish.SNAPSHOT_AND_RELEASE
+}
diff --git a/credentials/credentials-fido/credentials-fido/proguard-rules.pro b/credentials/credentials-fido/credentials-fido/proguard-rules.pro
new file mode 100644
index 0000000..4cced1e
--- /dev/null
+++ b/credentials/credentials-fido/credentials-fido/proguard-rules.pro
@@ -0,0 +1,4 @@
+-if class androidx.credentials.CredentialManager
+-keep class androidx.credentials.passkeys.** {
+  *;
+}
\ No newline at end of file
diff --git a/credentials/credentials-fido/credentials-fido/src/androidTest/AndroidManifest.xml b/credentials/credentials-fido/credentials-fido/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..dc727a5
--- /dev/null
+++ b/credentials/credentials-fido/credentials-fido/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<!--
+  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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <application>
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/credentials/credentials-fido/credentials-fido/src/main/AndroidManifest.xml b/credentials/credentials-fido/credentials-fido/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d8a4ecd
--- /dev/null
+++ b/credentials/credentials-fido/credentials-fido/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<!--
+  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.
+  -->
+
+<manifest xmlns:tools="http://schemas.android.com/tools"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <application>
+    </application>
+</manifest>
diff --git a/credentials/credentials-fido/credentials-fido/src/main/androidx/credentials/androidx-credentials-credentials-fido-documentation.md b/credentials/credentials-fido/credentials-fido/src/main/androidx/credentials/androidx-credentials-credentials-fido-documentation.md
new file mode 100644
index 0000000..b0450b7
--- /dev/null
+++ b/credentials/credentials-fido/credentials-fido/src/main/androidx/credentials/androidx-credentials-credentials-fido-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+CREDENTIALS CREDENTIALS FIDO
+
+# Package androidx.credentials.fido
+
+This package contains utility functions for FIDO app developers.
\ No newline at end of file
diff --git a/credentials/credentials-fido/credentials-fido/src/main/java/androidx/credentials/fido/EmptyActivity.kt b/credentials/credentials-fido/credentials-fido/src/main/java/androidx/credentials/fido/EmptyActivity.kt
new file mode 100644
index 0000000..20839f9
--- /dev/null
+++ b/credentials/credentials-fido/credentials-fido/src/main/java/androidx/credentials/fido/EmptyActivity.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.credentials.fido
+
+import android.app.Activity
+import androidx.annotation.RestrictTo
+
+/** Remove this class when we add the code (b/292103206). */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@Suppress("Deprecation", "ForbiddenSuperClass")
+open class EmptyActivity : Activity()
diff --git a/credentials/credentials-play-services-auth/build.gradle b/credentials/credentials-play-services-auth/build.gradle
index 890c5d9..3b5972d 100644
--- a/credentials/credentials-play-services-auth/build.gradle
+++ b/credentials/credentials-play-services-auth/build.gradle
@@ -26,7 +26,7 @@
     api(libs.kotlinStdlib)
     api project(":credentials:credentials")
 
-    implementation("com.google.android.libraries.identity.googleid:googleid:1.0.0"){
+    implementation("com.google.android.libraries.identity.googleid:googleid:1.1.0"){
         exclude group: "androidx.credentials"
     }
 
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.kt
index f1cf823..4c5226d 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.kt
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.kt
@@ -75,13 +75,16 @@
                 val superValues = superset.get(key)
 
                 if ((values::class.java != superValues::class.java || values::class.java !=
-                    requiredValues::class.java) && requiredValues !is Boolean
+                        requiredValues::class.java) && requiredValues !is Boolean
                 ) {
                     return false
                 }
                 if (requiredValues is JSONObject) {
-                    if (!isSubsetJson(superValues as JSONObject, values as JSONObject,
-                            requiredValues)) {
+                    if (!isSubsetJson(
+                            superValues as JSONObject, values as JSONObject,
+                            requiredValues
+                        )
+                    ) {
                         return false
                     }
                 } else if (values is JSONArray) {
@@ -147,6 +150,7 @@
             ConnectionResult.SERVICE_INVALID, ConnectionResult.SERVICE_MISSING,
             ConnectionResult.SERVICE_MISSING_PERMISSION, ConnectionResult.SERVICE_UPDATING,
             ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, ConnectionResult.SIGN_IN_FAILED,
-            ConnectionResult.SIGN_IN_REQUIRED, ConnectionResult.TIMEOUT)
+            ConnectionResult.SIGN_IN_REQUIRED, ConnectionResult.TIMEOUT
+        )
     }
 }
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/beginsignin/CredentialProviderBeginSignInControllerTest.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/beginsignin/CredentialProviderBeginSignInControllerTest.kt
index d245c4b..a387feb 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/beginsignin/CredentialProviderBeginSignInControllerTest.kt
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/beginsignin/CredentialProviderBeginSignInControllerTest.kt
@@ -124,7 +124,6 @@
             TestCredentialsActivity::class.java
         )
         activityScenario.onActivity { activity: TestCredentialsActivity? ->
-
             val firstInstance = getInstance(activity!!)
             val secondInstance = getInstance(activity)
             assertThat(firstInstance).isEqualTo(secondInstance)
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CreatePublicKeyCredentialControllerTestUtils.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CreatePublicKeyCredentialControllerTestUtils.kt
index de8ef2b..2dbf889 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CreatePublicKeyCredentialControllerTestUtils.kt
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CreatePublicKeyCredentialControllerTestUtils.kt
@@ -17,6 +17,7 @@
 package androidx.credentials.playservices.createkeycredential
 
 import androidx.credentials.playservices.TestUtils
+import androidx.credentials.playservices.controllers.CreatePublicKeyCredential.PublicKeyCredentialControllerUtility
 import com.google.android.gms.fido.common.Transport
 import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions
 import org.json.JSONArray
@@ -26,6 +27,8 @@
 class CreatePublicKeyCredentialControllerTestUtils {
     companion object {
 
+        const val TAG = "PasskeyTestUtils"
+
          // optional and not required key 'transports' is missing in the JSONObject that composes
          // up the JSONArray found at key 'excludeCredentials'
          const val OPTIONAL_FIELD_MISSING_OPTIONAL_SUBFIELD = ("{\"rp\": {\"name\": " +
@@ -323,5 +326,122 @@
                 }
             }
         }
+
+        /**
+         * Helps generate a PublicKeyCredential response json format to start tests with, locally
+         * for example.
+         *
+         * Usage details as follows:
+         *
+         *     val byteArrayClientDataJson = byteArrayOf(0x48, 101, 108, 108, 111)
+         *     val byteArrayAuthenticatorData = byteArrayOf(0x48, 101, 108, 108, 112)
+         *     val byteArraySignature = byteArrayOf(0x48, 101, 108, 108, 113)
+         *     val byteArrayUserHandle = byteArrayOf(0x48, 101, 108, 108, 114)
+         *     val publicKeyCredId = "id"
+         *     val publicKeyCredRawId = byteArrayOf(0x48, 101, 108, 108, 115)
+         *     val publicKeyCredType = "type"
+         *     val authenticatorAttachment = "platform"
+         *     val hasClientExtensionOutputs = true
+         *     val isDiscoverableCredential = true
+         *     val expectedClientExtensions = "{\"credProps\":{\"rk\":true}}"
+         *
+         *     val json = PublicKeyCredentialControllerUtility.beginSignInAssertionResponse(
+         *       byteArrayClientDataJson,
+         *       byteArrayAuthenticatorData,
+         *       byteArraySignature,
+         *       byteArrayUserHandle,
+         *       publicKeyCredId,
+         *       publicKeyCredRawId,
+         *       publicKeyCredType,
+         *       authenticatorAttachment,
+         *       hasClientExtensionOutputs,
+         *       isDiscoverableCredential
+         *     )
+         *
+         * The json can be used as necessary, even if only to generate a log with which to pull
+         * the string from (to then further use that string in other test cases).
+         */
+        fun getPublicKeyCredentialResponseGenerator(
+            clientDataJSON: ByteArray,
+            authenticatorData: ByteArray,
+            signature: ByteArray,
+            userHandle: ByteArray?,
+            publicKeyCredId: String,
+            publicKeyCredRawId: ByteArray,
+            publicKeyCredType: String,
+            authenticatorAttachment: String?,
+            hasClientExtensionResults: Boolean,
+            isDiscoverableCredential: Boolean?
+        ): JSONObject {
+            val json = JSONObject()
+            val responseJson = JSONObject()
+            responseJson.put(
+                PublicKeyCredentialControllerUtility.JSON_KEY_CLIENT_DATA,
+                PublicKeyCredentialControllerUtility.b64Encode(clientDataJSON)
+            )
+            responseJson.put(
+                PublicKeyCredentialControllerUtility.JSON_KEY_AUTH_DATA,
+                PublicKeyCredentialControllerUtility.b64Encode(authenticatorData)
+            )
+            responseJson.put(
+                PublicKeyCredentialControllerUtility.JSON_KEY_SIGNATURE,
+                PublicKeyCredentialControllerUtility.b64Encode(signature)
+            )
+            userHandle?.let {
+                responseJson.put(
+                    PublicKeyCredentialControllerUtility.JSON_KEY_USER_HANDLE,
+                    PublicKeyCredentialControllerUtility.b64Encode(userHandle)
+                )
+            }
+            json.put(PublicKeyCredentialControllerUtility.JSON_KEY_RESPONSE, responseJson)
+            json.put(PublicKeyCredentialControllerUtility.JSON_KEY_ID, publicKeyCredId)
+            json.put(
+                PublicKeyCredentialControllerUtility.JSON_KEY_RAW_ID,
+                PublicKeyCredentialControllerUtility.b64Encode(publicKeyCredRawId)
+            )
+            json.put(PublicKeyCredentialControllerUtility.JSON_KEY_TYPE, publicKeyCredType)
+            addOptionalAuthenticatorAttachmentAndRequiredExtensions(
+                authenticatorAttachment,
+                hasClientExtensionResults,
+                isDiscoverableCredential,
+                json
+            )
+            return json;
+        }
+
+        // This can be shared by both get and create flow response parsers, fills 'json'.
+        private fun addOptionalAuthenticatorAttachmentAndRequiredExtensions(
+            authenticatorAttachment: String?,
+            hasClientExtensionResults: Boolean,
+            isDiscoverableCredential: Boolean?,
+            json: JSONObject
+        ) {
+
+            json.putOpt(
+                PublicKeyCredentialControllerUtility.JSON_KEY_AUTH_ATTACHMENT,
+                authenticatorAttachment
+            )
+
+            val clientExtensionsJson = JSONObject()
+
+            if (hasClientExtensionResults) {
+                if (isDiscoverableCredential != null) {
+                    val credPropsObject = JSONObject()
+                    credPropsObject.put(
+                        PublicKeyCredentialControllerUtility.JSON_KEY_RK,
+                        isDiscoverableCredential
+                    )
+                    clientExtensionsJson.put(
+                        PublicKeyCredentialControllerUtility.JSON_KEY_CRED_PROPS,
+                        credPropsObject
+                    )
+                }
+            }
+
+            json.put(
+                PublicKeyCredentialControllerUtility.JSON_KEY_CLIENT_EXTENSION_RESULTS,
+                clientExtensionsJson
+            )
+        }
     }
 }
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/PublicKeyCredentialControllerUtilityTest.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/PublicKeyCredentialControllerUtilityTest.kt
index 80617b9..17af5dc 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/PublicKeyCredentialControllerUtilityTest.kt
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/PublicKeyCredentialControllerUtilityTest.kt
@@ -149,226 +149,6 @@
   }
 
   @Test
-  fun toAssertPasskeyResponse_authenticatorAssertionResponse_success() {
-    val byteArrayClientDataJson = byteArrayOf(0x48, 101, 108, 108, 111)
-    val byteArrayAuthenticatorData = byteArrayOf(0x48, 101, 108, 108, 112)
-    val byteArraySignature = byteArrayOf(0x48, 101, 108, 108, 113)
-    val byteArrayUserHandle = byteArrayOf(0x48, 101, 108, 108, 114)
-    val json = JSONObject()
-    val publicKeyCredId = "id"
-    val publicKeyCredRawId = byteArrayOf(0x48, 101, 108, 108, 115)
-    val publicKeyCredType = "type"
-    val authenticatorAttachment = "platform"
-    val hasClientExtensionOutputs = true
-    val isDiscoverableCredential = true
-    val expectedClientExtensions = "{\"credProps\":{\"rk\":true}}"
-
-    PublicKeyCredentialControllerUtility.beginSignInAssertionResponse(
-      byteArrayClientDataJson,
-      byteArrayAuthenticatorData,
-      byteArraySignature,
-      byteArrayUserHandle,
-      json,
-      publicKeyCredId,
-      publicKeyCredRawId,
-      publicKeyCredType,
-      authenticatorAttachment,
-      hasClientExtensionOutputs,
-      isDiscoverableCredential
-    )
-
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_ID))
-      .isEqualTo(publicKeyCredId)
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_RAW_ID))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(publicKeyCredRawId))
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_TYPE))
-      .isEqualTo(publicKeyCredType)
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_AUTH_ATTACHMENT))
-      .isEqualTo(authenticatorAttachment)
-    assertThat(
-        json
-          .getJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_CLIENT_EXTENSION_RESULTS)
-          .toString()
-      )
-      .isEqualTo(expectedClientExtensions)
-
-    // There is some embedded JSON so we should make sure we test that.
-    var embeddedResponse =
-      json.getJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_RESPONSE)
-    assertThat(embeddedResponse.get(PublicKeyCredentialControllerUtility.JSON_KEY_CLIENT_DATA))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(byteArrayClientDataJson))
-    assertThat(embeddedResponse.get(PublicKeyCredentialControllerUtility.JSON_KEY_AUTH_DATA))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(byteArrayAuthenticatorData))
-    assertThat(embeddedResponse.get(PublicKeyCredentialControllerUtility.JSON_KEY_SIGNATURE))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(byteArraySignature))
-    assertThat(embeddedResponse.get(PublicKeyCredentialControllerUtility.JSON_KEY_USER_HANDLE))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(byteArrayUserHandle))
-
-    // ClientExtensions are another group of embedded JSON
-    var clientExtensions =
-      json.getJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_CLIENT_EXTENSION_RESULTS)
-    assertThat(clientExtensions.get(PublicKeyCredentialControllerUtility.JSON_KEY_CRED_PROPS))
-      .isNotNull()
-    assertThat(
-        clientExtensions
-          .getJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_CRED_PROPS)
-          .getBoolean(PublicKeyCredentialControllerUtility.JSON_KEY_RK)
-      )
-      .isTrue()
-  }
-
-  @Test
-  fun toAssertPasskeyResponse_authenticatorAssertionResponse_noUserHandle_success() {
-    val byteArrayClientDataJson = byteArrayOf(0x48, 101, 108, 108, 111)
-    val byteArrayAuthenticatorData = byteArrayOf(0x48, 101, 108, 108, 112)
-    val byteArraySignature = byteArrayOf(0x48, 101, 108, 108, 113)
-    val json = JSONObject()
-    val publicKeyCredId = "id"
-    val publicKeyCredRawId = byteArrayOf(0x48, 101, 108, 108, 115)
-    val publicKeyCredType = "type"
-    val authenticatorAttachment = "platform"
-    val hasClientExtensionOutputs = false
-
-    PublicKeyCredentialControllerUtility.beginSignInAssertionResponse(
-      byteArrayClientDataJson,
-      byteArrayAuthenticatorData,
-      byteArraySignature,
-      null,
-      json,
-      publicKeyCredId,
-      publicKeyCredRawId,
-      publicKeyCredType,
-      authenticatorAttachment,
-      hasClientExtensionOutputs,
-      null
-    )
-
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_ID))
-      .isEqualTo(publicKeyCredId)
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_RAW_ID))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(publicKeyCredRawId))
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_TYPE))
-      .isEqualTo(publicKeyCredType)
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_AUTH_ATTACHMENT))
-      .isEqualTo(authenticatorAttachment)
-    assertThat(
-        json
-          .getJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_CLIENT_EXTENSION_RESULTS)
-          .toString()
-      )
-      .isEqualTo(JSONObject().toString())
-
-    // There is some embedded JSON so we should make sure we test that.
-    var embeddedResponse =
-      json.getJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_RESPONSE)
-    assertThat(embeddedResponse.get(PublicKeyCredentialControllerUtility.JSON_KEY_CLIENT_DATA))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(byteArrayClientDataJson))
-    assertThat(embeddedResponse.get(PublicKeyCredentialControllerUtility.JSON_KEY_AUTH_DATA))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(byteArrayAuthenticatorData))
-    assertThat(embeddedResponse.get(PublicKeyCredentialControllerUtility.JSON_KEY_SIGNATURE))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(byteArraySignature))
-    assertThat(embeddedResponse.has(PublicKeyCredentialControllerUtility.JSON_KEY_USER_HANDLE))
-      .isFalse()
-  }
-
-  @Test
-  fun toAssertPasskeyResponse_authenticatorAssertionResponse_noAuthenticatorAttachment_success() {
-    val byteArrayClientDataJson = byteArrayOf(0x48, 101, 108, 108, 111)
-    val byteArrayAuthenticatorData = byteArrayOf(0x48, 101, 108, 108, 112)
-    val byteArraySignature = byteArrayOf(0x48, 101, 108, 108, 113)
-    val json = JSONObject()
-    val publicKeyCredId = "id"
-    val publicKeyCredRawId = byteArrayOf(0x48, 101, 108, 108, 115)
-    val publicKeyCredType = "type"
-    val hasClientExtensionOutputs = false
-
-    PublicKeyCredentialControllerUtility.beginSignInAssertionResponse(
-      byteArrayClientDataJson,
-      byteArrayAuthenticatorData,
-      byteArraySignature,
-      null,
-      json,
-      publicKeyCredId,
-      publicKeyCredRawId,
-      publicKeyCredType,
-      null,
-      hasClientExtensionOutputs,
-      null
-    )
-
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_ID))
-      .isEqualTo(publicKeyCredId)
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_RAW_ID))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(publicKeyCredRawId))
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_TYPE))
-      .isEqualTo(publicKeyCredType)
-    assertThat(json.optJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_AUTH_ATTACHMENT))
-      .isNull()
-    assertThat(
-        json
-          .getJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_CLIENT_EXTENSION_RESULTS)
-          .toString()
-      )
-      .isEqualTo(JSONObject().toString())
-
-    // There is some embedded JSON so we should make sure we test that.
-    var embeddedResponse =
-      json.getJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_RESPONSE)
-    assertThat(embeddedResponse.get(PublicKeyCredentialControllerUtility.JSON_KEY_CLIENT_DATA))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(byteArrayClientDataJson))
-    assertThat(embeddedResponse.get(PublicKeyCredentialControllerUtility.JSON_KEY_AUTH_DATA))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(byteArrayAuthenticatorData))
-    assertThat(embeddedResponse.get(PublicKeyCredentialControllerUtility.JSON_KEY_SIGNATURE))
-      .isEqualTo(PublicKeyCredentialControllerUtility.b64Encode(byteArraySignature))
-    assertThat(embeddedResponse.has(PublicKeyCredentialControllerUtility.JSON_KEY_USER_HANDLE))
-      .isFalse()
-  }
-
-  @Test
-  fun toCreatePasskeyResponseJson_addOptionalAuthenticatorAttachmentAndRequiredExt() {
-    val json = JSONObject()
-
-    PublicKeyCredentialControllerUtility.addOptionalAuthenticatorAttachmentAndRequiredExtensions(
-      "attachment",
-      true,
-      true,
-      json
-    )
-
-    var clientExtensionResults =
-      json.getJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_CLIENT_EXTENSION_RESULTS)
-    var credPropsObject =
-      clientExtensionResults.getJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_CRED_PROPS)
-
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_AUTH_ATTACHMENT))
-      .isEqualTo("attachment")
-    assertThat(credPropsObject.get(PublicKeyCredentialControllerUtility.JSON_KEY_RK))
-      .isEqualTo(true)
-  }
-
-  @Test
-  fun toCreatePasskeyResponseJson_addOptionalAuthenticatorAttachmentAndRequiredExt_noClientExt() {
-    val json = JSONObject()
-
-    PublicKeyCredentialControllerUtility.addOptionalAuthenticatorAttachmentAndRequiredExtensions(
-      "attachment",
-      false,
-      null,
-      json
-    )
-
-    var clientExtensionResults =
-      json.getJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_CLIENT_EXTENSION_RESULTS)
-
-    assertThat(json.get(PublicKeyCredentialControllerUtility.JSON_KEY_AUTH_ATTACHMENT))
-      .isEqualTo("attachment")
-    assertThat(
-        clientExtensionResults.optJSONObject(PublicKeyCredentialControllerUtility.JSON_KEY_RK)
-      )
-      .isNull()
-  }
-
-  @Test
   fun toCreatePasskeyResponseJson_addAuthenticatorAttestationResponse_success() {
     val json = JSONObject()
     val byteArrayClientDataJson = byteArrayOf(0x48, 101, 108, 108, 111)
@@ -417,16 +197,16 @@
       )
     var output = PublicKeyCredentialControllerUtility.convertJSON(json)
 
-    assertThat(output.getUser().getId()).isNotEmpty()
-    assertThat(output.getUser().getName()).isEqualTo("Name of User")
-    assertThat(output.getUser().getDisplayName()).isEqualTo("Display Name of User")
-    assertThat(output.getUser().getIcon()).isEqualTo("icon.png")
-    assertThat(output.getChallenge()).isNotEmpty()
-    assertThat(output.getRp().getId()).isNotEmpty()
-    assertThat(output.getRp().getName()).isEqualTo("Name of RP")
-    assertThat(output.getRp().getIcon()).isEqualTo("rpicon.png")
-    assertThat(output.getParameters().get(0).getAlgorithmIdAsInteger()).isEqualTo(-7)
-    assertThat(output.getParameters().get(0).getTypeAsString()).isEqualTo("public-key")
+    assertThat(output.user.id).isNotEmpty()
+    assertThat(output.user.name).isEqualTo("Name of User")
+    assertThat(output.user.displayName).isEqualTo("Display Name of User")
+    assertThat(output.user.icon).isEqualTo("icon.png")
+    assertThat(output.challenge).isNotEmpty()
+    assertThat(output.rp.id).isNotEmpty()
+    assertThat(output.rp.name).isEqualTo("Name of RP")
+    assertThat(output.rp.icon).isEqualTo("rpicon.png")
+    assertThat(output.parameters[0].algorithmIdAsInteger).isEqualTo(-7)
+    assertThat(output.parameters[0].typeAsString).isEqualTo("public-key")
   }
 
   @Test
@@ -680,9 +460,8 @@
       )
     var output = PublicKeyCredentialControllerUtility.convertJSON(json)
 
-    assertThat(output.getAuthenticationExtensions()
-        !!.getFidoAppIdExtension()!!.getAppId()).isEqualTo("https://www.android.com/appid1")
-    assertThat(output.getAuthenticationExtensions()
-        !!.getUserVerificationMethodExtension()!!.getUvm()).isTrue()
+    assertThat(output.authenticationExtensions!!.fidoAppIdExtension!!.appId)
+      .isEqualTo("https://www.android.com/appid1")
+    assertThat(output.authenticationExtensions!!.userVerificationMethodExtension!!.uvm).isTrue()
   }
 }
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/getsigninintent/CredentialProviderGetSignInIntentControllerTest.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/getsigninintent/CredentialProviderGetSignInIntentControllerTest.kt
new file mode 100644
index 0000000..d045cad
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/getsigninintent/CredentialProviderGetSignInIntentControllerTest.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.credentials.playservices.getsigninintent
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetPasswordOption
+import androidx.credentials.exceptions.GetCredentialUnsupportedException
+import androidx.credentials.playservices.TestCredentialsActivity
+import androidx.credentials.playservices.controllers.GetSignInIntent.CredentialProviderGetSignInIntentController.Companion.getInstance
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.android.gms.auth.api.identity.GetSignInIntentRequest
+import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@Suppress("deprecation")
+@RequiresApi(api = Build.VERSION_CODES.O)
+class CredentialProviderGetSignInIntentControllerTest {
+
+    @Test
+    fun convertRequestToPlayServices_success() {
+        val serverClientId: String = "server_client_id"
+        val activityScenario = ActivityScenario.launch(
+            TestCredentialsActivity::class.java
+        )
+        activityScenario.onActivity { activity: TestCredentialsActivity? ->
+            val actual: GetSignInIntentRequest = getInstance(activity!!)
+                .convertRequestToPlayServices(
+                    GetCredentialRequest(
+                        listOf(
+                            GetSignInWithGoogleOption.Builder(serverClientId).build()
+                        )
+                    )
+                )
+            assertThat(
+                actual.serverClientId
+            ).isEqualTo(serverClientId)
+        }
+    }
+
+    @Test
+    fun convertRequestToPlayServices_moreThanOneOption_failure() {
+        val serverClientId: String = "server_client_id"
+        val activityScenario = ActivityScenario.launch(
+            TestCredentialsActivity::class.java
+        )
+        activityScenario.onActivity { activity: TestCredentialsActivity? ->
+            assertThrows(GetCredentialUnsupportedException::class.java) {
+                getInstance(activity!!)
+                    .convertRequestToPlayServices(
+                        GetCredentialRequest(
+                            listOf(
+                                GetPasswordOption(),
+                                GetSignInWithGoogleOption.Builder(serverClientId).build()
+                            )
+                        )
+                    )
+            }
+        }
+    }
+}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
index f65e129..def1c27 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
@@ -37,9 +37,11 @@
 import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController
 import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController
 import androidx.credentials.playservices.controllers.CreatePublicKeyCredential.CredentialProviderCreatePublicKeyCredentialController
+import androidx.credentials.playservices.controllers.GetSignInIntent.CredentialProviderGetSignInIntentController
 import com.google.android.gms.auth.api.identity.Identity
 import com.google.android.gms.common.ConnectionResult
 import com.google.android.gms.common.GoogleApiAvailability
+import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
 import java.util.concurrent.Executor
 
 /**
@@ -60,8 +62,15 @@
         callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>
     ) {
         if (cancellationReviewer(cancellationSignal)) { return }
-        CredentialProviderBeginSignInController(context).invokePlayServices(
-            request, callback, executor, cancellationSignal)
+        if (isGetSignInIntentRequest(request)) {
+            CredentialProviderGetSignInIntentController(context).invokePlayServices(
+                request, callback, executor, cancellationSignal
+            )
+        } else {
+            CredentialProviderBeginSignInController(context).invokePlayServices(
+                request, callback, executor, cancellationSignal
+            )
+        }
     }
 
     @SuppressWarnings("deprecated")
@@ -162,5 +171,14 @@
             }
             return false
         }
+
+        internal fun isGetSignInIntentRequest(request: GetCredentialRequest): Boolean {
+            for (option in request.credentialOptions) {
+                if (option is GetSignInWithGoogleOption) {
+                    return true
+                }
+            }
+            return false
+        }
     }
 }
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/HiddenActivity.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/HiddenActivity.kt
index bde5471..0e21647 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/HiddenActivity.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/HiddenActivity.kt
@@ -31,6 +31,7 @@
 import androidx.credentials.playservices.controllers.CredentialProviderBaseController.Companion.GET_NO_CREDENTIALS
 import androidx.credentials.playservices.controllers.CredentialProviderBaseController.Companion.GET_UNKNOWN
 import com.google.android.gms.auth.api.identity.BeginSignInRequest
+import com.google.android.gms.auth.api.identity.GetSignInIntentRequest
 import com.google.android.gms.auth.api.identity.Identity
 import com.google.android.gms.auth.api.identity.SavePasswordRequest
 import com.google.android.gms.common.api.ApiException
@@ -72,6 +73,9 @@
             }
             CredentialProviderBaseController.CREATE_PUBLIC_KEY_CREDENTIAL_TAG -> {
                 handleCreatePublicKeyCredential()
+            }
+            CredentialProviderBaseController.SIGN_IN_INTENT_TAG -> {
+                handleGetSignInIntent()
             } else -> {
                 Log.w(TAG, "Activity handed an unsupported type")
                 finish()
@@ -144,6 +148,47 @@
         super.onSaveInstanceState(outState)
     }
 
+    private fun handleGetSignInIntent() {
+        val params: GetSignInIntentRequest? = intent.getParcelableExtra(
+            CredentialProviderBaseController.REQUEST_TAG)
+        val requestCode: Int = intent.getIntExtra(
+            CredentialProviderBaseController.ACTIVITY_REQUEST_CODE_TAG,
+            DEFAULT_VALUE)
+        params?.let {
+            Identity.getSignInClient(this).getSignInIntent(params).addOnSuccessListener {
+                try {
+                    mWaitingForActivityResult = true
+                    startIntentSenderForResult(
+                        it.intentSender,
+                        requestCode,
+                        null,
+                        0,
+                        0,
+                        0,
+                        null
+                    )
+                } catch (e: IntentSender.SendIntentException) {
+                    setupFailure(resultReceiver!!,
+                        GET_UNKNOWN,
+                        "During get sign-in intent, one tap ui intent sender " +
+                            "failure: ${e.message}")
+                }
+            }.addOnFailureListener { e: Exception ->
+                var errName: String = GET_NO_CREDENTIALS
+                if (e is ApiException && e.statusCode in
+                    CredentialProviderBaseController.retryables) {
+                    errName = GET_INTERRUPTED
+                }
+                setupFailure(resultReceiver!!, errName,
+                    "During get sign-in intent, failure response from one tap: ${e.message}")
+            }
+        } ?: run {
+            Log.i(TAG, "During get sign-in intent, params is null, nothing to launch for " +
+                "get sign-in intent")
+            finish()
+        }
+    }
+
     private fun handleBeginSignIn() {
         val params: BeginSignInRequest? = intent.getParcelableExtra(
             CredentialProviderBaseController.REQUEST_TAG)
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
index 1a37158..e86df83 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
@@ -186,9 +186,12 @@
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     public override fun convertResponseToCredentialManager(response: PublicKeyCredential):
         CreateCredentialResponse {
-        return CreatePublicKeyCredentialResponse(
-            PublicKeyCredentialControllerUtility.toCreatePasskeyResponseJson(response)
-        )
+            try {
+                return CreatePublicKeyCredentialResponse(response.toJson())
+            } catch (t: Throwable) {
+                throw CreateCredentialUnknownException("The PublicKeyCredential response json " +
+                    "had an unexpected exception when parsing: ${t.message}")
+            }
     }
 
     private fun JSONExceptionToPKCError(exception: JSONException):
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
index 4b38772..b38500b 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
@@ -24,6 +24,7 @@
 import androidx.credentials.exceptions.CreateCredentialException
 import androidx.credentials.exceptions.GetCredentialCancellationException
 import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.exceptions.GetCredentialUnknownException
 import androidx.credentials.exceptions.domerrors.AbortError
 import androidx.credentials.exceptions.domerrors.ConstraintError
 import androidx.credentials.exceptions.domerrors.DataError
@@ -45,7 +46,6 @@
 import com.google.android.gms.fido.fido2.api.common.AttestationConveyancePreference
 import com.google.android.gms.fido.fido2.api.common.AuthenticationExtensions
 import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse
-import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse
 import com.google.android.gms.fido.fido2.api.common.AuthenticatorErrorResponse
 import com.google.android.gms.fido.fido2.api.common.AuthenticatorResponse
 import com.google.android.gms.fido.fido2.api.common.AuthenticatorSelectionCriteria
@@ -136,39 +136,6 @@
       return builder.build()
     }
 
-    /** Converts the response from fido back to json so it can be passed into CredentialManager. */
-    fun toCreatePasskeyResponseJson(cred: PublicKeyCredential): String {
-      val json = JSONObject()
-      val authenticatorResponse = cred.response
-      if (authenticatorResponse is AuthenticatorAttestationResponse) {
-        val transportArray = convertToProperNamingScheme(authenticatorResponse)
-        addAuthenticatorAttestationResponse(
-          authenticatorResponse.clientDataJSON,
-          authenticatorResponse.attestationObject,
-          transportArray,
-          json
-        )
-      } else {
-        Log.e(
-          TAG,
-          "Authenticator response expected registration response but " +
-            "got: ${authenticatorResponse.javaClass.name}"
-        )
-      }
-
-      addOptionalAuthenticatorAttachmentAndRequiredExtensions(
-        cred.authenticatorAttachment,
-        cred.clientExtensionResults != null,
-        cred.clientExtensionResults?.credProps?.isDiscoverableCredential,
-        json
-      )
-
-      json.put(JSON_KEY_ID, cred.id)
-      json.put(JSON_KEY_RAW_ID, b64Encode(cred.rawId))
-      json.put(JSON_KEY_TYPE, cred.type)
-      return json.toString()
-    }
-
     internal fun addAuthenticatorAttestationResponse(
       clientDataJSON: ByteArray,
       attestationObject: ByteArray,
@@ -182,52 +149,6 @@
       json.put(JSON_KEY_RESPONSE, responseJson)
     }
 
-    private fun convertToProperNamingScheme(
-      authenticatorResponse: AuthenticatorAttestationResponse
-    ): Array<out String> {
-      val transportArray = authenticatorResponse.transports
-      var ix = 0
-      for (transport in transportArray) {
-        if (transport == "cable") {
-          transportArray[ix] = "hybrid"
-        }
-        ix += 1
-      }
-      return transportArray
-    }
-
-    // This can be shared by both get and create flow response parsers
-    internal fun addOptionalAuthenticatorAttachmentAndRequiredExtensions(
-      authenticatorAttachment: String?,
-      hasClientExtensionResults: Boolean,
-      isDiscoverableCredential: Boolean?,
-      json: JSONObject
-    ) {
-
-      if (authenticatorAttachment != null) {
-        json.put(JSON_KEY_AUTH_ATTACHMENT, authenticatorAttachment)
-      }
-
-      val clientExtensionsJson = JSONObject()
-
-      if (hasClientExtensionResults) {
-        try {
-          if (isDiscoverableCredential != null) {
-            val credPropsObject = JSONObject()
-            credPropsObject.put(JSON_KEY_RK, isDiscoverableCredential)
-            clientExtensionsJson.put(JSON_KEY_CRED_PROPS, credPropsObject)
-          }
-        } catch (t: Throwable) {
-          Log.e(
-            TAG,
-            "ClientExtensionResults faced possible implementation " +
-              "inconsistency in uvmEntries - $t"
-          )
-        }
-      }
-      json.put(JSON_KEY_CLIENT_EXTENSION_RESULTS, clientExtensionsJson)
-    }
-
     fun toAssertPasskeyResponse(cred: SignInCredential): String {
       var json = JSONObject()
       val publicKeyCred = cred.publicKeyCredential
@@ -240,61 +161,24 @@
           )
         }
         is AuthenticatorAssertionResponse -> {
-          beginSignInAssertionResponse(
-            authenticatorResponse.clientDataJSON,
-            authenticatorResponse.authenticatorData,
-            authenticatorResponse.signature,
-            authenticatorResponse.userHandle,
-            json,
-            publicKeyCred.id,
-            publicKeyCred.rawId,
-            publicKeyCred.type,
-            publicKeyCred.authenticatorAttachment,
-            publicKeyCred.clientExtensionResults != null,
-            publicKeyCred.clientExtensionResults?.credProps?.isDiscoverableCredential
-          )
+          try {
+            return publicKeyCred.toJson()
+          } catch (t: Throwable) {
+            throw GetCredentialUnknownException("The PublicKeyCredential response json had " +
+                "an unexpected exception when parsing: ${t.message}")
+          }
         }
         else -> {
           Log.e(
             TAG,
             "AuthenticatorResponse expected assertion response but " +
-              "got: ${authenticatorResponse.javaClass.name}"
+                "got: ${authenticatorResponse.javaClass.name}"
           )
         }
       }
       return json.toString()
     }
 
-    internal fun beginSignInAssertionResponse(
-      clientDataJSON: ByteArray,
-      authenticatorData: ByteArray,
-      signature: ByteArray,
-      userHandle: ByteArray?,
-      json: JSONObject,
-      publicKeyCredId: String,
-      publicKeyCredRawId: ByteArray,
-      publicKeyCredType: String,
-      authenticatorAttachment: String?,
-      hasClientExtensionResults: Boolean,
-      isDiscoverableCredential: Boolean?
-    ) {
-      val responseJson = JSONObject()
-      responseJson.put(JSON_KEY_CLIENT_DATA, b64Encode(clientDataJSON))
-      responseJson.put(JSON_KEY_AUTH_DATA, b64Encode(authenticatorData))
-      responseJson.put(JSON_KEY_SIGNATURE, b64Encode(signature))
-      userHandle?.let { responseJson.put(JSON_KEY_USER_HANDLE, b64Encode(userHandle)) }
-      json.put(JSON_KEY_RESPONSE, responseJson)
-      json.put(JSON_KEY_ID, publicKeyCredId)
-      json.put(JSON_KEY_RAW_ID, b64Encode(publicKeyCredRawId))
-      json.put(JSON_KEY_TYPE, publicKeyCredType)
-      addOptionalAuthenticatorAttachmentAndRequiredExtensions(
-        authenticatorAttachment,
-        hasClientExtensionResults,
-        isDiscoverableCredential,
-        json
-      )
-    }
-
     /**
      * Converts from the Credential Manager public key credential option to the Play Auth Module
      * passkey json option.
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt
index 3040f9d..c744990 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt
@@ -67,6 +67,9 @@
         // Value for the specific begin sign in type
         const val BEGIN_SIGN_IN_TAG = "BEGIN_SIGN_IN"
 
+        // Key for the Sign-in Intent flow
+        const val SIGN_IN_INTENT_TAG = "SIGN_IN_INTENT"
+
         // Value for the specific create password type
         const val CREATE_PASSWORD_TAG = "CREATE_PASSWORD"
 
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/GetSignInIntent/CredentialProviderGetSignInIntentController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/GetSignInIntent/CredentialProviderGetSignInIntentController.kt
new file mode 100644
index 0000000..41a9fed7
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/GetSignInIntent/CredentialProviderGetSignInIntentController.kt
@@ -0,0 +1,285 @@
+/*
+ * 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.credentials.playservices.controllers.GetSignInIntent
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.CancellationSignal
+import android.os.Handler
+import android.os.Looper
+import android.os.ResultReceiver
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.credentials.Credential
+import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.exceptions.GetCredentialCancellationException
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.exceptions.GetCredentialInterruptedException
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.exceptions.GetCredentialUnsupportedException
+import androidx.credentials.playservices.CredentialProviderPlayServicesImpl
+import androidx.credentials.playservices.HiddenActivity
+import androidx.credentials.playservices.controllers.CredentialProviderBaseController
+import androidx.credentials.playservices.controllers.CredentialProviderController
+import com.google.android.gms.auth.api.identity.GetSignInIntentRequest
+import com.google.android.gms.auth.api.identity.Identity
+import com.google.android.gms.auth.api.identity.SignInCredential
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.common.api.CommonStatusCodes
+import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
+import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
+import java.util.concurrent.Executor
+
+/**
+ * A controller to handle the GetSignInIntent flow with play services.
+ */
+@Suppress("deprecation")
+internal class CredentialProviderGetSignInIntentController(private val context: Context) :
+    CredentialProviderController<GetCredentialRequest, GetSignInIntentRequest,
+        SignInCredential, GetCredentialResponse, GetCredentialException>(context) {
+
+    /**
+     * The callback object state, used in the protected handleResponse method.
+     */
+    @VisibleForTesting
+    lateinit var callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>
+
+    /**
+     * The callback requires an executor to invoke it.
+     */
+    @VisibleForTesting
+    lateinit var executor: Executor
+
+    /**
+     * The cancellation signal, which is shuttled around to stop the flow at any moment prior to
+     * returning data.
+     */
+    @VisibleForTesting
+    private var cancellationSignal: CancellationSignal? = null
+
+    private val resultReceiver = object : ResultReceiver(
+        Handler(Looper.getMainLooper())
+    ) {
+        public override fun onReceiveResult(
+            resultCode: Int,
+            resultData: Bundle
+        ) {
+            if (maybeReportErrorFromResultReceiver(
+                    resultData,
+                 CredentialProviderBaseController.Companion::getCredentialExceptionTypeToException,
+                    executor = executor,
+                    callback = callback,
+                    cancellationSignal
+                )
+            ) return
+            handleResponse(
+                resultData.getInt(ACTIVITY_REQUEST_CODE_TAG),
+                resultCode,
+                resultData.getParcelable(RESULT_DATA_TAG)
+            )
+        }
+    }
+
+    override fun invokePlayServices(
+        request: GetCredentialRequest,
+        callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
+        executor: Executor,
+        cancellationSignal: CancellationSignal?
+    ) {
+        this.cancellationSignal = cancellationSignal
+        this.callback = callback
+        this.executor = executor
+
+        if (CredentialProviderPlayServicesImpl.cancellationReviewer(cancellationSignal)) {
+            return
+        }
+
+        try {
+            val convertedRequest: GetSignInIntentRequest =
+                this.convertRequestToPlayServices(request)
+
+            val hiddenIntent = Intent(context, HiddenActivity::class.java)
+            hiddenIntent.putExtra(REQUEST_TAG, convertedRequest)
+            generateHiddenActivityIntent(resultReceiver, hiddenIntent, SIGN_IN_INTENT_TAG)
+            context.startActivity(hiddenIntent)
+        } catch (e: Exception) {
+            when (e) {
+                is GetCredentialUnsupportedException ->
+                    cancelOrCallbackExceptionOrResult(cancellationSignal) {
+                        this.executor.execute {
+                            this.callback.onError(e)
+                        }
+                    }
+                else ->
+                    cancelOrCallbackExceptionOrResult(cancellationSignal) {
+                        this.executor.execute {
+                            this.callback.onError(
+                                GetCredentialUnknownException(ERROR_MESSAGE_START_ACTIVITY_FAILED)
+                            )
+                        }
+                    }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public override fun convertRequestToPlayServices(request: GetCredentialRequest):
+        GetSignInIntentRequest {
+        if (request.credentialOptions.count() != 1) {
+            throw GetCredentialUnsupportedException(
+                "GetSignInWithGoogleOption cannot be combined with other options."
+            )
+        }
+        val option = request.credentialOptions[0] as GetSignInWithGoogleOption
+        return GetSignInIntentRequest.builder()
+            .setServerClientId(option.serverClientId)
+            .filterByHostedDomain(option.hostedDomainFilter)
+            .setNonce(option.nonce)
+            .build()
+    }
+
+    override fun convertResponseToCredentialManager(response: SignInCredential):
+        GetCredentialResponse {
+        var cred: Credential? = null
+        if (response.googleIdToken != null) {
+            cred = createGoogleIdCredential(response)
+        } else {
+            Log.w(TAG, "Credential returned but no google Id found")
+        }
+        if (cred == null) {
+            throw GetCredentialUnknownException(
+                "When attempting to convert get response, " + "null credential found"
+            )
+        }
+        return GetCredentialResponse(cred)
+    }
+
+    @VisibleForTesting
+    fun createGoogleIdCredential(response: SignInCredential): GoogleIdTokenCredential {
+        var cred = GoogleIdTokenCredential.Builder().setId(response.id)
+        try {
+            cred.setIdToken(response.googleIdToken!!)
+        } catch (e: Exception) {
+            throw GetCredentialUnknownException(
+                "When attempting to convert get response, " + "null Google ID Token found"
+            )
+        }
+
+        if (response.displayName != null) {
+            cred.setDisplayName(response.displayName)
+        }
+
+        if (response.givenName != null) {
+            cred.setGivenName(response.givenName)
+        }
+
+        if (response.familyName != null) {
+            cred.setFamilyName(response.familyName)
+        }
+
+        if (response.phoneNumber != null) {
+            cred.setPhoneNumber(response.phoneNumber)
+        }
+
+        if (response.profilePictureUri != null) {
+            cred.setProfilePictureUri(response.profilePictureUri)
+        }
+
+        return cred.build()
+    }
+
+    internal fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
+        if (uniqueRequestCode != CONTROLLER_REQUEST_CODE) {
+            Log.w(
+                TAG,
+                "Returned request code $CONTROLLER_REQUEST_CODE which " +
+                    " does not match what was given $uniqueRequestCode"
+            )
+            return
+        }
+        if (maybeReportErrorResultCodeGet(
+                resultCode,
+                { s, f -> cancelOrCallbackExceptionOrResult(s, f) },
+                { e ->
+                    this.executor.execute {
+                        this.callback.onError(e)
+                    }
+                },
+                cancellationSignal
+            )
+        ) return
+        try {
+            val signInCredential =
+                Identity.getSignInClient(context).getSignInCredentialFromIntent(data)
+            val response = convertResponseToCredentialManager(signInCredential)
+            cancelOrCallbackExceptionOrResult(cancellationSignal) {
+                this.executor.execute {
+                    this.callback.onResult(response)
+                }
+            }
+        } catch (e: ApiException) {
+            var exception: GetCredentialException = GetCredentialUnknownException(e.message)
+            if (e.statusCode == CommonStatusCodes.CANCELED) {
+                exception = GetCredentialCancellationException(e.message)
+            } else if (e.statusCode in retryables) {
+                exception = GetCredentialInterruptedException(e.message)
+            }
+            cancelOrCallbackExceptionOrResult(cancellationSignal) {
+                executor.execute {
+                    callback.onError(exception)
+                }
+            }
+            return
+        } catch (e: GetCredentialException) {
+            cancelOrCallbackExceptionOrResult(cancellationSignal) {
+                executor.execute {
+                    callback.onError(e)
+                }
+            }
+        } catch (t: Throwable) {
+            val e = GetCredentialUnknownException(t.message)
+            cancelOrCallbackExceptionOrResult(cancellationSignal) {
+                executor.execute {
+                    callback.onError(e)
+                }
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "GetSignInIntent"
+        private var controller: CredentialProviderGetSignInIntentController? = null
+
+        /**
+         * This finds a past version of the [CredentialProviderGetSignInIntentController] if it exists,
+         * otherwise it generates a new instance.
+         *
+         * @param context the calling context for this controller
+         * @return a credential provider controller for a specific begin sign in credential request
+         */
+        @JvmStatic
+        fun getInstance(context: Context): CredentialProviderGetSignInIntentController {
+            if (controller == null) {
+                controller = CredentialProviderGetSignInIntentController(context)
+            }
+            return controller!!
+        }
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java
index b909602..5b332ac 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Intent;
+import android.os.Build;
 
 import androidx.annotation.RequiresApi;
 import androidx.credentials.CreatePasswordResponse;
@@ -42,6 +43,10 @@
 
     @Test
     public void test_setGetCreateCredentialException() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return;
+        }
+
         Intent intent = new Intent();
 
         CreateCredentialInterruptedException initialException =
@@ -57,6 +62,10 @@
 
     @Test
     public void test_setGetCreateCredentialException_throwsWhenEmptyIntent() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return;
+        }
+
         assertThat(
                         IntentHandlerConverters.getCreateCredentialException(
                                 BLANK_INTENT))
@@ -65,6 +74,10 @@
 
     @Test
     public void test_credentialException() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return;
+        }
+
         Intent intent = new Intent();
         GetCredentialInterruptedException initialException =
                 new GetCredentialInterruptedException("message");
@@ -79,12 +92,19 @@
 
     @Test
     public void test_credentialException_throwsWhenEmptyIntent() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return;
+        }
+
         assertThat(IntentHandlerConverters.getGetCredentialException(BLANK_INTENT))
                 .isNull();
     }
 
     @Test
     public void test_beginGetResponse() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return;
+        }
 
         Intent intent = new Intent();
         BeginGetCredentialResponse initialResponse =
@@ -100,12 +120,20 @@
 
     @Test
     public void test_beginGetResponse_throwsWhenEmptyIntent() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return;
+        }
+
         assertThat(IntentHandlerConverters.getBeginGetResponse(BLANK_INTENT))
                 .isNull();
     }
 
     @Test
     public void test_credentialResponse() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return;
+        }
+
         Intent intent = new Intent();
         PasswordCredential credential = new PasswordCredential("a", "b");
         GetCredentialResponse initialResponse = new GetCredentialResponse(credential);
@@ -120,12 +148,20 @@
 
     @Test
     public void test_credentialResponse_throwsWhenEmptyIntent() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return;
+        }
+
         assertThat(IntentHandlerConverters.getGetCredentialResponse(BLANK_INTENT))
                 .isNull();
     }
 
     @Test
     public void test_createCredentialCredentialResponse() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return;
+        }
+
         Intent intent = new Intent();
         CreatePasswordResponse initialResponse = new CreatePasswordResponse();
 
@@ -140,6 +176,10 @@
 
     @Test
     public void test_createCredentialCredentialResponse_throwsWhenEmptyIntent() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return;
+        }
+
         assertThat(
                         IntentHandlerConverters
                                 .getCreateCredentialCredentialResponse(BLANK_INTENT))
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt
index 7e9e1a2..c9ed499 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt
@@ -16,6 +16,7 @@
 package androidx.credentials.provider
 
 import android.content.Intent
+import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.credentials.CreatePasswordResponse
 import androidx.credentials.GetCredentialResponse
@@ -36,6 +37,10 @@
 class PendingIntentHandlerTest {
     @Test
     fun test_createCredentialException() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return
+        }
+
         val intent = Intent()
         val initialException = CreateCredentialInterruptedException("message")
 
@@ -48,12 +53,20 @@
 
     @Test()
     fun test_createCredentialException_throwsWhenEmptyIntent() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return
+        }
+
         val intent = Intent()
         assertThat(intent.getCreateCredentialException()).isNull()
     }
 
     @Test
     fun test_credentialException() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return
+        }
+
         val intent = Intent()
         val initialException = GetCredentialInterruptedException("message")
 
@@ -66,12 +79,20 @@
 
     @Test
     fun test_credentialException_throwsWhenEmptyIntent() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return
+        }
+
         val intent = Intent()
         assertThat(intent.getGetCredentialException()).isNull()
     }
 
     @Test
     fun test_beginGetResponse() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return
+        }
+
         val intent = Intent()
         val initialResponse = BeginGetCredentialResponse.Builder().build()
 
@@ -84,12 +105,20 @@
 
     @Test
     fun test_beginGetResponse_throwsWhenEmptyIntent() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return
+        }
+
         val intent = Intent()
         assertThat(intent.getBeginGetResponse()).isNull()
     }
 
     @Test
     fun test_credentialResponse() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return
+        }
+
         val intent = Intent()
         val credential = PasswordCredential("a", "b")
         val initialResponse = GetCredentialResponse(credential)
@@ -103,12 +132,20 @@
 
     @Test
     fun test_credentialResponse_throwsWhenEmptyIntent() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return
+        }
+
         val intent = Intent()
         assertThat(intent.getGetCredentialResponse()).isNull()
     }
 
     @Test
     fun test_createCredentialCredentialResponse() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return
+        }
+
         val intent = Intent()
         val initialResponse = CreatePasswordResponse()
 
@@ -121,6 +158,10 @@
 
     @Test
     fun test_createCredentialCredentialResponse_throwsWhenEmptyIntent() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return
+        }
+
         val intent = Intent()
         val r = intent.getCreateCredentialCredentialResponse()
         assertThat(r).isNull()
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CallingAppInfo.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CallingAppInfo.kt
index daf5878..b0044ff 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/CallingAppInfo.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CallingAppInfo.kt
@@ -61,9 +61,14 @@
      * Additionally, in order to get the origin, the credential provider must
      * provide an allowlist of privileged browsers/apps that it trusts.
      * This allowlist must be in the form of a valid, non-empty JSON. The
-     * origin will only be returned if the [packageName] and the fingerprints of certificates
-     * obtained from the [signingInfo] match with that of an app allowlisted
-     * in [privilegedAllowlist]. The format of this JSON must adhere to the following sample.
+     * origin will only be returned if the [packageName] and the SHA256 hash of the newest
+     * signature obtained from the [signingInfo], is present in the [privilegedAllowlist].
+     *
+     * Packages that are signed with multiple signers will only receive the origin if all of the
+     * signatures are present in the [privilegedAllowlist].
+     *
+     * The format of this [privilegedAllowlist] JSON must adhere to the following sample.
+     *
      * ```
      * {"apps": [
      *    {
@@ -134,23 +139,20 @@
         candidateApps: List<PrivilegedApp>
     ): Boolean {
         for (app in candidateApps) {
-            if (app.packageName == packageName &&
-                !app.fingerprints.intersect(getSignatureFingerprints(signingInfo)).isEmpty()
-            ) {
-                return true
+            if (app.packageName == packageName) {
+                return isAppPrivileged(app.fingerprints)
             }
         }
         return false
     }
 
-    private fun getSignatureFingerprints(signingInfo: SigningInfo): Set<String> {
-        val fingerprints = mutableSetOf<String>()
+    private fun isAppPrivileged(candidateFingerprints: Set<String>): Boolean {
         if (Build.VERSION.SDK_INT >= 28) {
-            return SignatureParserApi28(signingInfo).getSignatureFingerprints()
-        } else {
-            // TODO("Extend to <= 28 if needed")
+            return SignatureVerifierApi28(signingInfo)
+                .verifySignatureFingerprints(candidateFingerprints)
         }
-        return fingerprints
+        // TODO("Extend to <= 28 if needed")
+        return false
     }
 
     init {
@@ -158,19 +160,14 @@
     }
 
     @RequiresApi(28)
-    private class SignatureParserApi28(private val signingInfo: SigningInfo) {
-        fun getSignatureFingerprints(): Set<String> {
+    private class SignatureVerifierApi28(private val signingInfo: SigningInfo) {
+        private fun getSignatureFingerprints(): Set<String> {
             val fingerprints = mutableSetOf<String>()
-            if (signingInfo.hasMultipleSigners()) {
-                val signatures = signingInfo.apkContentsSigners
-                if (signatures != null) {
-                    fingerprints.addAll(convertToFingerprints(signatures))
-                }
-            } else {
-                val signatures = signingInfo.signingCertificateHistory
-                if (signatures != null) {
-                    fingerprints.addAll(convertToFingerprints(signatures))
-                }
+            if (signingInfo.hasMultipleSigners() && signingInfo.apkContentsSigners != null) {
+                fingerprints.addAll(convertToFingerprints(signingInfo.apkContentsSigners))
+            } else if (signingInfo.signingCertificateHistory != null) {
+                fingerprints.addAll(convertToFingerprints(
+                    arrayOf(signingInfo.signingCertificateHistory[0])))
             }
             return fingerprints
         }
@@ -184,5 +181,14 @@
             }
             return fingerprints
         }
+
+        fun verifySignatureFingerprints(candidateSigFingerprints: Set<String>): Boolean {
+            val appSigFingerprints = getSignatureFingerprints()
+            return if (signingInfo.hasMultipleSigners()) {
+                candidateSigFingerprints.containsAll(appSigFingerprints)
+            } else {
+                candidateSigFingerprints.intersect(appSigFingerprints).isNotEmpty()
+            }
+        }
     }
 }
diff --git a/datastore/datastore-core/build.gradle b/datastore/datastore-core/build.gradle
index f9faa96..8feb7e2 100644
--- a/datastore/datastore-core/build.gradle
+++ b/datastore/datastore-core/build.gradle
@@ -19,6 +19,7 @@
 import androidx.build.PlatformIdentifier
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
+import com.google.protobuf.gradle.ProtobufExtract
 
 plugins {
     id("AndroidXPlugin")
@@ -57,6 +58,11 @@
     }
 }
 
+def protoDir = project.layout.projectDirectory.dir("src/androidInstrumentedTest/proto")
+tasks.named("extractAndroidTestProto").configure {
+    it.inputFiles.from(project.files(protoDir))
+}
+
 androidXMultiplatform {
     jvm()
     mac()
@@ -110,7 +116,14 @@
             }
         }
 
-        androidTest {
+        androidUnitTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.protobufLite)
+            }
+        }
+
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.protobufLite)
@@ -125,8 +138,8 @@
             }
         }
 
-        androidAndroidTest {
-            dependsOn(androidTest)
+        androidInstrumentedTest {
+            dependsOn(androidUnitTest)
         }
 
         if (enableNative) {
diff --git a/datastore/datastore-core/src/androidAndroidTest/AndroidManifest.xml b/datastore/datastore-core/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from datastore/datastore-core/src/androidAndroidTest/AndroidManifest.xml
rename to datastore/datastore-core/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreFactoryTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/MultiProcessDataStoreFactoryTest.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreFactoryTest.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/MultiProcessDataStoreFactoryTest.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreSingleProcessFileTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/MultiProcessDataStoreSingleProcessFileTest.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreSingleProcessFileTest.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/MultiProcessDataStoreSingleProcessFileTest.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreSingleProcessOkioTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/MultiProcessDataStoreSingleProcessOkioTest.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreSingleProcessOkioTest.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/MultiProcessDataStoreSingleProcessOkioTest.kt
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/MultiProcessDataStoreSingleProcessTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/MultiProcessDataStoreSingleProcessTest.kt
new file mode 100644
index 0000000..24bd9db
--- /dev/null
+++ b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/MultiProcessDataStoreSingleProcessTest.kt
@@ -0,0 +1,988 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.core
+
+import android.os.StrictMode
+import androidx.datastore.TestFile
+import androidx.datastore.TestIO
+import androidx.datastore.TestingSerializerConfig
+import androidx.datastore.core.handlers.NoOpCorruptionHandler
+import androidx.test.filters.FlakyTest
+import androidx.test.filters.LargeTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.util.concurrent.Executors
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.AbstractCoroutineContextElement
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.newSingleThreadContext
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * A testing class based on duplicate from "SingleProcessDataStoreTest" that only tests the features
+ * in a single process use case. More tests are added for StrictMode.
+ */
+@OptIn(DelicateCoroutinesApi::class)
+@ExperimentalCoroutinesApi
+@LargeTest
+@RunWith(JUnit4::class)
+abstract class MultiProcessDataStoreSingleProcessTest<F : TestFile<F>>(
+    protected val testIO: TestIO<F, *>
+) {
+    protected lateinit var store: DataStore<Byte>
+    private lateinit var serializerConfig: TestingSerializerConfig
+    protected lateinit var testFile: F
+    protected lateinit var tempFolder: F
+    protected lateinit var dataStoreScope: TestScope
+
+    abstract fun getJavaFile(file: F): File
+
+    private fun newDataStore(
+        file: F = testFile,
+        scope: CoroutineScope = dataStoreScope,
+        initTasksList: List<suspend (api: InitializerApi<Byte>) -> Unit> = listOf(),
+        corruptionHandler: CorruptionHandler<Byte> = NoOpCorruptionHandler<Byte>()
+    ): DataStore<Byte> {
+        return DataStoreImpl(
+            storage = testIO.getStorage(
+                serializerConfig,
+                {
+                    MultiProcessCoordinator(
+                        dataStoreScope.coroutineContext,
+                        getJavaFile(testFile)
+                    )
+                }) { file },
+            scope = scope,
+            initTasksList = initTasksList,
+            corruptionHandler = corruptionHandler
+        )
+    }
+
+    @Before
+    fun setUp() {
+        serializerConfig = TestingSerializerConfig()
+        tempFolder = testIO.newTempFile().also { it.mkdirs() }
+        testFile = testIO.newTempFile(parentFile = tempFolder)
+        dataStoreScope = TestScope(UnconfinedTestDispatcher() + Job())
+        store = testIO.getStore(
+            serializerConfig,
+            dataStoreScope,
+            { MultiProcessCoordinator(dataStoreScope.coroutineContext, getJavaFile(testFile)) }
+        ) { testFile }
+    }
+
+    @Test
+    fun testReadNewMessage() = runTest {
+        assertThat(store.data.first()).isEqualTo(0)
+    }
+
+    @Test
+    fun testReadWithNewInstance() = runBlocking {
+        runTest {
+            val newStore = newDataStore(testFile, scope = backgroundScope)
+            newStore.updateData { 1 }
+        }
+        runTest {
+            val newStore = newDataStore(testFile, scope = backgroundScope)
+            assertThat(newStore.data.first()).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun testScopeCancelledWithActiveFlow() = runTest {
+        val storeScope = CoroutineScope(Job())
+        val dataStore = newDataStore(scope = storeScope)
+        val collection = async {
+            dataStore.data.take(2).collect {
+                // Do nothing, this will wait on another element which will never arrive
+            }
+        }
+
+        storeScope.cancel()
+        collection.join()
+
+        assertThat(collection.isCompleted).isTrue()
+        assertThat(collection.isActive).isFalse()
+    }
+
+    @Test
+    fun testWriteAndRead() = runTest {
+        store.updateData { 1 }
+        assertThat(store.data.first()).isEqualTo(1)
+    }
+
+    @Test
+    fun testWritesDontBlockReadsInSameProcess() = runTest {
+        val transformStarted = CompletableDeferred<Unit>()
+        val continueTransform = CompletableDeferred<Unit>()
+
+        val slowUpdate = async {
+            store.updateData {
+                transformStarted.complete(Unit)
+                continueTransform.await()
+                it.inc()
+            }
+        }
+        // Wait for the transform to begin.
+        transformStarted.await()
+
+        // Read is not blocked.
+        assertThat(store.data.first()).isEqualTo(0)
+
+        continueTransform.complete(Unit)
+        slowUpdate.await()
+
+        // After update completes, update runs, and read shows new data.
+        assertThat(store.data.first()).isEqualTo(1)
+    }
+
+    @Test
+    fun testWriteMultiple() = runTest {
+        store.updateData { 2 }
+
+        assertThat(store.data.first()).isEqualTo(2)
+
+        store.updateData { it.dec() }
+
+        assertThat(store.data.first()).isEqualTo(1)
+    }
+
+    @Test
+    fun testReadAfterTransientBadWrite() = runBlocking {
+        val file = testIO.newTempFile(tempFolder)
+        runTest {
+            val store = newDataStore(file, scope = backgroundScope)
+            store.updateData { 1 }
+            serializerConfig.failingWrite = true
+            assertThrows<IOException> { store.updateData { 2 } }
+        }
+
+        runTest {
+            val newStore = newDataStore(file, scope = backgroundScope)
+            assertThat(newStore.data.first()).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun testWriteToNonExistentDir() = runBlocking {
+        val fileInNonExistentDir = testIO.newTempFile(
+            relativePath = "/this/does/not/exist/ds.txt"
+        )
+        assertThat(fileInNonExistentDir.exists()).isFalse()
+        assertThat(fileInNonExistentDir.parentFile()!!.exists()).isFalse()
+        runTest {
+            val newStore = newDataStore(fileInNonExistentDir, scope = backgroundScope)
+
+            newStore.updateData { 1 }
+
+            assertThat(newStore.data.first()).isEqualTo(1)
+        }
+
+        runTest {
+            val newStore = newDataStore(fileInNonExistentDir, scope = backgroundScope)
+            assertThat(newStore.data.first()).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun testReadFromNonExistentFile() = runTest {
+        val newStore = newDataStore(testFile)
+        assertThat(newStore.data.first()).isEqualTo(0)
+    }
+
+    @Test
+    fun testWriteToDirFails() = runTest {
+        val directoryFile = testIO.newTempFile(relativePath = "this/is/a/directory").also {
+            it.mkdirs()
+        }
+
+        assertThat(directoryFile.isDirectory()).isTrue()
+
+        val newStore = newDataStore(directoryFile)
+        assertThrows<IOException> { newStore.data.first() }
+    }
+
+    @Test
+    fun testExceptionWhenCreatingFilePropagates() = runTest {
+        var failFileProducer = true
+
+        val fileProducer = {
+            if (failFileProducer) {
+                throw IOException("Exception when producing file")
+            }
+            testFile
+        }
+
+        val newStore = testIO.getStore(
+            serializerConfig,
+            dataStoreScope,
+            {
+                MultiProcessCoordinator(
+                    dataStoreScope.coroutineContext,
+                    getJavaFile(fileProducer())
+                )
+            },
+            fileProducer
+        )
+
+        assertThrows<IOException> { newStore.data.first() }.hasMessageThat().isEqualTo(
+            "Exception when producing file"
+        )
+
+        failFileProducer = false
+
+        assertThat(newStore.data.first()).isEqualTo(0)
+    }
+
+    @Test
+    fun testWriteTransformCancellation() = runTest {
+        val transform = CompletableDeferred<Byte>()
+
+        val write = async { store.updateData { transform.await() } }
+
+        assertThat(write.isCompleted).isFalse()
+
+        transform.cancel()
+
+        assertThrows<CancellationException> { write.await() }
+
+        // Check that the datastore's scope is still active:
+
+        assertThat(store.updateData { it.inc().inc() }).isEqualTo(2)
+    }
+
+    @Test
+    fun testWriteAfterTransientBadRead() = runTest {
+        testFile.write("")
+        assertThat(testFile.exists()).isTrue()
+
+        serializerConfig.failingRead = true
+
+        assertThrows<IOException> { store.data.first() }
+
+        serializerConfig.failingRead = false
+
+        store.updateData { 1 }
+        assertThat(store.data.first()).isEqualTo(1)
+    }
+
+    @Test
+    fun testWriteWithBadReadFails() = runTest {
+        testFile.write("")
+        assertThat(testFile.exists()).isTrue()
+
+        serializerConfig.failingRead = true
+
+        assertThrows<IOException> { store.updateData { 1 } }
+    }
+
+    @Test
+    fun testCancellingDataStoreScopePropagatesToWrites() = runBlocking<Unit> {
+        val scope = CoroutineScope(Job())
+
+        val dataStore = newDataStore(scope = scope)
+
+        val latch = CompletableDeferred<Unit>()
+
+        val slowUpdate = async {
+            dataStore.updateData {
+                latch.await()
+                it.inc()
+            }
+        }
+
+        val notStartedUpdate = async {
+            dataStore.updateData {
+                it.inc()
+            }
+        }
+
+        scope.cancel()
+
+        assertThrows<CancellationException> { slowUpdate.await() }
+
+        assertThrows<CancellationException> { notStartedUpdate.await() }
+
+        assertThrows<CancellationException> { dataStore.updateData { 123 } }
+    }
+
+    @Test
+    fun testCancellingCallerScopePropagatesToWrites() = runBlocking<Unit> {
+        val dsScope = CoroutineScope(Job())
+        val callerScope = CoroutineScope(Job())
+
+        val dataStore = newDataStore(scope = dsScope)
+
+        val latch = CompletableDeferred<Unit>()
+
+        // The ordering of the following are not guaranteed but I think they won't be flaky with
+        // Dispatchers.Unconfined
+        val awaitingCancellation = callerScope.async(Dispatchers.Unconfined) {
+            dataStore.updateData { awaitCancellation() }
+        }
+
+        val started = dsScope.async(Dispatchers.Unconfined) {
+            dataStore.updateData {
+                latch.await()
+                it.inc()
+            }
+        }
+
+        val notStarted = callerScope.async(Dispatchers.Unconfined) {
+            dataStore.updateData { it.inc() }
+        }
+
+        callerScope.coroutineContext.job.cancelAndJoin()
+
+        assertThat(awaitingCancellation.isCancelled).isTrue()
+        assertThat(notStarted.isCancelled).isTrue()
+
+        // wait for coroutine to complete to prevent it from outliving the test, which is flaky
+        latch.complete(Unit)
+        started.await()
+        assertThat(dataStore.data.first()).isEqualTo(1)
+    }
+
+    @Test
+    fun testCanWriteFromInitTask() = runTest {
+        store = newDataStore(initTasksList = listOf { api -> api.updateData { 1 } })
+
+        assertThat(store.data.first()).isEqualTo(1)
+    }
+
+    @FlakyTest(bugId = 242765370)
+    @Test
+    fun testInitTaskFailsFirstTimeDueToReadFail() = runTest {
+        store = newDataStore(initTasksList = listOf { api -> api.updateData { 1 } })
+
+        serializerConfig.failingRead = true
+        assertThrows<IOException> { store.updateData { 2 } }
+
+        serializerConfig.failingRead = false
+        store.updateData { it.inc().inc() }
+
+        assertThat(store.data.first()).isEqualTo(3)
+    }
+
+    @Test
+    fun testInitTaskFailsFirstTimeDueToException() = runTest {
+        val failInit = AtomicBoolean(true)
+        store = newDataStore(
+            initTasksList = listOf { _ ->
+                if (failInit.get()) {
+                    throw IOException("I was asked to fail init")
+                }
+            }
+        )
+        assertThrows<IOException> { store.updateData { 5 } }
+
+        failInit.set(false)
+
+        store.updateData { it.inc() }
+        assertThat(store.data.first()).isEqualTo(1)
+    }
+
+    @Test
+    fun testInitTaskOnlyRunsOnce() = runTest {
+        val count = AtomicInteger()
+        val newStore = newDataStore(
+            testFile,
+            initTasksList = listOf { _ ->
+                count.incrementAndGet()
+            }
+        )
+
+        repeat(10) {
+            newStore.updateData { it.inc() }
+            newStore.data.first()
+        }
+
+        assertThat(count.get()).isEqualTo(1)
+    }
+
+    @Test
+    fun testWriteDuringInit() = runTest {
+        val continueInit = CompletableDeferred<Unit>()
+
+        store = newDataStore(
+            initTasksList = listOf { api ->
+                continueInit.await()
+                api.updateData { 1 }
+            }
+        )
+
+        val update = async {
+            store.updateData { b ->
+                assertThat(b).isEqualTo(1)
+                b
+            }
+        }
+
+        continueInit.complete(Unit)
+        update.await()
+
+        assertThat(store.data.first()).isEqualTo(1)
+    }
+
+    @Test
+    fun testCancelDuringInit() = runTest {
+        val continueInit = CompletableDeferred<Unit>()
+
+        store = newDataStore(
+            initTasksList = listOf { api ->
+                continueInit.await()
+                api.updateData { 1 }
+            }
+        )
+
+        val update = async {
+            store.updateData { it }
+        }
+
+        val read = async {
+            store.data.first()
+        }
+
+        update.cancel()
+        read.cancel()
+        continueInit.complete(Unit)
+
+        assertThrows<CancellationException> { update.await() }
+        assertThrows<CancellationException> { read.await() }
+
+        store.updateData { it.inc().inc() }
+
+        assertThat(store.data.first()).isEqualTo(3)
+    }
+
+    @Test
+    fun testConcurrentUpdatesInit() = runTest {
+        val continueUpdate = CompletableDeferred<Unit>()
+
+        val concurrentUpdateInitializer: suspend (InitializerApi<Byte>) -> Unit = { api ->
+            val update1 = async {
+                api.updateData {
+                    continueUpdate.await()
+                    it.inc().inc()
+                }
+            }
+            api.updateData {
+                it.inc()
+            }
+            update1.await()
+        }
+
+        store = newDataStore(initTasksList = listOf(concurrentUpdateInitializer))
+        val getData = async { store.data.first() }
+        continueUpdate.complete(Unit)
+
+        assertThat(getData.await()).isEqualTo(3)
+    }
+
+    @Test
+    fun testInitUpdateBlockRead() = runTest {
+        val continueInit = CompletableDeferred<Unit>()
+        val continueUpdate = CompletableDeferred<Unit>()
+
+        val updateInitializer: suspend (InitializerApi<Byte>) -> Unit = { api ->
+            api.updateData {
+                continueInit.await()
+                it.inc()
+            }
+        }
+
+        store = newDataStore(initTasksList = listOf(updateInitializer))
+        val getData = async { store.data.first() }
+        val updateData = async {
+            store.updateData {
+                continueUpdate.await()
+                it.inc()
+            }
+        }
+
+        assertThat(getData.isCompleted).isFalse()
+        assertThat(getData.isActive).isTrue()
+
+        continueInit.complete(Unit)
+        assertThat(getData.await()).isEqualTo(1)
+
+        assertThat(updateData.isCompleted).isFalse()
+        assertThat(updateData.isActive).isTrue()
+
+        continueUpdate.complete(Unit)
+        assertThat(updateData.await()).isEqualTo(2)
+        assertThat(store.data.first()).isEqualTo(2)
+    }
+
+    @Test
+    fun testUpdateSuccessfullyCommittedInit() = runTest {
+        var otherStorage: Byte = 123
+
+        val initializer: suspend (InitializerApi<Byte>) -> Unit = { api ->
+            api.updateData {
+                otherStorage
+            }
+            // Similar to cleanUp():
+            otherStorage = 0
+        }
+
+        val store = newDataStore(initTasksList = listOf(initializer))
+
+        serializerConfig.failingWrite = true
+        assertThrows<IOException> { store.data.first() }
+
+        serializerConfig.failingWrite = false
+        assertThat(store.data.first()).isEqualTo(123)
+    }
+
+    @Test
+    fun testInitApiUpdateThrowsAfterInitTasksComplete() = runTest {
+        var savedApi: InitializerApi<Byte>? = null
+
+        val initializer: suspend (InitializerApi<Byte>) -> Unit = { api ->
+            savedApi = api
+        }
+
+        val store = newDataStore(initTasksList = listOf(initializer))
+
+        assertThat(store.data.first()).isEqualTo(0)
+
+        assertThrows<IllegalStateException> { savedApi?.updateData { 123 } }
+    }
+
+    @Test
+    fun testFlowReceivesUpdates() = runTest {
+        val collectedBytes = mutableListOf<Byte>()
+
+        val flowCollectionJob = async {
+            store.data.take(8).toList(collectedBytes)
+        }
+
+        repeat(7) {
+            store.updateData { it.inc() }
+        }
+
+        flowCollectionJob.join()
+
+        assertThat(collectedBytes).isEqualTo(mutableListOf<Byte>(0, 1, 2, 3, 4, 5, 6, 7))
+    }
+
+    @Test
+    fun testMultipleFlowsReceiveData() = runTest {
+        val flowOf8 = store.data.take(8)
+
+        val bytesFromFirstCollect = mutableListOf<Byte>()
+        val bytesFromSecondCollect = mutableListOf<Byte>()
+
+        val flowCollection1 = async {
+            flowOf8.toList(bytesFromFirstCollect)
+        }
+
+        val flowCollection2 = async {
+            flowOf8.toList(bytesFromSecondCollect)
+        }
+
+        repeat(7) {
+            store.updateData { it.inc() }
+        }
+
+        flowCollection1.join()
+        flowCollection2.join()
+
+        // This test only works because runTest ensures consistent behavior
+        // Otherwise, we cannot really expect the collector to read every single value
+        // (we provide eventual consistency, so it would also be OK if it missed some intermediate
+        // values as long as it received 7 at the end).
+        assertThat(bytesFromFirstCollect).isEqualTo(mutableListOf<Byte>(0, 1, 2, 3, 4, 5, 6, 7))
+        assertThat(bytesFromSecondCollect).isEqualTo(mutableListOf<Byte>(0, 1, 2, 3, 4, 5, 6, 7))
+    }
+
+    @Test
+    fun testExceptionInFlowDoesNotBreakUpstream() = runTest {
+        val flowOf8 = store.data.take(8)
+
+        val collectedBytes = mutableListOf<Byte>()
+
+        val failedFlowCollection = async {
+            assertThrows<Exception> {
+                flowOf8.collect {
+                    throw Exception("Failure while collecting")
+                }
+            }.hasMessageThat().contains("Failure while collecting")
+        }
+
+        val successfulFlowCollection = async {
+            flowOf8.take(8).toList(collectedBytes)
+        }
+
+        repeat(7) {
+            store.updateData { it.inc() }
+        }
+
+        successfulFlowCollection.join()
+        failedFlowCollection.await()
+
+        assertThat(collectedBytes).isEqualTo(mutableListOf<Byte>(0, 1, 2, 3, 4, 5, 6, 7))
+    }
+
+    @Test
+    fun testSlowConsumerDoesntBlockOtherConsumers() = runTest {
+        val flowOf8 = store.data.take(8)
+
+        val collectedBytes = mutableListOf<Byte>()
+
+        val flowCollection2 = async {
+            flowOf8.toList(collectedBytes)
+        }
+
+        val blockedCollection = async {
+            flowOf8.collect {
+                flowCollection2.await()
+            }
+        }
+
+        repeat(15) {
+            store.updateData { it.inc() }
+        }
+
+        flowCollection2.await()
+        assertThat(collectedBytes).isEqualTo(mutableListOf<Byte>(0, 1, 2, 3, 4, 5, 6, 7))
+
+        blockedCollection.await()
+    }
+
+    @Test
+    fun testHandlerNotCalledGoodData() = runBlocking {
+        runTest {
+            newDataStore(testFile, scope = backgroundScope).updateData { 1 }
+        }
+
+        runTest {
+            val testingHandler: TestingCorruptionHandler = TestingCorruptionHandler()
+            val newStore = newDataStore(corruptionHandler = testingHandler, file = testFile)
+
+            newStore.updateData { 2 }
+            newStore.data.first()
+
+            assertThat(testingHandler.numCalls).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun handlerNotCalledNonCorruption() = runBlocking {
+        runTest {
+            newDataStore(testFile, scope = backgroundScope).updateData { 1 }
+        }
+
+        runTest {
+            val testingHandler = TestingCorruptionHandler()
+            serializerConfig.failingRead = true
+            val newStore = newDataStore(corruptionHandler = testingHandler, file = testFile)
+
+            assertThrows<IOException> { newStore.updateData { 2 } }
+            assertThrows<IOException> { newStore.data.first() }
+
+            assertThat(testingHandler.numCalls).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun testHandlerCalledCorruptDataRead() = runBlocking {
+        runTest {
+            val newStore = newDataStore(testFile, scope = backgroundScope)
+            newStore.updateData { 1 } // Pre-seed the data so the file exists.
+        }
+
+        runTest {
+            val testingHandler: TestingCorruptionHandler = TestingCorruptionHandler()
+            serializerConfig.failReadWithCorruptionException = true
+            val newStore = newDataStore(corruptionHandler = testingHandler, file = testFile)
+
+            assertThrows<IOException> { newStore.data.first() }.hasMessageThat().contains(
+                "Handler thrown exception."
+            )
+
+            assertThat(testingHandler.numCalls).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun testHandlerCalledCorruptDataWrite() = runBlocking {
+        runTest {
+            val newStore = newDataStore(file = testFile, scope = backgroundScope)
+            newStore.updateData { 1 }
+        }
+
+        runTest {
+            val testingHandler: TestingCorruptionHandler = TestingCorruptionHandler()
+            serializerConfig.failReadWithCorruptionException = true
+            val newStore = newDataStore(corruptionHandler = testingHandler, file = testFile)
+
+            assertThrows<IOException> { newStore.updateData { 1 } }.hasMessageThat().contains(
+                "Handler thrown exception."
+            )
+
+            assertThat(testingHandler.numCalls).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun testHandlerReplaceData() = runBlocking {
+        runTest {
+            newDataStore(file = testFile, scope = backgroundScope).updateData { 1 }
+        }
+
+        runTest {
+            val testingHandler: TestingCorruptionHandler =
+                TestingCorruptionHandler(replaceWith = 10)
+            serializerConfig.failReadWithCorruptionException = true
+            val newStore = newDataStore(
+                corruptionHandler = testingHandler, file = testFile,
+                scope = backgroundScope
+            )
+
+            assertThat(newStore.data.first()).isEqualTo(10)
+        }
+    }
+
+    @Test
+    fun testDefaultValueUsedWhenNoDataOnDisk() = runTest {
+        val dataStore = testIO.getStore(
+            TestingSerializerConfig(defaultValue = 99),
+            dataStoreScope,
+            { MultiProcessCoordinator(dataStoreScope.coroutineContext, getJavaFile(testFile)) }) {
+            testFile
+        }
+
+        assertThat(dataStore.data.first()).isEqualTo(99)
+    }
+
+    @Test
+    fun testTransformRunInCallersContext() = runBlocking<Unit> {
+        suspend fun getContext(): CoroutineContext {
+            return kotlin.coroutines.coroutineContext
+        }
+
+        withContext(TestElement("123")) {
+            store.updateData {
+                val context = getContext()
+                assertThat(context[TestElement.Key]!!.name).isEqualTo("123")
+                it.inc()
+            }
+        }
+    }
+
+    private class TestElement(
+        val name: String
+    ) : AbstractCoroutineContextElement(Key) {
+        companion object Key : CoroutineContext.Key<TestElement>
+    }
+
+    @Test
+    fun testCancelInflightWrite() = doBlockingWithTimeout(1000) {
+        val myScope =
+            CoroutineScope(Job() + Executors.newSingleThreadExecutor().asCoroutineDispatcher())
+
+        val updateStarted = CompletableDeferred<Unit>()
+        myScope.launch {
+            store.updateData {
+                updateStarted.complete(Unit)
+                awaitCancellation()
+            }
+        }
+        updateStarted.await()
+        myScope.coroutineContext[Job]!!.cancelAndJoin()
+    }
+
+    @Test
+    fun testWrite_afterCanceledWrite_succeeds() = doBlockingWithTimeout(1000) {
+        val myScope =
+            CoroutineScope(Job() + Executors.newSingleThreadExecutor().asCoroutineDispatcher())
+
+        val cancelNow = CompletableDeferred<Unit>()
+
+        myScope.launch {
+            store.updateData {
+                cancelNow.complete(Unit)
+                awaitCancellation()
+            }
+        }
+
+        cancelNow.await()
+        myScope.coroutineContext[Job]!!.cancelAndJoin()
+
+        store.updateData { 123 }
+    }
+
+    @Test
+    fun testWrite_fromOtherScope_doesntGetCancelledFromDifferentScope() =
+    doBlockingWithTimeout(1000) {
+        val otherScope = CoroutineScope(Job())
+
+        val callerScope = CoroutineScope(Job())
+
+        val firstUpdateStarted = CompletableDeferred<Unit>()
+        val finishFirstUpdate = CompletableDeferred<Byte>()
+
+        val firstUpdate = otherScope.async(Dispatchers.Unconfined) {
+            store.updateData {
+                firstUpdateStarted.complete(Unit)
+                finishFirstUpdate.await()
+            }
+        }
+
+        callerScope.launch(Dispatchers.Unconfined) {
+            store.updateData {
+                awaitCancellation()
+            }
+        }
+
+        firstUpdateStarted.await()
+        callerScope.coroutineContext.job.cancelAndJoin()
+        finishFirstUpdate.complete(1)
+        firstUpdate.await()
+
+        // It's still usable:
+        assertThat(store.updateData { it.inc() }).isEqualTo(2)
+    }
+
+    @Test
+    fun testCreateDuplicateActiveDataStore() = runTest {
+        val file = testIO.newTempFile(parentFile = tempFolder)
+        val dataStore = newDataStore(file = file, scope = CoroutineScope(Job()))
+
+        dataStore.data.first()
+
+        val duplicateDataStore = newDataStore(file = file, scope = CoroutineScope(Job()))
+
+        assertThrows<IllegalStateException> {
+            duplicateDataStore.data.first()
+        }
+    }
+
+    @Test
+    fun testCreateDataStore_withSameFileAsInactiveDataStore() = runTest {
+        val file = testIO.newTempFile(parentFile = tempFolder)
+        val scope1 = CoroutineScope(Job())
+        val dataStore1 = newDataStore(file = file, scope = scope1)
+
+        dataStore1.data.first()
+
+        scope1.coroutineContext.job.cancelAndJoin()
+
+        val dataStore2 = newDataStore(file = file, scope = CoroutineScope(Job()))
+
+        // This shouldn't throw an exception bc the scope1 has been cancelled.
+        dataStore2.data.first()
+    }
+
+    @Test
+    fun testCreateDataStoreAndRead_withStrictMode() = runTest {
+        StrictMode.setThreadPolicy(
+            StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().penaltyDeath()
+                .build()
+        )
+        val dataStore =
+            newDataStore(file = testFile, scope = CoroutineScope(newSingleThreadContext("test")))
+        assertThat(dataStore.data.first()).isEqualTo(0)
+        StrictMode.allowThreadDiskReads()
+        StrictMode.allowThreadDiskWrites()
+    }
+
+    @Test
+    fun testCreateDataStoreAndUpdate_withStrictMode() = runTest {
+        StrictMode.setThreadPolicy(
+            StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().penaltyDeath()
+                .build()
+        )
+        val dataStore =
+            newDataStore(file = testFile, scope = CoroutineScope(newSingleThreadContext("test")))
+        dataStore.updateData { it.inc() }
+        assertThat(dataStore.data.first()).isEqualTo(1)
+        StrictMode.allowThreadDiskReads()
+        StrictMode.allowThreadDiskWrites()
+    }
+
+    // Mutable wrapper around a byte
+    data class ByteWrapper(var byte: Byte) {
+        internal class ByteWrapperSerializer() : Serializer<ByteWrapper> {
+            private val delegate = TestingSerializer()
+
+            override val defaultValue = ByteWrapper(delegate.defaultValue)
+
+            override suspend fun readFrom(input: InputStream): ByteWrapper {
+                return ByteWrapper(delegate.readFrom(input))
+            }
+
+            override suspend fun writeTo(t: ByteWrapper, output: OutputStream) {
+                delegate.writeTo(t.byte, output)
+            }
+        }
+    }
+
+    private class TestingCorruptionHandler(
+        private val replaceWith: Byte? = null
+    ) : CorruptionHandler<Byte> {
+
+        @Volatile
+        var numCalls = 0
+
+        override suspend fun handleCorruption(ex: CorruptionException): Byte {
+            numCalls++
+
+            replaceWith?.let {
+                return it
+            }
+
+            throw IOException("Handler thrown exception.")
+        }
+    }
+}
+
+fun doBlockingWithTimeout(ms: Long, block: suspend () -> Unit): Unit = runBlocking<Unit> {
+    withTimeout(ms) { block() }
+}
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MulticastFileObserverTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/MulticastFileObserverTest.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MulticastFileObserverTest.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/MulticastFileObserverTest.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/ProtoOkioSerializer.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/ProtoOkioSerializer.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/ProtoOkioSerializer.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/ProtoOkioSerializer.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/ProtoSerializer.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/ProtoSerializer.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/ProtoSerializer.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/ProtoSerializer.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/SharedCounterTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/SharedCounterTest.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/SharedCounterTest.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/SharedCounterTest.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/InterProcessCompletableTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/InterProcessCompletableTest.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/InterProcessCompletableTest.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/InterProcessCompletableTest.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/MultiProcessTestRule.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessTestRule.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/MultiProcessTestRule.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessTestRule.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/MultipleDataStoresInMultipleProcessesTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultipleDataStoresInMultipleProcessesTest.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/MultipleDataStoresInMultipleProcessesTest.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultipleDataStoresInMultipleProcessesTest.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/TwoWayIpcTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/TwoWayIpcTest.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/TwoWayIpcTest.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/TwoWayIpcTest.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/ipcActions/CreateDatastoreAction.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/CreateDatastoreAction.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/ipcActions/CreateDatastoreAction.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/CreateDatastoreAction.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/ipcActions/ReadTextAction.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/ReadTextAction.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/ipcActions/ReadTextAction.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/ReadTextAction.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/ipcActions/SetTextAction.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/SetTextAction.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/multiprocess/ipcActions/SetTextAction.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/SetTextAction.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/CompositeServiceSubjectModel.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/CompositeServiceSubjectModel.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/CompositeServiceSubjectModel.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/CompositeServiceSubjectModel.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/InterProcessCompletable.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/InterProcessCompletable.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/InterProcessCompletable.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/InterProcessCompletable.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/IpcAction.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/IpcAction.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/IpcAction.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/IpcAction.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/IpcLogger.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/IpcLogger.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/IpcLogger.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/IpcLogger.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/TwoWayIpcBus.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcBus.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/TwoWayIpcBus.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcBus.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/TwoWayIpcConnection.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcConnection.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/TwoWayIpcConnection.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcConnection.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/TwoWayIpcService.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcService.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/TwoWayIpcService.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcService.kt
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/TwoWayIpcSubject.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcSubject.kt
similarity index 100%
rename from datastore/datastore-core/src/androidTest/java/androidx/datastore/core/twoWayIpc/TwoWayIpcSubject.kt
rename to datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcSubject.kt
diff --git a/datastore/datastore-core/src/androidTest/proto/test.proto b/datastore/datastore-core/src/androidInstrumentedTest/proto/test.proto
similarity index 100%
rename from datastore/datastore-core/src/androidTest/proto/test.proto
rename to datastore/datastore-core/src/androidInstrumentedTest/proto/test.proto
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreSingleProcessTest.kt b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreSingleProcessTest.kt
deleted file mode 100644
index 952fc5d..0000000
--- a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreSingleProcessTest.kt
+++ /dev/null
@@ -1,983 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.datastore.core
-
-import android.os.StrictMode
-import androidx.datastore.TestFile
-import androidx.datastore.TestIO
-import androidx.datastore.TestingSerializerConfig
-import androidx.datastore.core.handlers.NoOpCorruptionHandler
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.LargeTest
-import androidx.testutils.assertThrows
-import com.google.common.truth.Truth.assertThat
-import java.io.File
-import java.io.InputStream
-import java.io.OutputStream
-import java.util.concurrent.Executors
-import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.atomic.AtomicInteger
-import kotlin.coroutines.AbstractCoroutineContextElement
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.asCoroutineDispatcher
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.take
-import kotlinx.coroutines.flow.toList
-import kotlinx.coroutines.job
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.newSingleThreadContext
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.withContext
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-/**
- * A testing class based on duplicate from "SingleProcessDataStoreTest" that only tests the features
- * in a single process use case. More tests are added for StrictMode.
- */
-@OptIn(DelicateCoroutinesApi::class)
-@ExperimentalCoroutinesApi
-@LargeTest
-@RunWith(JUnit4::class)
-abstract class MultiProcessDataStoreSingleProcessTest<F : TestFile<F>>(
-    protected val testIO: TestIO<F, *>
-) {
-    protected lateinit var store: DataStore<Byte>
-    private lateinit var serializerConfig: TestingSerializerConfig
-    protected lateinit var testFile: F
-    protected lateinit var tempFolder: F
-    protected lateinit var dataStoreScope: TestScope
-
-    abstract fun getJavaFile(file: F): File
-
-    private fun newDataStore(
-        file: F = testFile,
-        scope: CoroutineScope = dataStoreScope,
-        initTasksList: List<suspend (api: InitializerApi<Byte>) -> Unit> = listOf(),
-        corruptionHandler: CorruptionHandler<Byte> = NoOpCorruptionHandler<Byte>()
-    ): DataStore<Byte> {
-        return DataStoreImpl(
-            storage = testIO.getStorage(
-                serializerConfig,
-                {
-                    MultiProcessCoordinator(
-                        dataStoreScope.coroutineContext,
-                        getJavaFile(testFile)
-                    )
-                }) { file },
-            scope = scope,
-            initTasksList = initTasksList,
-            corruptionHandler = corruptionHandler
-        )
-    }
-
-    @Before
-    fun setUp() {
-        serializerConfig = TestingSerializerConfig()
-        tempFolder = testIO.newTempFile().also { it.mkdirs() }
-        testFile = testIO.newTempFile(parentFile = tempFolder)
-        dataStoreScope = TestScope(UnconfinedTestDispatcher() + Job())
-        store = testIO.getStore(
-            serializerConfig,
-            dataStoreScope,
-            { MultiProcessCoordinator(dataStoreScope.coroutineContext, getJavaFile(testFile)) }
-        ) { testFile }
-    }
-
-    @Test
-    fun testReadNewMessage() = runTest {
-        assertThat(store.data.first()).isEqualTo(0)
-    }
-
-    @Test
-    fun testReadWithNewInstance() = runBlocking {
-        runTest {
-            val newStore = newDataStore(testFile, scope = backgroundScope)
-            newStore.updateData { 1 }
-        }
-        runTest {
-            val newStore = newDataStore(testFile, scope = backgroundScope)
-            assertThat(newStore.data.first()).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun testScopeCancelledWithActiveFlow() = runTest {
-        val storeScope = CoroutineScope(Job())
-        val dataStore = newDataStore(scope = storeScope)
-        val collection = async {
-            dataStore.data.take(2).collect {
-                // Do nothing, this will wait on another element which will never arrive
-            }
-        }
-
-        storeScope.cancel()
-        collection.join()
-
-        assertThat(collection.isCompleted).isTrue()
-        assertThat(collection.isActive).isFalse()
-    }
-
-    @Test
-    fun testWriteAndRead() = runTest {
-        store.updateData { 1 }
-        assertThat(store.data.first()).isEqualTo(1)
-    }
-
-    @Test
-    fun testWritesDontBlockReadsInSameProcess() = runTest {
-        val transformStarted = CompletableDeferred<Unit>()
-        val continueTransform = CompletableDeferred<Unit>()
-
-        val slowUpdate = async {
-            store.updateData {
-                transformStarted.complete(Unit)
-                continueTransform.await()
-                it.inc()
-            }
-        }
-        // Wait for the transform to begin.
-        transformStarted.await()
-
-        // Read is not blocked.
-        assertThat(store.data.first()).isEqualTo(0)
-
-        continueTransform.complete(Unit)
-        slowUpdate.await()
-
-        // After update completes, update runs, and read shows new data.
-        assertThat(store.data.first()).isEqualTo(1)
-    }
-
-    @Test
-    fun testWriteMultiple() = runTest {
-        store.updateData { 2 }
-
-        assertThat(store.data.first()).isEqualTo(2)
-
-        store.updateData { it.dec() }
-
-        assertThat(store.data.first()).isEqualTo(1)
-    }
-
-    @Test
-    fun testReadAfterTransientBadWrite() = runBlocking {
-        val file = testIO.newTempFile(tempFolder)
-        runTest {
-            val store = newDataStore(file, scope = backgroundScope)
-            store.updateData { 1 }
-            serializerConfig.failingWrite = true
-            assertThrows<IOException> { store.updateData { 2 } }
-        }
-
-        runTest {
-            val newStore = newDataStore(file, scope = backgroundScope)
-            assertThat(newStore.data.first()).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun testWriteToNonExistentDir() = runBlocking {
-        val fileInNonExistentDir = testIO.newTempFile(
-            relativePath = "/this/does/not/exist/ds.txt"
-        )
-        assertThat(fileInNonExistentDir.exists()).isFalse()
-        assertThat(fileInNonExistentDir.parentFile()!!.exists()).isFalse()
-        runTest {
-            val newStore = newDataStore(fileInNonExistentDir, scope = backgroundScope)
-
-            newStore.updateData { 1 }
-
-            assertThat(newStore.data.first()).isEqualTo(1)
-        }
-
-        runTest {
-            val newStore = newDataStore(fileInNonExistentDir, scope = backgroundScope)
-            assertThat(newStore.data.first()).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun testReadFromNonExistentFile() = runTest {
-        val newStore = newDataStore(testFile)
-        assertThat(newStore.data.first()).isEqualTo(0)
-    }
-
-    @Test
-    fun testWriteToDirFails() = runTest {
-        val directoryFile = testIO.newTempFile(relativePath = "this/is/a/directory").also {
-            it.mkdirs()
-        }
-
-        assertThat(directoryFile.isDirectory()).isTrue()
-
-        val newStore = newDataStore(directoryFile)
-        assertThrows<IOException> { newStore.data.first() }
-    }
-
-    @Test
-    fun testExceptionWhenCreatingFilePropagates() = runTest {
-        var failFileProducer = true
-
-        val fileProducer = {
-            if (failFileProducer) {
-                throw IOException("Exception when producing file")
-            }
-            testFile
-        }
-
-        val newStore = testIO.getStore(
-            serializerConfig,
-            dataStoreScope,
-            {
-                MultiProcessCoordinator(
-                    dataStoreScope.coroutineContext,
-                    getJavaFile(fileProducer())
-                )
-            },
-            fileProducer
-        )
-
-        assertThrows<IOException> { newStore.data.first() }.hasMessageThat().isEqualTo(
-            "Exception when producing file"
-        )
-
-        failFileProducer = false
-
-        assertThat(newStore.data.first()).isEqualTo(0)
-    }
-
-    @Test
-    fun testWriteTransformCancellation() = runTest {
-        val transform = CompletableDeferred<Byte>()
-
-        val write = async { store.updateData { transform.await() } }
-
-        assertThat(write.isCompleted).isFalse()
-
-        transform.cancel()
-
-        assertThrows<CancellationException> { write.await() }
-
-        // Check that the datastore's scope is still active:
-
-        assertThat(store.updateData { it.inc().inc() }).isEqualTo(2)
-    }
-
-    @Test
-    fun testWriteAfterTransientBadRead() = runTest {
-        testFile.write("")
-        assertThat(testFile.exists()).isTrue()
-
-        serializerConfig.failingRead = true
-
-        assertThrows<IOException> { store.data.first() }
-
-        serializerConfig.failingRead = false
-
-        store.updateData { 1 }
-        assertThat(store.data.first()).isEqualTo(1)
-    }
-
-    @Test
-    fun testWriteWithBadReadFails() = runTest {
-        testFile.write("")
-        assertThat(testFile.exists()).isTrue()
-
-        serializerConfig.failingRead = true
-
-        assertThrows<IOException> { store.updateData { 1 } }
-    }
-
-    @Test
-    fun testCancellingDataStoreScopePropagatesToWrites() = runBlocking<Unit> {
-        val scope = CoroutineScope(Job())
-
-        val dataStore = newDataStore(scope = scope)
-
-        val latch = CompletableDeferred<Unit>()
-
-        val slowUpdate = async {
-            dataStore.updateData {
-                latch.await()
-                it.inc()
-            }
-        }
-
-        val notStartedUpdate = async {
-            dataStore.updateData {
-                it.inc()
-            }
-        }
-
-        scope.cancel()
-
-        assertThrows<CancellationException> { slowUpdate.await() }
-
-        assertThrows<CancellationException> { notStartedUpdate.await() }
-
-        assertThrows<CancellationException> { dataStore.updateData { 123 } }
-    }
-
-    @Test
-    fun testCancellingCallerScopePropagatesToWrites() = runBlocking<Unit> {
-        val dsScope = CoroutineScope(Job())
-        val callerScope = CoroutineScope(Job())
-
-        val dataStore = newDataStore(scope = dsScope)
-
-        val latch = CompletableDeferred<Unit>()
-
-        // The ordering of the following are not guaranteed but I think they won't be flaky with
-        // Dispatchers.Unconfined
-        val awaitingCancellation = callerScope.async(Dispatchers.Unconfined) {
-            dataStore.updateData { awaitCancellation() }
-        }
-
-        val started = dsScope.async(Dispatchers.Unconfined) {
-            dataStore.updateData {
-                latch.await()
-                it.inc()
-            }
-        }
-
-        val notStarted = callerScope.async(Dispatchers.Unconfined) {
-            dataStore.updateData { it.inc() }
-        }
-
-        callerScope.coroutineContext.job.cancelAndJoin()
-
-        assertThat(awaitingCancellation.isCancelled).isTrue()
-        assertThat(notStarted.isCancelled).isTrue()
-
-        // wait for coroutine to complete to prevent it from outliving the test, which is flaky
-        latch.complete(Unit)
-        started.await()
-        assertThat(dataStore.data.first()).isEqualTo(1)
-    }
-
-    @Test
-    fun testCanWriteFromInitTask() = runTest {
-        store = newDataStore(initTasksList = listOf { api -> api.updateData { 1 } })
-
-        assertThat(store.data.first()).isEqualTo(1)
-    }
-
-    @FlakyTest(bugId = 242765370)
-    @Test
-    fun testInitTaskFailsFirstTimeDueToReadFail() = runTest {
-        store = newDataStore(initTasksList = listOf { api -> api.updateData { 1 } })
-
-        serializerConfig.failingRead = true
-        assertThrows<IOException> { store.updateData { 2 } }
-
-        serializerConfig.failingRead = false
-        store.updateData { it.inc().inc() }
-
-        assertThat(store.data.first()).isEqualTo(3)
-    }
-
-    @Test
-    fun testInitTaskFailsFirstTimeDueToException() = runTest {
-        val failInit = AtomicBoolean(true)
-        store = newDataStore(
-            initTasksList = listOf { _ ->
-                if (failInit.get()) {
-                    throw IOException("I was asked to fail init")
-                }
-            }
-        )
-        assertThrows<IOException> { store.updateData { 5 } }
-
-        failInit.set(false)
-
-        store.updateData { it.inc() }
-        assertThat(store.data.first()).isEqualTo(1)
-    }
-
-    @Test
-    fun testInitTaskOnlyRunsOnce() = runTest {
-        val count = AtomicInteger()
-        val newStore = newDataStore(
-            testFile,
-            initTasksList = listOf { _ ->
-                count.incrementAndGet()
-            }
-        )
-
-        repeat(10) {
-            newStore.updateData { it.inc() }
-            newStore.data.first()
-        }
-
-        assertThat(count.get()).isEqualTo(1)
-    }
-
-    @Test
-    fun testWriteDuringInit() = runTest {
-        val continueInit = CompletableDeferred<Unit>()
-
-        store = newDataStore(
-            initTasksList = listOf { api ->
-                continueInit.await()
-                api.updateData { 1 }
-            }
-        )
-
-        val update = async {
-            store.updateData { b ->
-                assertThat(b).isEqualTo(1)
-                b
-            }
-        }
-
-        continueInit.complete(Unit)
-        update.await()
-
-        assertThat(store.data.first()).isEqualTo(1)
-    }
-
-    @Test
-    fun testCancelDuringInit() = runTest {
-        val continueInit = CompletableDeferred<Unit>()
-
-        store = newDataStore(
-            initTasksList = listOf { api ->
-                continueInit.await()
-                api.updateData { 1 }
-            }
-        )
-
-        val update = async {
-            store.updateData { it }
-        }
-
-        val read = async {
-            store.data.first()
-        }
-
-        update.cancel()
-        read.cancel()
-        continueInit.complete(Unit)
-
-        assertThrows<CancellationException> { update.await() }
-        assertThrows<CancellationException> { read.await() }
-
-        store.updateData { it.inc().inc() }
-
-        assertThat(store.data.first()).isEqualTo(3)
-    }
-
-    @Test
-    fun testConcurrentUpdatesInit() = runTest {
-        val continueUpdate = CompletableDeferred<Unit>()
-
-        val concurrentUpdateInitializer: suspend (InitializerApi<Byte>) -> Unit = { api ->
-            val update1 = async {
-                api.updateData {
-                    continueUpdate.await()
-                    it.inc().inc()
-                }
-            }
-            api.updateData {
-                it.inc()
-            }
-            update1.await()
-        }
-
-        store = newDataStore(initTasksList = listOf(concurrentUpdateInitializer))
-        val getData = async { store.data.first() }
-        continueUpdate.complete(Unit)
-
-        assertThat(getData.await()).isEqualTo(3)
-    }
-
-    @Test
-    fun testInitUpdateBlockRead() = runTest {
-        val continueInit = CompletableDeferred<Unit>()
-        val continueUpdate = CompletableDeferred<Unit>()
-
-        val updateInitializer: suspend (InitializerApi<Byte>) -> Unit = { api ->
-            api.updateData {
-                continueInit.await()
-                it.inc()
-            }
-        }
-
-        store = newDataStore(initTasksList = listOf(updateInitializer))
-        val getData = async { store.data.first() }
-        val updateData = async {
-            store.updateData {
-                continueUpdate.await()
-                it.inc()
-            }
-        }
-
-        assertThat(getData.isCompleted).isFalse()
-        assertThat(getData.isActive).isTrue()
-
-        continueInit.complete(Unit)
-        assertThat(getData.await()).isEqualTo(1)
-
-        assertThat(updateData.isCompleted).isFalse()
-        assertThat(updateData.isActive).isTrue()
-
-        continueUpdate.complete(Unit)
-        assertThat(updateData.await()).isEqualTo(2)
-        assertThat(store.data.first()).isEqualTo(2)
-    }
-
-    @Test
-    fun testUpdateSuccessfullyCommittedInit() = runTest {
-        var otherStorage: Byte = 123
-
-        val initializer: suspend (InitializerApi<Byte>) -> Unit = { api ->
-            api.updateData {
-                otherStorage
-            }
-            // Similar to cleanUp():
-            otherStorage = 0
-        }
-
-        val store = newDataStore(initTasksList = listOf(initializer))
-
-        serializerConfig.failingWrite = true
-        assertThrows<IOException> { store.data.first() }
-
-        serializerConfig.failingWrite = false
-        assertThat(store.data.first()).isEqualTo(123)
-    }
-
-    @Test
-    fun testInitApiUpdateThrowsAfterInitTasksComplete() = runTest {
-        var savedApi: InitializerApi<Byte>? = null
-
-        val initializer: suspend (InitializerApi<Byte>) -> Unit = { api ->
-            savedApi = api
-        }
-
-        val store = newDataStore(initTasksList = listOf(initializer))
-
-        assertThat(store.data.first()).isEqualTo(0)
-
-        assertThrows<IllegalStateException> { savedApi?.updateData { 123 } }
-    }
-
-    @Test
-    fun testFlowReceivesUpdates() = runTest {
-        val collectedBytes = mutableListOf<Byte>()
-
-        val flowCollectionJob = async {
-            store.data.take(8).toList(collectedBytes)
-        }
-
-        repeat(7) {
-            store.updateData { it.inc() }
-        }
-
-        flowCollectionJob.join()
-
-        assertThat(collectedBytes).isEqualTo(mutableListOf<Byte>(0, 1, 2, 3, 4, 5, 6, 7))
-    }
-
-    @Test
-    fun testMultipleFlowsReceiveData() = runTest {
-        val flowOf8 = store.data.take(8)
-
-        val bytesFromFirstCollect = mutableListOf<Byte>()
-        val bytesFromSecondCollect = mutableListOf<Byte>()
-
-        val flowCollection1 = async {
-            flowOf8.toList(bytesFromFirstCollect)
-        }
-
-        val flowCollection2 = async {
-            flowOf8.toList(bytesFromSecondCollect)
-        }
-
-        repeat(7) {
-            store.updateData { it.inc() }
-        }
-
-        flowCollection1.join()
-        flowCollection2.join()
-
-        // This test only works because runTest ensures consistent behavior
-        // Otherwise, we cannot really expect the collector to read every single value
-        // (we provide eventual consistency, so it would also be OK if it missed some intermediate
-        // values as long as it received 7 at the end).
-        assertThat(bytesFromFirstCollect).isEqualTo(mutableListOf<Byte>(0, 1, 2, 3, 4, 5, 6, 7))
-        assertThat(bytesFromSecondCollect).isEqualTo(mutableListOf<Byte>(0, 1, 2, 3, 4, 5, 6, 7))
-    }
-
-    @Test
-    fun testExceptionInFlowDoesNotBreakUpstream() = runTest {
-        val flowOf8 = store.data.take(8)
-
-        val collectedBytes = mutableListOf<Byte>()
-
-        val failedFlowCollection = async {
-            assertThrows<Exception> {
-                flowOf8.collect {
-                    throw Exception("Failure while collecting")
-                }
-            }.hasMessageThat().contains("Failure while collecting")
-        }
-
-        val successfulFlowCollection = async {
-            flowOf8.take(8).toList(collectedBytes)
-        }
-
-        repeat(7) {
-            store.updateData { it.inc() }
-        }
-
-        successfulFlowCollection.join()
-        failedFlowCollection.await()
-
-        assertThat(collectedBytes).isEqualTo(mutableListOf<Byte>(0, 1, 2, 3, 4, 5, 6, 7))
-    }
-
-    @Test
-    fun testSlowConsumerDoesntBlockOtherConsumers() = runTest {
-        val flowOf8 = store.data.take(8)
-
-        val collectedBytes = mutableListOf<Byte>()
-
-        val flowCollection2 = async {
-            flowOf8.toList(collectedBytes)
-        }
-
-        val blockedCollection = async {
-            flowOf8.collect {
-                flowCollection2.await()
-            }
-        }
-
-        repeat(15) {
-            store.updateData { it.inc() }
-        }
-
-        flowCollection2.await()
-        assertThat(collectedBytes).isEqualTo(mutableListOf<Byte>(0, 1, 2, 3, 4, 5, 6, 7))
-
-        blockedCollection.await()
-    }
-
-    @Test
-    fun testHandlerNotCalledGoodData() = runBlocking {
-        runTest {
-            newDataStore(testFile, scope = backgroundScope).updateData { 1 }
-        }
-
-        runTest {
-            val testingHandler: TestingCorruptionHandler = TestingCorruptionHandler()
-            val newStore = newDataStore(corruptionHandler = testingHandler, file = testFile)
-
-            newStore.updateData { 2 }
-            newStore.data.first()
-
-            assertThat(testingHandler.numCalls).isEqualTo(0)
-        }
-    }
-
-    @Test
-    fun handlerNotCalledNonCorruption() = runBlocking {
-        runTest {
-            newDataStore(testFile, scope = backgroundScope).updateData { 1 }
-        }
-
-        runTest {
-            val testingHandler = TestingCorruptionHandler()
-            serializerConfig.failingRead = true
-            val newStore = newDataStore(corruptionHandler = testingHandler, file = testFile)
-
-            assertThrows<IOException> { newStore.updateData { 2 } }
-            assertThrows<IOException> { newStore.data.first() }
-
-            assertThat(testingHandler.numCalls).isEqualTo(0)
-        }
-    }
-
-    @Test
-    fun testHandlerCalledCorruptDataRead() = runBlocking {
-        runTest {
-            val newStore = newDataStore(testFile, scope = backgroundScope)
-            newStore.updateData { 1 } // Pre-seed the data so the file exists.
-        }
-
-        runTest {
-            val testingHandler: TestingCorruptionHandler = TestingCorruptionHandler()
-            serializerConfig.failReadWithCorruptionException = true
-            val newStore = newDataStore(corruptionHandler = testingHandler, file = testFile)
-
-            assertThrows<IOException> { newStore.data.first() }.hasMessageThat().contains(
-                "Handler thrown exception."
-            )
-
-            assertThat(testingHandler.numCalls).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun testHandlerCalledCorruptDataWrite() = runBlocking {
-        runTest {
-            val newStore = newDataStore(file = testFile, scope = backgroundScope)
-            newStore.updateData { 1 }
-        }
-
-        runTest {
-            val testingHandler: TestingCorruptionHandler = TestingCorruptionHandler()
-            serializerConfig.failReadWithCorruptionException = true
-            val newStore = newDataStore(corruptionHandler = testingHandler, file = testFile)
-
-            assertThrows<IOException> { newStore.updateData { 1 } }.hasMessageThat().contains(
-                "Handler thrown exception."
-            )
-
-            assertThat(testingHandler.numCalls).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun testHandlerReplaceData() = runBlocking {
-        runTest {
-            newDataStore(file = testFile, scope = backgroundScope).updateData { 1 }
-        }
-
-        runTest {
-            val testingHandler: TestingCorruptionHandler =
-                TestingCorruptionHandler(replaceWith = 10)
-            serializerConfig.failReadWithCorruptionException = true
-            val newStore = newDataStore(
-                corruptionHandler = testingHandler, file = testFile,
-                scope = backgroundScope
-            )
-
-            assertThat(newStore.data.first()).isEqualTo(10)
-        }
-    }
-
-    @Test
-    fun testDefaultValueUsedWhenNoDataOnDisk() = runTest {
-        val dataStore = testIO.getStore(
-            TestingSerializerConfig(defaultValue = 99),
-            dataStoreScope,
-            { MultiProcessCoordinator(dataStoreScope.coroutineContext, getJavaFile(testFile)) }) {
-            testFile
-        }
-
-        assertThat(dataStore.data.first()).isEqualTo(99)
-    }
-
-    @Test
-    fun testTransformRunInCallersContext() = runBlocking<Unit> {
-        suspend fun getContext(): CoroutineContext {
-            return kotlin.coroutines.coroutineContext
-        }
-
-        withContext(TestElement("123")) {
-            store.updateData {
-                val context = getContext()
-                assertThat(context[TestElement.Key]!!.name).isEqualTo("123")
-                it.inc()
-            }
-        }
-    }
-
-    private class TestElement(
-        val name: String
-    ) : AbstractCoroutineContextElement(Key) {
-        companion object Key : CoroutineContext.Key<TestElement>
-    }
-
-    @Test
-    fun testCancelInflightWrite() = runBlocking<Unit> {
-        val myScope =
-            CoroutineScope(Job() + Executors.newSingleThreadExecutor().asCoroutineDispatcher())
-
-        val updateStarted = CompletableDeferred<Unit>()
-        myScope.launch {
-            store.updateData {
-                updateStarted.complete(Unit)
-                awaitCancellation()
-            }
-        }
-        updateStarted.await()
-        myScope.coroutineContext[Job]!!.cancelAndJoin()
-    }
-
-    @Test
-    fun testWrite_afterCanceledWrite_succeeds() = runBlocking<Unit> {
-        val myScope =
-            CoroutineScope(Job() + Executors.newSingleThreadExecutor().asCoroutineDispatcher())
-
-        val cancelNow = CompletableDeferred<Unit>()
-
-        myScope.launch {
-            store.updateData {
-                cancelNow.complete(Unit)
-                awaitCancellation()
-            }
-        }
-
-        cancelNow.await()
-        myScope.coroutineContext[Job]!!.cancelAndJoin()
-
-        store.updateData { 123 }
-    }
-
-    @Test
-    fun testWrite_fromOtherScope_doesntGetCancelledFromDifferentScope() = runBlocking<Unit> {
-
-        val otherScope = CoroutineScope(Job())
-
-        val callerScope = CoroutineScope(Job())
-
-        val firstUpdateStarted = CompletableDeferred<Unit>()
-        val finishFirstUpdate = CompletableDeferred<Byte>()
-
-        val firstUpdate = otherScope.async(Dispatchers.Unconfined) {
-            store.updateData {
-                firstUpdateStarted.complete(Unit)
-                finishFirstUpdate.await()
-            }
-        }
-
-        callerScope.launch(Dispatchers.Unconfined) {
-            store.updateData {
-                awaitCancellation()
-            }
-        }
-
-        firstUpdateStarted.await()
-        callerScope.coroutineContext.job.cancelAndJoin()
-        finishFirstUpdate.complete(1)
-        firstUpdate.await()
-
-        // It's still usable:
-        assertThat(store.updateData { it.inc() }).isEqualTo(2)
-    }
-
-    @Test
-    fun testCreateDuplicateActiveDataStore() = runTest {
-        val file = testIO.newTempFile(parentFile = tempFolder)
-        val dataStore = newDataStore(file = file, scope = CoroutineScope(Job()))
-
-        dataStore.data.first()
-
-        val duplicateDataStore = newDataStore(file = file, scope = CoroutineScope(Job()))
-
-        assertThrows<IllegalStateException> {
-            duplicateDataStore.data.first()
-        }
-    }
-
-    @Test
-    fun testCreateDataStore_withSameFileAsInactiveDataStore() = runTest {
-        val file = testIO.newTempFile(parentFile = tempFolder)
-        val scope1 = CoroutineScope(Job())
-        val dataStore1 = newDataStore(file = file, scope = scope1)
-
-        dataStore1.data.first()
-
-        scope1.coroutineContext.job.cancelAndJoin()
-
-        val dataStore2 = newDataStore(file = file, scope = CoroutineScope(Job()))
-
-        // This shouldn't throw an exception bc the scope1 has been cancelled.
-        dataStore2.data.first()
-    }
-
-    @Test
-    fun testCreateDataStoreAndRead_withStrictMode() = runTest {
-        StrictMode.setThreadPolicy(
-            StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().penaltyDeath()
-                .build()
-        )
-        val dataStore =
-            newDataStore(file = testFile, scope = CoroutineScope(newSingleThreadContext("test")))
-        assertThat(dataStore.data.first()).isEqualTo(0)
-        StrictMode.allowThreadDiskReads()
-        StrictMode.allowThreadDiskWrites()
-    }
-
-    @Test
-    fun testCreateDataStoreAndUpdate_withStrictMode() = runTest {
-        StrictMode.setThreadPolicy(
-            StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().penaltyDeath()
-                .build()
-        )
-        val dataStore =
-            newDataStore(file = testFile, scope = CoroutineScope(newSingleThreadContext("test")))
-        dataStore.updateData { it.inc() }
-        assertThat(dataStore.data.first()).isEqualTo(1)
-        StrictMode.allowThreadDiskReads()
-        StrictMode.allowThreadDiskWrites()
-    }
-
-    // Mutable wrapper around a byte
-    data class ByteWrapper(var byte: Byte) {
-        internal class ByteWrapperSerializer() : Serializer<ByteWrapper> {
-            private val delegate = TestingSerializer()
-
-            override val defaultValue = ByteWrapper(delegate.defaultValue)
-
-            override suspend fun readFrom(input: InputStream): ByteWrapper {
-                return ByteWrapper(delegate.readFrom(input))
-            }
-
-            override suspend fun writeTo(t: ByteWrapper, output: OutputStream) {
-                delegate.writeTo(t.byte, output)
-            }
-        }
-    }
-
-    private class TestingCorruptionHandler(
-        private val replaceWith: Byte? = null
-    ) : CorruptionHandler<Byte> {
-
-        @Volatile
-        var numCalls = 0
-
-        override suspend fun handleCorruption(ex: CorruptionException): Byte {
-            numCalls++
-
-            replaceWith?.let {
-                return it
-            }
-
-            throw IOException("Handler thrown exception.")
-        }
-    }
-}
diff --git a/datastore/datastore-preferences/build.gradle b/datastore/datastore-preferences/build.gradle
index 8750330..e8cbf29 100644
--- a/datastore/datastore-preferences/build.gradle
+++ b/datastore/datastore-preferences/build.gradle
@@ -64,11 +64,11 @@
         androidMain {
             dependsOn(jvmMain)
         }
-        androidTest {
+        androidUnitTest {
             dependsOn(jvmTest)
         }
-        androidAndroidTest {
-            dependsOn(androidTest)
+        androidInstrumentedTest {
+            dependsOn(androidUnitTest)
             dependencies {
                 implementation(libs.testRunner)
                 implementation(libs.testCore)
diff --git a/datastore/datastore-preferences/src/androidAndroidTest/AndroidManifest.xml b/datastore/datastore-preferences/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from datastore/datastore-preferences/src/androidAndroidTest/AndroidManifest.xml
rename to datastore/datastore-preferences/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/datastore/datastore-preferences/src/androidAndroidTest/kotlin/androidx/datastore/preferences/PreferenceDataStoreDelegateTest.kt b/datastore/datastore-preferences/src/androidInstrumentedTest/kotlin/androidx/datastore/preferences/PreferenceDataStoreDelegateTest.kt
similarity index 100%
rename from datastore/datastore-preferences/src/androidAndroidTest/kotlin/androidx/datastore/preferences/PreferenceDataStoreDelegateTest.kt
rename to datastore/datastore-preferences/src/androidInstrumentedTest/kotlin/androidx/datastore/preferences/PreferenceDataStoreDelegateTest.kt
diff --git a/datastore/datastore-preferences/src/androidAndroidTest/kotlin/androidx/datastore/preferences/PreferenceDataStoreFileTest.kt b/datastore/datastore-preferences/src/androidInstrumentedTest/kotlin/androidx/datastore/preferences/PreferenceDataStoreFileTest.kt
similarity index 100%
rename from datastore/datastore-preferences/src/androidAndroidTest/kotlin/androidx/datastore/preferences/PreferenceDataStoreFileTest.kt
rename to datastore/datastore-preferences/src/androidInstrumentedTest/kotlin/androidx/datastore/preferences/PreferenceDataStoreFileTest.kt
diff --git a/datastore/datastore-preferences/src/androidAndroidTest/kotlin/androidx/datastore/preferences/SharedPreferencesToPreferencesTest.kt b/datastore/datastore-preferences/src/androidInstrumentedTest/kotlin/androidx/datastore/preferences/SharedPreferencesToPreferencesTest.kt
similarity index 100%
rename from datastore/datastore-preferences/src/androidAndroidTest/kotlin/androidx/datastore/preferences/SharedPreferencesToPreferencesTest.kt
rename to datastore/datastore-preferences/src/androidInstrumentedTest/kotlin/androidx/datastore/preferences/SharedPreferencesToPreferencesTest.kt
diff --git a/datastore/datastore/build.gradle b/datastore/datastore/build.gradle
index 6f9b832..0eaed8a 100644
--- a/datastore/datastore/build.gradle
+++ b/datastore/datastore/build.gradle
@@ -75,7 +75,7 @@
                 implementation(libs.okio)
             }
         }
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.junit)
diff --git a/datastore/datastore/src/androidAndroidTest/AndroidManifest.xml b/datastore/datastore/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from datastore/datastore/src/androidAndroidTest/AndroidManifest.xml
rename to datastore/datastore/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/DataStoreDelegateTest.kt b/datastore/datastore/src/androidInstrumentedTest/kotlin/androidx/datastore/DataStoreDelegateTest.kt
similarity index 100%
rename from datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/DataStoreDelegateTest.kt
rename to datastore/datastore/src/androidInstrumentedTest/kotlin/androidx/datastore/DataStoreDelegateTest.kt
diff --git a/datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/DataStoreFileTest.kt b/datastore/datastore/src/androidInstrumentedTest/kotlin/androidx/datastore/DataStoreFileTest.kt
similarity index 100%
rename from datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/DataStoreFileTest.kt
rename to datastore/datastore/src/androidInstrumentedTest/kotlin/androidx/datastore/DataStoreFileTest.kt
diff --git a/datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/TestingSerializer.kt b/datastore/datastore/src/androidInstrumentedTest/kotlin/androidx/datastore/TestingSerializer.kt
similarity index 100%
rename from datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/TestingSerializer.kt
rename to datastore/datastore/src/androidInstrumentedTest/kotlin/androidx/datastore/TestingSerializer.kt
diff --git a/datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt b/datastore/datastore/src/androidInstrumentedTest/kotlin/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
similarity index 100%
rename from datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
rename to datastore/datastore/src/androidInstrumentedTest/kotlin/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index f56473d..0f56a50 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -22,7 +22,7 @@
 application@android:debuggable was tagged at .*\.xml:[0-9]+ to replace other declarations but no other declaration present
 \$OUT_DIR/androidx/benchmark/integration\-tests/dry\-run\-benchmark/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
 # > Task :compose:runtime:runtime-saveable:processDebugAndroidTestManifest
-\$SUPPORT/compose/runtime/runtime\-saveable/src/androidAndroidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
+\$SUPPORT/compose/runtime/runtime\-saveable/src/androidInstrumentedTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
 # > Task :buildOnServer
 [0-9]+ actionable tasks: [0-9]+ executed, [0-9]+ up\-to\-date
 See the profiling report at: file://\$GRADLE_USER_HOME/daemon/.*/reports/profile/profile\-[0-9]+\-[0-9]+\-[0-9]+\-[0-9]+\-[0-9]+\-[0-9]+\.html
@@ -436,6 +436,14 @@
 WARN: Missing @param tag for parameter `previousPageKey` of function androidx\.paging\/PageKeyedDataSource\.LoadInitialCallback\/onResult\/#kotlin\.collections\.List\[TypeParam\(bounds=\[kotlin\.Any\?\]\)\]#kotlin\.Int#kotlin\.Int#TypeParam\(bounds=\[kotlin\.Any\?\]\)\?#TypeParam\(bounds=\[kotlin\.Any\?\]\)\?\/PointingToDeclaration\/
 WARN: Missing @param tag for parameter `priority` of function androidx\.camera\.camera2\.interop\/CaptureRequestOptions\/retrieveOptionWithPriority\/#androidx\.camera\.core\.impl\.Config\.Option<ValueT>#androidx\.camera\.core\.impl\.Config\.OptionPriority\/PointingToDeclaration\/
 WARN: Missing @param tag for parameter `priority` of function androidx\.camera\.core\/CameraXConfig\/retrieveOptionWithPriority\/#androidx\.camera\.core\.impl\.Config\.Option<ValueT>#androidx\.camera\.core\.impl\.Config\.OptionPriority\/PointingToDeclaration\/
+WARN: Missing @param tag for parameter `width` of function androidx\.camera\.testing\.impl\.fakes/FakeImageReaderProxy/newInstance/\#int\#int\#int\#int\#long/PointingToDeclaration/
+WARN: Missing @param tag for parameter `height` of function androidx\.camera\.testing\.impl\.fakes/FakeImageReaderProxy/newInstance/\#int\#int\#int\#int\#long/PointingToDeclaration/
+WARN: Missing @param tag for parameter `format` of function androidx\.camera\.testing\.impl\.fakes/FakeImageReaderProxy/newInstance/\#int\#int\#int\#int\#long/PointingToDeclaration/
+WARN: Missing @param tag for parameter `usage` of function androidx\.camera\.testing\.impl\.fakes/FakeImageReaderProxy/newInstance/\#int\#int\#int\#int\#long/PointingToDeclaration/
+WARN: Missing @param tag for parameter `priority` of function androidx\.camera\.testing\.impl\.fakes/FakeUseCaseConfig/retrieveOptionWithPriority/\#androidx\.camera\.core\.impl\.Config\.Option<ValueT>\#androidx\.camera\.core\.impl\.Config\.OptionPriority/PointingToDeclaration/
+WARN: Missing @param tag for parameter `classType` of function androidx\.camera\.testing\.impl\.mocks/MockConsumer/verifyAcceptCall/\#java\.lang\.Class<\?>\#boolean\#androidx\.camera\.testing\.impl\.mocks\.helpers\.CallTimes\#androidx\.camera\.testing\.impl\.mocks\.helpers\.ArgumentCaptor<T>/PointingToDeclaration/
+WARN: Missing @param tag for parameter `inOrder` of function androidx\.camera\.testing\.impl\.mocks/MockConsumer/verifyAcceptCall/\#java\.lang\.Class<\?>\#boolean\#androidx\.camera\.testing\.impl\.mocks\.helpers\.CallTimes\#androidx\.camera\.testing\.impl\.mocks\.helpers\.ArgumentCaptor<T>/PointingToDeclaration/
+WARN: Missing @param tag for parameter `callTimes` of function androidx\.camera\.testing\.impl\.mocks/MockConsumer/verifyAcceptCall/\#java\.lang\.Class<\?>\#boolean\#androidx\.camera\.testing\.impl\.mocks\.helpers\.CallTimes\#androidx\.camera\.testing\.impl\.mocks\.helpers\.ArgumentCaptor<T>/PointingToDeclaration/
 WARN: Missing @param tag for parameter `query` of function androidx\.leanback\.app\/SearchFragment\/createArgs\/#android\.os\.Bundle#java\.lang\.String\/PointingToDeclaration\/
 WARN: Missing @param tag for parameter `query` of function androidx\.leanback\.app\/SearchSupportFragment\/createArgs\/#android\.os\.Bundle#java\.lang\.String\/PointingToDeclaration\/
 WARN: Missing @param tag for parameter `radians` of function androidx\.compose\.ui\.graphics\/\/rotateRad\/androidx\.compose\.ui\.graphics\.Canvas#kotlin\.Float#kotlin\.Float#kotlin\.Float\/PointingToDeclaration\/
@@ -650,7 +658,7 @@
 WARN: Unable to find what is referred to by "@param supportedTypes" in DClass Builder\. Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of Builder in file .*\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line 686\.
 WARN: Use @androidx\.annotation\.Nullable, not @org\.checkerframework\.checker\.nullness\.qual\/Nullable\/\/\/PointingToDeclaration\/
 # > Task :compose:ui:ui-tooling:processDebugAndroidTestManifest
-\$SUPPORT/compose/ui/ui\-tooling/src/androidAndroidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
+\$SUPPORT/compose/ui/ui\-tooling/src/androidInstrumentedTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
 # ./gradlew tasks warns as we have warnings present
 You can use \'\-\-warning\-mode all\' to show the individual deprecation warnings and determine if they come from your own scripts or plugins\.
 For more on this\, please refer to https\:\/\/docs\.gradle\.org\/.*\/userguide\/command_line_interface\.html\#sec\:command_line_warnings in the Gradle documentation\.
@@ -673,6 +681,8 @@
 public abstract java\.util\.List<androidx\.room\.integration\.kotlintestapp\.test\.JvmNameInDaoTest\.JvmNameEntity> jvmQuery\(\);
 public abstract androidx\.room\.integration\.kotlintestapp\.test\.JvmNameInDaoTest\.JvmNameDao jvmDao\(\);
 \^
+\$SUPPORT/slice/slice\-benchmark/src/androidTest/java/androidx/slice/SliceViewMetrics\.java:[0-9]+: warning: \[deprecation\] SliceView in androidx\.slice\.widget has been deprecated
+import androidx\.slice\.widget\.SliceView;
 # b/296419682
 \$SUPPORT/concurrent/concurrent\-futures/src/test/java/androidx/concurrent/futures/AbstractResolvableFutureTest\.java:[0-9]+: warning: \[removal\] resume\(\) in Thread has been deprecated and marked for removal
 thread\.resume\(\);
@@ -706,17 +716,36 @@
 # > Task :core:core:compileDebugAndroidTestKotlin
 w: file://\$SUPPORT/core/core/src/androidTest/java/androidx/core/util/TypedValueCompatTest\.kt:[0-9]+:[0-9]+ 'scaledDensity: Float' is deprecated\. Deprecated in Java
 # > Task :compose:foundation:foundation:processDebugAndroidTestManifest
-\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
+\$SUPPORT/compose/foundation/foundation/src/androidInstrumentedTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
 # > Task :graphics:graphics-core:compileDebugAndroidTestKotlin
 w: file://\$SUPPORT/graphics/graphics\-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
 w: file://\$SUPPORT/graphics/graphics\-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
 # > Task :compose:foundation:foundation:compileDebugAndroidTestKotlin
-w: file://\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
-w: file://\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
-w: file://\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
-w: file://\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest\.kt:[0-9]+:[0-9]+ 'getter for windowLayoutParams: EspressoOptional<WindowManager\.LayoutParams!>!' is deprecated\. Deprecated in Java
-w: file://\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldVisualTransformationMagnifierTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest\.kt:[0-9]+:[0-9]+ 'getter for windowLayoutParams: EspressoOptional<WindowManager\.LayoutParams!>!' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/TextFieldVisualTransformationMagnifierTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
 # > Task :compose:ui:ui:compileDebugAndroidTestKotlin
-w: file://\$SUPPORT/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils\.kt:[0-9]+:[0-9]+ 'getter for windowLayoutParams: EspressoOptional<WindowManager\.LayoutParams!>!' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTestUtils\.kt:[0-9]+:[0-9]+ 'getter for windowLayoutParams: EspressoOptional<WindowManager\.LayoutParams!>!' is deprecated\. Deprecated in Java
 # b/271306193 remove after aosp/2589888 :emoji:emoji:spdxSbomForRelease
 spdx sboms require a version but project: noto\-emoji\-compat\-flatbuffers has no specified version
+# > Configure project :internal-testutils-appcompat
+WARNING: The option setting 'android\.experimental\.lint\.reservedMemoryPerTask=[0-9]+g' is experimental\.
+# > Task :slice:slice-builders-ktx:compileDebugKotlin
+w: file://\$SUPPORT/slice/slice\-builders\-ktx/src/main/java/androidx/slice/builders/ListBuilder\.kt:[0-9]+:[0-9]+ 'ListBuilder' is deprecated\. Deprecated in Java
+# > Task :slice:slice-builders-ktx:compileDebugAndroidTestKotlin
+w: file://\$SUPPORT/slice/slice\-builders\-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest\.kt:[0-9]+:[0-9]+ 'SliceProvider' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/slice/slice\-builders\-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest\.kt:[0-9]+:[0-9]+ 'SliceSpecs' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/slice/slice\-builders\-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest\.kt:[0-9]+:[0-9]+ 'ListBuilder' is deprecated\. Deprecated in Java
+# > Task :slice:slice-benchmark:compileReleaseAndroidTestJavaWithJavac
+\$SUPPORT/slice/slice\-benchmark/src/androidTest/java/androidx/slice/SliceSerializeMetrics\.java:[0-9]+: warning: \[deprecation\] SliceHints in androidx\.slice\.core has been deprecated
+import androidx\.slice\.core\.SliceHints;
+# b/300090636
+Could not load custom lint check jar file \$GRADLE_USER_HOME/caches/transforms\-[0-9]+/[0-9a-f]{32}/transformed/material\-[0-9]+\.[0-9]+\.[0-9]+/jars/lint\.jar
+java\.lang\.ClassCastException: class java\.util\.HashMap\$Node cannot be cast to class java\.util\.HashMap\$TreeNode \(java\.util\.HashMap\$Node and java\.util\.HashMap\$TreeNode are in module java\.base of loader 'bootstrap'\)
+at com\.android\.tools\.lint\.client\.api\.JarFileIssueRegistry\$Factory\.get\(JarFileIssueRegistry\.kt:[0-9]+\)
+at com\.android\.tools\.lint\.client\.api\.JarFileIssueRegistry\$Factory\.get\$default\(JarFileIssueRegistry\.kt:[0-9]+\)
+at com\.android\.tools\.lint\.client\.api\.LintDriver\.registerCustomDetectors\(LintDriver\.kt:[0-9]+\)
+at com\.android\.tools\.lint\.client\.api\.LintDriver\.initializeExtraRegistries\(LintDriver\.kt:[0-9]+\)
+at jdk\.internal\.reflect\.GeneratedMethodAccessor[0-9]+\.invoke\(Unknown Source\)
diff --git a/development/project-creator/compose-template/groupId/artifactId/build.gradle b/development/project-creator/compose-template/groupId/artifactId/build.gradle
index 17cf909..7c0c4a1 100644
--- a/development/project-creator/compose-template/groupId/artifactId/build.gradle
+++ b/development/project-creator/compose-template/groupId/artifactId/build.gradle
@@ -79,7 +79,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRules)
diff --git a/development/update-verification-metadata.sh b/development/update-verification-metadata.sh
index 7023439..a0487f3 100755
--- a/development/update-verification-metadata.sh
+++ b/development/update-verification-metadata.sh
@@ -1,13 +1,38 @@
 #!/bin/bash
 set -e
 
+# This script updates trust entries in gradle/verification-metadata.xml
+
+# Usage: $0 [--no-dry-run] [<task>]
+
+# --no-dry-run
+#   Don't pass --dry-run to Gradle, so Gradle executes the corresponding tasks.
+#   This is not normally necessary but in some cases can be a useful workaround.
+#   When https://github.com/gradle/gradle/issues/26289 is resolved, we should reevaluate this behavior
+#
+# <task>
+#   The task to ask Gradle to run. By default this is 'bOS'
+#   When --no-dry-run is removed, we should reevaluate this behavior
+
+dryrun=true
+task="bOS"
+
+while [ "$1" != "" ]; do
+  arg="$1"
+  shift
+  if [ "$arg" == "--no-dry-run" ]; then
+    dryrun=false
+    continue
+  fi
+  task="$arg"
+done
+
 function runGradle() {
-  kmpArgs="-Pandroidx.enabled.kmp.target.platforms=+native"
-  echo running ./gradlew $kmpArgs "$@"
-  if ./gradlew $kmpArgs "$@"; then
-    echo succeeded: ./gradlew $kmpArgs "$@"
+  echo running ./gradlew "$@"
+  if ./gradlew "$@"; then
+    echo succeeded: ./gradlew "$@"
   else
-    echo failed: ./gradlew $kmpArgs "$@"
+    echo failed: ./gradlew "$@"
     return 1
   fi
 }
@@ -18,20 +43,31 @@
   # regenerate metadata
   # Need to run a clean build, https://github.com/gradle/gradle/issues/19228
   # Resolving Configurations before task execution is expected. b/297394547
-  runGradle --stacktrace --write-verification-metadata pgp,sha256 --export-keys --dry-run --clean -Pandroidx.update.signatures=true -Pandroid.dependencyResolutionAtConfigurationTime.disallow=false bOS
+  dryrunArg=""
+  if [ "$dryrun" == "true" ]; then
+    dryrunArg="--dry-run"
+  fi
+  runGradle --stacktrace --write-verification-metadata pgp,sha256 --export-keys $dryrunArg --clean -Pandroidx.update.signatures=true -Pandroid.dependencyResolutionAtConfigurationTime.disallow=false -Pandroidx.enabled.kmp.target.platforms=+native $task
 
   # update verification metadata file
-  # also remove 'version=' lines, https://github.com/gradle/gradle/issues/20192
-  cat gradle/verification-metadata.dryrun.xml | sed 's/ \(trusted-key.*\)version="[^"]*"/\1/' > gradle/verification-metadata.xml
+
+  # first, make sure the resulting file is named "verification-metadata.xml"
+  if [ "$dryrun" == "true" ]; then
+    mv gradle/verification-metadata.dryrun.xml gradle/verification-metadata.xml
+  fi
+
+  # next, remove 'version=' lines https://github.com/gradle/gradle/issues/20192
+  sed -i 's/\(trusted-key.*\)version="[^"]*"/\1/' gradle/verification-metadata.xml
 
   # rename keyring
-  mv gradle/verification-keyring-dryrun.keys gradle/verification-keyring.keys
+  mv gradle/verification-keyring-dryrun.keys gradle/verification-keyring.keys 2>/dev/null || true
 
   # remove temporary files
   rm -f gradle/verification-keyring-dryrun.gpg
-  rm -f gradle/verification-metadata.dryrun.xml
+  rm -f gradle/verification-keyring.gpg
 }
 regenerateVerificationMetadata
 
 echo
 echo 'Done. Please check that these changes look correct (`git diff`)'
+echo "If Gradle did not make all expected updates to verification-metadata.xml, you can try '--no-dry-run'. This is slow so you may also want to specify a task. Example: $0 --dry-run exportSboms"
diff --git a/development/update_studio.sh b/development/update_studio.sh
index 2239505..3bdab1e 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -5,10 +5,12 @@
   eval "$@"
 }
 
-# Get versions
+# Versions that the user should update when running this script
 echo Getting Studio version and link
-AGP_VERSION=${1:-8.2.0-beta01}
-STUDIO_VERSION_STRING=${2:-"Android Studio Hedgehog | 2023.1.1 Beta 1"}
+AGP_VERSION=${1:-8.3.0-alpha02}
+STUDIO_VERSION_STRING=${2:-"Android Studio Iguana | 2023.2.1 Canary 2"}
+
+# Get studio version number from version name
 STUDIO_IFRAME_LINK=`curl "https://developer.android.com/studio/archive.html" | grep "<iframe " | sed "s/.* src=\"\([^\"]*\)\".*/\1/g"`
 echo iframe link $STUDIO_IFRAME_LINK
 STUDIO_IFRAME_REDIRECT=`curl -s $STUDIO_IFRAME_LINK | grep href | sed 's/.*href="\([^"]*\)".*/\1/g'`
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 1c2c888..2954cc7 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -1,5 +1,5 @@
 plugins {
-    id("com.android.library")
+id("com.android.library")
     id("AndroidXDocsPlugin")
 }
 
@@ -8,15 +8,15 @@
 }
 
 dependencies {
-    docs("androidx.activity:activity:1.8.0-alpha07")
-    docs("androidx.activity:activity-compose:1.8.0-alpha07")
-    samples("androidx.activity:activity-compose-samples:1.8.0-alpha07")
-    docs("androidx.activity:activity-ktx:1.8.0-alpha07")
+    docs("androidx.activity:activity:1.8.0-beta01")
+    docs("androidx.activity:activity-compose:1.8.0-beta01")
+    samples("androidx.activity:activity-compose-samples:1.8.0-beta01")
+    docs("androidx.activity:activity-ktx:1.8.0-beta01")
     // ads-identifier is deprecated
     docsWithoutApiSince("androidx.ads:ads-identifier:1.0.0-alpha05")
     docsWithoutApiSince("androidx.ads:ads-identifier-common:1.0.0-alpha05")
     docsWithoutApiSince("androidx.ads:ads-identifier-provider:1.0.0-alpha05")
-    kmpDocs("androidx.annotation:annotation:1.7.0-rc01")
+    kmpDocs("androidx.annotation:annotation:1.7.0")
     docs("androidx.annotation:annotation-experimental:1.4.0-alpha01")
     docs("androidx.appcompat:appcompat:1.7.0-alpha03")
     docs("androidx.appcompat:appcompat-resources:1.7.0-alpha03")
@@ -32,10 +32,10 @@
     docs("androidx.asynclayoutinflater:asynclayoutinflater:1.1.0-alpha01")
     docs("androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01")
     docs("androidx.autofill:autofill:1.3.0-alpha01")
-    docs("androidx.benchmark:benchmark-common:1.2.0-beta04")
-    docs("androidx.benchmark:benchmark-junit4:1.2.0-beta04")
-    docs("androidx.benchmark:benchmark-macro:1.2.0-beta04")
-    docs("androidx.benchmark:benchmark-macro-junit4:1.2.0-beta04")
+    docs("androidx.benchmark:benchmark-common:1.2.0-beta05")
+    docs("androidx.benchmark:benchmark-junit4:1.2.0-beta05")
+    docs("androidx.benchmark:benchmark-macro:1.2.0-beta05")
+    docs("androidx.benchmark:benchmark-macro-junit4:1.2.0-beta05")
     docs("androidx.biometric:biometric:1.2.0-alpha05")
     docs("androidx.biometric:biometric-ktx:1.2.0-alpha05")
     samples("androidx.biometric:biometric-ktx-samples:1.2.0-alpha05")
@@ -58,57 +58,57 @@
     docs("androidx.car.app:app-projected:1.4.0-beta01")
     docs("androidx.car.app:app-testing:1.4.0-beta01")
     docs("androidx.cardview:cardview:1.0.0")
-    kmpDocs("androidx.collection:collection:1.3.0-beta01")
-    docs("androidx.collection:collection-ktx:1.3.0-beta01")
-    kmpDocs("androidx.compose.animation:animation:1.6.0-alpha04")
-    kmpDocs("androidx.compose.animation:animation-core:1.6.0-alpha04")
-    kmpDocs("androidx.compose.animation:animation-graphics:1.6.0-alpha04")
-    samples("androidx.compose.animation:animation-samples:1.6.0-alpha04")
-    samples("androidx.compose.animation:animation-core-samples:1.6.0-alpha04")
-    samples("androidx.compose.animation:animation-graphics-samples:1.6.0-alpha04")
-    kmpDocs("androidx.compose.foundation:foundation:1.6.0-alpha04")
-    kmpDocs("androidx.compose.foundation:foundation-layout:1.6.0-alpha04")
-    samples("androidx.compose.foundation:foundation-layout-samples:1.6.0-alpha04")
-    samples("androidx.compose.foundation:foundation-samples:1.6.0-alpha04")
-    kmpDocs("androidx.compose.material3:material3:1.2.0-alpha06")
-    samples("androidx.compose.material3:material3-samples:1.2.0-alpha06")
-    kmpDocs("androidx.compose.material3:material3-window-size-class:1.2.0-alpha06")
-    samples("androidx.compose.material3:material3-window-size-class-samples:1.2.0-alpha06")
-    kmpDocs("androidx.compose.material:material:1.6.0-alpha04")
-    kmpDocs("androidx.compose.material:material-icons-core:1.6.0-alpha04")
-    samples("androidx.compose.material:material-icons-core-samples:1.6.0-alpha04")
-    kmpDocs("androidx.compose.material:material-ripple:1.6.0-alpha04")
-    samples("androidx.compose.material:material-samples:1.6.0-alpha04")
-    kmpDocs("androidx.compose.runtime:runtime:1.6.0-alpha04")
-    docs("androidx.compose.runtime:runtime-livedata:1.6.0-alpha04")
-    samples("androidx.compose.runtime:runtime-livedata-samples:1.6.0-alpha04")
-    docs("androidx.compose.runtime:runtime-rxjava2:1.6.0-alpha04")
-    samples("androidx.compose.runtime:runtime-rxjava2-samples:1.6.0-alpha04")
-    docs("androidx.compose.runtime:runtime-rxjava3:1.6.0-alpha04")
-    samples("androidx.compose.runtime:runtime-rxjava3-samples:1.6.0-alpha04")
-    kmpDocs("androidx.compose.runtime:runtime-saveable:1.6.0-alpha04")
-    samples("androidx.compose.runtime:runtime-saveable-samples:1.6.0-alpha04")
-    samples("androidx.compose.runtime:runtime-samples:1.6.0-alpha04")
-    docs("androidx.compose.runtime:runtime-tracing:1.0.0-alpha04")
-    kmpDocs("androidx.compose.ui:ui:1.6.0-alpha04")
-    kmpDocs("androidx.compose.ui:ui-geometry:1.6.0-alpha04")
-    kmpDocs("androidx.compose.ui:ui-graphics:1.6.0-alpha04")
-    samples("androidx.compose.ui:ui-graphics-samples:1.6.0-alpha04")
-    kmpDocs("androidx.compose.ui:ui-test:1.6.0-alpha04")
-    kmpDocs("androidx.compose.ui:ui-test-junit4:1.6.0-alpha04")
-    samples("androidx.compose.ui:ui-test-samples:1.6.0-alpha04")
-    kmpDocs("androidx.compose.ui:ui-text:1.6.0-alpha04")
-    docs("androidx.compose.ui:ui-text-google-fonts:1.6.0-alpha04")
-    samples("androidx.compose.ui:ui-text-samples:1.6.0-alpha04")
-    kmpDocs("androidx.compose.ui:ui-tooling:1.6.0-alpha04")
-    kmpDocs("androidx.compose.ui:ui-tooling-data:1.6.0-alpha04")
-    kmpDocs("androidx.compose.ui:ui-tooling-preview:1.6.0-alpha04")
-    kmpDocs("androidx.compose.ui:ui-unit:1.6.0-alpha04")
-    samples("androidx.compose.ui:ui-unit-samples:1.6.0-alpha04")
-    kmpDocs("androidx.compose.ui:ui-util:1.6.0-alpha04")
-    docs("androidx.compose.ui:ui-viewbinding:1.6.0-alpha04")
-    samples("androidx.compose.ui:ui-viewbinding-samples:1.6.0-alpha04")
-    samples("androidx.compose.ui:ui-samples:1.6.0-alpha04")
+    kmpDocs("androidx.collection:collection:1.3.0-rc01")
+    docs("androidx.collection:collection-ktx:1.3.0-rc01")
+    kmpDocs("androidx.compose.animation:animation:1.6.0-alpha05")
+    kmpDocs("androidx.compose.animation:animation-core:1.6.0-alpha05")
+    kmpDocs("androidx.compose.animation:animation-graphics:1.6.0-alpha05")
+    samples("androidx.compose.animation:animation-samples:1.6.0-alpha05")
+    samples("androidx.compose.animation:animation-core-samples:1.6.0-alpha05")
+    samples("androidx.compose.animation:animation-graphics-samples:1.6.0-alpha05")
+    kmpDocs("androidx.compose.foundation:foundation:1.6.0-alpha05")
+    kmpDocs("androidx.compose.foundation:foundation-layout:1.6.0-alpha05")
+    samples("androidx.compose.foundation:foundation-layout-samples:1.6.0-alpha05")
+    samples("androidx.compose.foundation:foundation-samples:1.6.0-alpha05")
+    kmpDocs("androidx.compose.material3:material3:1.2.0-alpha07")
+    samples("androidx.compose.material3:material3-samples:1.2.0-alpha07")
+    kmpDocs("androidx.compose.material3:material3-window-size-class:1.2.0-alpha07")
+    samples("androidx.compose.material3:material3-window-size-class-samples:1.2.0-alpha07")
+    kmpDocs("androidx.compose.material:material:1.6.0-alpha05")
+    kmpDocs("androidx.compose.material:material-icons-core:1.6.0-alpha05")
+    samples("androidx.compose.material:material-icons-core-samples:1.6.0-alpha05")
+    kmpDocs("androidx.compose.material:material-ripple:1.6.0-alpha05")
+    samples("androidx.compose.material:material-samples:1.6.0-alpha05")
+    kmpDocs("androidx.compose.runtime:runtime:1.6.0-alpha05")
+    docs("androidx.compose.runtime:runtime-livedata:1.6.0-alpha05")
+    samples("androidx.compose.runtime:runtime-livedata-samples:1.6.0-alpha05")
+    docs("androidx.compose.runtime:runtime-rxjava2:1.6.0-alpha05")
+    samples("androidx.compose.runtime:runtime-rxjava2-samples:1.6.0-alpha05")
+    docs("androidx.compose.runtime:runtime-rxjava3:1.6.0-alpha05")
+    samples("androidx.compose.runtime:runtime-rxjava3-samples:1.6.0-alpha05")
+    kmpDocs("androidx.compose.runtime:runtime-saveable:1.6.0-alpha05")
+    samples("androidx.compose.runtime:runtime-saveable-samples:1.6.0-alpha05")
+    samples("androidx.compose.runtime:runtime-samples:1.6.0-alpha05")
+    docs("androidx.compose.runtime:runtime-tracing:1.0.0-alpha05")
+    kmpDocs("androidx.compose.ui:ui:1.6.0-alpha05")
+    kmpDocs("androidx.compose.ui:ui-geometry:1.6.0-alpha05")
+    kmpDocs("androidx.compose.ui:ui-graphics:1.6.0-alpha05")
+    samples("androidx.compose.ui:ui-graphics-samples:1.6.0-alpha05")
+    kmpDocs("androidx.compose.ui:ui-test:1.6.0-alpha05")
+    kmpDocs("androidx.compose.ui:ui-test-junit4:1.6.0-alpha05")
+    samples("androidx.compose.ui:ui-test-samples:1.6.0-alpha05")
+    kmpDocs("androidx.compose.ui:ui-text:1.6.0-alpha05")
+    docs("androidx.compose.ui:ui-text-google-fonts:1.6.0-alpha05")
+    samples("androidx.compose.ui:ui-text-samples:1.6.0-alpha05")
+    kmpDocs("androidx.compose.ui:ui-tooling:1.6.0-alpha05")
+    kmpDocs("androidx.compose.ui:ui-tooling-data:1.6.0-alpha05")
+    kmpDocs("androidx.compose.ui:ui-tooling-preview:1.6.0-alpha05")
+    kmpDocs("androidx.compose.ui:ui-unit:1.6.0-alpha05")
+    samples("androidx.compose.ui:ui-unit-samples:1.6.0-alpha05")
+    kmpDocs("androidx.compose.ui:ui-util:1.6.0-alpha05")
+    docs("androidx.compose.ui:ui-viewbinding:1.6.0-alpha05")
+    samples("androidx.compose.ui:ui-viewbinding-samples:1.6.0-alpha05")
+    samples("androidx.compose.ui:ui-samples:1.6.0-alpha05")
     docs("androidx.concurrent:concurrent-futures:1.2.0-alpha02")
     docs("androidx.concurrent:concurrent-futures-ktx:1.2.0-alpha02")
     docs("androidx.constraintlayout:constraintlayout:2.2.0-alpha12")
@@ -124,13 +124,13 @@
     docs("androidx.core:core-i18n:1.0.0-alpha01")
     docs("androidx.core:core-ktx:1.12.0-rc01")
     docs("androidx.core:core-location-altitude:1.0.0-alpha01")
-    docs("androidx.core:core-performance:1.0.0-alpha03")
-    docs("androidx.core:core-performance-play-services:1.0.0-alpha03")
-    samples("androidx.core:core-performance-samples:1.0.0-alpha03")
-    docs("androidx.core:core-performance-testing:1.0.0-alpha03")
+    docs("androidx.core:core-performance:1.0.0-beta01")
+    docs("androidx.core:core-performance-play-services:1.0.0-beta01")
+    samples("androidx.core:core-performance-samples:1.0.0-beta01")
+    docs("androidx.core:core-performance-testing:1.0.0-beta01")
     docs("androidx.core:core-remoteviews:1.0.0-rc01")
     docs("androidx.core:core-role:1.2.0-alpha01")
-    docs("androidx.core:core-splashscreen:1.1.0-alpha01")
+    docs("androidx.core:core-splashscreen:1.1.0-alpha02")
     docs("androidx.core:core-telecom:1.0.0-alpha01")
     docs("androidx.core:core-testing:1.12.0-rc01")
     docs("androidx.core.uwb:uwb:1.0.0-alpha07")
@@ -142,15 +142,15 @@
     docs("androidx.customview:customview:1.2.0-alpha02")
     // TODO(b/294531403): Turn on apiSince for customview-poolingcontainer when it releases as alpha
     docsWithoutApiSince("androidx.customview:customview-poolingcontainer:1.0.0-rc01")
-    kmpDocs("androidx.datastore:datastore:1.1.0-alpha04")
-    kmpDocs("androidx.datastore:datastore-core:1.1.0-alpha04")
-    kmpDocs("androidx.datastore:datastore-core-okio:1.1.0-alpha04")
-    kmpDocs("androidx.datastore:datastore-preferences:1.1.0-alpha04")
-    kmpDocs("androidx.datastore:datastore-preferences-core:1.1.0-alpha04")
-    docs("androidx.datastore:datastore-preferences-rxjava2:1.1.0-alpha04")
-    docs("androidx.datastore:datastore-preferences-rxjava3:1.1.0-alpha04")
-    docs("androidx.datastore:datastore-rxjava2:1.1.0-alpha04")
-    docs("androidx.datastore:datastore-rxjava3:1.1.0-alpha04")
+    kmpDocs("androidx.datastore:datastore:1.1.0-alpha05")
+    kmpDocs("androidx.datastore:datastore-core:1.1.0-alpha05")
+    kmpDocs("androidx.datastore:datastore-core-okio:1.1.0-alpha05")
+    kmpDocs("androidx.datastore:datastore-preferences:1.1.0-alpha05")
+    kmpDocs("androidx.datastore:datastore-preferences-core:1.1.0-alpha05")
+    docs("androidx.datastore:datastore-preferences-rxjava2:1.1.0-alpha05")
+    docs("androidx.datastore:datastore-preferences-rxjava3:1.1.0-alpha05")
+    docs("androidx.datastore:datastore-rxjava2:1.1.0-alpha05")
+    docs("androidx.datastore:datastore-rxjava3:1.1.0-alpha05")
     docs("androidx.documentfile:documentfile:1.1.0-alpha01")
     docs("androidx.draganddrop:draganddrop:1.0.0")
     docs("androidx.drawerlayout:drawerlayout:1.2.0")
@@ -167,23 +167,24 @@
     docs("androidx.enterprise:enterprise-feedback:1.1.0")
     docs("androidx.enterprise:enterprise-feedback-testing:1.1.0")
     docs("androidx.exifinterface:exifinterface:1.3.6")
-    docs("androidx.fragment:fragment:1.7.0-alpha03")
-    docs("androidx.fragment:fragment-ktx:1.7.0-alpha03")
-    docs("androidx.fragment:fragment-testing:1.7.0-alpha03")
-    docs("androidx.glance:glance:1.0.0-rc01")
-    docs("androidx.glance:glance-appwidget:1.0.0-rc01")
-    samples("androidx.glance:glance-appwidget-samples:1.0.0-rc01")
+    docs("androidx.fragment:fragment:1.7.0-alpha04")
+    docs("androidx.fragment:fragment-ktx:1.7.0-alpha04")
+    docs("androidx.fragment:fragment-testing:1.7.0-alpha04")
+    docs("androidx.glance:glance:1.0.0")
+    docs("androidx.glance:glance-appwidget:1.0.0")
+    samples("androidx.glance:glance-appwidget-samples:1.0.0")
     docs("androidx.glance:glance-appwidget-preview:1.0.0-alpha06")
-    docs("androidx.glance:glance-material:1.0.0-rc01")
-    docs("androidx.glance:glance-material3:1.0.0-rc01")
+    docs("androidx.glance:glance-material:1.0.0")
+    docs("androidx.glance:glance-material3:1.0.0")
     docs("androidx.glance:glance-preview:1.0.0-alpha06")
     docs("androidx.glance:glance-template:1.0.0-alpha06")
     docs("androidx.glance:glance-wear-tiles:1.0.0-alpha06")
     docs("androidx.glance:glance-wear-tiles-preview:1.0.0-alpha06")
-    docs("androidx.graphics:graphics-core:1.0.0-alpha04")
+    docs("androidx.graphics:graphics-core:1.0.0-alpha05")
+    samples("androidx.graphics:graphics-core-samples:1.0.0-alpha05")
     docs("androidx.gridlayout:gridlayout:1.1.0-beta01")
-    docs("androidx.health.connect:connect-client:1.1.0-alpha03")
-    samples("androidx.health.connect:connect-client-samples:1.1.0-alpha03")
+    docs("androidx.health.connect:connect-client:1.1.0-alpha04")
+    samples("androidx.health.connect:connect-client-samples:1.1.0-alpha04")
     docs("androidx.health:health-services-client:1.1.0-alpha01")
     docs("androidx.heifwriter:heifwriter:1.1.0-alpha02")
     docs("androidx.hilt:hilt-common:1.1.0-alpha01")
@@ -195,32 +196,32 @@
     docs("androidx.input:input-motionprediction:1.0.0-beta02")
     docs("androidx.interpolator:interpolator:1.0.0")
     docs("androidx.javascriptengine:javascriptengine:1.0.0-alpha05")
-    docs("androidx.leanback:leanback:1.2.0-alpha02")
-    docs("androidx.leanback:leanback-grid:1.0.0-alpha01")
-    docs("androidx.leanback:leanback-paging:1.1.0-alpha09")
-    docs("androidx.leanback:leanback-preference:1.2.0-alpha02")
+    docs("androidx.leanback:leanback:1.2.0-alpha03")
+    docs("androidx.leanback:leanback-grid:1.0.0-alpha02")
+    docs("androidx.leanback:leanback-paging:1.1.0-alpha10")
+    docs("androidx.leanback:leanback-preference:1.2.0-alpha03")
     docs("androidx.leanback:leanback-tab:1.1.0-beta01")
-    docs("androidx.lifecycle:lifecycle-common:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-common-java8:2.7.0-alpha01")
+    docs("androidx.lifecycle:lifecycle-common:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-common-java8:2.7.0-alpha02")
     docs("androidx.lifecycle:lifecycle-extensions:2.2.0")
-    docs("androidx.lifecycle:lifecycle-livedata:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-livedata-core:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-process:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-reactivestreams:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-runtime:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-runtime-compose:2.7.0-alpha01")
-    samples("androidx.lifecycle:lifecycle-runtime-compose-samples:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-runtime-testing:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-service:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-viewmodel:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0-alpha01")
-    samples("androidx.lifecycle:lifecycle-viewmodel-compose-samples:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0-alpha01")
-    docs("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0-alpha01")
+    docs("androidx.lifecycle:lifecycle-livedata:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-livedata-core:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-process:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-reactivestreams:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-runtime:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-runtime-compose:2.7.0-alpha02")
+    samples("androidx.lifecycle:lifecycle-runtime-compose-samples:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-runtime-testing:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-service:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-viewmodel:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0-alpha02")
+    samples("androidx.lifecycle:lifecycle-viewmodel-compose-samples:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0-alpha02")
+    docs("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0-alpha02")
     docs("androidx.loader:loader:1.1.0")
     // localbroadcastmanager is deprecated
     docsWithoutApiSince("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
@@ -258,31 +259,31 @@
     docs("androidx.mediarouter:mediarouter:1.6.0-rc01")
     docs("androidx.mediarouter:mediarouter-testing:1.6.0-rc01")
     docs("androidx.metrics:metrics-performance:1.0.0-alpha04")
-    docs("androidx.navigation:navigation-common:2.7.1")
-    docs("androidx.navigation:navigation-common-ktx:2.7.1")
-    docs("androidx.navigation:navigation-compose:2.7.1")
-    samples("androidx.navigation:navigation-compose-samples:2.7.1")
-    docs("androidx.navigation:navigation-dynamic-features-fragment:2.7.1")
-    docs("androidx.navigation:navigation-dynamic-features-runtime:2.7.1")
-    docs("androidx.navigation:navigation-fragment:2.7.1")
-    docs("androidx.navigation:navigation-fragment-ktx:2.7.1")
-    docs("androidx.navigation:navigation-runtime:2.7.1")
-    docs("androidx.navigation:navigation-runtime-ktx:2.7.1")
-    docs("androidx.navigation:navigation-testing:2.7.1")
-    docs("androidx.navigation:navigation-ui:2.7.1")
-    docs("androidx.navigation:navigation-ui-ktx:2.7.1")
-    docs("androidx.paging:paging-common:3.2.0")
-    docs("androidx.paging:paging-common-ktx:3.2.0")
-    docs("androidx.paging:paging-compose:3.2.0")
-    samples("androidx.paging:paging-compose-samples:3.2.0")
-    docs("androidx.paging:paging-guava:3.2.0")
-    docs("androidx.paging:paging-runtime:3.2.0")
-    docs("androidx.paging:paging-runtime-ktx:3.2.0")
-    docs("androidx.paging:paging-rxjava2:3.2.0")
-    docs("androidx.paging:paging-rxjava2-ktx:3.2.0")
-    docs("androidx.paging:paging-rxjava3:3.2.0")
-    samples("androidx.paging:paging-samples:3.2.0")
-    docs("androidx.paging:paging-testing:3.2.0")
+    docs("androidx.navigation:navigation-common:2.7.2")
+    docs("androidx.navigation:navigation-common-ktx:2.7.2")
+    docs("androidx.navigation:navigation-compose:2.7.2")
+    samples("androidx.navigation:navigation-compose-samples:2.7.2")
+    docs("androidx.navigation:navigation-dynamic-features-fragment:2.7.2")
+    docs("androidx.navigation:navigation-dynamic-features-runtime:2.7.2")
+    docs("androidx.navigation:navigation-fragment:2.7.2")
+    docs("androidx.navigation:navigation-fragment-ktx:2.7.2")
+    docs("androidx.navigation:navigation-runtime:2.7.2")
+    docs("androidx.navigation:navigation-runtime-ktx:2.7.2")
+    docs("androidx.navigation:navigation-testing:2.7.2")
+    docs("androidx.navigation:navigation-ui:2.7.2")
+    docs("androidx.navigation:navigation-ui-ktx:2.7.2")
+    docsWithoutApiSince("androidx.paging:paging-common:3.2.1")
+    docsWithoutApiSince("androidx.paging:paging-common-ktx:3.2.1")
+    docsWithoutApiSince("androidx.paging:paging-compose:3.2.1")
+    samples("androidx.paging:paging-compose-samples:3.2.1")
+    docsWithoutApiSince("androidx.paging:paging-guava:3.2.1")
+    docsWithoutApiSince("androidx.paging:paging-runtime:3.2.1")
+    docsWithoutApiSince("androidx.paging:paging-runtime-ktx:3.2.1")
+    docsWithoutApiSince("androidx.paging:paging-rxjava2:3.2.1")
+    docsWithoutApiSince("androidx.paging:paging-rxjava2-ktx:3.2.1")
+    docsWithoutApiSince("androidx.paging:paging-rxjava3:3.2.1")
+    samples("androidx.paging:paging-samples:3.2.1")
+    docsWithoutApiSince("androidx.paging:paging-testing:3.2.1")
     docs("androidx.palette:palette:1.0.0")
     docs("androidx.palette:palette-ktx:1.0.0")
     docs("androidx.percentlayout:percentlayout:1.0.1")
@@ -293,7 +294,7 @@
     docs("androidx.privacysandbox.ads:ads-adservices-java:1.1.0-beta01")
     docs("androidx.privacysandbox.sdkruntime:sdkruntime-client:1.0.0-alpha08")
     docs("androidx.privacysandbox.sdkruntime:sdkruntime-core:1.0.0-alpha08")
-    docs("androidx.privacysandbox.tools:tools:1.0.0-alpha05")
+    docs("androidx.privacysandbox.tools:tools:1.0.0-alpha06")
     docs("androidx.privacysandbox.ui:ui-client:1.0.0-alpha05")
     docs("androidx.privacysandbox.ui:ui-core:1.0.0-alpha05")
     docs("androidx.privacysandbox.ui:ui-provider:1.0.0-alpha05")
@@ -358,15 +359,15 @@
     docsWithoutApiSince("androidx.textclassifier:textclassifier:1.0.0-alpha04")
     docs("androidx.tracing:tracing:1.3.0-alpha02")
     docs("androidx.tracing:tracing-ktx:1.3.0-alpha02")
-    docs("androidx.tracing:tracing-perfetto:1.0.0-beta02")
+    docs("androidx.tracing:tracing-perfetto:1.0.0-beta03")
     // TODO(243405142) clean-up
     docsWithoutApiSince("androidx.tracing:tracing-perfetto-common:1.0.0-alpha16")
-    docs("androidx.tracing:tracing-perfetto-handshake:1.0.0-beta02")
-    docs("androidx.transition:transition:1.5.0-alpha01")
-    docs("androidx.transition:transition-ktx:1.5.0-alpha01")
-    docs("androidx.tv:tv-foundation:1.0.0-alpha08")
-    docs("androidx.tv:tv-material:1.0.0-alpha08")
-    samples("androidx.tv:tv-samples:1.0.0-alpha08")
+    docs("androidx.tracing:tracing-perfetto-handshake:1.0.0-beta03")
+    docs("androidx.transition:transition:1.5.0-alpha02")
+    docs("androidx.transition:transition-ktx:1.5.0-alpha02")
+    docs("androidx.tv:tv-foundation:1.0.0-alpha09")
+    docs("androidx.tv:tv-material:1.0.0-alpha09")
+    samples("androidx.tv:tv-samples:1.0.0-alpha09")
     docs("androidx.tvprovider:tvprovider:1.1.0-alpha01")
     docs("androidx.vectordrawable:vectordrawable:1.2.0-beta01")
     docs("androidx.vectordrawable:vectordrawable-animated:1.2.0-alpha01")
@@ -374,16 +375,16 @@
     docs("androidx.versionedparcelable:versionedparcelable:1.1.1")
     docs("androidx.viewpager2:viewpager2:1.1.0-beta02")
     docs("androidx.viewpager:viewpager:1.1.0-alpha01")
-    docs("androidx.wear.compose:compose-foundation:1.3.0-alpha04")
-    samples("androidx.wear.compose:compose-foundation-samples:1.3.0-alpha04")
-    docs("androidx.wear.compose:compose-material:1.3.0-alpha04")
-    docs("androidx.wear.compose:compose-material-core:1.3.0-alpha04")
-    samples("androidx.wear.compose:compose-material-samples:1.3.0-alpha04")
-    docs("androidx.wear.compose:compose-material3:1.0.0-alpha10")
-    samples("androidx.wear.compose:compose-material3-samples:1.3.0-alpha04")
-    docs("androidx.wear.compose:compose-navigation:1.3.0-alpha04")
-    samples("androidx.wear.compose:compose-navigation-samples:1.3.0-alpha04")
-    docs("androidx.wear.compose:compose-ui-tooling:1.3.0-alpha04")
+    docs("androidx.wear.compose:compose-foundation:1.3.0-alpha05")
+    samples("androidx.wear.compose:compose-foundation-samples:1.3.0-alpha05")
+    docs("androidx.wear.compose:compose-material:1.3.0-alpha05")
+    docs("androidx.wear.compose:compose-material-core:1.3.0-alpha05")
+    samples("androidx.wear.compose:compose-material-samples:1.3.0-alpha05")
+    docs("androidx.wear.compose:compose-material3:1.0.0-alpha11")
+    samples("androidx.wear.compose:compose-material3-samples:1.3.0-alpha05")
+    docs("androidx.wear.compose:compose-navigation:1.3.0-alpha05")
+    samples("androidx.wear.compose:compose-navigation-samples:1.3.0-alpha05")
+    docs("androidx.wear.compose:compose-ui-tooling:1.3.0-alpha05")
     docs("androidx.wear.protolayout:protolayout:1.0.0")
     docs("androidx.wear.protolayout:protolayout-expression:1.0.0")
     docs("androidx.wear.protolayout:protolayout-expression-pipeline:1.0.0")
@@ -394,22 +395,22 @@
     docs("androidx.wear.tiles:tiles-renderer:1.2.0")
     docs("androidx.wear.tiles:tiles-testing:1.2.0")
     docs("androidx.wear.tiles:tiles-tooling:1.2.0-alpha07")
-    docs("androidx.wear.watchface:watchface:1.2.0-beta01")
-    docs("androidx.wear.watchface:watchface-client:1.2.0-beta01")
-    docs("androidx.wear.watchface:watchface-client-guava:1.2.0-beta01")
-    docs("androidx.wear.watchface:watchface-complications:1.2.0-beta01")
-    docs("androidx.wear.watchface:watchface-complications-data:1.2.0-beta01")
-    docs("androidx.wear.watchface:watchface-complications-data-source:1.2.0-beta01")
-    docs("androidx.wear.watchface:watchface-complications-data-source-ktx:1.2.0-beta01")
-    samples("androidx.wear.watchface:watchface-complications-permission-dialogs-sample:1.2.0-beta01")
-    docs("androidx.wear.watchface:watchface-complications-rendering:1.2.0-beta01")
-    docs("androidx.wear.watchface:watchface-data:1.2.0-beta01")
-    docs("androidx.wear.watchface:watchface-editor:1.2.0-beta01")
-    docs("androidx.wear.watchface:watchface-editor-guava:1.2.0-beta01")
-    samples("androidx.wear.watchface:watchface-editor-samples:1.2.0-beta01")
-    docs("androidx.wear.watchface:watchface-guava:1.2.0-beta01")
-    samples("androidx.wear.watchface:watchface-samples:1.2.0-beta01")
-    docs("androidx.wear.watchface:watchface-style:1.2.0-beta01")
+    docs("androidx.wear.watchface:watchface:1.2.0-beta02")
+    docs("androidx.wear.watchface:watchface-client:1.2.0-beta02")
+    docs("androidx.wear.watchface:watchface-client-guava:1.2.0-beta02")
+    docs("androidx.wear.watchface:watchface-complications:1.2.0-beta02")
+    docs("androidx.wear.watchface:watchface-complications-data:1.2.0-beta02")
+    docs("androidx.wear.watchface:watchface-complications-data-source:1.2.0-beta02")
+    docs("androidx.wear.watchface:watchface-complications-data-source-ktx:1.2.0-beta02")
+    samples("androidx.wear.watchface:watchface-complications-permission-dialogs-sample:1.2.0-beta02")
+    docs("androidx.wear.watchface:watchface-complications-rendering:1.2.0-beta02")
+    docs("androidx.wear.watchface:watchface-data:1.2.0-beta02")
+    docs("androidx.wear.watchface:watchface-editor:1.2.0-beta02")
+    docs("androidx.wear.watchface:watchface-editor-guava:1.2.0-beta02")
+    samples("androidx.wear.watchface:watchface-editor-samples:1.2.0-beta02")
+    docs("androidx.wear.watchface:watchface-guava:1.2.0-beta02")
+    samples("androidx.wear.watchface:watchface-samples:1.2.0-beta02")
+    docs("androidx.wear.watchface:watchface-style:1.2.0-beta02")
     // TODO(b/294531403): Turn on apiSince for wear when it releases as alpha
     docsWithoutApiSince("androidx.wear:wear:1.3.0-rc01")
     stubs(fileTree(dir: "../wear/wear_stubs/", include: ["com.google.android.wearable-stubs.jar"]))
@@ -420,7 +421,7 @@
     docs("androidx.wear:wear-phone-interactions:1.1.0-alpha03")
     docs("androidx.wear:wear-remote-interactions:1.1.0-alpha01")
     docs("androidx.wear:wear-tooling-preview:1.0.0-alpha01")
-    docs("androidx.webkit:webkit:1.8.0-rc01")
+    docs("androidx.webkit:webkit:1.8.0")
     docs("androidx.window.extensions.core:core:1.0.0")
     docs("androidx.window:window:1.2.0-beta01")
     stubs(fileTree(dir: "../window/stubs/", include: ["window-sidecar-release-0.1.0-alpha01.aar"]))
@@ -431,12 +432,12 @@
     docs("androidx.window:window-rxjava3:1.2.0-beta01")
     samples("androidx.window:window-samples:1.2.0-beta01")
     docs("androidx.window:window-testing:1.2.0-beta01")
-    docs("androidx.work:work-gcm:2.9.0-alpha02")
-    docs("androidx.work:work-multiprocess:2.9.0-alpha02")
-    docs("androidx.work:work-runtime:2.9.0-alpha02")
-    docs("androidx.work:work-runtime-ktx:2.9.0-alpha02")
-    docs("androidx.work:work-rxjava2:2.9.0-alpha02")
-    docs("androidx.work:work-rxjava3:2.9.0-alpha02")
-    docs("androidx.work:work-testing:2.9.0-alpha02")
+    docs("androidx.work:work-gcm:2.9.0-beta01")
+    docs("androidx.work:work-multiprocess:2.9.0-beta01")
+    docs("androidx.work:work-runtime:2.9.0-beta01")
+    docs("androidx.work:work-runtime-ktx:2.9.0-beta01")
+    docs("androidx.work:work-rxjava2:2.9.0-beta01")
+    docs("androidx.work:work-rxjava3:2.9.0-beta01")
+    docs("androidx.work:work-testing:2.9.0-beta01")
 }
 
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index d65c0f9..2af5022 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -63,6 +63,7 @@
     docs(project(":camera:camera-lifecycle"))
     docs(project(":camera:camera-mlkit-vision"))
     // camera-previewview is not hosted in androidx
+    docsWithoutApiSince(project(":camera:camera-testing"))
     docs(project(":camera:camera-video"))
     docs(project(":camera:camera-view"))
     docs(project(":camera:camera-viewfinder"))
diff --git a/docs/api_guidelines/dependencies.md b/docs/api_guidelines/dependencies.md
index 011fab9..5a43db3 100644
--- a/docs/api_guidelines/dependencies.md
+++ b/docs/api_guidelines/dependencies.md
@@ -107,9 +107,16 @@
 image and are made available to developers through the `<uses-library>` manifest
 tag.
 
-Examples include Wear OS extensions (`com.google.android.wearable`), Camera OEM
-extensions (`androidx.camera.extensions.impl`), and Window OEM extensions
-(`androix.window.extensions`).
+Examples include Camera OEM extensions (`androidx.camera.extensions.impl`) and
+Window OEM extensions (`androidx.window.extensions`).
+
+Extension libraries may be defined in AndroidX library projects (see
+`androidx.window.extensions`) or externally, ex. in AOSP alongside the platform.
+In either case, we recommend that libraries use extensions as pinned, rather
+than project-type, dependencies to facilitate versioning across repositories.
+
+*Do not* ship extension interfaces to Google Maven. Teams may choose to ship
+stub JARs publicly, but that is not covered by AndroidX workflows.
 
 Project dependencies on extension libraries **must** use `compileOnly`:
 
diff --git a/docs/api_guidelines/deprecation.md b/docs/api_guidelines/deprecation.md
index f5dc6d8..2d7069e 100644
--- a/docs/api_guidelines/deprecation.md
+++ b/docs/api_guidelines/deprecation.md
@@ -91,8 +91,9 @@
     artifact as `@Deprecated` and update the API files
     ([example CL](https://android-review.googlesource.com/c/platform/frameworks/support/+/1938773))
 1.  Schedule a release of the artifact as a new minor version. When you populate
-    the release notes, explain that the entire artifact has been deprecated.
-    Include the reason for deprecation and the migration strategy.
+    the release notes, explain that the entire artifact has been deprecated and
+    will no longer receive new features or bug fixes. Include the reason for
+    deprecation and the migration strategy.
 1.  After the artifact has been released, remove the artifact from the source
     tree, versions file, and tip-of-tree docs configuration
     ([example CL](https://android-review.googlesource.com/c/platform/frameworks/support/+/2061731/))
@@ -107,3 +108,58 @@
 
 After an artifact has been released as fully-deprecated, it can be removed from
 the source tree.
+
+#### Long-term support
+
+Artifacts which have been fully deprecated and removed are not required to fix
+any bugs -- including security issues -- which are reported after the library
+has been removed from source control; however, library owners *may* utilize
+release branches to provide long-term support.
+
+When working on long-term support in a release branch, you may encounter the
+following issues:
+
+-   Release metadata produced by the build system is not compatible with the
+    release scheduling tool
+-   Build targets associated with the release branch do not match targets used
+    by the snapped build ID
+-   Delta between last snapped build ID and proposed snap build ID is too large
+    and cannot be processed by the release branch management tool
+
+### Discouraging usage in Play Store
+
+[Google Play SDK Console](https://play.google.com/sdk-console/) allows library
+owners to annotate specific library versions with notes, which are shown to app
+developers in the Play Store Console, or permanently mark them as outdated,
+which shows a warning in Play Store Console asking app developers to upgrade.
+
+In both cases, library owners have the option to prevent app developers from
+releasing apps to Play Store that have been built against specific library
+versions.
+
+Generally, Jetpack discourages the use of either notes or marking versions as
+outdated. There are few cases that warrant pushing notifications to app
+developers, and it is easy to abuse notes as advertising to drive adoption. As a
+rule, upgrades to Jetpack libraries should be driven by the needs of app
+developers.
+
+Cases where notes may be used include:
+
+1.  The library is used directly, rather than transitively, and contains `P0` or
+    `P1` (ship-blocking, from the app's perspective) issues
+    -   Transitively-included libraries should instead urge their dependent
+        libraries to bump their pinned dependency versions
+1.  The library contains ship-blocking security issues. In this case, we
+    recommend preventing app releases since developers may be less aware of
+    security issues.
+1.  The library was built against a pre-release SDK which has been superseded by
+    a finalized SDK. In this case, we recommend preventing app releases since
+    the library may crash or show unexpected behavior.
+
+Cases where marking a version as outdated maybe used:
+
+1.  The library has security implications and the version is no longer receiving
+    security updates, e.g. the release branch has moved to the next version.
+
+In all cases, there must be a newer stable or bugfix release of the library that
+app developers can use to replace the outdated version.
diff --git a/docs/onboarding_images/image10.png b/docs/onboarding_images/image10.png
new file mode 100644
index 0000000..ed1fd74
--- /dev/null
+++ b/docs/onboarding_images/image10.png
Binary files differ
diff --git a/docs/onboarding_images/image6.png b/docs/onboarding_images/image6.png
new file mode 100644
index 0000000..41795ce
--- /dev/null
+++ b/docs/onboarding_images/image6.png
Binary files differ
diff --git a/docs/onboarding_images/image7.png b/docs/onboarding_images/image7.png
new file mode 100644
index 0000000..27eef7d
--- /dev/null
+++ b/docs/onboarding_images/image7.png
Binary files differ
diff --git a/docs/onboarding_images/image8.png b/docs/onboarding_images/image8.png
new file mode 100644
index 0000000..da0dc66
--- /dev/null
+++ b/docs/onboarding_images/image8.png
Binary files differ
diff --git a/docs/onboarding_images/image9.png b/docs/onboarding_images/image9.png
new file mode 100644
index 0000000..d90dcc2
--- /dev/null
+++ b/docs/onboarding_images/image9.png
Binary files differ
diff --git a/docs/testing.md b/docs/testing.md
index 8050e0a..84e47d2 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -40,6 +40,164 @@
 ([example](https://r.android.com/2428721)). You can run these tests just like
 any other JVM test using `test` Gradle task.
 
+### Adding screenshots tests using scuba library
+
+#### Prerequisites
+
+Golden project: Make sure that you have the golden directory in your root
+checkout (sibling of frameworks directory). If not re-init your repo to fetch
+the latest manifest file:
+
+```
+$ repo init -u sso://android/platform/manifest \
+    -b androidx-main && repo sync -c -j8
+```
+
+Set up your module: If your module is not using screenshot tests yet, you need
+to do the initial setup.
+
+1.  Modify your gradle file: Add dependency on the diffing library into your
+    gradle file:
+
+    ```
+    androidTestImplementation project(“:test:screenshot:screenshot”)
+    ```
+
+    Important step: Add golden asset directory to be linked to your test apk:
+
+    ```
+    android {
+        sourceSets.androidTest.assets.srcDirs +=
+            // For androidx project (not in ui dir) use "/../../golden/project"
+            project.rootDir.absolutePath + "/../../golden/compose/material/material"
+    }
+    ```
+
+    This will bundle the goldens into your apk so they can be retrieved during
+    the test.
+
+2.  Create directory and variable: In the golden directory, create a new
+    directory for your module (the directory that you added to your gradle file,
+    which in case of material was “compose/material/material”).
+
+    In your test module, create a variable pointing at your new directory:
+
+    ```
+    const val GOLDEN_MATERIAL = "compose/material/material"
+    ```
+
+#### Adding a screenshot test
+
+Here is an example of a minimal screenshot test for compose material.
+
+```
+@LargeTest
+@RunWith(JUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class CheckboxScreenshotTest {
+    @get:Rule val composeTestRule = createComposeRule()
+    @get:Rule val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL)
+
+    @Test
+    fun checkBoxTest_checked() {
+        composeTestRule.setMaterialContent {
+            Checkbox(Modifier.wrapContentSize(Alignment.TopStart),
+                checked = true,
+                onCheckedChange = {}
+            )
+        }
+        find(isToggleable())
+            .captureToBitmap()
+            .assertAgainstGolden(screenshotRule, "checkbox_checked")
+    }
+}
+```
+
+NOTE: The string “checkbox_checked” is the unique identifier of your golden in
+your module. We use that string to name the golden file so avoid special
+characters. Please avoid any substrings like: golden, image etc. as there is no
+need - instead just describe what the image contains.
+
+#### Guidance around diffing
+
+Try to take the smallest screenshot possible. This will reduce interference from
+other elements.
+
+By default we use a MSSIM comparer. This one is based on similarity. However we
+have quite a high bar currently which is 0.98 (1 is an exact match). You can
+provide your own threshold or even opt into a pixel perfect comparer for some
+reason.
+
+Note: The bigger screenshots you take the more you sacrifice in the precision as
+you can aggregate larger diffing errors, see the examples below.
+
+![alt_text](onboarding_images/image6.png "screenshot diff at different MSSIM")
+
+#### Generating your goldens in CI (Gerrit)
+
+Upload your CL to gerrit and run presubmit. You should see your test fail.
+
+Step 1: Click on the “Test” button below:
+
+![alt_text](onboarding_images/image7.png "Presubmit link to failed test")
+
+Step 2: Click on the “Update scuba goldens” below:
+![alt_text](onboarding_images/image8.png "Update scuba button")
+
+Step 3: You should see a dashboard similar to the example below. Check-out if
+the new screenshots look as expected and if yes click approve. This will create
+a new CL.
+![alt_text](onboarding_images/image9.png "Button to approve scuba changes")
+
+Step 4: Link your original CL with the new goldens CL by setting the same Topic
+field in both CLs (any arbitrary string will do). This tells Gerrit to submit
+the CLs together, effectively providing a reference from the original CL to the
+new goldens. And re-run presubmit. Your tests should now pass!
+![alt_text](onboarding_images/image10.png "Topic for connecting cls")
+
+#### Running manually / debugging
+
+Screenshot tests can be run locally using pixel 2 api33 emulator. Start the
+emulator using [these](#emulator) steps.
+
+Wait until the emulator is running and run the tests as you would on a regular
+device.
+
+```
+$ ./gradlew <module>:cAT -Pandroid.testInstrumentationRunnerArguments.class=<class>
+```
+
+If the test passes, the results are limited to a .textproto file for each
+screenshot test. If the test fails, the results will also contain the actual
+screenshot and, if available, the golden reference image and the diff between
+the two. Note that this means that if you want to regenerate the golden image,
+you have to remove the golden image before running the test.
+
+To get the screenshot related results from the device onto your workstation, you
+can run
+
+```
+$ adb pull /sdcard/Android/data/<test-package>/cache/androidx_screenshots
+```
+
+where test-package is the identifier of you test apk, e.g.
+androidx.compose.material.test
+
+#### Locally updating the golden images
+
+After you run a screenshot test and pull the results to a desired location,
+verify that the actual images are the correct ones and copy them to the golden
+screenshots directory (the one you use to create the AndroidXScreenshotTestRule
+with) using this script.
+
+```
+androidx-main/frameworks/support/development/copy_screenshots_to_golden_repo.py \
+--input-dir=/tmp/androidx_screenshots/ --output-dir=androidx-main/golden/<test>/
+```
+
+Repeat for all screenshots, then create and upload a CL in the golden
+repository.
+
 ### What gets tested, and when {#affected-module-detector}
 
 With over 45000 tests executed on every CI run, it is necessary for us to run
@@ -81,12 +239,6 @@
 
 #### Disabling tests {#disabling-tests}
 
-To disable a device-side test in presubmit testing only -- but still have it run
-in postsubmit -- use the
-[`@FlakyTest`](https://developer.android.com/reference/androidx/test/filters/FlakyTest)
-annotation. There is currently no support for presubmit-only disabling of
-host-side tests.
-
 If you need to stop a host- or device-side test from running entirely, use
 JUnit's [`@Ignore`](http://junit.sourceforge.net/javadoc/org/junit/Ignore.html)
 annotation. Do *not* use Android's `@Suppress` annotation, which only works with
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
index feb3289..bb95933 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
@@ -17,7 +17,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="emoji_category_recent" msgid="7142376595414250279">"USADO RECIENTEMENTE"</string>
+    <string name="emoji_category_recent" msgid="7142376595414250279">"USADOS RECIENTEMENTE"</string>
     <string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTICONOS Y EMOCIONES"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSONAS"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALES Y NATURALEZA"</string>
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index 2f50bc8..d1e02606 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -451,21 +451,28 @@
     ctor public FragmentTransitionImpl();
     method public abstract void addTarget(Object, android.view.View);
     method public abstract void addTargets(Object, java.util.ArrayList<android.view.View!>);
+    method public void animateToEnd(Object);
+    method public void animateToStart(Object);
     method public abstract void beginDelayedTransition(android.view.ViewGroup, Object?);
     method protected static void bfsAddViewChildren(java.util.List<android.view.View!>!, android.view.View!);
     method public abstract boolean canHandle(Object);
     method public abstract Object! cloneTransition(Object?);
+    method public Object? controlDelayedTransition(android.view.ViewGroup, Object);
     method protected void getBoundsOnScreen(android.view.View!, android.graphics.Rect!);
     method protected static boolean isNullOrEmpty(java.util.List!);
+    method public boolean isSeekingSupported();
+    method public boolean isSeekingSupported(Object);
     method public abstract Object! mergeTransitionsInSequence(Object?, Object?, Object?);
     method public abstract Object! mergeTransitionsTogether(Object?, Object?, Object?);
     method public abstract void removeTarget(Object, android.view.View);
     method public abstract void replaceTargets(Object, java.util.ArrayList<android.view.View!>!, java.util.ArrayList<android.view.View!>!);
     method public abstract void scheduleHideFragmentView(Object, android.view.View, java.util.ArrayList<android.view.View!>);
     method public abstract void scheduleRemoveTargets(Object, Object?, java.util.ArrayList<android.view.View!>?, Object?, java.util.ArrayList<android.view.View!>?, Object?, java.util.ArrayList<android.view.View!>?);
+    method public void setCurrentPlayTime(Object, float);
     method public abstract void setEpicenter(Object, android.graphics.Rect);
     method public abstract void setEpicenter(Object, android.view.View?);
     method public void setListenerForTransitionEnd(androidx.fragment.app.Fragment, Object, androidx.core.os.CancellationSignal, Runnable);
+    method public void setListenerForTransitionEnd(androidx.fragment.app.Fragment, Object, androidx.core.os.CancellationSignal, Runnable?, Runnable);
     method public abstract void setSharedElementTargets(Object, android.view.View, java.util.ArrayList<android.view.View!>);
     method public abstract void swapSharedElementTargets(Object?, java.util.ArrayList<android.view.View!>?, java.util.ArrayList<android.view.View!>?);
     method public abstract Object! wrapTransitionInSet(Object?);
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
index ab3b7d6..7ca4c07 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
@@ -32,6 +32,7 @@
 import androidx.core.view.ViewCompat
 import androidx.fragment.app.test.FragmentTestActivity
 import androidx.fragment.test.R
+import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.ViewModelStore
 import androidx.test.annotation.UiThreadTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -725,6 +726,83 @@
         assertThat(fragment1.requireView().parent).isNotNull()
     }
 
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun replaceOperationWithAnimatorsThenCancel() {
+        val fm1 = activityRule.activity.supportFragmentManager
+
+        val fragment1 = AnimatorFragment(R.layout.scene1)
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment1, "1")
+            .addToBackStack(null)
+            .commit()
+        activityRule.waitForExecution()
+
+        val fragment2 = AnimatorFragment()
+
+        fm1.beginTransaction()
+            .setCustomAnimations(
+                android.R.animator.fade_in,
+                android.R.animator.fade_out,
+                android.R.animator.fade_in,
+                android.R.animator.fade_out
+            )
+            .replace(R.id.fragmentContainer, fragment2, "2")
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(fragment2.wasStarted).isTrue()
+        // We need to wait for the exit animation to end
+        assertThat(fragment1.endLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        val dispatcher = activityRule.activity.onBackPressedDispatcher
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        fragment2.resumeLatch = CountDownLatch(1)
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackProgressed(
+                BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+            )
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        if (FragmentManager.USE_PREDICTIVE_BACK) {
+            assertThat(fragment1.startLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+            assertThat(fragment2.startLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+            assertThat(fragment1.inProgress).isTrue()
+            assertThat(fragment2.inProgress).isTrue()
+        } else {
+            assertThat(fragment1.inProgress).isFalse()
+            assertThat(fragment1.inProgress).isFalse()
+        }
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackCancelled()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(fragment2.wasStarted).isTrue()
+        // Now fragment1 should be animating away
+        assertThat(fragment1.isAdded).isFalse()
+
+        // Now fragment2 should be animating back in
+        assertThat(fragment2.isAdded).isTrue()
+        assertThat(fm1.findFragmentByTag("2")).isEqualTo(fragment2)
+
+        assertThat(fragment2.resumeLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment2.view?.parent).isNotNull()
+
+        assertThat(fragment1.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+        assertThat(fragment1.view).isNull()
+    }
+
     private fun assertEnterPopExit(fragment: AnimatorFragment) {
         assertFragmentAnimation(fragment, 1, true, ENTER)
 
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt
index a35f0c1..852fe8a 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt
@@ -29,6 +29,7 @@
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
 import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -164,6 +165,7 @@
         }
     }
 
+    @Ignore // Ignore this test until we find a way to better test this scenario.
     @MediumTest
     @Test
     fun ensureOnlyChangeContainerStatusForCompletedOperation() {
@@ -220,17 +222,10 @@
             operation2.addCompletionListener {
                 awaitingChanges = operation2.isAwaitingContainerChanges
             }
+            val operation = controller.operationsToExecute[0]
             onActivity {
-                fragmentStateManager1.moveToExpectedState()
-                fragmentStateManager2.moveToExpectedState()
-                // However, executePendingOperations(), since we're using our
-                // TestSpecialEffectsController, does immediately call complete()
-                // which in turn calls moveToExpectedState()
-                controller.executePendingOperations()
+                operation.complete()
             }
-            // Assert that we actually moved to the STARTED state
-            assertThat(fragment1.lifecycle.currentState)
-                .isEqualTo(Lifecycle.State.STARTED)
             assertThat(awaitingChanges).isTrue()
         }
     }
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
index 6240244..a5f9e91 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
@@ -188,13 +188,16 @@
         firstOut: Operation?,
         lastIn: Operation?
     ) {
-        // First verify that we can run all transitions together
-        val transitionImpl = transitionInfos.filterNot { transitionInfo ->
+        val filteredInfos = transitionInfos.filterNot { transitionInfo ->
             // If there is no change in visibility, we can skip the TransitionInfo
             transitionInfo.isVisibilityUnchanged
         }.filter { transitionInfo ->
             transitionInfo.handlingImpl != null
-        }.fold(null as FragmentTransitionImpl?) { chosenImpl, transitionInfo ->
+        }
+        // First verify that we can run all transitions together
+        val transitionImpl = filteredInfos.fold(
+            null as FragmentTransitionImpl?
+        ) { chosenImpl, transitionInfo ->
             val handlingImpl = transitionInfo.handlingImpl
             require(chosenImpl == null || handlingImpl === chosenImpl) {
                 "Mixing framework transitions and AndroidX transitions is not allowed. Fragment " +
@@ -215,7 +218,7 @@
         var exitingNames = ArrayList<String>()
         val firstOutViews = ArrayMap<String, View>()
         val lastInViews = ArrayMap<String, View>()
-        for (transitionInfo: TransitionInfo in transitionInfos) {
+        for (transitionInfo: TransitionInfo in filteredInfos) {
             val hasSharedElementTransition = transitionInfo.hasSharedElementTransition()
             // Compute the shared element transition between the firstOut and lastIn Fragments
             if (hasSharedElementTransition && (firstOut != null) && (lastIn != null)) {
@@ -344,12 +347,12 @@
         }
 
         val transitionEffect = TransitionEffect(
-            transitionInfos, firstOut, lastIn, transitionImpl, sharedElementTransition,
+            filteredInfos, firstOut, lastIn, transitionImpl, sharedElementTransition,
             sharedElementFirstOutViews, sharedElementLastInViews, sharedElementNameMapping,
             enteringNames, exitingNames, firstOutViews, lastInViews, isPop
         )
 
-        transitionInfos.forEach { transitionInfo ->
+        filteredInfos.forEach { transitionInfo ->
             transitionInfo.operation.addEffect(transitionEffect)
         }
     }
@@ -594,7 +597,7 @@
                     if (isHideOperation) {
                         // Specifically for hide operations with Animator, we can't
                         // applyState until the Animator finishes
-                        operation.finalState.applyState(viewToAnimate)
+                        operation.finalState.applyState(viewToAnimate, container)
                     }
                     animatorInfo.operation.completeEffect(this@AnimatorEffect)
                     if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
@@ -700,7 +703,134 @@
     ) : Effect() {
         val transitionSignal = CancellationSignal()
 
+        var controller: Any? = null
+
+        override val isSeekingSupported: Boolean
+            get() = transitionImpl.isSeekingSupported &&
+                transitionInfos.all {
+                    Build.VERSION.SDK_INT >= 34 &&
+                        it.transition != null &&
+                        transitionImpl.isSeekingSupported(it.transition)
+                }
+
+        val transitioning: Boolean
+            get() = transitionInfos.all {
+                it.operation.fragment.mTransitioning
+            }
+
+        override fun onStart(container: ViewGroup) {
+            // If the container has never been laid out, transitions will not start so
+            // so lets instantly complete them.
+            if (!ViewCompat.isLaidOut(container)) {
+                transitionInfos.forEach { transitionInfo: TransitionInfo ->
+                    val operation: Operation = transitionInfo.operation
+                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                        Log.v(FragmentManager.TAG,
+                            "SpecialEffectsController: Container $container has not been " +
+                                "laid out. Skipping onStart for operation $operation")
+                    }
+                }
+                return
+            }
+            if (isSeekingSupported && transitioning) {
+                // We need to set the listener before we create the controller, but we need the
+                // controller to do the desired cancel behavior (animateToStart). So we use this
+                // lambda to set the proper cancel behavior to pass into the listener before the
+                // function is created.
+                var seekCancelLambda: (() -> Unit)? = null
+                // Now set up our completion signal on the completely merged transition set
+                val (enteringViews, mergedTransition) =
+                    createMergedTransition(container, lastIn, firstOut)
+                transitionInfos.map { it.operation }.forEach { operation ->
+                    val cancelRunnable = Runnable { seekCancelLambda?.invoke() }
+                    transitionImpl.setListenerForTransitionEnd(
+                        operation.fragment,
+                        mergedTransition,
+                        transitionSignal,
+                        cancelRunnable,
+                        Runnable {
+                            if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                                Log.v(FragmentManager.TAG,
+                                    "Transition for operation $operation has completed")
+                            }
+                            operation.completeEffect(this)
+                        })
+                }
+
+                runTransition(enteringViews, container) {
+                    controller =
+                        transitionImpl.controlDelayedTransition(container, mergedTransition)
+                    // If we fail to create a controller, it must be because of the container or
+                    // the transition so we should throw an error.
+                    check(controller != null) {
+                        "Unable to start transition $mergedTransition for container $container."
+                    }
+                    seekCancelLambda = { transitionImpl.animateToStart(controller!!) }
+                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                        Log.v(FragmentManager.TAG,
+                            "Started executing operations from $firstOut to $lastIn")
+                    }
+                }
+            }
+        }
+
+        override fun onProgress(backEvent: BackEventCompat, container: ViewGroup) {
+            controller?.let { transitionImpl.setCurrentPlayTime(it, backEvent.progress) }
+        }
+
         override fun onCommit(container: ViewGroup) {
+            // If the container has never been laid out, transitions will not start so
+            // so lets instantly complete them.
+            if (!ViewCompat.isLaidOut(container)) {
+                transitionInfos.forEach { transitionInfo: TransitionInfo ->
+                    val operation: Operation = transitionInfo.operation
+                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                        Log.v(FragmentManager.TAG,
+                            "SpecialEffectsController: Container $container has not been " +
+                                "laid out. Completing operation $operation")
+                    }
+                    transitionInfo.operation.completeEffect(this)
+                }
+                return
+            }
+            if (controller != null) {
+                transitionImpl.animateToEnd(controller!!)
+                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                    Log.v(FragmentManager.TAG,
+                        "Ending execution of operations from $firstOut to $lastIn")
+                }
+            } else {
+                val (enteringViews, mergedTransition) =
+                    createMergedTransition(container, lastIn, firstOut)
+                // Now set up our completion signal on the completely merged transition set
+                transitionInfos.map { it.operation }.forEach { operation ->
+                    transitionImpl.setListenerForTransitionEnd(
+                        operation.fragment,
+                        mergedTransition,
+                        transitionSignal,
+                        Runnable {
+                            if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                                Log.v(FragmentManager.TAG,
+                                    "Transition for operation $operation has completed")
+                            }
+                            operation.completeEffect(this)
+                        })
+                }
+                runTransition(enteringViews, container) {
+                    transitionImpl.beginDelayedTransition(container, mergedTransition)
+                }
+                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                    Log.v(FragmentManager.TAG,
+                        "Completed executing operations from $firstOut to $lastIn")
+                }
+            }
+        }
+
+        private fun createMergedTransition(
+            container: ViewGroup,
+            lastIn: Operation?,
+            firstOut: Operation?
+        ): Pair<ArrayList<View>, Any> {
             // Every transition needs to target at least one View so that they
             // don't interfere with one another. This is the view we use
             // in cases where there are no real views to target
@@ -772,26 +902,13 @@
             // Now iterate through the set of transitions and merge them together
             for (transitionInfo: TransitionInfo in transitionInfos) {
                 val operation: Operation = transitionInfo.operation
-                if (transitionInfo.isVisibilityUnchanged) {
-                    // No change in visibility, so we can immediately complete the transition
-                    transitionInfo.operation.completeEffect(this)
-                    continue
-                }
                 val transition = transitionImpl.cloneTransition(transitionInfo.transition)
-                val involvedInSharedElementTransition = (sharedElementTransition != null &&
-                    (operation === firstOut || operation === lastIn))
-                if (transition == null) {
-                    // Nothing more to do if the transition is null
-                    if (!involvedInSharedElementTransition) {
-                        // Only complete the transition if this fragment isn't involved
-                        // in the shared element transition (as otherwise we need to wait
-                        // for that to finish)
-                        transitionInfo.operation.completeEffect(this)
-                    }
-                } else {
+                if (transition != null) {
                     // Target the Transition to *only* the set of transitioning views
                     val transitioningViews = ArrayList<View>()
                     captureTransitioningViews(transitioningViews, operation.fragment.mView)
+                    val involvedInSharedElementTransition = (sharedElementTransition != null &&
+                        (operation === firstOut || operation === lastIn))
                     if (involvedInSharedElementTransition) {
                         // Remove all of the shared element views from the transition
                         if (operation === firstOut) {
@@ -804,8 +921,10 @@
                         transitionImpl.addTarget(transition, nonExistentView)
                     } else {
                         transitionImpl.addTargets(transition, transitioningViews)
-                        transitionImpl.scheduleRemoveTargets(transition, transition,
-                            transitioningViews, null, null, null, null)
+                        transitionImpl.scheduleRemoveTargets(
+                            transition, transition,
+                            transitioningViews, null, null, null, null
+                        )
                         if (operation.finalState === Operation.State.GONE) {
                             // We're hiding the Fragment. This requires a bit of extra work
                             // First, we need to avoid immediately applying the container change as
@@ -815,8 +934,10 @@
                             // essentially doing what applyState() would do for us
                             val transitioningViewsToHide = ArrayList(transitioningViews)
                             transitioningViewsToHide.remove(operation.fragment.mView)
-                            transitionImpl.scheduleHideFragmentView(transition,
-                                operation.fragment.mView, transitioningViewsToHide)
+                            transitionImpl.scheduleHideFragmentView(
+                                transition,
+                                operation.fragment.mView, transitioningViewsToHide
+                            )
                             // This OneShotPreDrawListener gets fired before the delayed start of
                             // the Transition and changes the visibility of any exiting child views
                             // that *ARE NOT* shared element transitions. The TransitionManager then
@@ -840,11 +961,13 @@
                     if (transitionInfo.isOverlapAllowed) {
                         // Overlap is allowed, so add them to the mergeTransition set
                         mergedTransition = transitionImpl.mergeTransitionsTogether(
-                            mergedTransition, transition, null)
+                            mergedTransition, transition, null
+                        )
                     } else {
                         // Overlap is not allowed, add them to the mergedNonOverlappingTransition
                         mergedNonOverlappingTransition = transitionImpl.mergeTransitionsTogether(
-                            mergedNonOverlappingTransition, transition, null)
+                            mergedNonOverlappingTransition, transition, null
+                        )
                     }
                 }
             }
@@ -854,51 +977,14 @@
             mergedTransition = transitionImpl.mergeTransitionsInSequence(mergedTransition,
                 mergedNonOverlappingTransition, sharedElementTransition)
 
-            // If there's no transitions playing together, no non-overlapping transitions,
-            // and no shared element transitions, mergedTransition will be null and
-            // there's nothing else we need to do
-            if (mergedTransition == null) {
-                return
-            }
-            // Now set up our completion signal on the completely merged transition set
-            transitionInfos.filterNot { transitionInfo ->
-                // If there's change in visibility, we've already completed the transition
-                transitionInfo.isVisibilityUnchanged
-            }.forEach { transitionInfo: TransitionInfo ->
-                val transition: Any? = transitionInfo.transition
-                val operation: Operation = transitionInfo.operation
-                val involvedInSharedElementTransition = sharedElementTransition != null &&
-                    (operation === firstOut || operation === lastIn)
-                if (transition != null || involvedInSharedElementTransition) {
-                    // If the container has never been laid out, transitions will not start so
-                    // so lets instantly complete them.
-                    if (!ViewCompat.isLaidOut(container)) {
-                        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
-                            Log.v(FragmentManager.TAG,
-                                "SpecialEffectsController: Container $container has not been " +
-                                    "laid out. Completing operation $operation")
-                        }
-                        transitionInfo.operation.completeEffect(this)
-                    } else {
-                        transitionImpl.setListenerForTransitionEnd(
-                            transitionInfo.operation.fragment,
-                            mergedTransition,
-                            transitionSignal,
-                            Runnable {
-                                transitionInfo.operation.completeEffect(this)
-                                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
-                                    Log.v(FragmentManager.TAG,
-                                        "Transition for operation $operation has completed")
-                                }
-                            })
-                    }
-                }
-            }
-            // Transitions won't run if the container isn't laid out so
-            // we can return early here to avoid doing unnecessary work.
-            if (!ViewCompat.isLaidOut(container)) {
-                return
-            }
+            return Pair(enteringViews, mergedTransition)
+        }
+
+        private fun runTransition(
+            enteringViews: ArrayList<View>,
+            container: ViewGroup,
+            executeTransition: (() -> Unit)
+        ) {
             // First, hide all of the entering views so they're in
             // the correct initial state
             setViewVisibility(enteringViews, View.INVISIBLE)
@@ -917,7 +1003,7 @@
                 }
             }
             // Now actually start the transition
-            transitionImpl.beginDelayedTransition(container, mergedTransition)
+            executeTransition.invoke()
             transitionImpl.setNameOverridesReordered(container, sharedElementFirstOutViews,
                 sharedElementLastInViews, inNames, sharedElementNameMapping)
             // Then, show all of the entering views, putting them into
@@ -925,11 +1011,6 @@
             setViewVisibility(enteringViews, View.VISIBLE)
             transitionImpl.swapSharedElementTargets(sharedElementTransition,
                 sharedElementFirstOutViews, sharedElementLastInViews)
-
-            if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
-                Log.v(FragmentManager.TAG,
-                    "Completed executing operations from $firstOut to $lastIn")
-            }
         }
 
         override fun onCancel(container: ViewGroup) {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index 0718eba..3a1220f 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -2922,7 +2922,9 @@
                 mHost.getHandler().post(new Runnable() {
                     @Override
                     public void run() {
-                        controller.executePendingOperations();
+                        if (controller.isPendingExecute()) {
+                            controller.executePendingOperations();
+                        }
                     }
                 });
             } else {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java
index 0f0145d..8c7d538 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java
@@ -21,6 +21,7 @@
 import android.transition.Transition;
 import android.transition.TransitionManager;
 import android.transition.TransitionSet;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -221,6 +222,27 @@
     }
 
     @Override
+    public boolean isSeekingSupported() {
+        if (FragmentManager.isLoggingEnabled(Log.INFO)) {
+            Log.i(FragmentManager.TAG,
+                    "Predictive back not available using Framework Transitions. Please switch"
+                            + " to AndroidX Transition 1.5.0 or higher to enable seeking.");
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isSeekingSupported(@NonNull Object transition) {
+        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+            Log.v(FragmentManager.TAG,
+                    "Predictive back not available for framework transition "
+                            + transition + ". Please switch to AndroidX Transition 1.5.0 or higher "
+                            + "to enable seeking.");
+        }
+        return false;
+    }
+
+    @Override
     public void scheduleRemoveTargets(@NonNull final Object overallTransitionObj,
             @Nullable final Object enterTransition, @Nullable final ArrayList<View> enteringViews,
             @Nullable final Object exitTransition, @Nullable final ArrayList<View> exitingViews,
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java
index e9a7514..6a8d444 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java
@@ -21,6 +21,7 @@
 import android.annotation.SuppressLint;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewParent;
@@ -150,6 +151,49 @@
             @Nullable Object transition);
 
     /**
+     * Returns {@code true} if the Transition is seekable.
+     */
+    public boolean isSeekingSupported() {
+        if (FragmentManager.isLoggingEnabled(Log.INFO)) {
+            Log.i(FragmentManager.TAG,
+                    "Older versions of AndroidX Transition do not support seeking. Add dependency "
+                            + "on AndroidX Transition 1.5.0 or higher to enable seeking.");
+        }
+        return false;
+    }
+
+    /**
+     * Returns {@code true} if the Transition is seekable.
+     */
+    public boolean isSeekingSupported(@NonNull Object transition) {
+        return false;
+    }
+
+    /**
+     * Allows for controlling a seekable transition
+     */
+    @Nullable
+    public Object controlDelayedTransition(@NonNull ViewGroup sceneRoot,
+            @NonNull Object transition) {
+        return null;
+    }
+
+    /**
+     * Uses given progress to set the current play time of the transition.
+     */
+    public void setCurrentPlayTime(@NonNull Object transitionController, float progress) { }
+
+    /**
+     * Animate the transition to end.
+     */
+    public void animateToEnd(@NonNull Object transitionController) { }
+
+    /**
+     * Animate the transition to start.
+     */
+    public void animateToStart(@NonNull Object transitionController) { }
+
+    /**
      * Prepares for setting the shared element names by gathering the names of the incoming
      * shared elements and clearing them. {@link #setNameOverridesReordered(View, ArrayList,
      * ArrayList, ArrayList, Map)} must be called after this to complete setting the shared element
@@ -230,6 +274,32 @@
     public void setListenerForTransitionEnd(@NonNull final Fragment outFragment,
             @NonNull Object transition, @NonNull CancellationSignal signal,
             @NonNull Runnable transitionCompleteRunnable) {
+        setListenerForTransitionEnd(
+                outFragment, transition, signal, null, transitionCompleteRunnable
+        );
+    }
+
+    /**
+     * Set a listener for Transition end events. The default behavior immediately completes the
+     * transition.
+     *
+     * Use this when the given transition is seeking. The cancelRunnable should handle
+     * cleaning up the transition when seeking is cancelled.
+     *
+     * If the transition is not seeking, you should use
+     * {@link #setListenerForTransitionEnd(Fragment, Object, CancellationSignal, Runnable)}.
+     *
+     * @param outFragment The first fragment that is exiting
+     * @param transition all transitions to be executed on a single container
+     * @param signal used indicate the desired behavior on transition cancellation
+     * @param cancelRunnable runnable to handle the logic when the signal is cancelled
+     * @param transitionCompleteRunnable used to notify the FragmentManager when a transition is
+     *                                   complete
+     */
+    public void setListenerForTransitionEnd(@NonNull final Fragment outFragment,
+            @NonNull Object transition, @NonNull CancellationSignal signal,
+            @Nullable Runnable cancelRunnable,
+            @NonNull Runnable transitionCompleteRunnable) {
         transitionCompleteRunnable.run();
     }
 
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/SpecialEffectsController.kt b/fragment/fragment/src/main/java/androidx/fragment/app/SpecialEffectsController.kt
index 8a67e9c..0e3ebf0 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/SpecialEffectsController.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/SpecialEffectsController.kt
@@ -149,7 +149,7 @@
             // Ensure that we still run the applyState() call for pending operations
             operation.addCompletionListener {
                 if (pendingOperations.contains(operation)) {
-                    operation.finalState.applyState(operation.fragment.mView)
+                    operation.finalState.applyState(operation.fragment.mView, container)
                 }
             }
             // Ensure that we remove the Operation from the list of
@@ -180,6 +180,10 @@
         }
     }
 
+    fun isPendingExecute(): Boolean {
+        return pendingOperations.isNotEmpty()
+    }
+
     fun forcePostponedExecutePendingOperations() {
         if (isContainerPostponed) {
             if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
@@ -206,7 +210,29 @@
             return
         }
         synchronized(pendingOperations) {
-            if (pendingOperations.isNotEmpty()) {
+            if (pendingOperations.isEmpty()) {
+                val currentlyRunningOperations = runningOperations.toMutableList()
+                runningOperations.clear()
+                for (operation in currentlyRunningOperations) {
+                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                        Log.v(
+                            FragmentManager.TAG,
+                            "SpecialEffectsController: Cancelling operation $operation " +
+                                "with no incoming pendingOperations"
+                        )
+                    }
+                    // Cancel the currently running operation immediately as this is the case
+                    // where we got an handleOnBackCanceled callback and we don't want to run
+                    // any effects back to start cause they will have already been seeked to
+                    // start
+                    operation.cancel(container, false)
+                    if (!operation.isComplete) {
+                        // Re-add any animations that didn't synchronously call complete()
+                        // to continue to track them as running operations
+                        runningOperations.add(operation)
+                    }
+                }
+            } else {
                 val currentlyRunningOperations = runningOperations.toMutableList()
                 runningOperations.clear()
                 for (operation in currentlyRunningOperations) {
@@ -216,7 +242,9 @@
                             "SpecialEffectsController: Cancelling operation $operation"
                         )
                     }
-                    // Cancel with seeking if the fragment is transitioning
+                    // Cancel with seeking if the fragment is transitioning as this is the case
+                    // where another operation is about to run while we are still seeking
+                    // so we should move our current effect back to the start.
                     operation.cancel(container, operation.fragment.mTransitioning)
                     if (!operation.isComplete) {
                         // Re-add any animations that didn't synchronously call complete()
@@ -275,7 +303,7 @@
 
     internal fun applyContainerChangesToOperation(operation: Operation) {
         if (operation.isAwaitingContainerChanges) {
-            operation.finalState.applyState(operation.fragment.requireView())
+            operation.finalState.applyState(operation.fragment.requireView(), container)
             operation.isAwaitingContainerChanges = false
         }
     }
@@ -473,8 +501,9 @@
              * Applies this state to the given View.
              *
              * @param view The View to apply this state to.
+             * @param container The ViewGroup to add the view too if it does not have a parent.
              */
-            fun applyState(view: View) {
+            fun applyState(view: View, container: ViewGroup) {
                 when (this) {
                     REMOVED -> {
                         val parent = view.parent as? ViewGroup
@@ -496,6 +525,16 @@
                                     "Setting view $view to VISIBLE"
                             )
                         }
+                        val parent = view.parent as? ViewGroup
+                        if (parent == null) {
+                            if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                                Log.v(
+                                    FragmentManager.TAG, "SpecialEffectsController: " +
+                                        "Adding view $view to Container $container"
+                                )
+                            }
+                            container.addView(view)
+                        }
                         view.visibility = View.VISIBLE
                     }
 
@@ -633,6 +672,7 @@
                     // moves it back to ADDING
                     this.finalState = State.VISIBLE
                     this.lifecycleImpact = LifecycleImpact.ADDING
+                    this.isAwaitingContainerChanges = true
                 }
                 LifecycleImpact.REMOVING -> {
                     if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
@@ -646,6 +686,7 @@
                     // Any REMOVING operation overrides whatever we had before
                     this.finalState = State.REMOVED
                     this.lifecycleImpact = LifecycleImpact.REMOVING
+                    this.isAwaitingContainerChanges = true
                 }
                 LifecycleImpact.NONE -> // This is a hide or show operation
                     if (this.finalState != State.REMOVED) {
diff --git a/glance/OWNERS b/glance/OWNERS
index 8589cda..233edb7 100644
--- a/glance/OWNERS
+++ b/glance/OWNERS
@@ -1,9 +1,6 @@
 # Bug component: 1096766
-andreykulikov@google.com
-jgarside@google.com
-pbdr@google.com
-zoepage@google.com
+bbade@google.com
+justinkoh@google.com
+shamalip@google.com
 wvk@google.com
-truongvi@google.com
 zakcohen@google.com
-msab@google.com
diff --git a/glance/glance-appwidget-preview/lint-baseline.xml b/glance/glance-appwidget-preview/lint-baseline.xml
new file mode 100644
index 0000000..88ba6f5
--- /dev/null
+++ b/glance/glance-appwidget-preview/lint-baseline.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="cli" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="                .all { it }"
+        errorLine2="                 ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/preview/ComposableInvoker.kt"/>
+    </issue>
+
+</issues>
diff --git a/glance/glance-appwidget-preview/src/main/java/androidx/glance/appwidget/preview/ComposableInvoker.kt b/glance/glance-appwidget-preview/src/main/java/androidx/glance/appwidget/preview/ComposableInvoker.kt
index 2e1cba7..799ee8e 100644
--- a/glance/glance-appwidget-preview/src/main/java/androidx/glance/appwidget/preview/ComposableInvoker.kt
+++ b/glance/glance-appwidget-preview/src/main/java/androidx/glance/appwidget/preview/ComposableInvoker.kt
@@ -33,7 +33,6 @@
      * Returns true if the [methodTypes] and [actualTypes] are compatible. This means that every
      * `actualTypes[n]` are assignable to `methodTypes[n]`.
      */
-    @Suppress("ListIterator")
     private fun compatibleTypes(
         methodTypes: Array<Class<*>>,
         actualTypes: Array<Class<*>>
diff --git a/glance/glance-appwidget-testing/api/current.txt b/glance/glance-appwidget-testing/api/current.txt
index 5886053..7dfb9e9 100644
--- a/glance/glance-appwidget-testing/api/current.txt
+++ b/glance/glance-appwidget-testing/api/current.txt
@@ -42,6 +42,9 @@
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasStartServiceAction(android.content.Intent intent, optional boolean isForegroundService);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasStartServiceAction(Class<? extends android.app.Service> serviceClass, optional boolean isForegroundService);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> isChecked();
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> isIndeterminateCircularProgressIndicator();
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> isIndeterminateLinearProgressIndicator();
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> isLinearProgressIndicator(float progress);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> isNotChecked();
   }
 
diff --git a/glance/glance-appwidget-testing/api/restricted_current.txt b/glance/glance-appwidget-testing/api/restricted_current.txt
index 5886053..7dfb9e9 100644
--- a/glance/glance-appwidget-testing/api/restricted_current.txt
+++ b/glance/glance-appwidget-testing/api/restricted_current.txt
@@ -42,6 +42,9 @@
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasStartServiceAction(android.content.Intent intent, optional boolean isForegroundService);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasStartServiceAction(Class<? extends android.app.Service> serviceClass, optional boolean isForegroundService);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> isChecked();
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> isIndeterminateCircularProgressIndicator();
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> isIndeterminateLinearProgressIndicator();
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> isLinearProgressIndicator(float progress);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> isNotChecked();
   }
 
diff --git a/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt
index 674c8c5..6826664 100644
--- a/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt
+++ b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt
@@ -34,8 +34,10 @@
 import androidx.glance.appwidget.RemoteViewsRoot
 import androidx.glance.session.globalSnapshotMonitor
 import androidx.glance.testing.GlanceNodeAssertion
+import androidx.glance.testing.GlanceNodeAssertionCollection
 import androidx.glance.testing.GlanceNodeMatcher
 import androidx.glance.testing.TestContext
+import androidx.glance.testing.matcherToSelector
 import androidx.glance.testing.unit.GlanceMappedNode
 import androidx.glance.testing.unit.MappedNode
 import kotlin.time.Duration
@@ -154,10 +156,17 @@
     ): GlanceNodeAssertion<MappedNode, GlanceMappedNode> {
         // Always let all the enqueued tasks finish before inspecting the tree.
         testScope.testScheduler.runCurrent()
-        // Calling onNode resets the previously matched nodes and starts a new matching chain.
-        testContext.reset()
         // Delegates matching to the next assertion.
-        return GlanceNodeAssertion(matcher, testContext)
+        return GlanceNodeAssertion(testContext, matcher.matcherToSelector())
+    }
+
+    override fun onAllNodes(
+        matcher: GlanceNodeMatcher<MappedNode>
+    ): GlanceNodeAssertionCollection<MappedNode, GlanceMappedNode> {
+        // Always let all the enqueued tasks finish before inspecting the tree.
+        testScope.testScheduler.runCurrent()
+        // Delegates matching to the next assertion.
+        return GlanceNodeAssertionCollection(testContext, matcher.matcherToSelector())
     }
 
     override fun setAppWidgetSize(size: DpSize) {
diff --git a/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/UnitTestAssertionExtensions.kt b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/UnitTestAssertionExtensions.kt
index 7bf1b3b..1cddd3a 100644
--- a/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/UnitTestAssertionExtensions.kt
+++ b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/UnitTestAssertionExtensions.kt
@@ -28,7 +28,8 @@
 import androidx.glance.testing.unit.MappedNode
 
 // This file contains (appWidget-specific) convenience assertion shorthands for unit tests that
-// delegate calls to "assert(matcher)". For assertions common to surfaces, see AssertionExtension
+// delegate calls to "assert(matcher)". For assertions common to surfaces, see equivalent file in
+// base layer testing library.
 
 internal typealias UnitTestAssertion = GlanceNodeAssertion<MappedNode, GlanceMappedNode>
 
@@ -115,7 +116,7 @@
  * Asserts that a given node has a clickable set with action that sends a broadcast.
  *
  * @param receiverClass class of the broadcast receiver that is expected to have been passed in the
- *                      actionSendBroadcast` method call.
+ *                      `actionSendBroadcast` method call.
  * @throws AssertionError if the matcher does not match or the node can no longer be found.
  */
 fun UnitTestAssertion.assertHasSendBroadcastClickAction(
@@ -128,7 +129,7 @@
  * @param intentAction the intent action of the broadcast receiver that is expected to  have been
  *                     passed in the `actionSendBroadcast` method call.
  * @param componentName optional [ComponentName] of the target broadcast receiver that is expected
- *                      to have been passed in the actionSendBroadcast` method call.
+ *                      to have been passed in the `actionSendBroadcast` method call.
  * @throws AssertionError if the matcher does not match or the node can no longer be found.
  */
 fun UnitTestAssertion.assertHasSendBroadcastClickAction(
@@ -140,7 +141,7 @@
  * Asserts that a given node has a clickable set with action that sends a broadcast.
  *
  * @param componentName [ComponentName] of the target broadcast receiver that is expected to have
- *                      been passed in the actionSendBroadcast` method call.
+ *                      been passed in the `actionSendBroadcast` method call.
  * @throws AssertionError if the matcher does not match or the node can no longer be found.
  */
 fun UnitTestAssertion.assertHasSendBroadcastClickAction(
diff --git a/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/UnitTestFilters.kt b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/UnitTestFilters.kt
index 0f3f231..95f5846 100644
--- a/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/UnitTestFilters.kt
+++ b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/UnitTestFilters.kt
@@ -25,6 +25,8 @@
 import androidx.glance.action.ActionModifier
 import androidx.glance.action.ActionParameters
 import androidx.glance.action.actionParametersOf
+import androidx.glance.appwidget.EmittableCircularProgressIndicator
+import androidx.glance.appwidget.EmittableLinearProgressIndicator
 import androidx.glance.appwidget.action.SendBroadcastActionAction
 import androidx.glance.appwidget.action.SendBroadcastClassAction
 import androidx.glance.appwidget.action.SendBroadcastComponentAction
@@ -33,6 +35,7 @@
 import androidx.glance.appwidget.action.StartServiceClassAction
 import androidx.glance.appwidget.action.StartServiceComponentAction
 import androidx.glance.appwidget.action.StartServiceIntentAction
+import androidx.glance.testing.GlanceNodeAssertionsProvider
 import androidx.glance.testing.GlanceNodeMatcher
 import androidx.glance.testing.unit.MappedNode
 
@@ -40,7 +43,8 @@
  * Returns a matcher that matches if a node is checkable (e.g. radio button, switch, checkbox)
  * and is checked.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  */
 fun isChecked(): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
@@ -54,7 +58,8 @@
  * Returns a matcher that matches if a node is checkable (e.g. radio button, switch, checkbox)
  * but is not checked.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  */
 fun isNotChecked(): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
@@ -67,7 +72,8 @@
 /**
  * Returns a matcher that matches if a node has a clickable set with action that starts an activity.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  *
  * @param intent the intent for launching an activity that is expected to have been passed in the
@@ -110,7 +116,8 @@
 /**
  * Returns a matcher that matches if a node has a clickable set with action that starts a service.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  *
  * @param serviceClass class of the service to launch that is expected to have been passed in the
@@ -143,7 +150,8 @@
 /**
  * Returns a matcher that matches if a node has a clickable set with action that starts a service.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  *
  * @param componentName component of the service to launch that is expected to have been passed in
@@ -176,7 +184,8 @@
 /**
  * Returns a matcher that matches if a node has a clickable set with action that starts a service.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  *
  * @param intent the intent for launching the service that is expected to have been passed in
@@ -209,7 +218,8 @@
 /**
  * Returns a matcher that matches if a node has a clickable set with action that sends a broadcast.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  *
  * @param receiverClass class of the broadcast receiver that is expected to have been passed in the
@@ -234,7 +244,8 @@
 /**
  * Returns a matcher that matches if a node has a clickable set with action that sends a broadcast.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  *
  * @param intentAction the intent action of the broadcast receiver that is expected to  have been
@@ -271,7 +282,8 @@
 /**
  * Returns a matcher that matches if a node has a clickable set with action that sends a broadcast.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  *
  * @param componentName [ComponentName] of the target broadcast receiver that is expected to have
@@ -296,7 +308,8 @@
 /**
  * Returns a matcher that matches if a node has a clickable set with action that sends a broadcast.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  *
  * @param intent the intent for sending broadcast  that is expected to  have been passed in the
@@ -317,3 +330,52 @@
         false
     }
 }
+
+/**
+ * Returns a matcher that matches if a given node is a linear progress indicator with given progress
+ * value.
+ *
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
+ * matching node(s) or in assertions to validate that node(s) satisfy the condition.
+ *
+ * @param progress the expected value of the current progress
+ */
+fun isLinearProgressIndicator(
+    /*@FloatRange(from = 0.0, to = 1.0)*/
+    progress: Float
+): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
+    description = "is a linear progress indicator with progress value: $progress"
+) { node ->
+    val emittable = node.value.emittable
+    emittable is EmittableLinearProgressIndicator &&
+        !emittable.indeterminate &&
+        emittable.progress == progress
+}
+
+/**
+ * Returns a matcher that matches if a given node is an indeterminate progress bar.
+ *
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
+ * matching node(s) or in assertions to validate that node(s) satisfy the condition.
+ */
+fun isIndeterminateLinearProgressIndicator(): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
+    description = "is an indeterminate linear progress indicator"
+) { node ->
+    val emittable = node.value.emittable
+    emittable is EmittableLinearProgressIndicator && emittable.indeterminate
+}
+
+/**
+ * Returns a matcher that matches if a given node is an indeterminate circular progress indicator.
+ *
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
+ * matching node(s) or in assertions to validate that node(s) satisfy the condition.
+ */
+fun isIndeterminateCircularProgressIndicator(): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
+    description = "is an indeterminate circular progress indicator"
+) { node ->
+    node.value.emittable is EmittableCircularProgressIndicator
+}
diff --git a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt
index a1449d8..3c4e2d9 100644
--- a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt
+++ b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt
@@ -32,9 +32,13 @@
 import androidx.glance.ImageProvider
 import androidx.glance.LocalSize
 import androidx.glance.appwidget.ImageProvider
+import androidx.glance.appwidget.lazy.GridCells
+import androidx.glance.appwidget.lazy.LazyColumn
+import androidx.glance.appwidget.lazy.LazyVerticalGrid
 import androidx.glance.appwidget.testing.test.R
 import androidx.glance.currentState
 import androidx.glance.layout.Column
+import androidx.glance.layout.Row
 import androidx.glance.layout.Spacer
 import androidx.glance.semantics.semantics
 import androidx.glance.semantics.testTag
@@ -164,6 +168,58 @@
 
         onNode(hasTestTag("mutable-test")).assert(hasText("initial"))
     }
+
+    @Test
+    fun runTest_onMultipleNodesMatchedAcrossHierarchy() = runGlanceAppWidgetUnitTest {
+        provideComposable {
+            Column {
+                Row {
+                    Text("text-row")
+                }
+                Spacer()
+                Text("text-in-column")
+            }
+        }
+
+        onAllNodes(hasText(text = "text-")).assertCountEquals(2)
+    }
+
+    @Test
+    fun runTest_lazyColumnChildren() = runGlanceAppWidgetUnitTest {
+        provideComposable {
+            LazyColumn(modifier = GlanceModifier.semantics { testTag = "test-list" }) {
+                item { Text("text-1") }
+                item { Text("text-2") }
+            }
+        }
+
+        onNode(hasTestTag("test-list"))
+            .onChildren()
+            .assertCountEquals(2)
+            .filter(hasText("text-1"))
+            .assertCountEquals(1)
+    }
+
+    @Test
+    fun runTest_lazyGridChildren() = runGlanceAppWidgetUnitTest {
+        provideComposable {
+            LazyVerticalGrid(
+                modifier = GlanceModifier.semantics { testTag = "test-list" },
+                gridCells = GridCells.Fixed(2)
+            ) {
+                item { Text("text-1") }
+                item { Text("text-2") }
+                item { Text("text-3") }
+                item { Text("text-4") }
+            }
+        }
+
+        onNode(hasTestTag("test-list"))
+            .onChildren()
+            .assertCountEquals(4)
+            .filter(hasText("text-1"))
+            .assertCountEquals(1)
+    }
 }
 
 private val toggleKey = booleanPreferencesKey("title_toggled_key")
diff --git a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/UnitTestFiltersTest.kt b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/UnitTestFiltersTest.kt
new file mode 100644
index 0000000..b444af1
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/UnitTestFiltersTest.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.glance.appwidget.testing.unit
+
+import androidx.glance.appwidget.EmittableCircularProgressIndicator
+import androidx.glance.appwidget.EmittableLinearProgressIndicator
+import androidx.glance.layout.EmittableColumn
+import androidx.glance.testing.unit.getGlanceNodeAssertionFor
+import com.google.common.truth.ExpectFailure.assertThat
+import org.junit.Assert
+import org.junit.Test
+
+class UnitTestFiltersTest {
+    @Test
+    fun isCircularProgressIndicator_match() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children += EmittableCircularProgressIndicator()
+            },
+            onNodeMatcher = isIndeterminateCircularProgressIndicator()
+        )
+
+        nodeAssertion.assertExists()
+        // no error
+    }
+
+    @Test
+    fun isCircularProgressIndicator_noMatch_assertionError() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children += EmittableLinearProgressIndicator()
+            },
+            onNodeMatcher = isIndeterminateCircularProgressIndicator()
+        )
+
+        val assertionError = Assert.assertThrows(AssertionError::class.java) {
+            nodeAssertion.assertExists()
+        }
+
+        assertThat(assertionError)
+            .hasMessageThat()
+            .contains(
+                "Failed assertExists" +
+                    "\nReason: Expected '1' node(s) matching condition: " +
+                    "is an indeterminate circular progress indicator, but found '0'"
+            )
+    }
+
+    @Test
+    fun isIndeterminateLinearProgressIndicator_match() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children += EmittableLinearProgressIndicator().apply {
+                    indeterminate = true
+                }
+            },
+            onNodeMatcher = isIndeterminateLinearProgressIndicator()
+        )
+
+        nodeAssertion.assertExists()
+        // no error
+    }
+
+    @Test
+    fun isIndeterminateLinearProgressIndicator_noMatch_assertionError() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children += EmittableCircularProgressIndicator()
+            },
+            onNodeMatcher = isIndeterminateLinearProgressIndicator()
+        )
+
+        val assertionError = Assert.assertThrows(AssertionError::class.java) {
+            nodeAssertion.assertExists()
+        }
+
+        assertThat(assertionError)
+            .hasMessageThat()
+            .contains(
+                "Failed assertExists" +
+                    "\nReason: Expected '1' node(s) matching condition: " +
+                    "is an indeterminate linear progress indicator, but found '0'"
+            )
+    }
+
+    @Test
+    fun isLinearProgressIndicator_match() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children += EmittableLinearProgressIndicator().apply {
+                    progress = 10.0f
+                }
+            },
+            onNodeMatcher = isLinearProgressIndicator(10.0f)
+        )
+
+        nodeAssertion.assertExists()
+        // no error
+    }
+
+    @Test
+    fun isLinearProgressIndicator_progressValueNotMatch_assertionError() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children += EmittableLinearProgressIndicator().apply {
+                    indeterminate = false
+                    progress = 10.0f
+                }
+            },
+            onNodeMatcher = isLinearProgressIndicator(11.0f)
+        )
+
+        val assertionError = Assert.assertThrows(AssertionError::class.java) {
+            nodeAssertion.assertExists()
+        }
+
+        assertThat(assertionError)
+            .hasMessageThat()
+            .contains(
+                "Failed assertExists" +
+                    "\nReason: Expected '1' node(s) matching condition: " +
+                    "is a linear progress indicator with progress value: 11.0, but found '0'"
+            )
+    }
+}
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index e22d9a9..3444a5e 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -208,6 +208,21 @@
 
 }
 
+package androidx.glance.appwidget.component {
+
+  public final class ButtonsKt {
+    method @androidx.compose.runtime.Composable public static void CircleIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider? backgroundColor, optional androidx.glance.unit.ColorProvider contentColor);
+    method @androidx.compose.runtime.Composable public static void CircleIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider? backgroundColor, optional androidx.glance.unit.ColorProvider contentColor, optional String? key);
+    method @androidx.compose.runtime.Composable public static void FilledButton(String text, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional androidx.glance.ButtonColors colors, optional int maxLines);
+    method @androidx.compose.runtime.Composable public static void FilledButton(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional androidx.glance.ButtonColors colors, optional int maxLines, optional String? key);
+    method @androidx.compose.runtime.Composable public static void OutlineButton(String text, androidx.glance.unit.ColorProvider contentColor, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional int maxLines);
+    method @androidx.compose.runtime.Composable public static void OutlineButton(String text, androidx.glance.unit.ColorProvider contentColor, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional int maxLines, optional String? key);
+    method @androidx.compose.runtime.Composable public static void SquareIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider backgroundColor, optional androidx.glance.unit.ColorProvider contentColor);
+    method @androidx.compose.runtime.Composable public static void SquareIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider backgroundColor, optional androidx.glance.unit.ColorProvider contentColor, optional String? key);
+  }
+
+}
+
 package androidx.glance.appwidget.lazy {
 
   public abstract sealed class GridCells {
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index e22d9a9..3444a5e 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -208,6 +208,21 @@
 
 }
 
+package androidx.glance.appwidget.component {
+
+  public final class ButtonsKt {
+    method @androidx.compose.runtime.Composable public static void CircleIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider? backgroundColor, optional androidx.glance.unit.ColorProvider contentColor);
+    method @androidx.compose.runtime.Composable public static void CircleIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider? backgroundColor, optional androidx.glance.unit.ColorProvider contentColor, optional String? key);
+    method @androidx.compose.runtime.Composable public static void FilledButton(String text, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional androidx.glance.ButtonColors colors, optional int maxLines);
+    method @androidx.compose.runtime.Composable public static void FilledButton(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional androidx.glance.ButtonColors colors, optional int maxLines, optional String? key);
+    method @androidx.compose.runtime.Composable public static void OutlineButton(String text, androidx.glance.unit.ColorProvider contentColor, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional int maxLines);
+    method @androidx.compose.runtime.Composable public static void OutlineButton(String text, androidx.glance.unit.ColorProvider contentColor, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional int maxLines, optional String? key);
+    method @androidx.compose.runtime.Composable public static void SquareIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider backgroundColor, optional androidx.glance.unit.ColorProvider contentColor);
+    method @androidx.compose.runtime.Composable public static void SquareIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider backgroundColor, optional androidx.glance.unit.ColorProvider contentColor, optional String? key);
+  }
+
+}
+
 package androidx.glance.appwidget.lazy {
 
   public abstract sealed class GridCells {
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
index 9107a59..8eb950b 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
@@ -254,5 +254,32 @@
                 android:name="android.appwidget.provider"
                 android:resource="@xml/default_app_widget_info" />
         </receiver>
+
+        <receiver
+            android:name="androidx.glance.appwidget.demos.ButtonsWidgetBroadcastReceiver"
+            android:enabled="@bool/glance_appwidget_available"
+            android:exported="false"
+            android:label="@string/buttons_widget_name">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+                <action android:name="android.intent.action.LOCALE_CHANGED" />
+            </intent-filter>
+            <meta-data
+                android:name="android.appwidget.provider"
+                android:resource="@xml/default_app_widget_info" />
+        </receiver>
+
+        <receiver
+            android:name="androidx.glance.appwidget.demos.BackgroundTintWidgetBroadcastReceiver"
+            android:label="@string/tint_widget"
+            android:enabled="@bool/glance_appwidget_available"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+            </intent-filter>
+            <meta-data
+                android:name="android.appwidget.provider"
+                android:resource="@xml/default_app_widget_info" />
+        </receiver>
     </application>
 </manifest>
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/BackgroundTintWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/BackgroundTintWidget.kt
new file mode 100644
index 0000000..58f22cc
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/BackgroundTintWidget.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.glance.appwidget.demos
+
+import android.content.Context
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.ImageProvider
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.SizeMode
+import androidx.glance.appwidget.provideContent
+import androidx.glance.background
+import androidx.glance.layout.Box
+import androidx.glance.layout.Column
+import androidx.glance.layout.size
+import androidx.glance.unit.ColorProvider
+
+class BackgroundTintWidgetBroadcastReceiver() : GlanceAppWidgetReceiver() {
+    override val glanceAppWidget: GlanceAppWidget
+        get() = BackgroundTintWidget()
+}
+
+/**
+ * Demonstrates tinting background drawables with [ColorFilter].
+ */
+class BackgroundTintWidget : GlanceAppWidget() {
+    override val sizeMode: SizeMode
+        get() = SizeMode.Exact
+
+    override suspend fun provideGlance(context: Context, id: GlanceId) {
+        provideContent {
+            GlanceTheme {
+                Column {
+                    Box(
+                        // Tint a <shape>
+                        modifier = GlanceModifier
+                            .size(width = 100.dp, height = 50.dp)
+                            .background(
+                                ImageProvider(R.drawable.shape_btn_demo),
+                                tint = ColorFilter.tint(GlanceTheme.colors.primary)
+                            ),
+                        content = {})
+                    Box(
+                        // tint an AVD
+                        modifier = GlanceModifier
+                            .size(width = 100.dp, height = 50.dp)
+                            .background(
+                                ImageProvider(R.drawable.ic_android),
+                                tint = ColorFilter.tint(ColorProvider(Color.Cyan))
+                            ),
+                        content = {}
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ButtonsWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ButtonsWidget.kt
new file mode 100644
index 0000000..a9d63be
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ButtonsWidget.kt
@@ -0,0 +1,210 @@
+/*
+ * 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.glance.appwidget.demos
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.glance.Button
+import androidx.glance.ButtonColors
+import androidx.glance.ButtonDefaults
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.ImageProvider
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.SizeMode
+import androidx.glance.appwidget.component.CircleIconButton
+import androidx.glance.appwidget.component.FilledButton
+import androidx.glance.appwidget.component.OutlineButton
+import androidx.glance.appwidget.component.SquareIconButton
+import androidx.glance.appwidget.lazy.LazyColumn
+import androidx.glance.appwidget.lazy.LazyItemScope
+import androidx.glance.appwidget.lazy.LazyListScope
+import androidx.glance.appwidget.provideContent
+import androidx.glance.background
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.height
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+
+class ButtonsWidgetBroadcastReceiver() : GlanceAppWidgetReceiver() {
+
+    override val glanceAppWidget: GlanceAppWidget
+        get() = ButtonsWidget()
+}
+
+/**
+ * Demonstrates different button styles. Outline buttons will render as standard buttons on
+ * apis <31.
+ */
+class ButtonsWidget() : GlanceAppWidget() {
+    override val sizeMode: SizeMode
+        get() = SizeMode.Exact // one callback each time widget resized
+
+    @RequiresApi(Build.VERSION_CODES.S)
+    override suspend fun provideGlance(context: Context, id: GlanceId) {
+
+        provideContent {
+            val primary = GlanceTheme.colors.primary
+            val onPrimary = GlanceTheme.colors.onPrimary
+            val colors = ButtonDefaults.buttonColors(
+                backgroundColor = primary,
+                contentColor = onPrimary
+            )
+
+            LazyColumn(
+                modifier = GlanceModifier.fillMaxSize()
+                    .background(Color.DarkGray)
+                    .padding(16.dp)
+            ) {
+
+                paddedItem {
+                    Button(
+                        text = "Standard Button",
+                        onClick = {},
+                        modifier = GlanceModifier,
+                        colors = colors,
+                        maxLines = 1
+                    )
+                }
+
+                paddedItem {
+                    FilledButton(
+                        text = "Filled Button",
+                        colors = colors,
+                        modifier = GlanceModifier,
+                        onClick = {},
+                    )
+                }
+
+                paddedItem {
+                    FilledButton(
+                        text = "Filled Button",
+                        icon = ImageProvider(R.drawable.baseline_add_24),
+                        colors = colors,
+                        modifier = GlanceModifier,
+                        onClick = {},
+                    )
+                }
+
+                paddedItem {
+                    OutlineButton(
+                        text = "Outline Button",
+                        contentColor = primary,
+                        modifier = GlanceModifier,
+                        onClick = {},
+                    )
+                }
+
+                paddedItem {
+                    OutlineButton(
+                        text = "Outline Button",
+                        icon = ImageProvider(R.drawable.baseline_add_24),
+                        contentColor = primary,
+                        modifier = GlanceModifier,
+                        onClick = {},
+                    )
+                }
+
+                paddedItem {
+                    LongTextButtons(GlanceModifier, colors)
+                }
+
+                paddedItem {
+                    IconButtons()
+                }
+            } // end lazy column
+        }
+    }
+}
+
+private fun LazyListScope.paddedItem(content: @Composable LazyItemScope.() -> Unit) {
+    this.item {
+        Column {
+            content()
+            Space()
+        }
+    }
+}
+
+@Composable
+private fun LongTextButtons(modifier: GlanceModifier, colors: ButtonColors) {
+    Row(modifier = modifier) {
+        FilledButton(
+            text = "Three\nLines\nof text",
+            icon = ImageProvider(R.drawable.baseline_add_24),
+            colors = colors,
+            modifier = GlanceModifier,
+            onClick = {},
+        )
+
+        Space()
+
+        FilledButton(
+            text = "Two\nLines\nof text",
+            icon = ImageProvider(R.drawable.baseline_add_24),
+            colors = colors,
+            modifier = GlanceModifier,
+            onClick = {},
+            maxLines = 2
+        )
+    }
+}
+
+@Composable
+private fun IconButtons() {
+    Row(
+        modifier = GlanceModifier.height(80.dp).padding(vertical = 8.dp),
+        verticalAlignment = Alignment.Vertical.CenterVertically
+    ) {
+        SquareIconButton(
+            imageProvider = ImageProvider(R.drawable.baseline_add_24),
+            contentDescription = "Add Button",
+            onClick = { }
+        )
+        Space()
+
+        CircleIconButton(
+            imageProvider = ImageProvider(R.drawable.baseline_local_phone_24),
+            contentDescription = "Call Button",
+            backgroundColor = GlanceTheme.colors.surfaceVariant,
+            contentColor = GlanceTheme.colors.onSurfaceVariant,
+            onClick = { }
+        )
+        Space()
+
+        CircleIconButton(
+            imageProvider = ImageProvider(R.drawable.baseline_local_phone_24),
+            contentDescription = "Call Button",
+            backgroundColor = null, // empty background
+            contentColor = GlanceTheme.colors.primary,
+            onClick = { }
+        )
+    }
+}
+
+@Composable
+private fun Space() = Spacer(GlanceModifier.size(8.dp))
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt
index 6c4eaec..fe0d90e 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt
@@ -80,6 +80,7 @@
             var checkbox3Checked by remember { mutableStateOf(false) }
             var switch1Checked by remember { mutableStateOf(false) }
             var switch2Checked by remember { mutableStateOf(false) }
+            @Suppress("AutoboxingStateCreation")
             var radioChecked by remember { mutableStateOf(0) }
 
             CheckBox(
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/RippleAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/RippleAppWidget.kt
index d25e148..8fea071 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/RippleAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/RippleAppWidget.kt
@@ -71,6 +71,7 @@
 
     @Composable
     private fun RippleDemoContent() {
+        @Suppress("AutoboxingStateCreation")
         var count by remember { mutableStateOf(0) }
         var type by remember { mutableStateOf(ContentScale.Fit) }
         var columnBgColors by remember { mutableStateOf(columnBgColorsA) }
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/baseline_add_24.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/baseline_add_24.xml
new file mode 100644
index 0000000..03d6cd3
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/baseline_add_24.xml
@@ -0,0 +1,21 @@
+<!--
+  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.
+  -->
+
+<vector android:height="24dp" android:tint="#000000"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+</vector>
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/baseline_local_phone_24.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/baseline_local_phone_24.xml
new file mode 100644
index 0000000..95c5f8a
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/baseline_local_phone_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M6.62,10.79c1.44,2.83 3.76,5.14 6.59,6.59l2.2,-2.2c0.27,-0.27 0.67,-0.36 1.02,-0.24 1.12,0.37 2.33,0.57 3.57,0.57 0.55,0 1,0.45 1,1V20c0,0.55 -0.45,1 -1,1 -9.39,0 -17,-7.61 -17,-17 0,-0.55 0.45,-1 1,-1h3.5c0.55,0 1,0.45 1,1 0,1.25 0.2,2.45 0.57,3.57 0.11,0.35 0.03,0.74 -0.25,1.02l-2.2,2.2z"/>
+</vector>
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/shape_btn_demo.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/shape_btn_demo.xml
new file mode 100644
index 0000000..12bdd47
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/shape_btn_demo.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <stroke android:color="#FF00FF"/>
+    <corners android:radius="16dp"/>
+
+</shape>
\ No newline at end of file
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
index e74682e..04943bf 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
@@ -37,5 +37,6 @@
     <string name="default_state_widget_name">Default State Widget</string>
     <string name="progress_indicator_widget_name">ProgressBar Widget</string>
     <string name="default_color_widget_name">Theme and Color Widget</string>
-
+    <string name="buttons_widget_name">Buttons Demo Widget</string>
+    <string name="tint_widget">Tint widget</string>
 </resources>
diff --git a/glance/glance-appwidget/lint-baseline.xml b/glance/glance-appwidget/lint-baseline.xml
index 87160dc..79615c9 100644
--- a/glance/glance-appwidget/lint-baseline.xml
+++ b/glance/glance-appwidget/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
+<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="cli" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
 
     <issue
         id="BanThreadSleep"
@@ -29,6 +29,465 @@
     </issue>
 
     <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        it.children.addAll(children.map { it.copy() })"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/AndroidRemoteViews.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="                    lambdas[event.key]?.forEach { it.block() }"
+        errorLine2="                                        ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/AppWidgetSession.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        sizes.map { DpSize(it.width.dp, it.height.dp) }"
+        errorLine2="              ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/AppWidgetUtils.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    }.minByOrNull { it.second }?.first"
+        errorLine2="      ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/AppWidgetUtils.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            rv.setContentDescription(viewDef.mainViewId, contentDescription.joinToString())"
+        errorLine2="                                                                            ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/ApplyModifiers.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    manager.getGlanceIds(javaClass).forEach { update(context, it) }"
+        errorLine2="                                    ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    manager.getGlanceIds(javaClass).forEach { glanceId ->"
+        errorLine2="                                    ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            }.toMap()"
+        errorLine2="              ~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        return receivers.flatMap { receiver ->"
+        errorLine2="                         ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="                    val info = appWidgetManager.installedProviders.first {"
+        errorLine2="                                                                   ~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            .filter { it.provider.packageName == packageName }"
+        errorLine2="             ~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            .map { it.provider.className }"
+        errorLine2="             ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            .toSet()"
+        errorLine2="             ~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="                toRemove.forEach { receiver -> remove(providerKey(receiver)) }"
+        errorLine2="                         ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        if (children.any { it.shouldIgnoreResult() }) return true"
+        errorLine2="                     ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/IgnoreResult.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        .forEach {"
+        errorLine2="         ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/LayoutSelection.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        itemList.forEachIndexed { index, (itemId, composable) ->"
+        errorLine2="                 ~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/lazy/LazyList.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        it.children.addAll(children.map { it.copy() })"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/lazy/LazyList.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        it.children.addAll(children.map { it.copy() })"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/lazy/LazyList.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        element.children.foldIndexed(false) { position, previous, itemEmittable ->"
+        errorLine2="                         ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/translators/LazyListTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        itemList.forEachIndexed { index, (itemId, composable) ->"
+        errorLine2="                 ~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        it.children.addAll(children.map { it.copy() })"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        it.children.addAll(children.map { it.copy() })"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        element.children.foldIndexed(false) { position, previous, itemEmittable ->"
+        errorLine2="                         ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    if (container.children.isNotEmpty() &amp;&amp; container.children.all { it is EmittableSizeBox }) {"
+        errorLine2="                                                              ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        for (item in container.children) {"
+        errorLine2="                  ~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    children.forEach { child ->"
+        errorLine2="             ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        children.any { child ->"
+        errorLine2="                 ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        children.any { child ->"
+        errorLine2="                 ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    children.forEachIndexed { index, child ->"
+        errorLine2="             ~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    children.foldIndexed("
+        errorLine2="             ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    fold(GlanceModifier) { acc: GlanceModifier, mod: GlanceModifier? ->"
+        errorLine2="    ~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        val layoutIdCount = views.map { it.layoutId }.distinct().count()"
+        errorLine2="                                                      ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteCollectionItems.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="                viewTypeCount = views.map { it.layoutId }.distinct().count()"
+        errorLine2="                                      ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteCollectionItems.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="                viewTypeCount = views.map { it.layoutId }.distinct().count()"
+        errorLine2="                                                          ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteCollectionItems.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        it.children.addAll(children.map { it.copy() })"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    if (children.all { it is EmittableSizeBox }) {"
+        errorLine2="                 ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        val views = children.map { child ->"
+        errorLine2="                             ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="                    Api31Impl.createRemoteViews(views.toMap())"
+        errorLine2="                                                      ~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="                    combineLandscapeAndPortrait(views.map { it.second })"
+        errorLine2="                                                      ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    element.children.forEach {"
+        errorLine2="                     ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    check(children.count { it is EmittableRadioButton &amp;&amp; it.checked } &lt;= 1) {"
+        errorLine2="                   ~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            element.children.forEachIndexed { index, child ->"
+        errorLine2="                             ~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    children.take(10).forEachIndexed { index, child ->"
+        errorLine2="             ~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    children.take(10).forEachIndexed { index, child ->"
+        errorLine2="                      ~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        it.children.addAll(children.map { it.copy() })"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/SizeBox.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="                .map { findBestSize(it, sizeMode.sizes) ?: smallestSize }"
+        errorLine2="                 ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/SizeBox.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    sizes.distinct().map { size ->"
+        errorLine2="                     ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/SizeBox.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    spans.forEach { span ->"
+        errorLine2="          ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/translators/TextTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            val layouts = config.layoutList.associate {"
+        errorLine2="                                            ~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/WidgetLayout.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            addAllChildren(element.children.map { createNode(context, it) })"
+        errorLine2="                                            ~~~">
+        <location
+            file="src/main/java/androidx/glance/appwidget/WidgetLayout.kt"/>
+    </issue>
+
+    <issue
         id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in method extractAllSizes has parameter &apos;minSize&apos; with type Function0&lt;DpSize>."
         errorLine1="internal fun Bundle.extractAllSizes(minSize: () -> DpSize): List&lt;DpSize> {"
diff --git a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt
index f42506b..5b14889 100644
--- a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt
+++ b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt
@@ -175,7 +175,8 @@
     }
 
     private var mLatch: CountDownLatch? = null
-    private var mRemoteViews: RemoteViews? = null
+    var mRemoteViews: RemoteViews? = null
+        private set
     private var mPortraitSize: DpSize = DpSize(0.dp, 0.dp)
     private var mLandscapeSize: DpSize = DpSize(0.dp, 0.dp)
 
diff --git a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
index ffb947e..fb842fb 100644
--- a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
+++ b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
@@ -26,14 +26,20 @@
 import androidx.glance.Button
 import androidx.glance.ButtonDefaults
 import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
 import androidx.glance.Image
 import androidx.glance.ImageProvider
 import androidx.glance.LocalContext
 import androidx.glance.action.actionStartActivity
+import androidx.glance.appwidget.component.CircleIconButton
+import androidx.glance.appwidget.component.FilledButton
+import androidx.glance.appwidget.component.OutlineButton
+import androidx.glance.appwidget.component.SquareIconButton
 import androidx.glance.appwidget.lazy.LazyColumn
 import androidx.glance.appwidget.test.R
 import androidx.glance.background
 import androidx.glance.color.ColorProvider
+import androidx.glance.color.colorProviders
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.Box
 import androidx.glance.layout.Column
@@ -589,6 +595,43 @@
         mHostRule.waitForListViewChildCount(count)
         mScreenshotRule.checkScreenshot(mHostRule.mHostView, "lazyColumn_alignment_start")
     }
+
+    @Test
+    fun buttonTests_createFilledButton() {
+        TestGlanceAppWidget.uiDefinition = { ButtonComponentsScreenshotTests.FilledButtonTest() }
+        mHostRule.startHost()
+        mScreenshotRule.checkScreenshot(mHostRule.mHostView, "buttonTests_createFilledButton")
+    }
+
+    @Test
+    fun buttonTests_createOutlineButton() {
+        TestGlanceAppWidget.uiDefinition = { ButtonComponentsScreenshotTests.OutlineButtonTest() }
+        mHostRule.startHost()
+        mScreenshotRule.checkScreenshot(mHostRule.mHostView, "buttonTests_createOutlineButton")
+    }
+
+    @Test
+    fun buttonTests_createSquareButton() {
+        TestGlanceAppWidget.uiDefinition = { ButtonComponentsScreenshotTests.SquareButtonTest() }
+        mHostRule.startHost()
+        mScreenshotRule.checkScreenshot(mHostRule.mHostView, "buttonTests_createSquareButton")
+    }
+
+    @Test
+    fun buttonTests_createCircleButton() {
+        TestGlanceAppWidget.uiDefinition = { ButtonComponentsScreenshotTests.CircleButtonTest() }
+        mHostRule.startHost()
+        mScreenshotRule.checkScreenshot(mHostRule.mHostView, "buttonTests_createCircleButton")
+    }
+
+    @Test
+    fun buttonTests_buttonDefaultColors() {
+        TestGlanceAppWidget.uiDefinition = {
+            ButtonComponentsScreenshotTests.ButtonDefaultColorsTest()
+        }
+        mHostRule.startHost()
+        mScreenshotRule.checkScreenshot(mHostRule.mHostView, "buttonTests_buttonDefaultColors")
+    }
 }
 
 @Composable
@@ -872,3 +915,166 @@
         )
     }
 }
+
+// Tests the opinionated button components
+private object ButtonComponentsScreenshotTests {
+
+    private val onClick: () -> Unit = {}
+
+    // a filled square
+    private val icon = ImageProvider(R.drawable.filled_oval)
+
+    private val buttonBg = ColorProvider(Color.Black)
+    private val buttonFg = ColorProvider(Color.White)
+
+    @Composable
+    private fun colors() = ButtonDefaults.buttonColors(
+        backgroundColor = buttonBg,
+        contentColor = buttonFg
+    )
+
+    @Composable
+    private fun Space() = Spacer(GlanceModifier.size(16.dp))
+
+    /**
+     * A rectangular magenta background
+     */
+    @Composable
+    private fun Background(content: @Composable () -> Unit) {
+        Box(
+            modifier = GlanceModifier.wrapContentSize().padding(16.dp).background(Color.Magenta),
+            content = content
+        )
+    }
+
+    @Composable
+    fun FilledButtonTest() {
+        Background {
+            Column {
+                FilledButton(
+                    text = "Filled button\nbg 0x00, fg 0xff",
+                    onClick = onClick,
+                    icon = null,
+                    colors = colors()
+                )
+                Space()
+                FilledButton(
+                    text = "Filled btn + icon\nbg 0x00, fg 0xff",
+                    onClick = onClick,
+                    icon = icon,
+                    colors = colors()
+                )
+            }
+        }
+    }
+
+    @Composable
+    fun OutlineButtonTest() {
+        Background {
+            Column {
+                OutlineButton(
+                    text = "Outline Button\nfg 0xff",
+                    onClick = onClick,
+                    icon = null,
+                    contentColor = buttonFg
+                )
+                Space()
+                OutlineButton(
+                    text = "Outline btn + icon\nfg 0xff",
+                    onClick = onClick,
+                    icon = icon,
+                    contentColor = buttonFg
+                )
+            }
+        }
+    }
+
+    @Composable
+    fun SquareButtonTest() {
+        Background {
+            // square button with rounded corners, icon (a square w/sharp corners)  in center.
+            SquareIconButton(
+                imageProvider = icon,
+                contentDescription = null,
+                onClick = onClick,
+                backgroundColor = buttonBg,
+                contentColor = buttonFg
+            )
+        }
+    }
+
+    @Composable
+    fun CircleButtonTest() {
+        Background {
+            Column {
+                // Circle button with icon
+                CircleIconButton(
+                    imageProvider = icon,
+                    contentDescription = null,
+                    onClick = onClick,
+                    backgroundColor = buttonBg,
+                    contentColor = buttonFg
+                )
+                Space()
+                // Icon only, no background
+                CircleIconButton(
+                    imageProvider = icon,
+                    contentDescription = null,
+                    onClick = onClick,
+                    backgroundColor = null,
+                    contentColor = buttonFg
+                )
+            }
+        }
+    }
+
+    /**
+     * Tests that buttons inherit the expected colors from their theme.
+     */
+    @Composable
+    fun ButtonDefaultColorsTest() {
+        val unused = ColorProvider(Color.Cyan)
+
+        val colors = colorProviders(
+            primary = ColorProvider(Color.Green),
+            onPrimary = ColorProvider(Color.Black),
+            surface = ColorProvider(Color.Gray),
+            onSurface = ColorProvider(Color.Red),
+            background = ColorProvider(Color.DarkGray),
+            error = unused,
+            errorContainer = unused,
+            inverseOnSurface = unused,
+            inversePrimary = unused,
+            inverseSurface = unused,
+            onBackground = unused,
+            onError = unused,
+            onErrorContainer = unused,
+            onPrimaryContainer = unused,
+            onSecondary = unused,
+            onSecondaryContainer = unused,
+            onSurfaceVariant = unused,
+            onTertiary = unused,
+            onTertiaryContainer = unused,
+            outline = unused,
+            primaryContainer = unused,
+            secondary = unused,
+            secondaryContainer = unused,
+            surfaceVariant = unused,
+            tertiary = unused,
+            tertiaryContainer = unused,
+        )
+
+        GlanceTheme(
+            colors = colors
+        ) {
+            Column {
+                FilledButton("Filled button", icon = icon, onClick = onClick)
+                // [OutlineButton] does not have a default color, so not important to test here
+                Space()
+                SquareIconButton(imageProvider = icon, contentDescription = null, onClick = onClick)
+                Space()
+                CircleIconButton(imageProvider = icon, contentDescription = null, onClick = onClick)
+            }
+        }
+    }
+}
diff --git a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index 22f4460..8a03aa3 100644
--- a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -40,6 +40,7 @@
 import android.widget.TextView
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -64,6 +65,7 @@
 import androidx.glance.action.actionStartActivity
 import androidx.glance.action.clickable
 import androidx.glance.action.toParametersKey
+import androidx.glance.appwidget.R.layout.glance_error_layout
 import androidx.glance.appwidget.action.ActionCallback
 import androidx.glance.appwidget.action.ToggleableStateKey
 import androidx.glance.appwidget.action.actionRunCallback
@@ -1192,6 +1194,83 @@
         }
     }
 
+    @Test
+    fun initialCompositionErrorUiLayout() = runBlocking {
+        TestGlanceAppWidget.withErrorLayout(glance_error_layout) {
+            TestGlanceAppWidget.uiDefinition = {
+                 throw Throwable("error")
+            }
+
+            mHostRule.startHost()
+            mHostRule.onHostView { hostView ->
+                val layoutId = assertNotNull(
+                    (hostView as TestAppWidgetHostView).mRemoteViews?.layoutId
+                )
+                assertThat(layoutId).isEqualTo(glance_error_layout)
+            }
+        }
+    }
+
+    @Test
+    fun recompositionErrorUiLayout() = runBlocking {
+        TestGlanceAppWidget.withErrorLayout(glance_error_layout) {
+            val runError = mutableStateOf(false)
+            TestGlanceAppWidget.uiDefinition = {
+                if (runError.value)
+                    throw Throwable("error")
+                else Text("Hello World")
+            }
+
+            mHostRule.startHost()
+            mHostRule.onUnboxedHostView<TextView> {
+                assertThat(it.text.toString()).isEqualTo("Hello World")
+            }
+            mHostRule.runAndWaitForUpdate { runError.value = true }
+            mHostRule.onHostView { hostView ->
+                val layoutId = assertNotNull(
+                    (hostView as TestAppWidgetHostView).mRemoteViews?.layoutId
+                )
+                assertThat(layoutId).isEqualTo(glance_error_layout)
+            }
+        }
+    }
+
+    @Test
+    fun sideEffectErrorUiLayout() = runBlocking {
+        TestGlanceAppWidget.withErrorLayout(glance_error_layout) {
+            TestGlanceAppWidget.uiDefinition = {
+                SideEffect { throw Throwable("error") }
+            }
+
+            mHostRule.startHost()
+            mHostRule.onHostView { hostView ->
+                val layoutId = assertNotNull(
+                    (hostView as TestAppWidgetHostView).mRemoteViews?.layoutId
+                )
+                assertThat(layoutId).isEqualTo(glance_error_layout)
+            }
+        }
+    }
+
+    @Test
+    fun provideGlanceErrorUiLayout() = runBlocking {
+        // This also tests LaunchedEffect error handling, since provideGlance is run in a
+        // LaunchedEffect through collectAsState.
+        TestGlanceAppWidget.withErrorLayout(glance_error_layout) {
+            TestGlanceAppWidget.onProvideGlance = {
+                throw Throwable("error")
+            }
+
+            mHostRule.startHost()
+            mHostRule.onHostView { hostView ->
+                val layoutId = assertNotNull(
+                    (hostView as TestAppWidgetHostView).mRemoteViews?.layoutId
+                )
+                assertThat(layoutId).isEqualTo(glance_error_layout)
+            }
+        }
+    }
+
     // Check there is a single span of the given type and that it passes the [check].
     private inline
     fun <reified T> SpannedString.checkHasSingleTypedSpan(check: (T) -> Unit) {
diff --git a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
index e0865a3..ab29527 100644
--- a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
+++ b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
@@ -41,7 +41,8 @@
     }
 }
 
-object TestGlanceAppWidget : GlanceAppWidget(errorUiLayout = 0) {
+object TestGlanceAppWidget : GlanceAppWidget() {
+    public override var errorUiLayout: Int = 0
 
     override var sizeMode: SizeMode = SizeMode.Single
 
@@ -49,9 +50,12 @@
         context: Context,
         id: GlanceId
     ) {
-        onProvideGlance?.invoke(this)
-        onProvideGlance = null
-        provideContent(uiDefinition)
+        try {
+            onProvideGlance?.invoke(this)
+                ?: provideContent(uiDefinition)
+        } finally {
+          onProvideGlance = null
+        }
     }
 
     var onProvideGlance: (suspend TestGlanceAppWidget.() -> Unit)? = null
@@ -71,4 +75,14 @@
     }
 
     var uiDefinition: @Composable () -> Unit = { }
+
+    inline fun withErrorLayout(layout: Int, block: () -> Unit) {
+        val previousErrorLayout = errorUiLayout
+        errorUiLayout = layout
+        try {
+            block()
+        } finally {
+            errorUiLayout = previousErrorLayout
+        }
+    }
 }
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AndroidRemoteViews.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AndroidRemoteViews.kt
index 8450bff..f199042 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AndroidRemoteViews.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AndroidRemoteViews.kt
@@ -20,7 +20,6 @@
 import android.widget.RemoteViews
 import androidx.annotation.IdRes
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.util.fastMap
 import androidx.glance.Emittable
 import androidx.glance.EmittableWithChildren
 import androidx.glance.GlanceModifier
@@ -80,7 +79,7 @@
             it.remoteViews = remoteViews
         }
         it.containerViewId = containerViewId
-        it.children.addAll(children.fastMap { it.copy() })
+        it.children.addAll(children.map { it.copy() })
     }
 
     override fun toString(): String = "AndroidRemoteViews(" +
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt
index 314e095..859c7cac 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt
@@ -32,7 +32,6 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.util.fastForEach
 import androidx.glance.EmittableWithChildren
 import androidx.glance.GlanceComposable
 import androidx.glance.LocalContext
@@ -144,13 +143,7 @@
         } catch (ex: CancellationException) {
             // Nothing to do
         } catch (throwable: Throwable) {
-            if (widget.errorUiLayout == 0) {
-                throw throwable
-            }
-            logException(throwable)
-            val rv = RemoteViews(context.packageName, widget.errorUiLayout)
-            appWidgetManager.updateAppWidget(id.appWidgetId, rv)
-            lastRemoteViews = rv
+            sendErrorLayoutIfPresent(context, throwable)
         } finally {
             layoutConfig.save()
             Tracing.endGlanceAppWidgetUpdate()
@@ -158,6 +151,10 @@
         return true
     }
 
+    override suspend fun onCompositionError(context: Context, throwable: Throwable) {
+        sendErrorLayoutIfPresent(context, throwable)
+    }
+
     override suspend fun processEvent(context: Context, event: Any) {
         when (event) {
             is UpdateGlanceState -> {
@@ -184,7 +181,7 @@
             is RunLambda -> {
                 if (DEBUG) Log.i(TAG, "Received RunLambda(${event.key}) action for session($key)")
                 Snapshot.withMutableSnapshot {
-                    lambdas[event.key]?.fastForEach { it.block() }
+                    lambdas[event.key]?.forEach { it.block() }
                 } ?: Log.w(TAG, "Triggering Action(${event.key}) for session($key) failed")
             }
             is WaitForReady -> event.resume.send(Unit)
@@ -215,6 +212,16 @@
         }
     }
 
+    private fun sendErrorLayoutIfPresent(context: Context, throwable: Throwable) {
+        if (widget.errorUiLayout == 0) {
+            throw throwable
+        }
+        logException(throwable)
+        val rv = RemoteViews(context.packageName, widget.errorUiLayout)
+        context.appWidgetManager.updateAppWidget(id.appWidgetId, rv)
+        lastRemoteViews = rv
+    }
+
     // Event types that this session supports.
     @VisibleForTesting
     internal object UpdateGlanceState
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetUtils.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetUtils.kt
index 514a998..1b39006 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetUtils.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetUtils.kt
@@ -32,8 +32,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastMap
-import androidx.compose.ui.util.fastMinByOrNull
 import androidx.glance.GlanceComposable
 import androidx.glance.GlanceId
 import java.util.concurrent.atomic.AtomicBoolean
@@ -87,7 +85,7 @@
     return if (sizes.isNullOrEmpty()) {
         estimateSizes(minSize)
     } else {
-        sizes.fastMap { DpSize(it.width.dp, it.height.dp) }
+        sizes.map { DpSize(it.width.dp, it.height.dp) }
     }
 }
 
@@ -145,7 +143,7 @@
         } else {
             null
         }
-    }.fastMinByOrNull { it.second }?.first
+    }.minByOrNull { it.second }?.first
 
 /**
  * @return the minimum size as configured by the App Widget provider.
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/ApplyModifiers.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/ApplyModifiers.kt
index 5fb7402..0c43fc1 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/ApplyModifiers.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/ApplyModifiers.kt
@@ -28,7 +28,6 @@
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
 import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.util.fastJoinToString
 import androidx.core.widget.RemoteViewsCompat.setTextViewHeight
 import androidx.core.widget.RemoteViewsCompat.setTextViewWidth
 import androidx.core.widget.RemoteViewsCompat.setViewBackgroundColor
@@ -80,28 +79,33 @@
                 }
                 actionModifier = modifier
             }
+
             is WidthModifier -> widthModifier = modifier
             is HeightModifier -> heightModifier = modifier
             is BackgroundModifier -> applyBackgroundModifier(context, rv, modifier, viewDef)
             is PaddingModifier -> {
                 paddingModifiers = paddingModifiers?.let { it + modifier } ?: modifier
             }
+
             is VisibilityModifier -> visibility = modifier.visibility
             is CornerRadiusModifier -> cornerRadius = modifier.radius
             is AppWidgetBackgroundModifier -> {
                 // This modifier is handled somewhere else.
             }
+
             is SelectableGroupModifier -> {
                 if (!translationContext.canUseSelectableGroup) {
                     error(
                         "GlanceModifier.selectableGroup() can only be used on Row or Column " +
-                        "composables."
+                            "composables."
                     )
                 }
             }
+
             is AlignmentModifier -> {
                 // This modifier is handled somewhere else.
             }
+
             is ClipToOutlineModifier -> clipToOutline = modifier
             is EnabledModifier -> enabled = modifier
             is SemanticsModifier -> semanticsModifier = modifier
@@ -136,7 +140,7 @@
         val contentDescription: List<String>? =
             semantics.configuration.getOrNull(SemanticsProperties.ContentDescription)
         if (contentDescription != null) {
-            rv.setContentDescription(viewDef.mainViewId, contentDescription.fastJoinToString())
+            rv.setContentDescription(viewDef.mainViewId, contentDescription.joinToString())
         }
     }
     rv.setViewVisibility(viewDef.mainViewId, visibility.toViewVisibility())
@@ -227,7 +231,8 @@
     }
     // Wrap and Expand are done in XML on Android S & Sv2
     if (Build.VERSION.SDK_INT < 33 &&
-        width in listOf(Dimension.Wrap, Dimension.Expand)) return
+        width in listOf(Dimension.Wrap, Dimension.Expand)
+    ) return
     ApplyModifiersApi31Impl.setViewWidth(rv, viewId, width)
 }
 
@@ -257,7 +262,8 @@
     }
     // Wrap and Expand are done in XML on Android S & Sv2
     if (Build.VERSION.SDK_INT < 33 &&
-        height in listOf(Dimension.Wrap, Dimension.Expand)) return
+        height in listOf(Dimension.Wrap, Dimension.Expand)
+    ) return
     ApplyModifiersApi31Impl.setViewHeight(rv, viewId, height)
 }
 
@@ -268,8 +274,9 @@
     viewDef: InsertedViewInfo
 ) {
     val viewId = viewDef.mainViewId
-    val imageProvider = modifier.imageProvider
-    if (imageProvider != null) {
+
+    fun applyBackgroundImageModifier(modifier: BackgroundModifier.Image) {
+        val imageProvider = modifier.imageProvider
         if (imageProvider is AndroidResourceImageProvider) {
             rv.setViewBackgroundResource(viewId, imageProvider.resId)
         }
@@ -277,24 +284,41 @@
         // (removing modifiers is not really possible).
         return
     }
-    when (val colorProvider = modifier.colorProvider) {
-        is FixedColorProvider -> rv.setViewBackgroundColor(viewId, colorProvider.color.toArgb())
-        is ResourceColorProvider -> rv.setViewBackgroundColorResource(
-            viewId,
-            colorProvider.resId
-        )
-        is DayNightColorProvider -> {
-            if (Build.VERSION.SDK_INT >= 31) {
-                rv.setViewBackgroundColor(
-                    viewId,
-                    colorProvider.day.toArgb(),
-                    colorProvider.night.toArgb()
-                )
-            } else {
-                rv.setViewBackgroundColor(viewId, colorProvider.getColor(context).toArgb())
+
+    fun applyBackgroundColorModifier(modifier: BackgroundModifier.Color) {
+        when (val colorProvider = modifier.colorProvider) {
+            is FixedColorProvider -> rv.setViewBackgroundColor(
+                viewId,
+                colorProvider.color.toArgb()
+            )
+
+            is ResourceColorProvider -> rv.setViewBackgroundColorResource(
+                viewId,
+                colorProvider.resId
+            )
+
+            is DayNightColorProvider -> {
+                if (Build.VERSION.SDK_INT >= 31) {
+                    rv.setViewBackgroundColor(
+                        viewId,
+                        colorProvider.day.toArgb(),
+                        colorProvider.night.toArgb()
+                    )
+                } else {
+                    rv.setViewBackgroundColor(viewId, colorProvider.getColor(context).toArgb())
+                }
             }
+
+            else -> Log.w(
+                GlanceAppWidgetTag,
+                "Unexpected background color modifier: $colorProvider"
+            )
         }
-        else -> Log.w(GlanceAppWidgetTag, "Unexpected background color modifier: $colorProvider")
+    }
+
+    when (modifier) {
+        is BackgroundModifier.Image -> applyBackgroundImageModifier(modifier)
+        is BackgroundModifier.Color -> applyBackgroundColorModifier(modifier)
     }
 }
 
@@ -320,6 +344,7 @@
             is Dimension.Wrap -> {
                 rv.setViewLayoutWidth(viewId, WRAP_CONTENT.toFloat(), COMPLEX_UNIT_PX)
             }
+
             is Dimension.Expand -> rv.setViewLayoutWidth(viewId, 0f, COMPLEX_UNIT_PX)
             is Dimension.Dp -> rv.setViewLayoutWidth(viewId, width.dp.value, COMPLEX_UNIT_DIP)
             is Dimension.Resource -> rv.setViewLayoutWidthDimen(viewId, width.res)
@@ -335,6 +360,7 @@
             is Dimension.Wrap -> {
                 rv.setViewLayoutHeight(viewId, WRAP_CONTENT.toFloat(), COMPLEX_UNIT_PX)
             }
+
             is Dimension.Expand -> rv.setViewLayoutHeight(viewId, 0f, COMPLEX_UNIT_PX)
             is Dimension.Dp -> rv.setViewLayoutHeight(viewId, height.dp.value, COMPLEX_UNIT_DIP)
             is Dimension.Resource -> rv.setViewLayoutHeightDimen(viewId, height.res)
@@ -351,9 +377,11 @@
             is Dimension.Dp -> {
                 rv.setViewOutlinePreferredRadius(viewId, radius.dp.value, COMPLEX_UNIT_DIP)
             }
+
             is Dimension.Resource -> {
                 rv.setViewOutlinePreferredRadiusDimen(viewId, radius.res)
             }
+
             else -> error("Rounded corners should not be ${radius.javaClass.canonicalName}")
         }
     }
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/CircularProgressIndicator.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/CircularProgressIndicator.kt
index 9e13d34..bf622db 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/CircularProgressIndicator.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/CircularProgressIndicator.kt
@@ -16,6 +16,7 @@
 
 package androidx.glance.appwidget
 
+import androidx.annotation.RestrictTo
 import androidx.compose.runtime.Composable
 import androidx.glance.Emittable
 import androidx.glance.GlanceModifier
@@ -42,7 +43,8 @@
     )
 }
 
-internal class EmittableCircularProgressIndicator : Emittable {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class EmittableCircularProgressIndicator : Emittable {
     override var modifier: GlanceModifier = GlanceModifier
     var color: ColorProvider = ProgressIndicatorDefaults.IndicatorColorProvider
 
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
index 2b0d9fa..c5f713f 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -26,7 +26,6 @@
 import androidx.annotation.RestrictTo
 import androidx.annotation.RestrictTo.Scope
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.util.fastForEach
 import androidx.glance.GlanceComposable
 import androidx.glance.GlanceId
 import androidx.glance.appwidget.state.getAppWidgetState
@@ -50,7 +49,7 @@
  */
 abstract class GlanceAppWidget(
     @LayoutRes
-    internal val errorUiLayout: Int = R.layout.glance_error_layout,
+    internal open val errorUiLayout: Int = R.layout.glance_error_layout,
 ) {
     private val sessionManager: SessionManager = GlanceSessionManager
 
@@ -203,7 +202,7 @@
 /** Update all App Widgets managed by the [GlanceAppWidget] class. */
 suspend fun GlanceAppWidget.updateAll(@Suppress("ContextFirst") context: Context) {
     val manager = GlanceAppWidgetManager(context)
-    manager.getGlanceIds(javaClass).fastForEach { update(context, it) }
+    manager.getGlanceIds(javaClass).forEach { update(context, it) }
 }
 
 /**
@@ -216,7 +215,7 @@
     val stateDef = stateDefinition
     requireNotNull(stateDef) { "GlanceAppWidget.updateIf cannot be used if no state is defined." }
     val manager = GlanceAppWidgetManager(context)
-    manager.getGlanceIds(javaClass).fastForEach { glanceId ->
+    manager.getGlanceIds(javaClass).forEach { glanceId ->
         val state = getAppWidgetState(context, stateDef, glanceId) as State
         if (predicate(state)) update(context, glanceId)
     }
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt
index 089f713..41c7633 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt
@@ -27,11 +27,6 @@
 import androidx.annotation.RequiresApi
 import androidx.annotation.VisibleForTesting
 import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.util.fastFilter
-import androidx.compose.ui.util.fastFirst
-import androidx.compose.ui.util.fastFlatMap
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastMap
 import androidx.datastore.core.DataStore
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.stringPreferencesKey
@@ -89,7 +84,6 @@
         val packageName = context.packageName
         val receivers = prefs[providersKey] ?: return State()
         return State(
-            @Suppress("ListIterator")
             receivers.mapNotNull { receiverName ->
                 val comp = ComponentName(packageName, receiverName)
                 val providerName = prefs[providerKey(receiverName)] ?: return@mapNotNull null
@@ -108,7 +102,7 @@
         val state = getState()
         val providerName = requireNotNull(provider.canonicalName) { "no canonical provider name" }
         val receivers = state.providerNameToReceivers[providerName] ?: return emptyList()
-        return receivers.fastFlatMap { receiver ->
+        return receivers.flatMap { receiver ->
             appWidgetManager.getAppWidgetIds(receiver).map { AppWidgetId(it) }
         }
     }
@@ -202,7 +196,7 @@
             val target = ComponentName(context.packageName, receiver.name)
             val previewBundle = Bundle().apply {
                 if (preview != null) {
-                    val info = appWidgetManager.installedProviders.fastFirst {
+                    val info = appWidgetManager.installedProviders.first {
                         it.provider == target
                     }
                     val snapshot = preview.compose(
@@ -228,10 +222,9 @@
     /** Check which receivers still exist, and clean the data store to only keep those. */
     internal suspend fun cleanReceivers() {
         val packageName = context.packageName
-        @Suppress("ListIterator")
         val receivers = appWidgetManager.installedProviders
-            .fastFilter { it.provider.packageName == packageName }
-            .fastMap { it.provider.className }
+            .filter { it.provider.packageName == packageName }
+            .map { it.provider.className }
             .toSet()
         dataStore.updateData { prefs ->
             val knownReceivers = prefs[providersKey] ?: return@updateData prefs
@@ -239,7 +232,7 @@
             if (toRemove.isEmpty()) return@updateData prefs
             prefs.toMutablePreferences().apply {
                 this[providersKey] = knownReceivers - toRemove
-                toRemove.fastForEach { receiver -> remove(providerKey(receiver)) }
+                toRemove.forEach { receiver -> remove(providerKey(receiver)) }
             }.toPreferences()
         }
     }
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/IgnoreResult.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/IgnoreResult.kt
index d6711c1..59062de 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/IgnoreResult.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/IgnoreResult.kt
@@ -17,7 +17,6 @@
 package androidx.glance.appwidget
 
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.util.fastAny
 import androidx.glance.Emittable
 import androidx.glance.EmittableWithChildren
 import androidx.glance.GlanceComposable
@@ -52,7 +51,7 @@
     if (this is EmittableIgnoreResult) {
         return true
     } else if (this is EmittableWithChildren) {
-        if (children.fastAny { it.shouldIgnoreResult() }) return true
+        if (children.any { it.shouldIgnoreResult() }) return true
     }
     return false
 }
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/LayoutSelection.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/LayoutSelection.kt
index b9bb899..e4ce545 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/LayoutSelection.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/LayoutSelection.kt
@@ -26,7 +26,6 @@
 import androidx.annotation.LayoutRes
 import androidx.annotation.RequiresApi
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastForEach
 import androidx.glance.GlanceModifier
 import androidx.glance.findModifier
 import androidx.glance.layout.Alignment
@@ -363,7 +362,7 @@
         ?: throw IllegalStateException("No child for position $pos and size $width x $height")
     children.values
         .filter { it != stubId }
-        .fastForEach {
+        .forEach {
             inflateViewStub(
                 translationContext, it, R.layout.glance_deleted_view, R.id.deletedViewId)
         }
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/LinearProgressIndicator.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/LinearProgressIndicator.kt
index e314c1a..a300ca5 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/LinearProgressIndicator.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/LinearProgressIndicator.kt
@@ -16,6 +16,7 @@
 
 package androidx.glance.appwidget
 
+import androidx.annotation.RestrictTo
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
 import androidx.glance.Emittable
@@ -76,7 +77,8 @@
     )
 }
 
-internal class EmittableLinearProgressIndicator : Emittable {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class EmittableLinearProgressIndicator : Emittable {
     override var modifier: GlanceModifier = GlanceModifier
     var progress: Float = 0.0f
     var indeterminate: Boolean = false
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt
index ec3d23d..7a1f57e 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt
@@ -18,11 +18,6 @@
 import android.os.Build
 import android.util.Log
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastFold
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastForEachIndexed
 import androidx.glance.BackgroundModifier
 import androidx.glance.Emittable
 import androidx.glance.EmittableButton
@@ -33,6 +28,9 @@
 import androidx.glance.ImageProvider
 import androidx.glance.action.ActionModifier
 import androidx.glance.action.LambdaAction
+import androidx.glance.action.NoRippleOverride
+import androidx.glance.addChild
+import androidx.glance.addChildIfNotNull
 import androidx.glance.appwidget.action.CompoundButtonAction
 import androidx.glance.extractModifier
 import androidx.glance.findModifier
@@ -68,8 +66,7 @@
  * [EmittableBox].
  */
 private fun coerceToOneChild(container: EmittableWithChildren) {
-    if (container.children.isNotEmpty() && container.children.fastAll { it is EmittableSizeBox }) {
-        @Suppress("ListIterator")
+    if (container.children.isNotEmpty() && container.children.all { it is EmittableSizeBox }) {
         for (item in container.children) {
             item as EmittableSizeBox
             if (item.children.size == 1) continue
@@ -95,20 +92,20 @@
  * fillMaxSize. Otherwise, the behavior depends on the version of Android.
  */
 private fun EmittableWithChildren.normalizeSizes() {
-    children.fastForEach { child ->
+    children.forEach { child ->
         if (child is EmittableWithChildren) {
             child.normalizeSizes()
         }
     }
     if ((modifier.findModifier<HeightModifier>()?.height ?: Dimension.Wrap) is Dimension.Wrap &&
-        children.fastAny { child ->
+        children.any { child ->
             child.modifier.findModifier<HeightModifier>()?.height is Dimension.Fill
         }
     ) {
         modifier = modifier.fillMaxHeight()
     }
     if ((modifier.findModifier<WidthModifier>()?.width ?: Dimension.Wrap) is Dimension.Wrap &&
-        children.fastAny { child ->
+        children.any { child ->
             child.modifier.findModifier<WidthModifier>()?.width is Dimension.Fill
         }
     ) {
@@ -118,7 +115,7 @@
 
 /** Transform each node in the tree. */
 private fun EmittableWithChildren.transformTree(block: (Emittable) -> Emittable) {
-    children.fastForEachIndexed { index, child ->
+    children.forEachIndexed { index, child ->
         val newChild = block(child)
         children[index] = newChild
         if (newChild is EmittableWithChildren) newChild.transformTree(block)
@@ -139,7 +136,6 @@
  * will be updated for the composition in all sizes. This is why there can be multiple LambdaActions
  * for each key, even after de-duping.
  */
-@Suppress("ListIterator")
 internal fun EmittableWithChildren.updateLambdaActionKeys(): Map<String, List<LambdaAction>> =
     children.foldIndexed(
         mutableMapOf<String, MutableList<LambdaAction>>()
@@ -148,7 +144,8 @@
             child.modifier.extractLambdaAction()
         if (action != null &&
             child !is EmittableSizeBox &&
-            child !is EmittableLazyItemWithChildren) {
+            child !is EmittableLazyItemWithChildren
+        ) {
             val newKey = action.key + "+$index"
             val newAction = LambdaAction(newKey, action.block)
             actions.getOrPut(newKey) { mutableListOf() }.add(newAction)
@@ -169,6 +166,7 @@
             action is LambdaAction -> action to modifiers
             action is CompoundButtonAction && action.innerAction is LambdaAction ->
                 action.innerAction to modifiers
+
             else -> null to modifiers
         }
     }
@@ -205,11 +203,11 @@
         // before the target in the wrapper box. This allows us to support content scale as well as
         // can help support additional processing on background images. Note: Button's don't support
         // bg image modifier.
-        (it is BackgroundModifier && it.imageProvider != null) ||
-        // R- buttons are implemented using box, images and text.
-        (isButton && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) ||
-         // Ripples are implemented by placing a drawable after the target in the wrapper box.
-        (it is ActionModifier && !hasBuiltinRipple())
+        (it is BackgroundModifier.Image) ||
+            // R- buttons are implemented using box, images and text.
+            (isButton && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) ||
+            // Ripples are implemented by placing a drawable after the target in the wrapper box.
+            (it is ActionModifier && !hasBuiltinRipple())
     }
     if (!shouldWrapTargetInABox) return target
 
@@ -234,7 +232,7 @@
                 // drawable's base was white/none, applying transparent tint will lead to black
                 // color. This shouldn't be issue for icon type drawables, but in this case we are
                 // emulating colored outline. So, we apply tint as well as alpha.
-                bgModifier.colorProvider?.let {
+                (bgModifier as? BackgroundModifier.Color)?.colorProvider?.let {
                     colorFilterParams = TintAndAlphaColorFilterParams(it)
                 }
                 contentScale = ContentScale.FillBounds
@@ -244,14 +242,19 @@
             // is applied back to the target. Note: We could have hoisted the bg color to box
             // instead of adding it back to the target, but for buttons, we also add an outline
             // background to the box.
-            if (bgModifier.imageProvider != null) {
-                backgroundImage = EmittableImage().apply {
-                    modifier = GlanceModifier.fillMaxSize()
-                    provider = bgModifier.imageProvider
-                    contentScale = bgModifier.contentScale
+            when (bgModifier) {
+                is BackgroundModifier.Image -> {
+                    backgroundImage = EmittableImage().apply {
+                        modifier = GlanceModifier.fillMaxSize()
+                        provider = bgModifier.imageProvider
+                        contentScale = bgModifier.contentScale
+                        colorFilterParams = bgModifier.colorFilter?.colorFilterParams
+                    }
                 }
-            } else { // is a background color modifier
-                targetModifiers += bgModifier
+
+                is BackgroundModifier.Color -> {
+                    targetModifiers += bgModifier
+                }
             }
         }
     }
@@ -263,9 +266,15 @@
         targetModifiersMinusBg.extractModifier<ActionModifier>()
     boxModifiers += actionModifier
     if (actionModifier != null && !hasBuiltinRipple()) {
+        val maybeRippleOverride = actionModifier.rippleOverride
         val rippleImageProvider =
-            if (isButton) ImageProvider(R.drawable.glance_button_ripple)
-            else ImageProvider(R.drawable.glance_ripple)
+            if (maybeRippleOverride != NoRippleOverride) {
+                ImageProvider(maybeRippleOverride)
+            } else if (isButton) {
+                ImageProvider(R.drawable.glance_button_ripple)
+            } else {
+                ImageProvider(R.drawable.glance_ripple)
+            }
         rippleImage = EmittableImage().apply {
             modifier = GlanceModifier.fillMaxSize()
             provider = rippleImageProvider
@@ -289,22 +298,24 @@
 
     return EmittableBox().apply {
         modifier = boxModifiers.collect()
+        target.modifier = targetModifiers.collect()
+
         if (isButton) contentAlignment = Alignment.Center
 
-        backgroundImage?.let { children += it }
-        children += target.apply { modifier = targetModifiers.collect() }
-        rippleImage?.let { children += it }
+        addChildIfNotNull(backgroundImage)
+        addChild(target)
+        addChildIfNotNull(rippleImage)
     }
 }
 
 private fun Emittable.hasBuiltinRipple() =
     this is EmittableSwitch ||
-    this is EmittableRadioButton ||
-    this is EmittableCheckBox ||
-     // S+ versions use a native button with fixed rounded corners and matching ripple set in
-     // layout xml. In R- versions, buttons are implemented using a background drawable with
-     // rounded corners and an EmittableText in R- versions.
-    (this is EmittableButton && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+        this is EmittableRadioButton ||
+        this is EmittableCheckBox ||
+        // S+ versions use a native button with fixed rounded corners and matching ripple set in
+        // layout xml. In R- versions, buttons are implemented using a background drawable with
+        // rounded corners and an EmittableText in R- versions.
+        (this is EmittableButton && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
 
 private data class ExtractedSizeModifiers(
     val sizeModifiers: GlanceModifier = GlanceModifier,
@@ -320,7 +331,8 @@
         foldIn(ExtractedSizeModifiers()) { acc, modifier ->
             if (modifier is WidthModifier ||
                 modifier is HeightModifier ||
-                modifier is CornerRadiusModifier) {
+                modifier is CornerRadiusModifier
+            ) {
                 acc.copy(sizeModifiers = acc.sizeModifiers.then(modifier))
             } else {
                 acc.copy(nonSizeModifiers = acc.nonSizeModifiers.then(modifier))
@@ -344,6 +356,6 @@
 }
 
 private fun MutableList<GlanceModifier?>.collect(): GlanceModifier =
-    fastFold(GlanceModifier) { acc: GlanceModifier, mod: GlanceModifier? ->
+    fold(GlanceModifier) { acc: GlanceModifier, mod: GlanceModifier? ->
         mod?.let { acc.then(mod) } ?: acc
     }
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteCollectionItems.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteCollectionItems.kt
index 8fff92a..61c8bcb 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteCollectionItems.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteCollectionItems.kt
@@ -18,7 +18,6 @@
 
 import android.annotation.SuppressLint
 import android.widget.RemoteViews
-import androidx.compose.ui.util.fastMap
 
 /** Representation of a fixed list of items to be displayed in a RemoteViews collection.  */
 internal class RemoteCollectionItems private constructor(
@@ -32,7 +31,6 @@
             "RemoteCollectionItems has different number of ids and views"
         }
         require(_viewTypeCount >= 1) { "View type count must be >= 1" }
-        @Suppress("ListIterator")
         val layoutIdCount = views.map { it.layoutId }.distinct().count()
         require(layoutIdCount <= _viewTypeCount) {
             "View type count is set to $_viewTypeCount, but the collection contains " +
@@ -133,8 +131,7 @@
             if (viewTypeCount < 1) {
                 // If a view type count wasn't specified, set it to be the number of distinct
                 // layout ids used in the items.
-                @Suppress("ListIterator")
-                viewTypeCount = views.fastMap { it.layoutId }.distinct().count()
+                viewTypeCount = views.map { it.layoutId }.distinct().count()
             }
             return RemoteCollectionItems(
                 ids.toLongArray(),
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt
index 7ca531c..df0e178 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt
@@ -18,7 +18,6 @@
 
 import androidx.annotation.RestrictTo
 import androidx.annotation.RestrictTo.Scope
-import androidx.compose.ui.util.fastMap
 import androidx.glance.Emittable
 import androidx.glance.EmittableWithChildren
 import androidx.glance.GlanceModifier
@@ -32,7 +31,7 @@
     override var modifier: GlanceModifier = GlanceModifier
     override fun copy(): Emittable = RemoteViewsRoot(maxDepth).also {
         it.modifier = modifier
-        it.children.addAll(children.fastMap { it.copy() })
+        it.children.addAll(children.map { it.copy() })
     }
 
     override fun toString(): String = "RemoteViewsRoot(" +
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt
index edb9d83..d94e52f 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt
@@ -31,10 +31,6 @@
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.isSpecified
-import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastForEachIndexed
-import androidx.compose.ui.util.fastMap
 import androidx.core.widget.RemoteViewsCompat.setLinearLayoutGravity
 import androidx.glance.Emittable
 import androidx.glance.EmittableButton
@@ -109,13 +105,13 @@
     children: List<Emittable>,
     rootViewIndex: Int
 ): RemoteViews {
-    if (children.fastAll { it is EmittableSizeBox }) {
+    if (children.all { it is EmittableSizeBox }) {
         // If the children of root are all EmittableSizeBoxes, then we must translate each
         // EmittableSizeBox into a distinct RemoteViews object. Then, we combine them into one
         // multi-sized RemoteViews (a RemoteViews that contains either landscape & portrait RVs or
         // multiple RVs mapped by size).
         val sizeMode = (children.first() as EmittableSizeBox).sizeMode
-        val views = children.fastMap { child ->
+        val views = children.map { child ->
             val size = (child as EmittableSizeBox).size
             val remoteViewsInfo = createRootView(translationContext, child.modifier, rootViewIndex)
             val rv = remoteViewsInfo.remoteViews.apply {
@@ -130,11 +126,10 @@
             is SizeMode.Single -> views.single().second
             is SizeMode.Responsive, SizeMode.Exact -> {
                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-                    @Suppress("ListIterator")
                     Api31Impl.createRemoteViews(views.toMap())
                 } else {
                     require(views.size == 1 || views.size == 2) { "unsupported views size" }
-                    combineLandscapeAndPortrait(views.fastMap { it.second })
+                    combineLandscapeAndPortrait(views.map { it.second })
                 }
             }
         }
@@ -312,7 +307,7 @@
         element.modifier,
         viewDef
     )
-    element.children.fastForEach {
+    element.children.forEach {
         it.modifier = it.modifier.then(AlignmentModifier(element.contentAlignment))
     }
     setChildren(
@@ -397,7 +392,6 @@
 }
 
 private fun checkSelectableGroupChildren(children: List<Emittable>) {
-    @Suppress("ListIterator")
     check(children.count { it is EmittableRadioButton && it.checked } <= 1) {
         "When using GlanceModifier.selectableGroup(), no more than one RadioButton " +
         "may be checked at a time."
@@ -416,7 +410,7 @@
         }
         element.remoteViews.copy().apply {
             removeAllViews(element.containerViewId)
-            element.children.fastForEachIndexed { index, child ->
+            element.children.forEachIndexed { index, child ->
                 val rvInfo = createRootView(translationContext, child.modifier, index)
                 val rv = rvInfo.remoteViews
                 rv.translateChild(translationContext.forRoot(rvInfo), child)
@@ -472,8 +466,7 @@
     parentDef: InsertedViewInfo,
     children: List<Emittable>
 ) {
-    @Suppress("ListIterator")
-    children.take(10).fastForEachIndexed { index, child ->
+    children.take(10).forEachIndexed { index, child ->
         translateChild(
             translationContext.forChild(parent = parentDef, pos = index),
             child,
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/SizeBox.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/SizeBox.kt
index e1727c0..4c41537 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/SizeBox.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/SizeBox.kt
@@ -20,7 +20,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.util.fastMap
 import androidx.glance.Emittable
 import androidx.glance.EmittableWithChildren
 import androidx.glance.GlanceModifier
@@ -49,7 +48,7 @@
     override fun copy(): Emittable = EmittableSizeBox().also {
         it.size = size
         it.sizeMode = sizeMode
-        it.children.addAll(children.fastMap { it.copy() })
+        it.children.addAll(children.map { it.copy() })
     }
 
     override fun toString(): String = "EmittableSizeBox(" +
@@ -107,11 +106,11 @@
         } else {
             val smallestSize = sizeMode.sizes.sortedBySize()[0]
             LocalAppWidgetOptions.current.extractOrientationSizes()
-                .fastMap { findBestSize(it, sizeMode.sizes) ?: smallestSize }
+                .map { findBestSize(it, sizeMode.sizes) ?: smallestSize }
                 .ifEmpty { listOf(smallestSize, smallestSize) }
         }
     }
-    sizes.distinct().fastMap { size ->
+    sizes.distinct().map { size ->
         SizeBox(size, sizeMode, content)
     }
 }
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/WidgetLayout.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/WidgetLayout.kt
index bae572a..49e0d30 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/WidgetLayout.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/WidgetLayout.kt
@@ -22,7 +22,6 @@
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
 import androidx.annotation.VisibleForTesting
-import androidx.compose.ui.util.fastMap
 import androidx.datastore.core.CorruptionException
 import androidx.datastore.core.DataStore
 import androidx.datastore.core.DataStoreFactory
@@ -115,7 +114,6 @@
                 )
                 LayoutProto.LayoutConfig.getDefaultInstance()
             }
-            @Suppress("ListIterator")
             val layouts = config.layoutList.associate {
                 it.layout to it.layoutIndex
             }.toMutableMap()
@@ -243,7 +241,7 @@
             is EmittableLazyColumn -> setLazyListColumn(element)
         }
         if (element is EmittableWithChildren && element !is EmittableLazyList) {
-            addAllChildren(element.children.fastMap { createNode(context, it) })
+            addAllChildren(element.children.map { createNode(context, it) })
         }
     }.build()
 
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/component/Buttons.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/component/Buttons.kt
new file mode 100644
index 0000000..e2c6559
--- /dev/null
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/component/Buttons.kt
@@ -0,0 +1,467 @@
+/*
+ * 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.glance.appwidget.component
+
+import android.os.Build
+import androidx.annotation.DimenRes
+import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.Button
+import androidx.glance.ButtonColors
+import androidx.glance.ButtonDefaults
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.action.Action
+import androidx.glance.action.NoRippleOverride
+import androidx.glance.action.action
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.R
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.enabled
+import androidx.glance.background
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+import androidx.glance.layout.width
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import androidx.glance.unit.ColorProvider
+
+/**
+ * A button styled per Material3. It has a filled background. It is more opinionated than [Button]
+ * and suitable for uses where M3 is preferred.
+ *
+ * @param text The text that this button will show.
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param icon An optional leading icon placed before the text.
+ * @param colors The colors to use for the background and content of the button.
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated.
+ * @param key A stable and unique key that identifies the action for this button. This ensures
+ * that the correct action is triggered, especially in cases of items that change order. If not
+ * provided we use the key that is automatically generated by the Compose runtime, which is unique
+ * for every exact code location in the composition tree.
+ */
+@Composable
+fun FilledButton(
+    text: String,
+    onClick: () -> Unit,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    icon: ImageProvider? = null,
+    colors: ButtonColors = ButtonDefaults.buttonColors(),
+    maxLines: Int = Int.MAX_VALUE,
+    key: String? = null
+) = FilledButton(
+    text = text,
+    onClick = action(block = onClick, key = key),
+    modifier = modifier,
+    enabled = enabled,
+    icon = icon,
+    colors = colors,
+    maxLines = maxLines,
+)
+
+/**
+ * A button styled per Material3. It has a filled background. It is more opinionated than [Button]
+ * and suitable for uses where M3 is preferred.
+ *
+ * @param text The text that this button will show.
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param icon An optional leading icon placed before the text.
+ * @param colors The colors to use for the background and content of the button.
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated.
+ */
+@Composable
+fun FilledButton(
+    text: String,
+    onClick: Action,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    icon: ImageProvider? = null,
+    colors: ButtonColors = ButtonDefaults.buttonColors(),
+    maxLines: Int = Int.MAX_VALUE,
+) = M3TextButton(
+    text = text,
+    modifier = modifier,
+    enabled = enabled,
+    icon = icon,
+    contentColor = colors.contentColor,
+    backgroundTint = colors.backgroundColor,
+    backgroundResource = R.drawable.glance_component_btn_filled,
+    onClick = onClick,
+    maxLines = maxLines,
+)
+
+/**
+ * An outline button styled per Material3. It has a transparent background. It is more opinionated
+ * than [Button] and suitable for uses where M3 is preferred.
+ *
+ * @param text The text that this button will show.
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param icon An optional leading icon placed before the text.
+ * @param contentColor The color used for the text, optional icon tint, and outline.
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated.
+ * @param key A stable and unique key that identifies the action for this button. This ensures
+ * that the correct action is triggered, especially in cases of items that change order. If not
+ * provided we use the key that is automatically generated by the Compose runtime, which is unique
+ * for every exact code location in the composition tree.
+ */
+@Composable
+fun OutlineButton(
+    text: String,
+    contentColor: ColorProvider,
+    onClick: () -> Unit,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    icon: ImageProvider? = null,
+    maxLines: Int = Int.MAX_VALUE,
+    key: String? = null
+) = OutlineButton(
+    text = text,
+    contentColor = contentColor,
+    onClick = action(block = onClick, key = key),
+    modifier = modifier,
+    enabled = enabled,
+    icon = icon,
+    maxLines = maxLines,
+)
+
+/**
+ * An outline button styled per Material3. It has a transparent background. It is more opinionated
+ * than [Button] and suitable for uses where M3 is preferred.
+ *
+ * @param text The text that this button will show.
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param icon An optional leading icon placed before the text.
+ * @param contentColor The color used for the text, optional icon tint, and outline.
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated.
+ */
+@Composable
+fun OutlineButton(
+    text: String,
+    contentColor: ColorProvider,
+    onClick: Action,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    icon: ImageProvider? = null,
+    maxLines: Int = Int.MAX_VALUE,
+) {
+    val bg: ColorProvider = contentColor
+    val fg: ColorProvider = contentColor
+
+    M3TextButton(
+        text = text,
+        onClick = onClick,
+        modifier = modifier,
+        enabled = enabled,
+        icon = icon,
+        contentColor = fg,
+        backgroundResource = R.drawable.glance_component_btn_outline,
+        backgroundTint = bg,
+        maxLines = maxLines,
+    )
+}
+
+/**
+ * Intended to fill the role of primary icon button or fab.
+ *
+ * @param imageProvider the icon to be drawn in the button
+ * @param contentDescription Text used by accessibility services to describe what this image
+ * represents. This text should be localized, such as by using
+ * androidx.compose.ui.res.stringResource or similar
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param backgroundColor The color to tint the button's background.
+ * @param contentColor The color to tint the button's icon.
+ * @param key A stable and unique key that identifies the action for this button. This ensures
+ * that the correct action is triggered, especially in cases of items that change order. If not
+ * provided we use the key that is automatically generated by the Compose runtime, which is unique
+ * for every exact code location in the composition tree.
+ */
+@Composable
+fun SquareIconButton(
+    imageProvider: ImageProvider,
+    contentDescription: String?,
+    onClick: () -> Unit,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    backgroundColor: ColorProvider = GlanceTheme.colors.primary,
+    contentColor: ColorProvider = GlanceTheme.colors.onPrimary,
+    key: String? = null
+) = SquareIconButton(
+    imageProvider = imageProvider,
+    contentDescription = contentDescription,
+    onClick = action(block = onClick, key = key),
+    modifier = modifier,
+    enabled = enabled,
+    backgroundColor = backgroundColor,
+    contentColor = contentColor,
+)
+
+/**
+ * Intended to fill the role of primary icon button or fab.
+ *
+ * @param imageProvider the icon to be drawn in the button
+ * @param contentDescription Text used by accessibility services to describe what this image
+ * represents. This text should be localized, such as by using
+ * androidx.compose.ui.res.stringResource or similar
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param backgroundColor The color to tint the button's background.
+ * @param contentColor The color to tint the button's icon.
+ */
+@Composable
+fun SquareIconButton(
+    imageProvider: ImageProvider,
+    contentDescription: String?,
+    onClick: Action,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    backgroundColor: ColorProvider = GlanceTheme.colors.primary,
+    contentColor: ColorProvider = GlanceTheme.colors.onPrimary,
+) = M3IconButton(
+    imageProvider = imageProvider,
+    contentDescription = contentDescription,
+    backgroundColor = backgroundColor,
+    contentColor = contentColor,
+    shape = IconButtonShape.Square,
+    modifier = modifier,
+    enabled = enabled,
+    onClick = onClick,
+)
+
+/**
+ * Intended to fill the role of secondary icon button.
+ * Background color may be null to have the button display as an icon with a 48x48dp hit area.
+ *
+ * @param imageProvider the icon to be drawn in the button
+ * @param contentDescription Text used by accessibility services to describe what this image
+ * represents. This text should be localized, such as by using
+ * androidx.compose.ui.res.stringResource or similar
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param backgroundColor The color to tint the button's background. May be null to make background
+ * transparent.
+ * @param contentColor The color to tint the button's icon.
+ * @param key A stable and unique key that identifies the action for this button. This ensures
+ * that the correct action is triggered, especially in cases of items that change order. If not
+ * provided we use the key that is automatically generated by the Compose runtime, which is unique
+ * for every exact code location in the composition tree.
+ */
+@Composable
+fun CircleIconButton(
+    imageProvider: ImageProvider,
+    contentDescription: String?,
+    onClick: () -> Unit,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    backgroundColor: ColorProvider? = GlanceTheme.colors.background,
+    contentColor: ColorProvider = GlanceTheme.colors.onSurface,
+    key: String? = null
+) = CircleIconButton(
+    imageProvider = imageProvider,
+    contentDescription = contentDescription,
+    backgroundColor = backgroundColor,
+    contentColor = contentColor,
+    modifier = modifier,
+    enabled = enabled,
+    onClick = action(block = onClick, key = key)
+)
+
+/**
+ * Intended to fill the role of secondary icon button.
+ * Background color may be null to have the button display as an icon with a 48x48dp hit area.
+ *
+ * @param imageProvider the icon to be drawn in the button
+ * @param contentDescription Text used by accessibility services to describe what this image
+ * represents. This text should be localized, such as by using
+ * androidx.compose.ui.res.stringResource or similar
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param backgroundColor The color to tint the button's background. May be null to make background
+ * transparent.
+ * @param contentColor The color to tint the button's icon.
+ */
+@Composable
+fun CircleIconButton(
+    imageProvider: ImageProvider,
+    contentDescription: String?,
+    onClick: Action,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    backgroundColor: ColorProvider? = GlanceTheme.colors.background,
+    contentColor: ColorProvider = GlanceTheme.colors.onSurface,
+) = M3IconButton(
+    imageProvider = imageProvider,
+    contentDescription = contentDescription,
+    backgroundColor = backgroundColor,
+    contentColor = contentColor,
+    shape = IconButtonShape.Circle,
+    modifier = modifier,
+    enabled = enabled,
+    onClick = onClick,
+)
+
+private enum class IconButtonShape(
+    @DrawableRes val shape: Int,
+    @DimenRes val cornerRadius: Int,
+    @DrawableRes val ripple: Int,
+    val defaultSize: Dp
+) {
+    Square(
+        R.drawable.glance_component_btn_square,
+        R.dimen.glance_component_square_icon_button_corners,
+        ripple = if (isAtLeastApi31) NoRippleOverride
+            else R.drawable.glance_component_square_button_ripple,
+        defaultSize = 60.dp
+    ),
+    Circle(
+        R.drawable.glance_component_btn_circle,
+        R.dimen.glance_component_circle_icon_button_corners,
+        ripple = if (isAtLeastApi31) NoRippleOverride
+            else R.drawable.glance_component_circle_button_ripple,
+        defaultSize = 48.dp
+    )
+}
+
+@Composable
+private fun M3IconButton(
+    imageProvider: ImageProvider,
+    contentDescription: String?,
+    contentColor: ColorProvider,
+    backgroundColor: ColorProvider?,
+    shape: IconButtonShape,
+    onClick: Action,
+    modifier: GlanceModifier,
+    enabled: Boolean,
+) {
+
+    val backgroundModifier = if (backgroundColor == null)
+        GlanceModifier
+    else GlanceModifier.background(
+        ImageProvider(shape.shape),
+        tint = ColorFilter.tint(backgroundColor)
+    )
+
+    Box(
+        contentAlignment = Alignment.Center,
+        modifier = GlanceModifier
+            .size(shape.defaultSize) // acts as a default if not overridden by [modifier]
+            .then(modifier)
+            .then(backgroundModifier)
+            .clickable(onClick = onClick, rippleOverride = shape.ripple)
+            .enabled(enabled)
+            .then(maybeRoundCorners(shape.cornerRadius))
+    ) {
+        Image(
+            provider = imageProvider,
+            contentDescription = contentDescription,
+            colorFilter = ColorFilter.tint(contentColor),
+            modifier = GlanceModifier.size(24.dp)
+        )
+    }
+}
+
+@Composable
+private fun M3TextButton(
+    text: String,
+    onClick: Action,
+    modifier: GlanceModifier,
+    enabled: Boolean = true,
+    icon: ImageProvider?,
+    contentColor: ColorProvider,
+    @DrawableRes backgroundResource: Int,
+    backgroundTint: ColorProvider,
+    maxLines: Int,
+) {
+    val iconSize = 18.dp
+    val totalHorizontalPadding = if (icon != null) 24.dp else 16.dp
+
+    val Text = @Composable {
+        Text(
+            text = text,
+            style = TextStyle(color = contentColor, fontSize = 14.sp, FontWeight.Medium),
+            maxLines = maxLines
+        )
+    }
+
+    Box(
+        modifier = modifier
+            .padding(start = 16.dp, end = totalHorizontalPadding, top = 10.dp, bottom = 10.dp)
+            .background(ImageProvider(backgroundResource), tint = ColorFilter.tint(backgroundTint))
+            .enabled(enabled)
+            .clickable(
+                onClick = onClick,
+                rippleOverride = if (isAtLeastApi31) NoRippleOverride
+                else R.drawable.glance_component_m3_button_ripple
+            )
+            .then(maybeRoundCorners(R.dimen.glance_component_button_corners)),
+        contentAlignment = Alignment.Center
+    ) {
+
+        if (icon != null) {
+            Row(verticalAlignment = Alignment.Vertical.CenterVertically) {
+                Image(
+                    provider = icon,
+                    contentDescription = null,
+                    colorFilter = ColorFilter.tint(contentColor),
+                    modifier = GlanceModifier.size(iconSize)
+                ) // TODO: do we need a content description for a button icon?
+                Spacer(GlanceModifier.width(8.dp))
+                Text()
+            }
+        } else {
+            Box(GlanceModifier.size(iconSize)) {
+                // for accessibility only: force button to be the same min height as the icon
+                // version.
+                // remove once b/290677181 is addressed
+            }
+            Text()
+        }
+    }
+}
+
+private val isAtLeastApi31 get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+private fun maybeRoundCorners(@DimenRes radius: Int) =
+    if (isAtLeastApi31)
+        GlanceModifier.cornerRadius(radius)
+    else GlanceModifier
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/lazy/LazyList.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/lazy/LazyList.kt
index 17e4528..2430387 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/lazy/LazyList.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/lazy/LazyList.kt
@@ -19,8 +19,6 @@
 import android.os.Bundle
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.key
-import androidx.compose.ui.util.fastForEachIndexed
-import androidx.compose.ui.util.fastMap
 import androidx.glance.Emittable
 import androidx.glance.EmittableLazyItemWithChildren
 import androidx.glance.EmittableWithChildren
@@ -123,7 +121,7 @@
     }
     listScopeImpl.apply(content)
     return {
-        itemList.fastForEachIndexed { index, (itemId, composable) ->
+        itemList.forEachIndexed { index, (itemId, composable) ->
             val id = itemId.takeIf { it != LazyListScope.UnspecifiedItemId }
                 ?: (ReservedItemIdRangeEnd - index)
             check(id != LazyListScope.UnspecifiedItemId) { "Implicit list item ids exhausted." }
@@ -299,7 +297,7 @@
     override fun copy(): Emittable = EmittableLazyListItem().also {
         it.itemId = itemId
         it.alignment = alignment
-        it.children.addAll(children.fastMap { it.copy() })
+        it.children.addAll(children.map { it.copy() })
     }
 
     override fun toString() =
@@ -312,6 +310,6 @@
         it.modifier = modifier
         it.horizontalAlignment = horizontalAlignment
         it.activityOptions = activityOptions
-        it.children.addAll(children.fastMap { it.copy() })
+        it.children.addAll(children.map { it.copy() })
     }
 }
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
index 72b8eb4..53a41ce 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
@@ -20,8 +20,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.key
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.util.fastForEachIndexed
-import androidx.compose.ui.util.fastMap
 import androidx.glance.Emittable
 import androidx.glance.EmittableLazyItemWithChildren
 import androidx.glance.EmittableWithChildren
@@ -130,7 +128,7 @@
     }
     listScopeImpl.apply(content)
     return {
-        itemList.fastForEachIndexed { index, (itemId, composable) ->
+        itemList.forEachIndexed { index, (itemId, composable) ->
             val id = itemId.takeIf { it != LazyVerticalGridScope.UnspecifiedItemId }
                 ?: (ReservedItemIdRangeEnd - index)
             check(id != LazyVerticalGridScope.UnspecifiedItemId) {
@@ -300,7 +298,7 @@
     override fun copy(): Emittable = EmittableLazyVerticalGridListItem().also {
         it.itemId = itemId
         it.alignment = alignment
-        it.children.addAll(children.fastMap { it.copy() })
+        it.children.addAll(children.map { it.copy() })
     }
 
     override fun toString(): String =
@@ -316,7 +314,7 @@
         it.horizontalAlignment = horizontalAlignment
         it.gridCells = gridCells
         it.activityOptions = activityOptions
-        it.children.addAll(children.fastMap { it.copy() })
+        it.children.addAll(children.map { it.copy() })
     }
 }
 
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyListTranslator.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyListTranslator.kt
index eb1c6fe..4810dd6 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyListTranslator.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyListTranslator.kt
@@ -75,7 +75,6 @@
     )
     val items = RemoteCollectionItems.Builder().apply {
         val childContext = translationContext.forLazyCollection(viewDef.mainViewId)
-        @Suppress("ListIterator")
         element.children.foldIndexed(false) { position, previous, itemEmittable ->
             itemEmittable as EmittableLazyListItem
             val itemId = itemEmittable.itemId
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
index d9b74d0..eda245b 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
@@ -88,7 +88,6 @@
     )
     val items = RemoteCollectionItems.Builder().apply {
         val childContext = translationContext.forLazyCollection(viewDef.mainViewId)
-        @Suppress("ListIterator")
         element.children.foldIndexed(false) { position, previous, itemEmittable ->
             itemEmittable as EmittableLazyVerticalGridListItem
             val itemId = itemEmittable.itemId
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/TextTranslator.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/TextTranslator.kt
index 806a7cc..6a219a7 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/TextTranslator.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/TextTranslator.kt
@@ -35,7 +35,6 @@
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
 import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.util.fastForEach
 import androidx.core.widget.RemoteViewsCompat.setTextViewGravity
 import androidx.core.widget.RemoteViewsCompat.setTextViewMaxLines
 import androidx.core.widget.RemoteViewsCompat.setTextViewTextColor
@@ -131,7 +130,7 @@
             spans.add(AlignmentSpan.Standard(align.toAlignment(translationContext.isRtl)))
         }
     }
-    spans.fastForEach { span ->
+    spans.forEach { span ->
         content.setSpan(span, 0, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     }
     setTextViewText(resId, content)
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_circle.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_circle.xml
new file mode 100644
index 0000000..efdd739
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_circle.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <size
+        android:width="48dp"
+        android:height="48dp" />
+    <solid android:color="@android:color/black" />
+</shape>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_filled.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_filled.xml
new file mode 100644
index 0000000..d267cc2
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_filled.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@android:color/black" />
+    <corners android:radius="@dimen/glance_component_button_corners"/>
+</shape>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_outline.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_outline.xml
new file mode 100644
index 0000000..e8b905f
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_outline.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="@dimen/glance_component_button_corners"/>
+    <stroke android:width="1dp" android:color="@android:color/black" />
+    <solid android:color="@android:color/transparent" />
+</shape>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_square.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_square.xml
new file mode 100644
index 0000000..df423d0
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_square.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="@dimen/glance_component_square_icon_button_corners" />
+    <solid android:color="@android:color/black" />
+</shape>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_circle_button_ripple.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_circle_button_ripple.xml
new file mode 100644
index 0000000..5c3c1e4
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_circle_button_ripple.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright (C) 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!-- Fixed radius ripple matching the button's outline -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/glance_component_circle_icon_button_corners"/>
+            <solid android:color="@android:color/white"/>
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_m3_button_ripple.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_m3_button_ripple.xml
new file mode 100644
index 0000000..8150c36
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_m3_button_ripple.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright (C) 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!-- Fixed radius ripple matching the button's outline -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/glance_component_button_corners"/>
+            <solid android:color="@android:color/white"/>
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_square_button_ripple.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_square_button_ripple.xml
new file mode 100644
index 0000000..db64c61
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_square_button_ripple.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright (C) 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!-- Fixed radius ripple matching the button's outline -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/glance_component_square_icon_button_corners"/>
+            <solid android:color="@android:color/white"/>
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/values/glance_component_dimens.xml b/glance/glance-appwidget/src/main/res/values/glance_component_dimens.xml
new file mode 100644
index 0000000..5a4c16b
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/values/glance_component_dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+  -->
+
+<resources>
+    <dimen name="glance_component_button_corners">24dp</dimen>
+    <dimen name="glance_component_square_icon_button_corners">16dp</dimen>
+    <dimen name="glance_component_circle_icon_button_corners">9999.dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
index 411726c..1558cce 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
@@ -123,15 +123,17 @@
 
     @Test
     fun processEmittableTree_catchesException() = runTest {
-        val root = RemoteViewsRoot(maxDepth = 1).apply {
-            children += object : Emittable {
-                override var modifier: GlanceModifier = GlanceModifier
-                override fun copy() = this
+        widget.withErrorLayout(R.layout.glance_error_layout) {
+            val root = RemoteViewsRoot(maxDepth = 1).apply {
+                children += object : Emittable {
+                    override var modifier: GlanceModifier = GlanceModifier
+                    override fun copy() = this
+                }
             }
-        }
 
-        session.processEmittableTree(context, root)
-        assertThat(session.lastRemoteViews!!.layoutId).isEqualTo(widget.errorUiLayout)
+            session.processEmittableTree(context, root)
+            assertThat(session.lastRemoteViews!!.layoutId).isEqualTo(R.layout.glance_error_layout)
+        }
     }
 
     @Test
@@ -207,6 +209,19 @@
         assertTrue(didRunSecond)
     }
 
+    @Test
+    fun onCompositionError() = runTest {
+        // Session should rethrow the error when widget.errorUiLayout == 0
+        val throwable = Exception("error")
+        var caught: Throwable? = null
+        try {
+            session.onCompositionError(context, throwable)
+        } catch (t: Throwable) {
+            caught = t
+        }
+        assertThat(caught).isEqualTo(throwable)
+    }
+
     private class TestGlanceState : ConfigManager {
 
         val getValueCalls = mutableListOf<String>()
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/TestUtils.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/TestUtils.kt
index 2be2889..0c6740c 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/TestUtils.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/TestUtils.kt
@@ -176,7 +176,9 @@
 internal class TestWidget(
     override val sizeMode: SizeMode = SizeMode.Single,
     val ui: @Composable () -> Unit,
-) : GlanceAppWidget() {
+) : GlanceAppWidget(errorUiLayout = 0) {
+    override var errorUiLayout: Int = 0
+
     val provideGlanceCalled = AtomicBoolean(false)
     override suspend fun provideGlance(
         context: Context,
@@ -185,6 +187,16 @@
         provideGlanceCalled.set(true)
         provideContent(ui)
     }
+
+    inline fun withErrorLayout(layout: Int, block: () -> Unit) {
+        val previousErrorLayout = errorUiLayout
+        errorUiLayout = layout
+        try {
+            block()
+        } finally {
+            errorUiLayout = previousErrorLayout
+        }
+    }
 }
 
 /** Count the number of children that are not gone. */
diff --git a/glance/glance-template/lint-baseline.xml b/glance/glance-template/lint-baseline.xml
new file mode 100644
index 0000000..8a73870
--- /dev/null
+++ b/glance/glance-template/lint-baseline.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="cli" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        textList.forEachIndexed { index, item ->"
+        errorLine2="                 ~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/template/GlanceAppWidgetTemplates.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            actionBlock.actionButtons.forEach { button ->"
+        errorLine2="                                      ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/template/GlanceAppWidgetTemplates.kt"/>
+    </issue>
+
+</issues>
diff --git a/glance/glance-template/src/main/java/androidx/glance/template/GlanceAppWidgetTemplates.kt b/glance/glance-template/src/main/java/androidx/glance/template/GlanceAppWidgetTemplates.kt
index 9937ec6..d5804bd 100644
--- a/glance/glance-template/src/main/java/androidx/glance/template/GlanceAppWidgetTemplates.kt
+++ b/glance/glance-template/src/main/java/androidx/glance/template/GlanceAppWidgetTemplates.kt
@@ -104,7 +104,6 @@
  *
  * @param textList the ordered list of text fields to display in the block
  */
-@Suppress("ListIterator")
 @Composable
 internal fun AppWidgetTextSection(textList: List<TemplateText>) {
     if (textList.isEmpty()) return
@@ -218,7 +217,6 @@
  *
  * @param actionBlock The [ActionBlock] data containing a list of buttons for display
  */
-@Suppress("ListIterator")
 @Composable
 internal fun ActionBlockTemplate(actionBlock: ActionBlock?) {
     if (actionBlock?.actionButtons?.isNotEmpty() == true) {
diff --git a/glance/glance-testing/api/current.txt b/glance/glance-testing/api/current.txt
index f12cf50..7524595 100644
--- a/glance/glance-testing/api/current.txt
+++ b/glance/glance-testing/api/current.txt
@@ -12,16 +12,29 @@
     method public androidx.glance.testing.GlanceNodeAssertion<R,T> assert(androidx.glance.testing.GlanceNodeMatcher<R> matcher, optional kotlin.jvm.functions.Function0<java.lang.String>? messagePrefixOnError);
     method public androidx.glance.testing.GlanceNodeAssertion<R,T> assertDoesNotExist();
     method public androidx.glance.testing.GlanceNodeAssertion<R,T> assertExists();
+    method public androidx.glance.testing.GlanceNodeAssertionCollection<R,T> onChildren();
+  }
+
+  public final class GlanceNodeAssertionCollection<R, T extends androidx.glance.testing.GlanceNode<R>> {
+    method public androidx.glance.testing.GlanceNodeAssertionCollection<R,T> assertAll(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
+    method public androidx.glance.testing.GlanceNodeAssertionCollection<R,T> assertAny(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
+    method public androidx.glance.testing.GlanceNodeAssertionCollection<R,T> assertCountEquals(int expectedCount);
+    method public androidx.glance.testing.GlanceNodeAssertionCollection<R,T> filter(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
+    method public operator androidx.glance.testing.GlanceNodeAssertion<R,T> get(int index);
   }
 
   public interface GlanceNodeAssertionsProvider<R, T extends androidx.glance.testing.GlanceNode<R>> {
+    method public androidx.glance.testing.GlanceNodeAssertionCollection<R,T> onAllNodes(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
     method public androidx.glance.testing.GlanceNodeAssertion<R,T> onNode(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
   }
 
   public final class GlanceNodeMatcher<R> {
     ctor public GlanceNodeMatcher(String description, kotlin.jvm.functions.Function1<? super androidx.glance.testing.GlanceNode<R>,java.lang.Boolean> matcher);
+    method public infix androidx.glance.testing.GlanceNodeMatcher<R> and(androidx.glance.testing.GlanceNodeMatcher<R> other);
     method public boolean matches(androidx.glance.testing.GlanceNode<R> node);
     method public boolean matchesAny(Iterable<? extends androidx.glance.testing.GlanceNode<R>> nodes);
+    method public operator androidx.glance.testing.GlanceNodeMatcher<R> not();
+    method public infix androidx.glance.testing.GlanceNodeMatcher<R> or(androidx.glance.testing.GlanceNodeMatcher<R> other);
   }
 
 }
@@ -40,8 +53,9 @@
   public final class UnitTestAssertionExtensionsKt {
     method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasClickAction(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>);
     method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasContentDescription(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String value);
-    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasContentDescription(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String value, optional boolean substring);
-    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasContentDescription(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String value, optional boolean substring, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasContentDescription(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String value, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasContentDescriptionEqualTo(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String value);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasContentDescriptionEqualTo(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String value, optional boolean ignoreCase);
     method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasNoClickAction(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>);
     method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasStartActivityClickAction(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, android.content.ComponentName componentName);
     method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasStartActivityClickAction(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
@@ -49,13 +63,19 @@
     method public static <T extends android.app.Activity> androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasStartActivityClickAction(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, Class<T> activityClass, optional androidx.glance.action.ActionParameters parameters);
     method public static <T extends android.app.Activity> androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasStartActivityClickAction(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, Class<T> activityClass, optional androidx.glance.action.ActionParameters parameters, optional android.os.Bundle? activityOptions);
     method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasTestTag(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String testTag);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasText(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String text);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasText(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String text, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasTextEqualTo(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String text);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasTextEqualTo(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String text, optional boolean ignoreCase);
   }
 
   public final class UnitTestFiltersKt {
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasAnyDescendant(androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> matcher);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasClickAction();
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasContentDescription(String value);
-    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasContentDescription(String value, optional boolean substring);
-    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasContentDescription(String value, optional boolean substring, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasContentDescription(String value, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasContentDescriptionEqualTo(String value);
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasContentDescriptionEqualTo(String value, optional boolean ignoreCase);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasNoClickAction();
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasStartActivityClickAction(android.content.ComponentName componentName);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasStartActivityClickAction(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
@@ -64,8 +84,9 @@
     method public static <T extends android.app.Activity> androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasStartActivityClickAction(Class<T> activityClass, optional androidx.glance.action.ActionParameters parameters, optional android.os.Bundle? activityOptions);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasTestTag(String testTag);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasText(String text);
-    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasText(String text, optional boolean substring);
-    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasText(String text, optional boolean substring, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasText(String text, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasTextEqualTo(String text);
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasTextEqualTo(String text, optional boolean ignoreCase);
   }
 
 }
diff --git a/glance/glance-testing/api/restricted_current.txt b/glance/glance-testing/api/restricted_current.txt
index f12cf50..7524595 100644
--- a/glance/glance-testing/api/restricted_current.txt
+++ b/glance/glance-testing/api/restricted_current.txt
@@ -12,16 +12,29 @@
     method public androidx.glance.testing.GlanceNodeAssertion<R,T> assert(androidx.glance.testing.GlanceNodeMatcher<R> matcher, optional kotlin.jvm.functions.Function0<java.lang.String>? messagePrefixOnError);
     method public androidx.glance.testing.GlanceNodeAssertion<R,T> assertDoesNotExist();
     method public androidx.glance.testing.GlanceNodeAssertion<R,T> assertExists();
+    method public androidx.glance.testing.GlanceNodeAssertionCollection<R,T> onChildren();
+  }
+
+  public final class GlanceNodeAssertionCollection<R, T extends androidx.glance.testing.GlanceNode<R>> {
+    method public androidx.glance.testing.GlanceNodeAssertionCollection<R,T> assertAll(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
+    method public androidx.glance.testing.GlanceNodeAssertionCollection<R,T> assertAny(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
+    method public androidx.glance.testing.GlanceNodeAssertionCollection<R,T> assertCountEquals(int expectedCount);
+    method public androidx.glance.testing.GlanceNodeAssertionCollection<R,T> filter(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
+    method public operator androidx.glance.testing.GlanceNodeAssertion<R,T> get(int index);
   }
 
   public interface GlanceNodeAssertionsProvider<R, T extends androidx.glance.testing.GlanceNode<R>> {
+    method public androidx.glance.testing.GlanceNodeAssertionCollection<R,T> onAllNodes(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
     method public androidx.glance.testing.GlanceNodeAssertion<R,T> onNode(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
   }
 
   public final class GlanceNodeMatcher<R> {
     ctor public GlanceNodeMatcher(String description, kotlin.jvm.functions.Function1<? super androidx.glance.testing.GlanceNode<R>,java.lang.Boolean> matcher);
+    method public infix androidx.glance.testing.GlanceNodeMatcher<R> and(androidx.glance.testing.GlanceNodeMatcher<R> other);
     method public boolean matches(androidx.glance.testing.GlanceNode<R> node);
     method public boolean matchesAny(Iterable<? extends androidx.glance.testing.GlanceNode<R>> nodes);
+    method public operator androidx.glance.testing.GlanceNodeMatcher<R> not();
+    method public infix androidx.glance.testing.GlanceNodeMatcher<R> or(androidx.glance.testing.GlanceNodeMatcher<R> other);
   }
 
 }
@@ -40,8 +53,9 @@
   public final class UnitTestAssertionExtensionsKt {
     method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasClickAction(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>);
     method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasContentDescription(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String value);
-    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasContentDescription(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String value, optional boolean substring);
-    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasContentDescription(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String value, optional boolean substring, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasContentDescription(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String value, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasContentDescriptionEqualTo(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String value);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasContentDescriptionEqualTo(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String value, optional boolean ignoreCase);
     method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasNoClickAction(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>);
     method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasStartActivityClickAction(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, android.content.ComponentName componentName);
     method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasStartActivityClickAction(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
@@ -49,13 +63,19 @@
     method public static <T extends android.app.Activity> androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasStartActivityClickAction(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, Class<T> activityClass, optional androidx.glance.action.ActionParameters parameters);
     method public static <T extends android.app.Activity> androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasStartActivityClickAction(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, Class<T> activityClass, optional androidx.glance.action.ActionParameters parameters, optional android.os.Bundle? activityOptions);
     method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasTestTag(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String testTag);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasText(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String text);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasText(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String text, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasTextEqualTo(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String text);
+    method public static androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> assertHasTextEqualTo(androidx.glance.testing.GlanceNodeAssertion<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode>, String text, optional boolean ignoreCase);
   }
 
   public final class UnitTestFiltersKt {
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasAnyDescendant(androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> matcher);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasClickAction();
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasContentDescription(String value);
-    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasContentDescription(String value, optional boolean substring);
-    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasContentDescription(String value, optional boolean substring, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasContentDescription(String value, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasContentDescriptionEqualTo(String value);
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasContentDescriptionEqualTo(String value, optional boolean ignoreCase);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasNoClickAction();
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasStartActivityClickAction(android.content.ComponentName componentName);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasStartActivityClickAction(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
@@ -64,8 +84,9 @@
     method public static <T extends android.app.Activity> androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasStartActivityClickAction(Class<T> activityClass, optional androidx.glance.action.ActionParameters parameters, optional android.os.Bundle? activityOptions);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasTestTag(String testTag);
     method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasText(String text);
-    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasText(String text, optional boolean substring);
-    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasText(String text, optional boolean substring, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasText(String text, optional boolean ignoreCase);
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasTextEqualTo(String text);
+    method public static androidx.glance.testing.GlanceNodeMatcher<androidx.glance.testing.unit.MappedNode> hasTextEqualTo(String text, optional boolean ignoreCase);
   }
 
 }
diff --git a/glance/glance-testing/lint-baseline.xml b/glance/glance-testing/lint-baseline.xml
new file mode 100644
index 0000000..bc6854b
--- /dev/null
+++ b/glance/glance-testing/lint-baseline.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.2.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-beta01)" variant="all" version="8.2.0-beta01">
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    nodes.forEachIndexed { index, glanceNode ->"
+        errorLine2="          ~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/testing/AssertionErrorMessages.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        children.forEach { child ->"
+        errorLine2="                 ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/testing/unit/GlanceMappedNode.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        return mappedNodes.toList()"
+        errorLine2="                           ~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/testing/unit/GlanceMappedNode.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        val violations = filteredNodes.filter {"
+        errorLine2="                                       ~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/testing/GlanceNodeAssertionCollection.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            this.allNodes = allNodes.toList()"
+        errorLine2="                                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/testing/TestContext.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            ?.joinToString()"
+        errorLine2="              ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/testing/unit/UnitTestFilters.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        return node.children().any { checkIfSubtreeMatchesRecursive(matcher, it) }"
+        errorLine2="                               ~~~">
+        <location
+            file="src/main/java/androidx/glance/testing/unit/UnitTestFilters.kt"/>
+    </issue>
+
+</issues>
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/AssertionErrorMessages.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/AssertionErrorMessages.kt
index 671a692..16155f6 100644
--- a/glance/glance-testing/src/main/java/androidx/glance/testing/AssertionErrorMessages.kt
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/AssertionErrorMessages.kt
@@ -19,23 +19,40 @@
 import java.lang.StringBuilder
 
 /**
- * Builds error message for case where expected amount of matching nodes does not match reality.
+ * Builds error message with reason appended.
  *
- * Provide [errorMessage] to explain which operation you were about to perform. This makes it
- * easier for developer to find where the failure happened.
+ * @param errorMessageOnFail message explaining which operation you were about to perform. This
+ *                           makes it easier for developer to find where the failure happened.
+ * @param reason the reason for failure
  */
-internal fun buildErrorMessageForCountMismatch(
-    errorMessage: String,
+internal fun buildErrorMessageWithReason(errorMessageOnFail: String, reason: String): String {
+    return "${errorMessageOnFail}\nReason: $reason"
+}
+
+/**
+ * Builds error reason for case where amount of matching nodes are less than needed to query given
+ * index and perform assertions on (e.g. if getting a node at index 2 but only 2 nodes exist in
+ * the collection).
+ */
+internal fun buildErrorReasonForIndexOutOfMatchedNodeBounds(
+    matcherDescription: String,
+    requestedIndex: Int,
+    actualCount: Int
+): String {
+    return "Not enough node(s) matching condition: ($matcherDescription) " +
+        "to get node at index '$requestedIndex'. Found '$actualCount' matching node(s)"
+}
+
+/**
+ * Builds error reason for case where expected amount of matching nodes does not match reality.
+ */
+internal fun buildErrorReasonForCountMismatch(
     matcherDescription: String,
     expectedCount: Int,
     actualCount: Int
 ): String {
     val sb = StringBuilder()
 
-    sb.append(errorMessage)
-    sb.append("\n")
-
-    sb.append("Reason: ")
     when (expectedCount) {
         0 -> {
             sb.append("Did not expect any node matching condition: $matcherDescription")
@@ -52,20 +69,54 @@
 }
 
 /**
- * Builds error message for general assertion errors.
+ * Builds error reason for assertions where at least one node was expected to be present to make
+ * assertions on (e.g. assertAny).
+ */
+internal fun buildErrorReasonForAtLeastOneNodeExpected(
+    matcherDescription: String
+): String {
+    return "Expected to receive at least 1 node " +
+        "but 0 nodes were found for condition: ($matcherDescription)"
+}
+
+/**
+ * Builds error message for general assertion errors involving a single node.
  *
  * <p>Provide [errorMessage] to explain which operation you were about to perform. This makes it
  * easier for developer to find where the failure happened.
  */
 internal fun <R> buildGeneralErrorMessage(
     errorMessage: String,
-    glanceNode: GlanceNode<R>
+    node: GlanceNode<R>
 ): String {
     val sb = StringBuilder()
     sb.append(errorMessage)
 
     sb.append("\n")
-    sb.append("Glance Node: ${glanceNode.toDebugString()}")
+    sb.append("Glance Node: ${node.toDebugString()}")
+
+    return sb.toString()
+}
+
+/**
+ * Builds error message for general assertion errors for multiple nodes.
+ *
+ * <p>Provide [errorMessage] to explain which operation you were about to perform. This makes it
+ * easier for developer to find where the failure happened.
+ */
+internal fun <R> buildGeneralErrorMessage(
+    errorMessage: String,
+    nodes: List<GlanceNode<R>>
+): String {
+    val sb = StringBuilder()
+    sb.append(errorMessage)
+
+    sb.append("\n")
+    sb.append("Found ${nodes.size} node(s) that don't match.")
+
+    nodes.forEachIndexed { index, glanceNode ->
+        sb.append("\nNon-matching Glance Node #${index + 1}: ${glanceNode.toDebugString()}")
+    }
 
     return sb.toString()
 }
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertion.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertion.kt
index ed47355..4095626 100644
--- a/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertion.kt
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertion.kt
@@ -22,12 +22,12 @@
 /**
  * Represents a Glance node from the tree that can be asserted on.
  *
- * An instance of [GlanceNodeAssertion] can be obtained from {@code onNode} and equivalent methods
- * on a GlanceNodeAssertionProvider
+ * An instance of [GlanceNodeAssertion] can be obtained from `onNode` and equivalent methods
+ * on a [GlanceNodeAssertionsProvider]
  */
 class GlanceNodeAssertion<R, T : GlanceNode<R>> @RestrictTo(Scope.LIBRARY_GROUP) constructor(
-    private val matcher: GlanceNodeMatcher<R>,
     private val testContext: TestContext<R, T>,
+    private val selector: GlanceNodeSelector<R>,
 ) {
     /**
      * Asserts that the node was found.
@@ -35,7 +35,7 @@
      * @throws [AssertionError] if the assert fails.
      */
     fun assertExists(): GlanceNodeAssertion<R, T> {
-        findSingleMatchingNode(finalErrorMessageOnFail = "Failed assertExists")
+        findSingleMatchingNode(errorMessageOnFail = "Failed assertExists")
         return this
     }
 
@@ -45,14 +45,17 @@
      * @throws [AssertionError] if the assert fails.
      */
     fun assertDoesNotExist(): GlanceNodeAssertion<R, T> {
-        val matchedNodesCount = findMatchingNodes().size
+        val errorMessageOnFail = "Failed assertDoesNotExist"
+        val matchedNodesCount = testContext.findMatchingNodes(selector, errorMessageOnFail).size
         if (matchedNodesCount != 0) {
             throw AssertionError(
-                buildErrorMessageForCountMismatch(
-                    errorMessage = "Failed assertDoesNotExist",
-                    matcherDescription = matcher.description,
-                    expectedCount = 0,
-                    actualCount = matchedNodesCount
+                buildErrorMessageWithReason(
+                    errorMessageOnFail = errorMessageOnFail,
+                    reason = buildErrorReasonForCountMismatch(
+                        matcherDescription = selector.description,
+                        expectedCount = 0,
+                        actualCount = matchedNodesCount
+                    )
                 )
             )
         }
@@ -93,40 +96,28 @@
         return this
     }
 
-    private fun findSingleMatchingNode(finalErrorMessageOnFail: String): GlanceNode<R> {
-        val matchingNodes = findMatchingNodes()
-        val matchedNodesCount = matchingNodes.size
-        if (matchedNodesCount != 1) {
+    /**
+     * Returns [GlanceNodeAssertionCollection] that allows performing assertions on the children of
+     * the node selected by this [GlanceNodeAssertion].
+     */
+    fun onChildren(): GlanceNodeAssertionCollection<R, T> {
+        return GlanceNodeAssertionCollection(testContext, selector.addChildrenSelector())
+    }
+
+    private fun findSingleMatchingNode(errorMessageOnFail: String): GlanceNode<R> {
+        val matchingNodes = testContext.findMatchingNodes(selector, errorMessageOnFail)
+        if (matchingNodes.size != 1) {
             throw AssertionError(
-                buildErrorMessageForCountMismatch(
-                    finalErrorMessageOnFail,
-                    matcher.description,
-                    expectedCount = 1,
-                    actualCount = matchedNodesCount
+                buildErrorMessageWithReason(
+                    errorMessageOnFail = errorMessageOnFail,
+                    reason = buildErrorReasonForCountMismatch(
+                        matcherDescription = selector.description,
+                        expectedCount = 1,
+                        actualCount = matchingNodes.size
+                    )
                 )
             )
         }
         return matchingNodes.single()
     }
-
-    private fun findMatchingNodes(): List<GlanceNode<R>> {
-        if (testContext.cachedMatchedNodes.isEmpty()) {
-            val rootGlanceNode =
-                checkNotNull(testContext.rootGlanceNode) { "No root GlanceNode found." }
-            testContext.cachedMatchedNodes = findMatchingNodes(rootGlanceNode)
-        }
-        return testContext.cachedMatchedNodes
-    }
-
-    @Suppress("ListIterator")
-    private fun findMatchingNodes(node: GlanceNode<R>): List<GlanceNode<R>> {
-        val matching = mutableListOf<GlanceNode<R>>()
-        if (matcher.matches(node)) {
-            matching.add(node)
-        }
-        for (child in node.children()) {
-            matching.addAll(findMatchingNodes(child))
-        }
-        return matching.toList()
-    }
 }
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionCollection.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionCollection.kt
new file mode 100644
index 0000000..cbe4572
--- /dev/null
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionCollection.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.glance.testing
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Represents a collection of Glance nodes from the tree that can be asserted on.
+ *
+ * An instance of [GlanceNodeAssertionCollection] can be obtained from
+ * [GlanceNodeAssertionsProvider.onAllNodes] and equivalent methods.
+ */
+// Equivalent to SemanticsNodeInteractionCollection in compose.
+class GlanceNodeAssertionCollection<R, T : GlanceNode<R>>
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor(
+    private val testContext: TestContext<R, T>,
+    private val selector: GlanceNodeSelector<R>
+) {
+    /**
+     * Asserts that this collection of nodes is equal to the given [expectedCount].
+     *
+     * @throws AssertionError if the size is not equal to [expectedCount]
+     */
+    fun assertCountEquals(
+        expectedCount: Int
+    ): GlanceNodeAssertionCollection<R, T> {
+        val errorMessageOnFail = "Failed to assert count of nodes"
+
+        val actualCount = testContext.findMatchingNodes(selector, errorMessageOnFail).size
+        if (actualCount != expectedCount) {
+            throw AssertionError(
+                buildErrorMessageWithReason(
+                    errorMessageOnFail = errorMessageOnFail,
+                    reason = buildErrorReasonForCountMismatch(
+                        matcherDescription = selector.description,
+                        expectedCount = expectedCount,
+                        actualCount = actualCount
+                    )
+                )
+            )
+        }
+        return this
+    }
+
+    /**
+     * Asserts that all the nodes in this collection satisfy the given [matcher].
+     *
+     * Doesn't throw error if the collection is empty. Use [assertCountEquals] to assert on expected
+     * size of the collection.
+     *
+     * @param matcher Matcher that has to be satisfied by all the nodes in the collection.
+     * @throws AssertionError if the collection contains at least one element that does not satisfy
+     * the given matcher.
+     */
+    fun assertAll(
+        matcher: GlanceNodeMatcher<R>,
+    ): GlanceNodeAssertionCollection<R, T> {
+        val errorMessageOnFail = "Failed to assertAll(${matcher.description})"
+
+        val filteredNodes = testContext.findMatchingNodes(selector, errorMessageOnFail)
+        val violations = filteredNodes.filter {
+            !matcher.matches(it)
+        }
+        if (violations.isNotEmpty()) {
+            throw AssertionError(buildGeneralErrorMessage(errorMessageOnFail, violations))
+        }
+        return this
+    }
+
+    /**
+     * Asserts that this collection contains at least one element that satisfies the given
+     * [matcher].
+     *
+     * @param matcher Matcher that has to be satisfied by at least one of the nodes in the
+     * collection.
+     * @throws AssertionError if not at least one matching node was found.
+     */
+    fun assertAny(
+        matcher: GlanceNodeMatcher<R>,
+    ): GlanceNodeAssertionCollection<R, T> {
+        val errorMessageOnFail = "Failed to assertAny(${matcher.description})"
+        val filteredNodes = testContext.findMatchingNodes(selector, errorMessageOnFail)
+
+        if (filteredNodes.isEmpty()) {
+            throw AssertionError(
+                buildErrorMessageWithReason(
+                    errorMessageOnFail = errorMessageOnFail,
+                    reason = buildErrorReasonForAtLeastOneNodeExpected(selector.description)
+                )
+            )
+        }
+
+        if (!matcher.matchesAny(filteredNodes)) {
+            throw AssertionError(buildGeneralErrorMessage(errorMessageOnFail, filteredNodes))
+        }
+        return this
+    }
+
+    /**
+     * Returns a [GlanceNodeAssertion] that can assert on the node at the given index of this
+     * collection.
+     *
+     * Any subsequent assertion on its result will throw error if index is out of bounds of the
+     * matching nodes found from previous operations.
+     */
+    operator fun get(index: Int): GlanceNodeAssertion<R, T> {
+        return GlanceNodeAssertion(
+            testContext = testContext,
+            selector = selector.addIndexedSelector(index)
+        )
+    }
+
+    /**
+     * Returns a new collection of nodes by filtering the given nodes using the provided [matcher].
+     */
+    fun filter(matcher: GlanceNodeMatcher<R>): GlanceNodeAssertionCollection<R, T> {
+        return GlanceNodeAssertionCollection(
+            testContext,
+            selector.addMatcherSelector(
+                selectorName = "filter",
+                matcher = matcher
+            )
+        )
+    }
+}
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt
index 624b529..8adddc0 100644
--- a/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt
@@ -30,4 +30,11 @@
      * @param matcher Matcher used for filtering
      */
     fun onNode(matcher: GlanceNodeMatcher<R>): GlanceNodeAssertion<R, T>
+
+    /**
+     * Finds all Glance nodes that matches the given condition.
+     *
+     * @param matcher Matcher used for filtering
+     */
+    fun onAllNodes(matcher: GlanceNodeMatcher<R>): GlanceNodeAssertionCollection<R, T>
 }
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeMatcher.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeMatcher.kt
index 0e9fe2a..6de4dc5 100644
--- a/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeMatcher.kt
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeMatcher.kt
@@ -38,4 +38,35 @@
     fun matchesAny(nodes: Iterable<GlanceNode<R>>): Boolean {
         return nodes.any(matcher)
     }
+
+    /**
+     * Returns whether the given node is matched by this and the [other] matcher.
+     *
+     * @param other matcher that should also match in addition to current matcher
+     */
+    infix fun and(other: GlanceNodeMatcher<R>): GlanceNodeMatcher<R> {
+        return GlanceNodeMatcher("($description) && (${other.description})") {
+            matcher(it) && other.matches(it)
+        }
+    }
+
+    /**
+     * Returns whether the given node is matched by this or the [other] matcher.
+     *
+     * @param other matcher that can be tested to match if the current matcher doesn't.
+     */
+    infix fun or(other: GlanceNodeMatcher<R>): GlanceNodeMatcher<R> {
+        return GlanceNodeMatcher("($description) || (${other.description})") {
+            matcher(it) || other.matches(it)
+        }
+    }
+
+    /**
+     * Returns whether the given node does not match the matcher.
+     */
+    operator fun not(): GlanceNodeMatcher<R> {
+        return GlanceNodeMatcher(("NOT ($description)")) {
+            !matcher(it)
+        }
+    }
 }
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeSelector.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeSelector.kt
new file mode 100644
index 0000000..f06d499
--- /dev/null
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeSelector.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.glance.testing
+
+import androidx.annotation.RestrictTo
+
+/**
+ * A chainable selector that allows specifying how to select nodes from a collection.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class GlanceNodeSelector<R>(
+    val description: String,
+    private val previousChainedSelector: GlanceNodeSelector<R>? = null,
+    private val selector: (Iterable<GlanceNode<R>>) -> SelectionResult<R>
+) {
+
+    /**
+     * Returns nodes selected by previous chained selectors followed by the current selector.
+     */
+    fun map(nodes: Iterable<GlanceNode<R>>): SelectionResult<R> {
+        val previousSelectionResult = previousChainedSelector?.map(nodes)
+        val inputNodes = previousSelectionResult?.selectedNodes ?: nodes
+        return selector(inputNodes)
+    }
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class SelectionResult<R>(
+    val selectedNodes: List<GlanceNode<R>>,
+    val errorMessageOnNoMatch: String? = null
+)
+
+/**
+ * Constructs an entry-point selector that selects nodes satisfying the matcher condition. Used at
+ * the entry points such as [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] where there is no previous chained selector.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun <R> GlanceNodeMatcher<R>.matcherToSelector(): GlanceNodeSelector<R> {
+    return GlanceNodeSelector(
+        description = description,
+        previousChainedSelector = null
+    ) { glanceNodes ->
+        SelectionResult(
+            selectedNodes = glanceNodes.filter { matches(it) }
+        )
+    }
+}
+
+/**
+ * Wraps the current selector with a chained selector that selects a node at a given index from the
+ * the result of current selection.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun <R> GlanceNodeSelector<R>.addIndexedSelector(index: Int): GlanceNodeSelector<R> {
+    return GlanceNodeSelector(
+        description = "(${this.description})[$index]",
+        previousChainedSelector = this
+    ) { nodes ->
+        val nodesList = nodes.toList()
+        val minimumExpectedCount = index + 1
+        if (index >= 0 && index < nodesList.size) {
+            SelectionResult(
+                selectedNodes = listOf(nodesList[index])
+            )
+        } else {
+            SelectionResult(
+                selectedNodes = emptyList(),
+                errorMessageOnNoMatch = buildErrorReasonForIndexOutOfMatchedNodeBounds(
+                    description,
+                    requestedIndex = minimumExpectedCount,
+                    actualCount = nodesList.size
+                )
+            )
+        }
+    }
+}
+
+/**
+ * Wraps the current selector with a chained matcher-based selector that filters the list of nodes
+ * to return ones matched by the matcher.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun <R> GlanceNodeSelector<R>.addMatcherSelector(
+    selectorName: String,
+    matcher: GlanceNodeMatcher<R>
+): GlanceNodeSelector<R> {
+    return GlanceNodeSelector(
+        description = "(${this.description}).$selectorName(${matcher.description})",
+        previousChainedSelector = this
+    ) { nodes ->
+        SelectionResult(
+            selectedNodes = nodes.filter { matcher.matches(it) }
+        )
+    }
+}
+
+/**
+ * Wraps the current selector with a chained matcher-based selector that ensures only one node is
+ * returned by current selector and selects children of that node.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun <R> GlanceNodeSelector<R>.addChildrenSelector(): GlanceNodeSelector<R> {
+    return GlanceNodeSelector(
+        description = "($description).children()",
+        previousChainedSelector = this
+    ) { nodes ->
+        if (nodes.count() != 1) {
+            SelectionResult(
+                selectedNodes = emptyList(),
+                errorMessageOnNoMatch = buildErrorReasonForCountMismatch(
+                    matcherDescription = description,
+                    expectedCount = 1,
+                    actualCount = nodes.count()
+                )
+            )
+        } else {
+            SelectionResult(
+                selectedNodes = nodes.single().children()
+            )
+        }
+    }
+}
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt
index 1ab76a3..419a304 100644
--- a/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt
@@ -23,13 +23,55 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 class TestContext<R, T : GlanceNode<R>> {
+    var rootGlanceNode: T? = null
+    private var allNodes: List<GlanceNode<R>> = emptyList()
+
     /**
-     * To be called on every onNode to restart matching and clear cache.
+     * Returns all nodes in single flat list (either from cache or by traversing the hierarchy from
+     * root glance node).
      */
-    fun reset() {
-        cachedMatchedNodes = emptyList()
+    private fun getAllNodes(): List<GlanceNode<R>> {
+        val rootGlanceNode =
+            checkNotNull(rootGlanceNode) { "No root GlanceNode found." }
+        if (this.allNodes.isEmpty()) {
+            val allNodes = mutableListOf<GlanceNode<R>>()
+
+            fun collectAllNodesRecursive(currentNode: GlanceNode<R>) {
+                allNodes.add(currentNode)
+                val children = currentNode.children()
+                for (index in children.indices) {
+                    collectAllNodesRecursive(children[index])
+                }
+            }
+
+            collectAllNodesRecursive(rootGlanceNode)
+            this.allNodes = allNodes.toList()
+        }
+
+        return this.allNodes
     }
 
-    var rootGlanceNode: T? = null
-    var cachedMatchedNodes: List<GlanceNode<R>> = emptyList()
+    /**
+     * Finds nodes matching the given selector from the list of all nodes in the hierarchy.
+     *
+     * @throws AssertionError if provided selector results in an error due to no match.
+     */
+    fun findMatchingNodes(
+        selector: GlanceNodeSelector<R>,
+        errorMessageOnFail: String
+    ): List<GlanceNode<R>> {
+        val allNodes = getAllNodes()
+        val selectionResult = selector.map(allNodes)
+
+        if (selectionResult.errorMessageOnNoMatch != null) {
+            throw AssertionError(
+                buildErrorMessageWithReason(
+                    errorMessageOnFail = errorMessageOnFail,
+                    reason = selectionResult.errorMessageOnNoMatch
+                )
+            )
+        }
+
+        return selectionResult.selectedNodes
+    }
 }
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/unit/GlanceMappedNode.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/unit/GlanceMappedNode.kt
index 0ac92ee..929fc80 100644
--- a/glance/glance-testing/src/main/java/androidx/glance/testing/unit/GlanceMappedNode.kt
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/unit/GlanceMappedNode.kt
@@ -19,6 +19,7 @@
 import androidx.annotation.RestrictTo
 import androidx.annotation.RestrictTo.Scope
 import androidx.glance.Emittable
+import androidx.glance.EmittableLazyItemWithChildren
 import androidx.glance.EmittableWithChildren
 import androidx.glance.testing.GlanceNode
 
@@ -59,15 +60,24 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     override fun children(): List<GlanceNode<MappedNode>> {
         val emittable = mappedNode.emittable
-        @Suppress("ListIterator")
         if (emittable is EmittableWithChildren) {
-            return emittable.children.map { child ->
-                GlanceMappedNode(child)
-            }
+            return emittable.toMappedNodes()
         }
         return emptyList()
     }
 
+    private fun EmittableWithChildren.toMappedNodes(): List<GlanceMappedNode> {
+        val mappedNodes = mutableListOf<GlanceMappedNode>()
+        children.forEach { child ->
+            if (child is EmittableLazyItemWithChildren) {
+                mappedNodes.addAll(child.toMappedNodes())
+            } else {
+                mappedNodes.add(GlanceMappedNode(child))
+            }
+        }
+        return mappedNodes.toList()
+    }
+
     @RestrictTo(Scope.LIBRARY_GROUP)
     override fun toDebugString(): String {
         // TODO(b/201779038): map to a more readable format.
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/unit/TestUtils.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/unit/TestUtils.kt
index 40539aa..17935b0 100644
--- a/glance/glance-testing/src/main/java/androidx/glance/testing/unit/TestUtils.kt
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/unit/TestUtils.kt
@@ -19,9 +19,12 @@
 import androidx.annotation.RestrictTo
 import androidx.glance.Emittable
 import androidx.glance.testing.GlanceNodeAssertion
+import androidx.glance.testing.GlanceNodeAssertionCollection
 import androidx.glance.testing.GlanceNodeMatcher
 import androidx.glance.testing.TestContext
+import androidx.glance.testing.matcherToSelector
 
+// Equivalent to calling GlanceNodeAssertionsProvider.onNode
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 fun getGlanceNodeAssertionFor(
     emittable: Emittable,
@@ -30,7 +33,21 @@
     val testContext = TestContext<MappedNode, GlanceMappedNode>()
     testContext.rootGlanceNode = GlanceMappedNode(emittable)
     return GlanceNodeAssertion(
-        matcher = onNodeMatcher,
-        testContext = testContext
+        testContext = testContext,
+        selector = onNodeMatcher.matcherToSelector()
+    )
+}
+
+// Equivalent to calling GlanceNodeAssertionsProvider.onAllNodes
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun getGlanceNodeAssertionCollectionFor(
+    emittable: Emittable,
+    onAllNodesMatcher: GlanceNodeMatcher<MappedNode>
+): GlanceNodeAssertionCollection<MappedNode, GlanceMappedNode> {
+    val testContext = TestContext<MappedNode, GlanceMappedNode>()
+    testContext.rootGlanceNode = GlanceMappedNode(emittable)
+    return GlanceNodeAssertionCollection(
+        testContext = testContext,
+        selector = onAllNodesMatcher.matcherToSelector()
     )
 }
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/unit/UnitTestAssertionExtensions.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/unit/UnitTestAssertionExtensions.kt
index 2aa1aca..715910f 100644
--- a/glance/glance-testing/src/main/java/androidx/glance/testing/unit/UnitTestAssertionExtensions.kt
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/unit/UnitTestAssertionExtensions.kt
@@ -30,6 +30,35 @@
 internal typealias UnitTestAssertion = GlanceNodeAssertion<MappedNode, GlanceMappedNode>
 
 /**
+ * Asserts that text on the given node is a text node and its text contains the provided [text] as
+ * substring.
+ *
+ * @param text value to match.
+ * @param ignoreCase whether to perform case insensitive matching
+ */
+@JvmOverloads
+fun UnitTestAssertion.assertHasText(
+    text: String,
+    ignoreCase: Boolean = false
+): UnitTestAssertion {
+    return assert(hasText(text, ignoreCase))
+}
+
+/**
+ * Asserts that text on the given node is text node and its text is equal to the provided [text].
+ *
+ * @param text value to match.
+ * @param ignoreCase whether to perform case insensitive matching
+ */
+@JvmOverloads
+fun UnitTestAssertion.assertHasTextEqualTo(
+    text: String,
+    ignoreCase: Boolean = false
+): UnitTestAssertion {
+    return assert(hasTextEqualTo(text, ignoreCase))
+}
+
+/**
  * Asserts that a given node is annotated by the given test tag.
  *
  * @param testTag value to match against the free form string specified in the `testTag` semantics
@@ -41,11 +70,10 @@
 }
 
 /**
- * Asserts that a given node matches content description with the provided [value]
+ * Asserts that the content description set on the node contains the provided [value] as substring.
  *
- * @param value value to match as one of the items in the list of content descriptions.
- * @param substring whether to use substring matching.
- * @param ignoreCase whether case should be ignored.
+ * @param value value that should be matched as a substring of the node's content description.
+ * @param ignoreCase whether case should be ignored. Defaults to case sensitive.
  *
  * @see SemanticsProperties.ContentDescription
  *
@@ -54,10 +82,27 @@
 @JvmOverloads
 fun UnitTestAssertion.assertHasContentDescription(
     value: String,
-    substring: Boolean = false,
     ignoreCase: Boolean = false
 ): UnitTestAssertion {
-    return assert(hasContentDescription(value, substring, ignoreCase))
+    return assert(hasContentDescription(value, ignoreCase))
+}
+
+/**
+ * Asserts that the content description set on the node is equal to the provided [value]
+ *
+ * @param value value that should be matched to be equal to the node's content description.
+ * @param ignoreCase whether case should be ignored. Defaults to case sensitive.
+ *
+ * @see SemanticsProperties.ContentDescription
+ *
+ * @throws AssertionError if the matcher does not match or the node can no longer be found.
+ */
+@JvmOverloads
+fun UnitTestAssertion.assertHasContentDescriptionEqualTo(
+    value: String,
+    ignoreCase: Boolean = false
+): UnitTestAssertion {
+    return assert(hasContentDescriptionEqualTo(value, ignoreCase))
 }
 
 /**
@@ -81,9 +126,6 @@
 /**
  * Asserts that a given node has a clickable set with action that starts an activity.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
- * matching node(s) or in assertions to validate that node(s) satisfy the condition.
- *
  * @param activityClass class of the activity that is expected to have been passed in the
  *                      `actionStartActivity` method call
  * @param parameters the parameters associated with the action that are expected to have been passed
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/unit/UnitTestFilters.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/unit/UnitTestFilters.kt
index 577caa5..6d83d4c 100644
--- a/glance/glance-testing/src/main/java/androidx/glance/testing/unit/UnitTestFilters.kt
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/unit/UnitTestFilters.kt
@@ -28,6 +28,8 @@
 import androidx.glance.semantics.SemanticsModifier
 import androidx.glance.semantics.SemanticsProperties
 import androidx.glance.semantics.SemanticsPropertyKey
+import androidx.glance.testing.GlanceNode
+import androidx.glance.testing.GlanceNodeAssertionsProvider
 import androidx.glance.testing.GlanceNodeMatcher
 
 // This file contains common filters that can be passed in "onNode", "onAllNodes" or
@@ -37,7 +39,8 @@
 /**
  * Returns a matcher that matches if a node is annotated by the given test tag.
  *
- * <p>This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  *
  * @param testTag value to match against the free form string specified in the `testTag` semantics
@@ -59,31 +62,67 @@
 }
 
 /**
- * Returns whether the node matches content description with the provided [value]
+ * Returns whether the content description set directly on the node contains the provided [value].
  *
- * @param value value to match as one of the items in the list of content descriptions.
- * @param substring whether to use substring matching.
- * @param ignoreCase whether case should be ignored.
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
+ * matching node(s) or in assertions to validate that node(s) satisfy the condition.
+ *
+ * @param value value that should be substring of the content description set directly on the node.
+ * @param ignoreCase whether case should be ignored. Default is case sensitive.
  *
  * @see SemanticsProperties.ContentDescription
  */
 @JvmOverloads
 fun hasContentDescription(
     value: String,
-    substring: Boolean = false,
     ignoreCase: Boolean = false
 ): GlanceNodeMatcher<MappedNode> =
     GlanceNodeMatcher(
-        description = if (substring) {
+        description =
             "${SemanticsProperties.ContentDescription.name} contains '$value'" +
-                " (ignoreCase: '$ignoreCase')"
-        } else {
-            "${SemanticsProperties.ContentDescription.name} = '$value' (ignoreCase: '$ignoreCase')"
-        }
+                " (ignoreCase: '$ignoreCase') as substring"
     ) { node ->
         node.value.emittable.modifier.any {
             it is SemanticsModifier &&
-                hasContentDescription(it, value, substring, ignoreCase)
+                hasContentDescription(
+                    semanticsModifier = it,
+                    value = value,
+                    substring = true,
+                    ignoreCase = ignoreCase)
+        }
+    }
+
+/**
+ * Returns whether the content description set directly on the node is equal to the provided
+ * [value].
+ *
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
+ * matching node(s) or in assertions to validate that node(s) satisfy the condition.
+ *
+ * @param value value that should match exactly with content description set directly on the node.
+ * @param ignoreCase whether case should be ignored. Default is case sensitive.
+ *
+ * @see SemanticsProperties.ContentDescription
+ */
+@JvmOverloads
+fun hasContentDescriptionEqualTo(
+    value: String,
+    ignoreCase: Boolean = false
+): GlanceNodeMatcher<MappedNode> =
+    GlanceNodeMatcher(
+        description =
+        "${SemanticsProperties.ContentDescription.name} == '$value' (ignoreCase: '$ignoreCase')"
+    ) { node ->
+        node.value.emittable.modifier.any {
+            it is SemanticsModifier &&
+                hasContentDescription(
+                    semanticsModifier = it,
+                    value = value,
+                    substring = false,
+                    ignoreCase = ignoreCase
+                )
         }
     }
 
@@ -93,7 +132,6 @@
     substring: Boolean = false,
     ignoreCase: Boolean = false
 ): Boolean {
-    @Suppress("ListIterator")
     val contentDescription =
         semanticsModifier.configuration.getOrNull(SemanticsProperties.ContentDescription)
             ?.joinToString()
@@ -106,43 +144,54 @@
 }
 
 /**
- * Returns a matcher that matches if text on node matches the provided text.
+ * Returns a matcher that matches if text on node contains the provided text as its substring.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  *
- * @param text value to match.
- * @param substring whether to perform substring matching
- * @param ignoreCase whether to perform case insensitive matching
+ * @param text value that should be matched as a substring of the node's text.
+ * @param ignoreCase whether to perform case insensitive matching. Defaults to case sensitive
+ *                   matching.
  */
 @JvmOverloads
 fun hasText(
     text: String,
-    substring: Boolean = false,
     ignoreCase: Boolean = false
 ): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
-    if (substring) {
-        "contains '$text' (ignoreCase: $ignoreCase) as substring"
-    } else {
-        "has text = '$text' (ignoreCase: '$ignoreCase')"
-    }
+    description = "contains text '$text' (ignoreCase: '$ignoreCase') as substring"
 ) { node ->
     val emittable = node.value.emittable
-    if (emittable is EmittableWithText) {
-        if (substring) {
-            emittable.text.contains(text, ignoreCase)
-        } else {
-            emittable.text.equals(text, ignoreCase)
-        }
-    } else {
-        false
-    }
+    emittable is EmittableWithText && emittable.text.contains(text, ignoreCase)
+}
+
+/**
+ * Returns a matcher that matches if node is a text node and its text is equal to the provided text.
+ *
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
+ * matching node(s) or in assertions to validate that node(s) satisfy the condition.
+ *
+ * @param text value that should exactly match the node's text.
+ * @param ignoreCase whether to perform case insensitive matching. Defaults to case sensitive
+ *                   matching.
+ */
+@JvmOverloads
+fun hasTextEqualTo(
+    text: String,
+    ignoreCase: Boolean = false
+): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
+    description = "text == '$text' (ignoreCase: '$ignoreCase')"
+) { node ->
+    val emittable = node.value.emittable
+    emittable is EmittableWithText && emittable.text.equals(text, ignoreCase)
 }
 
 /**
  * Returns a matcher that matches if the given node has clickable modifier set.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  */
 fun hasClickAction(): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
@@ -157,7 +206,8 @@
  * Returns a matcher that matches if the given node doesn't have a clickable modifier or `onClick`
  * set.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  */
 fun hasNoClickAction(): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
@@ -172,7 +222,8 @@
  * Returns a matcher that matches if a given node has a clickable set with action that starts an
  * activity.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  *
  * @param activityClass class of the activity that is expected to have been passed in the
@@ -214,10 +265,39 @@
 }
 
 /**
+ * Returns a matcher that matches if a given node has a descendant node somewhere in its
+ * sub-hierarchy that the matches the provided matcher.
+ *
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
+ * matching node(s) or in assertions to validate that node(s) satisfy the condition.
+ *
+ * @param matcher a matcher that needs to be satisfied for the descendant node to be matched
+ */
+fun hasAnyDescendant(matcher: GlanceNodeMatcher<MappedNode>): GlanceNodeMatcher<MappedNode> {
+
+    fun checkIfSubtreeMatchesRecursive(
+        matcher: GlanceNodeMatcher<MappedNode>,
+        node: GlanceNode<MappedNode>
+    ): Boolean {
+        if (matcher.matchesAny(node.children())) {
+            return true
+        }
+
+        return node.children().any { checkIfSubtreeMatchesRecursive(matcher, it) }
+    }
+
+    return GlanceNodeMatcher("hasAnyDescendantThat(${matcher.description})") {
+        checkIfSubtreeMatchesRecursive(matcher, it)
+    }
+}
+
+/**
  * Returns a matcher that matches if a given node has a clickable set with action that starts an
  * activity.
  *
- * This can be passed in "onNode" and "onNodeAll" functions on assertion providers to filter out
+ * This can be passed in [GlanceNodeAssertionsProvider.onNode] and
+ * [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
  * matching node(s) or in assertions to validate that node(s) satisfy the condition.
  *
  * @param componentName component of the activity that is expected to have been passed in the
diff --git a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/AssertionErrorMessagesTest.kt b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/AssertionErrorMessagesTest.kt
index e4bebe09..e82031c 100644
--- a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/AssertionErrorMessagesTest.kt
+++ b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/AssertionErrorMessagesTest.kt
@@ -24,11 +24,13 @@
 class AssertionErrorMessagesTest {
     @Test
     fun countMismatch_expectedNone() {
-        val resultMessage = buildErrorMessageForCountMismatch(
-            errorMessage = "Failed assert",
-            matcherDescription = "testTag = 'my-node'",
-            expectedCount = 0,
-            actualCount = 1
+        val resultMessage = buildErrorMessageWithReason(
+            errorMessageOnFail = "Failed assert",
+            reason = buildErrorReasonForCountMismatch(
+                matcherDescription = "testTag = 'my-node'",
+                expectedCount = 0,
+                actualCount = 1
+            )
         )
 
         assertThat(resultMessage).isEqualTo(
@@ -40,11 +42,13 @@
 
     @Test
     fun countMismatch_expectedButFoundNone() {
-        val resultMessage = buildErrorMessageForCountMismatch(
-            errorMessage = "Failed assert",
-            matcherDescription = "testTag = 'my-node'",
-            expectedCount = 2,
-            actualCount = 0
+        val resultMessage = buildErrorMessageWithReason(
+            errorMessageOnFail = "Failed assert",
+            reason = buildErrorReasonForCountMismatch(
+                matcherDescription = "testTag = 'my-node'",
+                expectedCount = 2,
+                actualCount = 0
+            )
         )
 
         assertThat(resultMessage).isEqualTo(
@@ -55,14 +59,14 @@
     }
 
     @Test
-    fun generalErrorMessage() {
+    fun generalErrorMessage_singleNode() {
         val node = GlanceMappedNode(
             EmittableText().also { it.text = "test text" }
         )
 
         val resultMessage = buildGeneralErrorMessage(
             errorMessage = "Failed to match the condition: (testTag = 'my-node')",
-            glanceNode = node
+            node = node
         )
 
         assertThat(resultMessage).isEqualTo(
@@ -70,4 +74,26 @@
                 "\nGlance Node: ${node.toDebugString()}"
         )
     }
+
+    @Test
+    fun generalErrorMessage_multipleNodes() {
+        val node1 = GlanceMappedNode(
+            EmittableText().also { it.text = "text1" }
+        )
+        val node2 = GlanceMappedNode(
+            EmittableText().also { it.text = "text2" }
+        )
+
+        val resultMessage = buildGeneralErrorMessage(
+            errorMessage = "Failed to match the condition: (testTag = 'my-node')",
+            nodes = listOf(node1, node2)
+        )
+
+        assertThat(resultMessage).isEqualTo(
+            "Failed to match the condition: (testTag = 'my-node')" +
+                "\nFound 2 node(s) that don't match." +
+                "\nNon-matching Glance Node #1: ${node1.toDebugString()}" +
+                "\nNon-matching Glance Node #2: ${node2.toDebugString()}"
+        )
+    }
 }
diff --git a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/GlanceNodeAssertionCollectionTest.kt b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/GlanceNodeAssertionCollectionTest.kt
new file mode 100644
index 0000000..7d1202a
--- /dev/null
+++ b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/GlanceNodeAssertionCollectionTest.kt
@@ -0,0 +1,600 @@
+/*
+ * 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.glance.testing
+
+import androidx.glance.GlanceModifier
+import androidx.glance.action.ActionModifier
+import androidx.glance.action.LambdaAction
+import androidx.glance.layout.EmittableColumn
+import androidx.glance.layout.EmittableSpacer
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.testing.unit.assertHasText
+import androidx.glance.testing.unit.getGlanceNodeAssertionCollectionFor
+import androidx.glance.testing.unit.getGlanceNodeAssertionFor
+import androidx.glance.testing.unit.hasClickAction
+import androidx.glance.testing.unit.hasTestTag
+import androidx.glance.testing.unit.hasText
+import androidx.glance.testing.unit.hasTextEqualTo
+import androidx.glance.text.EmittableText
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Test
+
+class GlanceNodeAssertionCollectionTest {
+    @Test
+    fun assertAll_noNodesToAssertOn() {
+        // This is the object that in real usage a onAllNodes(matcher) would return.
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply { text = "some text" })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply { text = "another text" })
+            }
+        )
+
+        assertion.assertAll(hasText("substring"))
+        // no error even if no nodes with click action were available to perform assertAll; on the
+        // other hand calling assertCountEquals(x) before assertAll would have thrown an error
+    }
+
+    @Test
+    fun assertAll_allMatchingNodes() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some substring text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another substring text"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+            }
+        )
+
+        assertion.assertAll(hasText("substring"))
+        // no error
+    }
+
+    @Test
+    fun assertAll_noneMatch_throwsError() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+            }
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            assertion.assertAll(hasText("substring"))
+        }
+
+        assertThat(assertionError).hasMessageThat().contains(
+            "Failed to assertAll(contains text 'substring' (ignoreCase: 'false') as substring)" +
+                "\nFound 2 node(s) that don't match."
+        )
+    }
+
+    @Test
+    fun assertAll_someNotMatch_throwsError() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some substring text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+            }
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            assertion.assertAll(hasText("substring"))
+        }
+
+        assertThat(assertionError).hasMessageThat().contains(
+            "Failed to assertAll(contains text 'substring' (ignoreCase: 'false') as substring)" +
+                "\nFound 1 node(s) that don't match."
+        )
+    }
+
+    @Test
+    fun assertAny_noNodesToAssertOn_assertionError() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply { text = "some text" })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply { text = "another text" })
+            }
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            assertion.assertAny(hasText("substring"))
+        }
+
+        assertThat(assertionError).hasMessageThat().contains(
+            "Failed to assertAny(contains text 'substring' (ignoreCase: 'false') as substring)" +
+                "\nReason: Expected to receive at least 1 node " +
+                "but 0 nodes were found for condition: (has click action)"
+        )
+    }
+
+    @Test
+    fun assertAny_noneMatch_assertionError() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+            }
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            assertion.assertAny(hasText("expected-substring"))
+        }
+
+        assertThat(assertionError).hasMessageThat().contains(
+            "Failed to assertAny(contains text 'expected-substring' " +
+                "(ignoreCase: 'false') as substring)" +
+                "\nFound 2 node(s) that don't match."
+        )
+    }
+
+    @Test
+    fun assertAny_oneMatch() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another substring text"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+            }
+        )
+
+        assertion.assertAny(hasText("substring"))
+    }
+
+    @Test
+    fun assertAny_multipleMatch() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another substring text"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "yet another substring text"
+                    modifier = ActionModifier(LambdaAction("3") {})
+                })
+            }
+        )
+
+        assertion.assertAny(hasText("substring"))
+    }
+
+    @Test
+    fun assertAllAfterFilter_matchingNodes() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some substring text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "yet another substring text"
+                    modifier = ActionModifier(LambdaAction("3") {})
+                })
+            }
+        )
+
+        assertion
+            .filter(hasText("substring"))
+            .assertAll(hasText("text"))
+    }
+
+    @Test
+    fun assertAllAfterFilter_noFilteredNodes_noError() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some substring text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "yet another substring text"
+                    modifier = ActionModifier(LambdaAction("3") {})
+                })
+            }
+        )
+
+        assertion
+            .filter(hasText("word"))
+            .assertAll(hasText("text"))
+    }
+
+    @Test
+    fun assertAnyAfterFilter_matchingNodes() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some substring"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "yet another substring text"
+                    modifier = ActionModifier(LambdaAction("3") {})
+                })
+            }
+        )
+
+        assertion
+            .filter(hasText("substring"))
+            .assertAny(hasText("text"))
+    }
+
+    @Test
+    fun assertAnyAfterFilter_noFilteredNodes_assertionError() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some substring text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "yet another substring text"
+                    modifier = ActionModifier(LambdaAction("3") {})
+                })
+            }
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            assertion
+                .filter(hasText("word"))
+                .assertAny(hasText("text"))
+        }
+
+        assertThat(assertionError).hasMessageThat().contains(
+            "Failed to assertAny(contains text 'text' (ignoreCase: 'false') as substring)" +
+                "\nReason: Expected to receive at least 1 node but 0 nodes were found for " +
+                "condition: " +
+                "((has click action).filter(contains text 'word' (ignoreCase: 'false') " +
+                "as substring))"
+        )
+    }
+
+    @Test
+    fun assertCountEqualsAfterFilter_matchingNodes() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some substring text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "yet another substring text"
+                    modifier = ActionModifier(LambdaAction("3") {})
+                })
+            }
+        )
+
+        assertion
+            .filter(hasText("text"))
+            .assertCountEquals(3)
+    }
+
+    @Test
+    fun assertCountEqualsAfterFilter_noFilteredNodes_assertionError() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some substring text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "yet another substring text"
+                    modifier = ActionModifier(LambdaAction("3") {})
+                })
+            }
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            assertion
+                .filter(hasText("word"))
+                .assertCountEquals(1)
+        }
+
+        assertThat(assertionError).hasMessageThat().contains(
+            "Failed to assert count of nodes" +
+                "\nReason: Expected '1' node(s) matching condition: " +
+                "(has click action).filter(contains text 'word' " +
+                "(ignoreCase: 'false') as substring), " +
+                "but found '0'"
+        )
+    }
+
+    @Test
+    fun multipleFilters() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another substring"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "yet another substring text"
+                    modifier = ActionModifier(LambdaAction("3") {})
+                })
+            }
+        )
+
+        assertion
+            .filter(hasText("text"))
+            .assertCountEquals(2)
+            .filter(hasText("substring"))
+            .assertCountEquals(1)
+    }
+
+    @Test
+    fun getIndexOnFilter() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another substring"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "yet another substring text"
+                    modifier = ActionModifier(LambdaAction("3") {})
+                })
+            }
+        )
+
+        assertion
+            .filter(hasText("text"))
+            .assertCountEquals(2)
+            .get(index = 1)
+            .assert(hasTextEqualTo("yet another substring text"))
+    }
+
+    @Test
+    fun collectionGetIndex_notEnoughNodes_assertionError() {
+        val assertion = getGlanceNodeAssertionCollectionFor(
+            onAllNodesMatcher = hasClickAction(),
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "some text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another substring"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "yet another substring text"
+                    modifier = ActionModifier(LambdaAction("3") {})
+                })
+            }
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            assertion
+                .filter(hasText("text"))
+                .assertCountEquals(2)
+                .get(index = 3) // index out of bounds.
+                .assert(hasTextEqualTo("yet another substring text"))
+        }
+
+        assertThat(assertionError).hasMessageThat().contains(
+            "Failed to assert condition: " +
+                "(text == 'yet another substring text' (ignoreCase: 'false'))" +
+                "\nReason: Not enough node(s) matching condition: " +
+                "((has click action).filter(contains text 'text' " +
+                "(ignoreCase: 'false') as substring)) " +
+                "to get node at index '4'. Found '2' matching node(s)"
+        )
+    }
+
+    @Test
+    fun assertOnChildren_multipleChildren() {
+        val assertion = getGlanceNodeAssertionFor(
+            onNodeMatcher = hasTestTag("test-list"),
+            emittable = EmittableColumn().apply {
+                modifier = GlanceModifier.semantics { testTag = "test-list" }
+                children.add(EmittableText().apply {
+                    text = "some text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableText().apply {
+                    text = "another substring"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+            }
+        )
+
+        assertion
+            .onChildren()
+            .assertCountEquals(2)
+            .assertAll(hasClickAction())
+    }
+
+    @Test
+    fun assertOnChildren_noChildren() {
+        val assertion = getGlanceNodeAssertionFor(
+            onNodeMatcher = hasTestTag("test-list"),
+            emittable = EmittableColumn().apply {
+                modifier = GlanceModifier.semantics { testTag = "test-list" }
+            }
+        )
+
+        assertion.onChildren().assertCountEquals(0)
+    }
+
+    @Test
+    fun assertAnyOnChildren_noChildren_assertionError() {
+        val assertion = getGlanceNodeAssertionFor(
+            onNodeMatcher = hasTestTag("test-list"),
+            emittable = EmittableColumn().apply {
+                modifier = GlanceModifier.semantics { testTag = "test-list" }
+            }
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            assertion.onChildren().assertCountEquals(1)
+        }
+
+        assertThat(assertionError).hasMessageThat().contains(
+            "Failed to assert count of nodes" +
+                "\nReason: Expected '1' node(s) matching condition: " +
+                "(TestTag = 'test-list').children(), but found '0'"
+        )
+    }
+
+    @Test
+    fun filterOnChildren() {
+        val assertion = getGlanceNodeAssertionFor(
+            onNodeMatcher = hasTestTag("test-list"),
+            emittable = EmittableColumn().apply {
+                modifier = GlanceModifier.semantics { testTag = "test-list" }
+                children.add(EmittableText().apply {
+                    text = "some text"
+                    modifier = ActionModifier(LambdaAction("1") {})
+                })
+                children.add(EmittableText().apply {
+                    text = "another substring"
+                    modifier = ActionModifier(LambdaAction("2") {})
+                })
+            }
+        )
+
+        assertion
+            .onChildren()
+            .filter(hasText("substring"))
+            .assertCountEquals(1)
+    }
+
+    @Test
+    fun getIndexOnChildren() {
+        val assertion = getGlanceNodeAssertionFor(
+            onNodeMatcher = hasTestTag("test-list"),
+            emittable = EmittableColumn().apply {
+                modifier = GlanceModifier.semantics { testTag = "test-list" }
+                children.add(EmittableText().apply { text = "text-1" })
+                children.add(EmittableText().apply { text = "text-2" })
+            }
+        )
+
+        assertion
+            .onChildren()
+            .get(index = 0)
+            .assertHasText("text-1")
+    }
+}
diff --git a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/GlanceNodeAssertionTest.kt b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/GlanceNodeAssertionTest.kt
index c27c769..6718627 100644
--- a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/GlanceNodeAssertionTest.kt
+++ b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/GlanceNodeAssertionTest.kt
@@ -21,8 +21,7 @@
 import androidx.glance.layout.EmittableSpacer
 import androidx.glance.semantics.semantics
 import androidx.glance.semantics.testTag
-import androidx.glance.testing.unit.GlanceMappedNode
-import androidx.glance.testing.unit.MappedNode
+import androidx.glance.testing.unit.getGlanceNodeAssertionFor
 import androidx.glance.testing.unit.hasTestTag
 import androidx.glance.testing.unit.hasText
 import androidx.glance.text.EmittableText
@@ -35,22 +34,17 @@
 class GlanceNodeAssertionTest {
     @Test
     fun assertExists_success() {
-        val testContext = TestContext<MappedNode, GlanceMappedNode>()
-        // set root node of test tree to be traversed
-        testContext.rootGlanceNode = GlanceMappedNode(
-            EmittableColumn().apply {
+        // This is the object that in real usage a rule.onNode(matcher) would return.
+        val assertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
                 children.add(EmittableText().apply { text = "some text" })
                 children.add(EmittableSpacer())
                 children.add(EmittableText().apply {
                     text = "another text"
                     modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
                 })
-            }
-        )
-        // This is the object that in real usage a rule.onNode(matcher) would return.
-        val assertion = GlanceNodeAssertion(
-            matcher = hasTestTag(testTag = "existing-test-tag"),
-            testContext = testContext
+            },
+            onNodeMatcher = hasTestTag(testTag = "existing-test-tag"),
         )
 
         assertion.assertExists()
@@ -59,23 +53,16 @@
 
     @Test
     fun assertExists_error() {
-        val testContext = TestContext<MappedNode, GlanceMappedNode>()
-        // set root node of test tree to be traversed
-        testContext.rootGlanceNode =
-            GlanceMappedNode(
-                EmittableColumn().apply {
-                    children.add(EmittableText().apply { text = "some text" })
-                    children.add(EmittableSpacer())
-                    children.add(EmittableText().apply {
-                        text = "another text"
-                        modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
-                    })
-                }
-            )
-        // This is the object that in real usage a rule.onNode(matcher) would return.
-        val assertion = GlanceNodeAssertion(
-            matcher = hasTestTag(testTag = "non-existing-test-tag"),
-            testContext = testContext
+        val assertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply { text = "some text" })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag(testTag = "non-existing-test-tag")
         )
 
         val assertionError = assertThrows(AssertionError::class.java) {
@@ -91,24 +78,16 @@
 
     @Test
     fun assertDoesNotExist_success() {
-        val testContext = TestContext<MappedNode, GlanceMappedNode>()
-        // set root node of test tree to be traversed
-        testContext.rootGlanceNode =
-            GlanceMappedNode(
-                EmittableColumn().apply {
-                    children.add(EmittableText().apply { text = "some text" })
-                    children.add(EmittableSpacer())
-                    children.add(EmittableText().apply {
-                        text = "another text"
-                        modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
-                    })
-                }
-            )
-
-        // This is the object that in real usage a rule.onNode(matcher) would return.
-        val assertion = GlanceNodeAssertion(
-            matcher = hasTestTag(testTag = "non-existing-test-tag"),
-            testContext = testContext
+        val assertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply { text = "some text" })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag(testTag = "non-existing-test-tag")
         )
 
         assertion.assertDoesNotExist()
@@ -117,23 +96,16 @@
 
     @Test
     fun assertDoesNotExist_error() {
-        val testContext = TestContext<MappedNode, GlanceMappedNode>()
-        // set root node of test tree to be traversed
-        testContext.rootGlanceNode =
-            GlanceMappedNode(
-                EmittableColumn().apply {
-                    children.add(EmittableText().apply { text = "some text" })
-                    children.add(EmittableSpacer())
-                    children.add(EmittableText().apply {
-                        text = "another text"
-                        modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
-                    })
-                }
-            )
-        // This is the object that in real usage a rule.onNode(matcher) would return.
-        val assertion = GlanceNodeAssertion(
-            matcher = hasTestTag(testTag = "existing-test-tag"),
-            testContext = testContext
+        val assertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply { text = "some text" })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag(testTag = "existing-test-tag")
         )
 
         val assertionError = assertThrows(AssertionError::class.java) {
@@ -149,23 +121,16 @@
 
     @Test
     fun assert_withMatcher_success() {
-        val testContext = TestContext<MappedNode, GlanceMappedNode>()
-        // set root node of test tree to be traversed
-        testContext.rootGlanceNode =
-            GlanceMappedNode(
-                EmittableColumn().apply {
-                    children.add(EmittableText().apply { text = "some text" })
-                    children.add(EmittableSpacer())
-                    children.add(EmittableText().apply {
-                        text = "another text"
-                        modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
-                    })
-                }
-            )
-        // This is the object that in real usage a rule.onNode(matcher) would return.
-        val assertion = GlanceNodeAssertion(
-            matcher = hasTestTag(testTag = "existing-test-tag"),
-            testContext = testContext
+        val assertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply { text = "some text" })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag(testTag = "existing-test-tag")
         )
 
         assertion.assert(hasText(text = "another text"))
@@ -174,24 +139,16 @@
 
     @Test
     fun chainAssertions() {
-        val testContext = TestContext<MappedNode, GlanceMappedNode>()
-        // set root node of test tree to be traversed
-        testContext.rootGlanceNode =
-            GlanceMappedNode(
-                MappedNode(
-                    EmittableColumn().apply {
-                        children.add(EmittableText().apply { text = "some text" })
-                        children.add(EmittableSpacer())
-                        children.add(EmittableText().apply {
-                            text = "another text"
-                            modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
-                        })
-                    })
-            )
-        // This is the object that in real usage a rule.onNode(matcher) would return.
-        val assertion = GlanceNodeAssertion(
-            matcher = hasTestTag(testTag = "existing-test-tag"),
-            testContext = testContext
+        val assertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply { text = "some text" })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag(testTag = "existing-test-tag")
         )
 
         assertion
@@ -202,25 +159,16 @@
 
     @Test
     fun chainAssertion_failureInFirst() {
-        val testContext = TestContext<MappedNode, GlanceMappedNode>()
-        // set root node of test tree to be traversed
-        testContext.rootGlanceNode =
-            GlanceMappedNode(
-                MappedNode(
-                    EmittableColumn().apply {
-                        children.add(EmittableText().apply { text = "some text" })
-                        children.add(EmittableSpacer())
-                        children.add(EmittableText().apply {
-                            text = "another text"
-                            modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
-                        })
-                    }
-                )
-            )
-        // This is the object that in real usage a rule.onNode(matcher) would return.
-        val assertion = GlanceNodeAssertion(
-            matcher = hasTestTag(testTag = "existing-test-tag"),
-            testContext = testContext
+        val assertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply { text = "some text" })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag(testTag = "existing-test-tag")
         )
 
         val assertionError = assertThrows(AssertionError::class.java) {
@@ -240,25 +188,16 @@
 
     @Test
     fun chainAssertion_failureInSecond() {
-        val testContext = TestContext<MappedNode, GlanceMappedNode>()
-        // set root node of test tree to be traversed
-        testContext.rootGlanceNode =
-            GlanceMappedNode(
-                MappedNode(
-                    EmittableColumn().apply {
-                        children.add(EmittableText().apply { text = "some text" })
-                        children.add(EmittableSpacer())
-                        children.add(EmittableText().apply {
-                            text = "another text"
-                            modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
-                        })
-                    }
-                )
-            )
-        // This is the object that in real usage a rule.onNode(matcher) would return.
-        val assertion = GlanceNodeAssertion(
-            matcher = hasTestTag(testTag = "existing-test-tag"),
-            testContext = testContext
+        val assertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply { text = "some text" })
+                children.add(EmittableSpacer())
+                children.add(EmittableText().apply {
+                    text = "another text"
+                    modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag(testTag = "existing-test-tag")
         )
 
         val assertionError = assertThrows(AssertionError::class.java) {
@@ -271,7 +210,7 @@
             .hasMessageThat()
             .startsWith(
                 "Failed to assert condition: " +
-                    "(has text = 'non-existing text' (ignoreCase: 'false'))" +
+                    "(contains text 'non-existing text' (ignoreCase: 'false') as substring)" +
                     "\nGlance Node:"
             )
     }
diff --git a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/GlanceMappedNodeFiltersAndMatcherTest.kt b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/GlanceMappedNodeFiltersAndMatcherTest.kt
deleted file mode 100644
index 2fb3c52..0000000
--- a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/GlanceMappedNodeFiltersAndMatcherTest.kt
+++ /dev/null
@@ -1,207 +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.glance.testing.unit
-
-import androidx.glance.GlanceModifier
-import androidx.glance.layout.EmittableColumn
-import androidx.glance.semantics.semantics
-import androidx.glance.semantics.testTag
-import androidx.glance.text.EmittableText
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-
-class GlanceMappedNodeFiltersAndMatcherTest {
-    @Test
-    fun matchAny_match_returnsTrue() {
-        val node1 = GlanceMappedNode(
-            EmittableText().apply {
-                text = "node1"
-            }
-        )
-        val node2 = GlanceMappedNode(
-            EmittableColumn().apply {
-                modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
-                EmittableText().apply { text = "node2" }
-            }
-        )
-
-        val result = hasTestTag("existing-test-tag").matchesAny(listOf(node1, node2))
-
-        assertThat(result).isTrue()
-    }
-
-    @Test
-    fun matchAny_noMatch_returnsFalse() {
-        val node1 = GlanceMappedNode(
-            EmittableText().apply {
-                text = "node1"
-            }
-        )
-        val node2 = GlanceMappedNode(
-            EmittableColumn().apply {
-                EmittableText().apply {
-                    text = "node2"
-                    // this won't be inspected, as EmittableColumn node is being run against
-                    // matcher, not its children
-                    modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
-                }
-            }
-        )
-
-        val result = hasTestTag("existing-test-tag").matchesAny(listOf(node1, node2))
-
-        assertThat(result).isFalse()
-    }
-
-    @Test
-    fun hasTestTag_match_returnsTrue() {
-        // a single node that will be matched against matcher returned by the filter under test
-        val testSingleNode = GlanceMappedNode(
-            EmittableText().apply {
-                text = "some text"
-                modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
-            }
-        )
-
-        val result = hasTestTag("existing-test-tag").matches(testSingleNode)
-
-        assertThat(result).isTrue()
-    }
-
-    @Test
-    fun hasTestTag_noMatch_returnsFalse() {
-        val testSingleNode = GlanceMappedNode(
-            EmittableText().apply {
-                text = "some text"
-                modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
-            }
-        )
-
-        val result = hasTestTag("non-existing-test-tag").matches(testSingleNode)
-
-        assertThat(result).isFalse()
-    }
-
-    @Test
-    fun hasText_match_returnsTrue() {
-        val testSingleNode = GlanceMappedNode(
-            EmittableText().apply {
-                text = "existing text"
-            }
-        )
-
-        val result = hasText("existing text").matches(testSingleNode)
-
-        assertThat(result).isTrue()
-    }
-
-    @Test
-    fun hasText_noMatch_returnsFalse() {
-        val testSingleNode = GlanceMappedNode(
-            EmittableText().apply {
-                text = "existing text"
-            }
-        )
-
-        val result = hasText("non-existing text").matches(testSingleNode)
-
-        assertThat(result).isFalse()
-    }
-
-    @Test
-    fun hasText_subStringMatch_returnsTrue() {
-        val testSingleNode = GlanceMappedNode(
-            EmittableText().apply {
-                text = "some existing text"
-            }
-        )
-
-        val result = hasText(text = "existing", substring = true).matches(testSingleNode)
-
-        assertThat(result).isTrue()
-    }
-
-    @Test
-    fun hasText_subStringNoMatch_returnsFalse() {
-        val testSingleNode = GlanceMappedNode(
-            EmittableText().apply {
-                text = "some existing text"
-            }
-        )
-
-        val result = hasText(text = "non-existing", substring = true).matches(testSingleNode)
-
-        assertThat(result).isFalse()
-    }
-
-    @Test
-    fun hasText_subStringCaseInsensitiveMatch_returnsTrue() {
-        val testSingleNode = GlanceMappedNode(
-            EmittableText().apply {
-                text = "some EXISTING text"
-            }
-        )
-
-        val result =
-            hasText(text = "existing", substring = true, ignoreCase = true).matches(testSingleNode)
-
-        assertThat(result).isTrue()
-    }
-
-    @Test
-    fun hasText_subStringCaseInsensitiveNoMatch_returnsFalse() {
-        val testSingleNode = GlanceMappedNode(
-            EmittableText().apply {
-                text = "some EXISTING text"
-            }
-        )
-
-        val result =
-            hasText(text = "non-EXISTING", substring = true, ignoreCase = true)
-                .matches(testSingleNode)
-
-        assertThat(result).isFalse()
-    }
-
-    @Test
-    fun hasText_caseInsensitiveMatch_returnsTrue() {
-        val testSingleNode = GlanceMappedNode(
-            EmittableText().apply {
-                text = "some EXISTING text"
-            }
-        )
-
-        val result =
-            hasText(text = "SOME existing TEXT", ignoreCase = true).matches(testSingleNode)
-
-        assertThat(result).isTrue()
-    }
-
-    @Test
-    fun hasText_caseInsensitiveNoMatch_returnsFalse() {
-        val testSingleNode = GlanceMappedNode(
-            EmittableText().apply {
-                text = "some EXISTING text"
-            }
-        )
-
-        val result =
-            hasText(text = "SOME non-existing TEXT", ignoreCase = true).matches(testSingleNode)
-
-        assertThat(result).isFalse()
-    }
-}
diff --git a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/GlanceMappedNodeMatcherTest.kt b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/GlanceMappedNodeMatcherTest.kt
new file mode 100644
index 0000000..880393f
--- /dev/null
+++ b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/GlanceMappedNodeMatcherTest.kt
@@ -0,0 +1,333 @@
+/*
+ * 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.glance.testing.unit
+
+import androidx.glance.GlanceModifier
+import androidx.glance.layout.EmittableColumn
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.text.EmittableText
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class GlanceMappedNodeMatcherTest {
+    @Test
+    fun matchAny_match_returnsTrue() {
+        val node1 = GlanceMappedNode(
+            EmittableText().apply {
+                text = "node1"
+            }
+        )
+        val node2 = GlanceMappedNode(
+            EmittableColumn().apply {
+                modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+                EmittableText().apply { text = "node2" }
+            }
+        )
+
+        val result = hasTestTag("existing-test-tag").matchesAny(listOf(node1, node2))
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun matchAny_noMatch_returnsFalse() {
+        val node1 = GlanceMappedNode(
+            EmittableText().apply {
+                text = "node1"
+            }
+        )
+        val node2 = GlanceMappedNode(
+            EmittableColumn().apply {
+                children += EmittableText().apply {
+                    text = "node2"
+                    // this won't be inspected, as EmittableColumn node is being run against
+                    // matcher, not its children
+                    modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+                }
+            }
+        )
+
+        val result = hasTestTag("existing-test-tag").matchesAny(listOf(node1, node2))
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun andBetweenMatchers_match_returnsTrue() {
+        val node = GlanceMappedNode(
+            EmittableText().apply {
+                text = "node"
+                modifier = GlanceModifier.semantics { testTag = "test-tag" }
+            }
+        )
+
+        val result = (hasTestTag("test-tag") and hasText("node")).matches(node)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun andBetweenMatchers_partialMatch_returnsFalse() {
+        val node = GlanceMappedNode(
+            EmittableText().apply {
+                text = "node"
+                modifier = GlanceModifier.semantics { testTag = "test-tag" }
+            }
+        )
+
+        val result = (hasTestTag("test-tag") and hasText("non-existing-node"))
+            .matches(node)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun andBetweenMatchers_noMatch_returnsFalse() {
+        val node = GlanceMappedNode(
+            EmittableText().apply {
+                text = "node"
+                modifier = GlanceModifier.semantics { testTag = "test-tag" }
+            }
+        )
+
+        val result = (hasTestTag("non-existing") and hasText("non-existing-node"))
+            .matches(node)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun orBetweenMatchers_bothMatch_returnsTrue() {
+        val node = GlanceMappedNode(
+            EmittableText().apply {
+                text = "node"
+                modifier = GlanceModifier.semantics { testTag = "test-tag" }
+            }
+        )
+
+        val result = (hasTestTag("test-tag") or hasText("node"))
+            .matches(node)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun orBetweenMatchers_secondMatch_returnsTrue() {
+        val node = GlanceMappedNode(
+            EmittableText().apply {
+                text = "node"
+                modifier = GlanceModifier.semantics { testTag = "test-tag" }
+            }
+        )
+
+        val result = (hasTestTag("non-existing-tag") or hasText("node"))
+            .matches(node)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun orBetweenMatchers_noneMatch_returnsFalse() {
+        val node = GlanceMappedNode(
+            EmittableText().apply {
+                text = "node"
+                modifier = GlanceModifier.semantics { testTag = "test-tag" }
+            }
+        )
+
+        val result = (hasTestTag("non-existing-tag") or hasText("non-existing-node"))
+            .matches(node)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun not_match_returnsTrue() {
+        val node = GlanceMappedNode(
+            EmittableText().apply {
+                text = "node"
+                modifier = GlanceModifier.semantics { testTag = "test-tag" }
+            }
+        )
+
+        val result = (!hasTestTag("non-existing-test-tag")).matches(node)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun not_noMatch_returnsFalse() {
+        val node = GlanceMappedNode(
+            EmittableText().apply {
+                text = "node"
+                modifier = GlanceModifier.semantics { testTag = "test-tag" }
+            }
+        )
+
+        val result = (!hasTestTag("test-tag")).matches(node)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun hasTestTag_match_returnsTrue() {
+        // a single node that will be matched against matcher returned by the filter under test
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some text"
+                modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+            }
+        )
+
+        val result = hasTestTag("existing-test-tag").matches(testSingleNode)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun hasTestTag_noMatch_returnsFalse() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some text"
+                modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+            }
+        )
+
+        val result = hasTestTag("non-existing-test-tag").matches(testSingleNode)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun hasTextEqualTo_match_returnsTrue() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "existing text"
+            }
+        )
+
+        val result = hasTextEqualTo("existing text").matches(testSingleNode)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun hasTextEqualTo_noMatch_returnsFalse() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "existing text"
+            }
+        )
+
+        val result = hasTextEqualTo("non-existing text").matches(testSingleNode)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun hasTextEqualTo_caseInsensitiveMatch_returnsTrue() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some EXISTING text"
+            }
+        )
+
+        val result =
+            hasTextEqualTo(
+                text = "SOME existing TEXT",
+                ignoreCase = true
+            ).matches(testSingleNode)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun hasTextEqualTo_caseInsensitiveButNoMatch_returnsFalse() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some EXISTING text"
+            }
+        )
+
+        val result =
+            hasTextEqualTo(
+                text = "SOME non-existing TEXT",
+                ignoreCase = true
+            ).matches(testSingleNode)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun hasText_match_returnsTrue() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some existing text"
+            }
+        )
+
+        val result = hasText("existing").matches(testSingleNode)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun hasText_noMatch_returnsFalse() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some existing text"
+            }
+        )
+
+        val result = hasText("non-existing").matches(testSingleNode)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun hasText_insensitiveMatch_returnsTrue() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some EXISTING text"
+            }
+        )
+
+        val result = hasText(
+            text = "existing",
+            ignoreCase = true
+        ).matches(testSingleNode)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun hasText_caseInsensitiveButNoMatch_returnsFalse() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some EXISTING text"
+            }
+        )
+
+        val result = hasText(
+            text = "non-EXISTING",
+            ignoreCase = true
+        ).matches(testSingleNode)
+
+        assertThat(result).isFalse()
+    }
+}
diff --git a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/UnitTestAssertionExtensionsTest.kt b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/UnitTestAssertionExtensionsTest.kt
index b404ec7..d4800b2 100644
--- a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/UnitTestAssertionExtensionsTest.kt
+++ b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/UnitTestAssertionExtensionsTest.kt
@@ -30,6 +30,162 @@
 // and relevant to unit tests
 class UnitTestAssertionExtensionsTest {
     @Test
+    fun assertHasTextEqualTo_matching() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics { testTag = "test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        nodeAssertion.assertHasTextEqualTo("test text")
+    }
+
+    @Test
+    fun assertHasTextEqualTo_ignoreCase_matching() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics { testTag = "test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        nodeAssertion.assertHasTextEqualTo(text = "TEST TEXT", ignoreCase = true)
+    }
+
+    @Test
+    fun assertHasTextEqualTo_notMatching_assertionError() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics { testTag = "test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            nodeAssertion.assertHasTextEqualTo("non-existing text")
+        }
+
+        assertThat(assertionError)
+            .hasMessageThat()
+            .contains(
+                "Failed to assert condition: " +
+                    "(text == 'non-existing text' (ignoreCase: 'false'))"
+            )
+    }
+
+    @Test
+    fun assertHasTextEqualTo_ignoreCaseAndNotMatching_assertionError() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics { testTag = "test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            nodeAssertion.assertHasTextEqualTo("NON-EXISTING TEXT", ignoreCase = true)
+        }
+
+        assertThat(assertionError)
+            .hasMessageThat()
+            .contains(
+                "Failed to assert condition: " +
+                    "(text == 'NON-EXISTING TEXT' (ignoreCase: 'true'))"
+            )
+    }
+
+    @Test
+    fun assertHasText_matching() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics { testTag = "test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        nodeAssertion.assertHasText(text = "text")
+    }
+
+    @Test
+    fun assertHasText_ignoreCase_matching() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics { testTag = "test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        nodeAssertion.assertHasText(text = "TEXT", ignoreCase = true)
+    }
+
+    @Test
+    fun assertHasText_notMatching_assertionError() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics { testTag = "test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            nodeAssertion.assertHasText("non-existing")
+        }
+
+        assertThat(assertionError)
+            .hasMessageThat()
+            .contains(
+                "Failed to assert condition: " +
+                    "(contains text 'non-existing' (ignoreCase: 'false') as substring)"
+            )
+    }
+
+    @Test
+    fun assertHasText_ignoreCaseAndNotMatching_assertionError() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics { testTag = "test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            nodeAssertion.assertHasText(text = "NON-EXISTING", ignoreCase = true)
+        }
+
+        assertThat(assertionError)
+            .hasMessageThat()
+            .contains(
+                "Failed to assert condition: " +
+                    "(contains text 'NON-EXISTING' (ignoreCase: 'true') as substring)"
+            )
+    }
+
+    @Test
     fun assertHasTestTag_matching() {
         val nodeAssertion = getGlanceNodeAssertionFor(
             emittable = EmittableColumn().apply {
@@ -66,6 +222,99 @@
     }
 
     @Test
+    fun assertHasContentDescriptionEqualTo_matching() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics {
+                        testTag = "test-tag"
+                        contentDescription = "test text description"
+                    }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        nodeAssertion.assertHasContentDescriptionEqualTo("test text description")
+    }
+
+    @Test
+    fun assertHasContentDescriptionEqualTo_ignoreCaseMatching() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics {
+                        testTag = "test-tag"
+                        contentDescription = "test text description"
+                    }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        nodeAssertion.assertHasContentDescriptionEqualTo(
+            value = "TEST TEXT DESCRIPTION",
+            ignoreCase = true
+        )
+    }
+
+    @Test
+    fun assertHasContentDescriptionEqualTo_notMatching_assertionError() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics { testTag = "test-tag" }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            nodeAssertion.assertHasContentDescriptionEqualTo("text description")
+        }
+
+        assertThat(assertionError)
+            .hasMessageThat()
+            .contains(
+                "Failed to assert condition: " +
+                    "(ContentDescription == 'text description' (ignoreCase: 'false'))"
+            )
+    }
+
+    @Test
+    fun assertHasContentDescriptionEqualTo_ignoreCaseAndNotMatching_assertionError() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics {
+                        testTag = "test-tag"
+                        contentDescription = "test text description"
+                    }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            nodeAssertion.assertHasContentDescriptionEqualTo(
+                value = "TEST DESCRIPTION",
+                ignoreCase = true
+            )
+        }
+
+        assertThat(assertionError)
+            .hasMessageThat()
+            .contains(
+                "Failed to assert condition: " +
+                    "(ContentDescription == 'TEST DESCRIPTION' (ignoreCase: 'true'))"
+            )
+    }
+
+    @Test
     fun assertHasContentDescription_matching() {
         val nodeAssertion = getGlanceNodeAssertionFor(
             emittable = EmittableColumn().apply {
@@ -80,7 +329,28 @@
             onNodeMatcher = hasTestTag("test-tag")
         )
 
-        nodeAssertion.assertHasContentDescription("test text description")
+        nodeAssertion.assertHasContentDescription("text")
+    }
+
+    @Test
+    fun assertHasContentDescription_ignoreCaseMatching() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics {
+                        testTag = "test-tag"
+                        contentDescription = "test text description"
+                    }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        nodeAssertion.assertHasContentDescription(
+            value = "TEXT",
+            ignoreCase = true
+        )
     }
 
     @Test
@@ -103,7 +373,39 @@
             .hasMessageThat()
             .contains(
                 "Failed to assert condition: " +
-                    "(ContentDescription = 'test text description' (ignoreCase: 'false'))"
+                    "(ContentDescription contains 'test text description' " +
+                    "(ignoreCase: 'false') as substring)"
+            )
+    }
+
+    @Test
+    fun assertHasContentDescription_ignoreCaseAndNotMatching_assertionError() {
+        val nodeAssertion = getGlanceNodeAssertionFor(
+            emittable = EmittableColumn().apply {
+                children.add(EmittableText().apply {
+                    text = "test text"
+                    modifier = GlanceModifier.semantics {
+                        testTag = "test-tag"
+                        contentDescription = "text"
+                    }
+                })
+            },
+            onNodeMatcher = hasTestTag("test-tag")
+        )
+
+        val assertionError = assertThrows(AssertionError::class.java) {
+            nodeAssertion.assertHasContentDescription(
+                value = "TEXT DESCRIPTION",
+                ignoreCase = true
+            )
+        }
+
+        assertThat(assertionError)
+            .hasMessageThat()
+            .contains(
+                "Failed to assert condition: " +
+                    "(ContentDescription contains 'TEXT DESCRIPTION' (ignoreCase: 'true') " +
+                    "as substring)"
             )
     }
 }
diff --git a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/UnitTestFiltersTest.kt b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/UnitTestFiltersTest.kt
new file mode 100644
index 0000000..69aa1a9
--- /dev/null
+++ b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/UnitTestFiltersTest.kt
@@ -0,0 +1,222 @@
+/*
+ * 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.glance.testing.unit
+
+import androidx.glance.GlanceModifier
+import androidx.glance.layout.EmittableColumn
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.text.EmittableText
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class UnitTestFiltersTest {
+    @Test
+    fun hasTestTag_match_returnsTrue() {
+        // a single node that will be matched against matcher returned by the filter under test
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some text"
+                modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+            }
+        )
+
+        val result = hasTestTag("existing-test-tag").matches(testSingleNode)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun hasTestTag_noMatch_returnsFalse() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some text"
+                modifier = GlanceModifier.semantics { testTag = "existing-test-tag" }
+            }
+        )
+
+        val result = hasTestTag("non-existing-test-tag").matches(testSingleNode)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun hasTextEqualTo_match_returnsTrue() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "existing text"
+            }
+        )
+
+        val result = hasTextEqualTo("existing text").matches(testSingleNode)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun hasTextEqualTo_noMatch_returnsFalse() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "existing text"
+            }
+        )
+
+        val result = hasTextEqualTo("non-existing text").matches(testSingleNode)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun hasTextEqualTo_caseInsensitiveMatch_returnsTrue() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some EXISTING text"
+            }
+        )
+
+        val result =
+            hasTextEqualTo(
+                text = "SOME existing TEXT",
+                ignoreCase = true
+            ).matches(testSingleNode)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun hasTextEqualTo_caseInsensitiveButNoMatch_returnsFalse() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some EXISTING text"
+            }
+        )
+
+        val result =
+            hasTextEqualTo(
+                text = "SOME non-existing TEXT",
+                ignoreCase = true
+            ).matches(testSingleNode)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun hasText_match_returnsTrue() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some existing text"
+            }
+        )
+
+        val result = hasText("existing").matches(testSingleNode)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun hasText_noMatch_returnsFalse() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some existing text"
+            }
+        )
+
+        val result = hasText("non-existing").matches(testSingleNode)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun hasText_insensitiveMatch_returnsTrue() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some EXISTING text"
+            }
+        )
+
+        val result = hasText(
+            text = "existing",
+            ignoreCase = true
+        ).matches(testSingleNode)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun hasText_caseInsensitiveButNoMatch_returnsFalse() {
+        val testSingleNode = GlanceMappedNode(
+            EmittableText().apply {
+                text = "some EXISTING text"
+            }
+        )
+
+        val result = hasText(
+            text = "non-EXISTING",
+            ignoreCase = true
+        ).matches(testSingleNode)
+
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun hasAnyDescendant_match_returnsTrue() {
+        val testNode = GlanceMappedNode(
+            EmittableColumn().apply {
+                children += EmittableText().apply {
+                    text = "node1"
+                }
+                children += EmittableColumn().apply {
+                    children += EmittableText().apply {
+                        text = "node2-a"
+                    }
+                    children += EmittableText().apply {
+                        text = "node2-b"
+                    }
+                }
+            }
+        )
+
+        val result =
+            hasAnyDescendant(hasText("node2-b")).matches(testNode)
+
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun hasAnyDescendant_noMatch_returnsFalse() {
+        val testNode = GlanceMappedNode(
+            EmittableColumn().apply {
+                children += EmittableText().apply {
+                    text = "node1"
+                }
+                children += EmittableColumn().apply {
+                    children += EmittableText().apply {
+                        text = "node2-a"
+                    }
+                    children += EmittableText().apply {
+                        text = "node2-b"
+                    }
+                }
+            }
+        )
+
+        val result =
+            hasAnyDescendant(hasText("node3-a")).matches(testNode)
+
+        assertThat(result).isFalse()
+    }
+}
diff --git a/glance/glance-wear-tiles-preview/lint-baseline.xml b/glance/glance-wear-tiles-preview/lint-baseline.xml
new file mode 100644
index 0000000..d2965f7
--- /dev/null
+++ b/glance/glance-wear-tiles-preview/lint-baseline.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="                .all { it }"
+        errorLine2="                 ~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/preview/ComposableInvoker.kt"/>
+    </issue>
+
+</issues>
diff --git a/glance/glance-wear-tiles-preview/src/main/java/androidx/glance/wear/tiles/preview/ComposableInvoker.kt b/glance/glance-wear-tiles-preview/src/main/java/androidx/glance/wear/tiles/preview/ComposableInvoker.kt
index dcfe376..2105f3c 100644
--- a/glance/glance-wear-tiles-preview/src/main/java/androidx/glance/wear/tiles/preview/ComposableInvoker.kt
+++ b/glance/glance-wear-tiles-preview/src/main/java/androidx/glance/wear/tiles/preview/ComposableInvoker.kt
@@ -33,7 +33,6 @@
      * Returns true if the [methodTypes] and [actualTypes] are compatible. This means that every
      * `actualTypes[n]` are assignable to `methodTypes[n]`.
      */
-    @Suppress("ListIterator")
     private fun compatibleTypes(
         methodTypes: Array<Class<*>>,
         actualTypes: Array<Class<*>>
diff --git a/glance/glance-wear-tiles/lint-baseline.xml b/glance/glance-wear-tiles/lint-baseline.xml
new file mode 100644
index 0000000..5a135eb
--- /dev/null
+++ b/glance/glance-wear-tiles/lint-baseline.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        curvedChildList.forEach { composable ->"
+        errorLine2="                        ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/curved/CurvedRow.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        it.children.addAll(children.map { it.copy() })"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/curved/CurvedRow.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        it.children.addAll(children.map { it.copy() })"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/curved/CurvedRow.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    children.mapIndexed { index, child ->"
+        errorLine2="             ~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/NormalizeCompositionTree.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    toDelete.forEach {"
+        errorLine2="             ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/NormalizeCompositionTree.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            children.forEach { child ->"
+        errorLine2="                     ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/NormalizeCompositionTree.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        textList.forEach { item ->"
+        errorLine2="                 ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            .setContentDescription(it.joinToString())"
+        errorLine2="                                      ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            .setContentDescription(it.joinToString())"
+        errorLine2="                                      ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            element.children.forEach {"
+        errorLine2="                             ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="                element.children.forEach {"
+        errorLine2="                                 ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="                element.children.forEach {"
+        errorLine2="                                 ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    element.children.forEach { curvedChild ->"
+        errorLine2="                     ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            curvedChild.children.forEach {"
+        errorLine2="                                 ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt"/>
+    </issue>
+
+</issues>
diff --git a/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/NormalizeCompositionTree.kt b/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/NormalizeCompositionTree.kt
index 8666f31..557632d 100644
--- a/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/NormalizeCompositionTree.kt
+++ b/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/NormalizeCompositionTree.kt
@@ -20,8 +20,6 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastMapIndexed
 import androidx.glance.AndroidResourceImageProvider
 import androidx.glance.BitmapImageProvider
 import androidx.glance.Emittable
@@ -55,7 +53,7 @@
 /** Transform each node in the tree. */
 private fun EmittableWithChildren.transformTree(block: (Emittable) -> Emittable?) {
     val toDelete = mutableListOf<Int>()
-    children.fastMapIndexed { index, child ->
+    children.mapIndexed { index, child ->
         val newChild = block(child)
         if (newChild == null) {
             toDelete += index
@@ -65,7 +63,7 @@
         if (newChild is EmittableWithChildren) newChild.transformTree(block)
     }
     toDelete.reverse()
-    toDelete.fastForEach {
+    toDelete.forEach {
         children.removeAt(it)
     }
 }
@@ -88,7 +86,7 @@
     return when (this) {
         is EmittableWithChildren -> {
             modifier = GlanceModifier.then(WidthModifier(width)).then(HeightModifier(height))
-            children.fastForEach { child ->
+            children.forEach { child ->
                 val visibility =
                     child.modifier.findModifier<VisibilityModifier>()?.visibility
                         ?: Visibility.Visible
diff --git a/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt b/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt
index 99565c4..72071c9 100644
--- a/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt
+++ b/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
+@file:Suppress("deprecation")
 package androidx.glance.wear.tiles
 
 import android.content.Context
@@ -23,8 +23,6 @@
 import android.view.ViewGroup
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastJoinToString
 import androidx.glance.AndroidResourceImageProvider
 import androidx.glance.BackgroundModifier
 import androidx.glance.BitmapImageProvider
@@ -78,6 +76,7 @@
 import androidx.glance.wear.tiles.curved.SweepAngleModifier
 import androidx.glance.wear.tiles.curved.ThicknessModifier
 import androidx.glance.wear.tiles.curved.findModifier
+import androidx.wear.tiles.ColorBuilders
 import java.io.ByteArrayOutputStream
 import java.util.Arrays
 
@@ -128,12 +127,13 @@
 @Suppress("deprecation") // for backward compatibility
 private fun BackgroundModifier.toProto(
     context: Context
-): androidx.wear.tiles.ModifiersBuilders.Background? =
-    this.colorProvider?.let { provider ->
+): androidx.wear.tiles.ModifiersBuilders.Background? = when (this) {
+    is BackgroundModifier.Color ->
         androidx.wear.tiles.ModifiersBuilders.Background.Builder()
-            .setColor(androidx.wear.tiles.ColorBuilders.argb(provider.getColorAsArgb(context)))
+            .setColor(ColorBuilders.argb(this.colorProvider.getColorAsArgb(context)))
             .build()
-    }
+    else -> error("Unexpected modifier $this")
+}
 
 @Suppress("deprecation") // for backward compatibility
 private fun BorderModifier.toProto(context: Context): androidx.wear.tiles.ModifiersBuilders.Border =
@@ -148,7 +148,7 @@
 private fun SemanticsModifier.toProto(): androidx.wear.tiles.ModifiersBuilders.Semantics? =
     this.configuration.getOrNull(SemanticsProperties.ContentDescription)?.let {
         androidx.wear.tiles.ModifiersBuilders.Semantics.Builder()
-            .setContentDescription(it.fastJoinToString())
+            .setContentDescription(it.joinToString())
             .build()
     }
 
@@ -156,7 +156,7 @@
 private fun SemanticsCurvedModifier.toProto(): androidx.wear.tiles.ModifiersBuilders.Semantics? =
     this.configuration.getOrNull(SemanticsProperties.ContentDescription)?.let {
         androidx.wear.tiles.ModifiersBuilders.Semantics.Builder()
-            .setContentDescription(it.fastJoinToString())
+            .setContentDescription(it.joinToString())
             .build()
     }
 
@@ -331,7 +331,7 @@
         .setWidth(element.modifier.getWidth(context).toContainerDimension())
         .setHeight(element.modifier.getHeight(context).toContainerDimension())
         .also { box ->
-            element.children.fastForEach {
+            element.children.forEach {
                 box.addContent(translateComposition(context, resourceBuilder, it))
             }
         }
@@ -351,7 +351,7 @@
             .setHeight(height.toContainerDimension())
             .setVerticalAlignment(element.verticalAlignment.toProto())
             .also { row ->
-                element.children.fastForEach {
+                element.children.forEach {
                     row.addContent(translateComposition(context, resourceBuilder, it))
                 }
             }
@@ -391,7 +391,7 @@
             .setWidth(width.toContainerDimension())
             .setHorizontalAlignment(element.horizontalAlignment.toProto())
             .also { column ->
-                element.children.fastForEach {
+                element.children.forEach {
                     column.addContent(translateComposition(context, resourceBuilder, it))
                 }
             }
@@ -637,9 +637,9 @@
             .setVerticalAlign(element.radialAlignment.toProto())
 
     // Add all the children first...
-    element.children.fastForEach { curvedChild ->
+    element.children.forEach { curvedChild ->
         if (curvedChild is EmittableCurvedChild) {
-            curvedChild.children.fastForEach {
+            curvedChild.children.forEach {
                 arcBuilder.addContent(
                     translateEmittableElementInArc(
                         context,
diff --git a/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/curved/CurvedRow.kt b/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/curved/CurvedRow.kt
index 06e5801..58489b1 100644
--- a/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/curved/CurvedRow.kt
+++ b/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/curved/CurvedRow.kt
@@ -18,8 +18,6 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastMap
 import androidx.glance.Emittable
 import androidx.glance.EmittableWithChildren
 import androidx.glance.GlanceModifier
@@ -125,7 +123,7 @@
     curvedScopeImpl.apply(content)
 
     return {
-        curvedChildList.fastForEach { composable ->
+        curvedChildList.forEach { composable ->
             object : CurvedChildScope {}.apply { composable() }
         }
     }
@@ -157,7 +155,7 @@
         it.anchorDegrees = anchorDegrees
         it.anchorType = anchorType
         it.radialAlignment = radialAlignment
-        it.children.addAll(children.fastMap { it.copy() })
+        it.children.addAll(children.map { it.copy() })
     }
 
     override fun toString(): String =
@@ -173,7 +171,7 @@
     override fun copy(): Emittable = EmittableCurvedChild().also {
         it.modifier = modifier
         it.rotateContent = rotateContent
-        it.children.addAll(children.fastMap { it.copy() })
+        it.children.addAll(children.map { it.copy() })
     }
 
     override fun toString(): String = "EmittableCurvedChild(" +
diff --git a/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt b/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt
index f22097a..8d5016b 100644
--- a/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt
+++ b/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt
@@ -114,7 +114,6 @@
     }
 }
 
-@Suppress("ListIterator")
 @Composable
 private fun TextSection(textList: List<TemplateText>) {
     if (textList.isEmpty()) return
diff --git a/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/BackgroundTest.kt b/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/BackgroundTest.kt
index 8018dd2..6b9a108 100644
--- a/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/BackgroundTest.kt
+++ b/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/BackgroundTest.kt
@@ -19,9 +19,13 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.toArgb
 import androidx.glance.BackgroundModifier
+import androidx.glance.ColorFilter
 import androidx.glance.GlanceModifier
+import androidx.glance.ImageProvider
+import androidx.glance.TintColorFilterParams
 import androidx.glance.background
 import androidx.glance.findModifier
+import androidx.glance.unit.ColorProvider
 import androidx.glance.unit.FixedColorProvider
 import androidx.glance.unit.ResourceColorProvider
 import androidx.glance.wear.tiles.test.R
@@ -32,9 +36,10 @@
 class BackgroundTest {
     @Test
     fun canUseBackgroundModifier() {
-        val modifier = GlanceModifier.background(color = Color(0xFF223344))
+        val modifier: GlanceModifier = GlanceModifier.background(color = Color(0xFF223344))
 
-        val addedModifier = requireNotNull(modifier.findModifier<BackgroundModifier>())
+        val addedModifier: BackgroundModifier.Color =
+            requireNotNull(modifier.findModifier<BackgroundModifier.Color>())
 
         val modifierColors = addedModifier.colorProvider
         assertIs<FixedColorProvider>(modifierColors)
@@ -48,10 +53,30 @@
     fun canUseBackgroundModifier_resId() {
         val modifier = GlanceModifier.background(color = R.color.color1)
 
-        val addedModifier = requireNotNull(modifier.findModifier<BackgroundModifier>())
+        val addedModifier: BackgroundModifier.Color =
+            requireNotNull(modifier.findModifier<BackgroundModifier.Color>())
 
         val modifierColors = addedModifier.colorProvider
         assertIs<ResourceColorProvider>(modifierColors)
         assertThat(modifierColors.resId).isEqualTo(R.color.color1)
     }
+
+    @Test
+    fun canUseBackgroundModifier_colorFilteredImage() {
+        fun tintColor() = ColorProvider(Color.Magenta)
+
+        val modifier = GlanceModifier.background(
+            ImageProvider(R.drawable.oval), ColorFilter.tint(
+                tintColor()
+            )
+        )
+
+        val addedModifier: BackgroundModifier.Image =
+            requireNotNull(modifier.findModifier<BackgroundModifier.Image>())
+
+        assertThat(
+            (addedModifier.colorFilter?.colorFilterParams as TintColorFilterParams).colorProvider)
+            .isEqualTo(tintColor()
+        )
+    }
 }
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index 818bc2f..e87350d 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -2,6 +2,7 @@
 package androidx.glance {
 
   public final class BackgroundKt {
+    method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, androidx.glance.ImageProvider imageProvider, androidx.glance.ColorFilter? tint, optional int contentScale);
     method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, androidx.glance.ImageProvider imageProvider, optional int contentScale);
     method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, androidx.glance.unit.ColorProvider colorProvider);
     method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, @ColorRes int color);
@@ -128,8 +129,11 @@
 
   public final class ActionKt {
     method public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, androidx.glance.action.Action onClick);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.glance.ExperimentalGlanceApi public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, optional String? key, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, androidx.glance.action.Action onClick, optional @DrawableRes int rippleOverride);
+    method @androidx.compose.runtime.Composable public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, optional @DrawableRes int rippleOverride, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.glance.ExperimentalGlanceApi public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, optional String? key, optional @DrawableRes int rippleOverride, kotlin.jvm.functions.Function0<kotlin.Unit> block);
     method @androidx.compose.runtime.Composable public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    field @DrawableRes public static final int NoRippleOverride = 0; // 0x0
   }
 
   public abstract class ActionParameters {
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index 818bc2f..e87350d 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -2,6 +2,7 @@
 package androidx.glance {
 
   public final class BackgroundKt {
+    method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, androidx.glance.ImageProvider imageProvider, androidx.glance.ColorFilter? tint, optional int contentScale);
     method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, androidx.glance.ImageProvider imageProvider, optional int contentScale);
     method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, androidx.glance.unit.ColorProvider colorProvider);
     method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, @ColorRes int color);
@@ -128,8 +129,11 @@
 
   public final class ActionKt {
     method public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, androidx.glance.action.Action onClick);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.glance.ExperimentalGlanceApi public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, optional String? key, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, androidx.glance.action.Action onClick, optional @DrawableRes int rippleOverride);
+    method @androidx.compose.runtime.Composable public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, optional @DrawableRes int rippleOverride, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.glance.ExperimentalGlanceApi public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, optional String? key, optional @DrawableRes int rippleOverride, kotlin.jvm.functions.Function0<kotlin.Unit> block);
     method @androidx.compose.runtime.Composable public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    field @DrawableRes public static final int NoRippleOverride = 0; // 0x0
   }
 
   public abstract class ActionParameters {
diff --git a/glance/glance/build.gradle b/glance/glance/build.gradle
index a27703a..0a8e136 100644
--- a/glance/glance/build.gradle
+++ b/glance/glance/build.gradle
@@ -25,7 +25,6 @@
 
 dependencies {
 
-    api(project(":compose:ui:ui-util"))
     api("androidx.annotation:annotation:1.2.0")
     api("androidx.compose.runtime:runtime:1.2.1")
     api("androidx.compose.ui:ui-graphics:1.1.1")
diff --git a/glance/glance/lint-baseline.xml b/glance/glance/lint-baseline.xml
index 2f50602..7b47edb 100644
--- a/glance/glance/lint-baseline.xml
+++ b/glance/glance/lint-baseline.xml
@@ -20,6 +20,87 @@
     </issue>
 
     <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        it.children.addAll(children.map { it.copy() })"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/glance/layout/Box.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        it.children.addAll(children.map { it.copy() })"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/glance/layout/Column.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        children.joinToString(&quot;,\n&quot;).prependIndent(&quot;  &quot;)"
+        errorLine2="                 ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/Emittables.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            events.forEach { addAction(it) }"
+        errorLine2="                   ~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/session/IdleEventBroadcastReceiver.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="    fold(0.dp) { acc, res ->"
+        errorLine2="    ~~~~">
+        <location
+            file="src/main/java/androidx/glance/layout/Padding.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        it.children.addAll(children.map { it.copy() })"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/glance/layout/Row.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            .any { it.state == WorkInfo.State.RUNNING } &amp;&amp; synchronized(sessions) {"
+        errorLine2="             ~~~">
+        <location
+            file="src/main/java/androidx/glance/session/SessionManager.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="            val mask = decorations.fold(0) { acc, decoration ->"
+        errorLine2="                                   ~~~~">
+        <location
+            file="src/main/java/androidx/glance/text/TextDecoration.kt"/>
+    </issue>
+
+    <issue
+        id="ListIterator"
+        message="Creating an unnecessary Iterator to iterate through a List"
+        errorLine1="        return &quot;TextDecoration[${values.joinToString(separator = &quot;, &quot;)}]&quot;"
+        errorLine2="                                        ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/glance/text/TextDecoration.kt"/>
+    </issue>
+
+    <issue
         id="PrimitiveInLambda"
         message="Use a functional interface instead of lambda syntax for lambdas with primitive values in constructor InteractiveFrameClock has parameter &apos;nanoTime&apos; with type Function0&lt;Long>."
         errorLine1="    private val nanoTime: () -> Long = { System.nanoTime() }"
diff --git a/glance/glance/src/main/java/androidx/glance/Background.kt b/glance/glance/src/main/java/androidx/glance/Background.kt
index 67e1195..4b254af 100644
--- a/glance/glance/src/main/java/androidx/glance/Background.kt
+++ b/glance/glance/src/main/java/androidx/glance/Background.kt
@@ -23,26 +23,22 @@
 import androidx.glance.unit.ColorProvider
 
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class BackgroundModifier private constructor(
-    val colorProvider: ColorProvider?,
-    val imageProvider: ImageProvider?,
-    val contentScale: ContentScale = ContentScale.FillBounds,
-) : GlanceModifier.Element {
-    init {
-        require((colorProvider != null) xor (imageProvider != null)) {
-            "Exactly one of colorProvider and imageProvider must be non-null"
-        }
+sealed interface BackgroundModifier : GlanceModifier.Element {
+
+    class Color(val colorProvider: ColorProvider) : BackgroundModifier {
+        override fun toString() =
+            "BackgroundModifier(colorProvider=$colorProvider)"
     }
 
-    constructor(colorProvider: ColorProvider) :
-        this(colorProvider = colorProvider, imageProvider = null)
-
-    constructor(imageProvider: ImageProvider, contentScale: ContentScale) :
-        this(colorProvider = null, imageProvider = imageProvider, contentScale = contentScale)
-
-    override fun toString() =
-        "BackgroundModifier(colorProvider=$colorProvider, imageProvider=$imageProvider, " +
-            "contentScale=$contentScale)"
+    class Image(
+        val imageProvider: ImageProvider?,
+        val contentScale: ContentScale = ContentScale.FillBounds,
+        val colorFilter: ColorFilter? = null
+    ) : BackgroundModifier {
+        override fun toString() =
+            "BackgroundModifier(colorFilter=$colorFilter, imageProvider=$imageProvider, " +
+                "contentScale=$contentScale)"
+    }
 }
 
 /**
@@ -67,7 +63,7 @@
  * the element.
  */
 fun GlanceModifier.background(colorProvider: ColorProvider): GlanceModifier =
-    this.then(BackgroundModifier(colorProvider))
+    this.then(BackgroundModifier.Color(colorProvider))
 
 /**
  * Apply a background image to the element this modifier is attached to.
@@ -76,4 +72,20 @@
     imageProvider: ImageProvider,
     contentScale: ContentScale = ContentScale.FillBounds
 ): GlanceModifier =
-    this.then(BackgroundModifier(imageProvider, contentScale))
+    this.then(BackgroundModifier.Image(imageProvider, contentScale))
+
+/**
+ * Apply a background image to the element this modifier is attached to.
+ */
+fun GlanceModifier.background(
+    imageProvider: ImageProvider,
+    tint: ColorFilter?,
+    contentScale: ContentScale = ContentScale.FillBounds,
+    ): GlanceModifier =
+    this.then(
+        BackgroundModifier.Image(
+            imageProvider = imageProvider,
+            contentScale = contentScale,
+            colorFilter = tint
+        )
+    )
diff --git a/glance/glance/src/main/java/androidx/glance/Emittables.kt b/glance/glance/src/main/java/androidx/glance/Emittables.kt
index 84d50c2..90a8168 100644
--- a/glance/glance/src/main/java/androidx/glance/Emittables.kt
+++ b/glance/glance/src/main/java/androidx/glance/Emittables.kt
@@ -17,7 +17,6 @@
 package androidx.glance
 
 import androidx.annotation.RestrictTo
-import androidx.compose.ui.util.fastJoinToString
 import androidx.glance.layout.Alignment
 import androidx.glance.text.TextStyle
 
@@ -35,7 +34,17 @@
     val children: MutableList<Emittable> = mutableListOf<Emittable>()
 
     protected fun childrenToString(): String =
-        children.fastJoinToString(",\n").prependIndent("  ")
+        children.joinToString(",\n").prependIndent("  ")
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun EmittableWithChildren.addChild(e: Emittable) {
+    this.children += e
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun EmittableWithChildren.addChildIfNotNull(e: Emittable?) {
+    if (e != null) this.children += e
 }
 
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
diff --git a/glance/glance/src/main/java/androidx/glance/Image.kt b/glance/glance/src/main/java/androidx/glance/Image.kt
index ae5f43e..4011c8c 100644
--- a/glance/glance/src/main/java/androidx/glance/Image.kt
+++ b/glance/glance/src/main/java/androidx/glance/Image.kt
@@ -86,7 +86,9 @@
 /**
  * Effects used to modify the color of an image.
  */
-class ColorFilter internal constructor(internal val colorFilterParams: ColorFilterParams) {
+class ColorFilter internal constructor(
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val colorFilterParams: ColorFilterParams
+) {
     companion object {
         /**
          * Set a tinting option for the image using the platform-specific default blending mode.
diff --git a/glance/glance/src/main/java/androidx/glance/action/Action.kt b/glance/glance/src/main/java/androidx/glance/action/Action.kt
index d214afe..91028c5e 100644
--- a/glance/glance/src/main/java/androidx/glance/action/Action.kt
+++ b/glance/glance/src/main/java/androidx/glance/action/Action.kt
@@ -17,6 +17,7 @@
 package androidx.glance.action
 
 import android.app.Activity
+import androidx.annotation.DrawableRes
 import androidx.annotation.RestrictTo
 import androidx.compose.runtime.Composable
 import androidx.glance.ExperimentalGlanceApi
@@ -31,12 +32,16 @@
 
 /**
  * Apply an [Action], to be executed in response to a user click.
+ *
+ * @param onClick The action to run.
  */
 fun GlanceModifier.clickable(onClick: Action): GlanceModifier =
     this.then(ActionModifier(onClick))
 
 /**
  * Run [block] in response to a user click.
+ *
+ * @param block The action to run.
  */
 @Composable
 fun GlanceModifier.clickable(
@@ -47,7 +52,36 @@
 /**
  * Run [block] in response to a user click.
  *
+ * @param rippleOverride A drawable resource to use as the onClick ripple. Use [NoRippleOverride]
+ * if no custom behavior is needed.
+ * @param block The action to run.v
+ */
+@Composable
+fun GlanceModifier.clickable(
+    @DrawableRes rippleOverride: Int = NoRippleOverride,
+    block: () -> Unit
+): GlanceModifier =
+    this.then(ActionModifier(action = action(block = block), rippleOverride = rippleOverride))
+
+/**
+ * Apply an [Action], to be executed in response to a user click.
+ *
+ * @param rippleOverride A drawable resource to use as the onClick ripple. Use [NoRippleOverride]
+ * if no custom behavior is needed.
+ * @param onClick The action to run.
+ */
+fun GlanceModifier.clickable(
+    onClick: Action,
+    @DrawableRes rippleOverride: Int = NoRippleOverride
+): GlanceModifier =
+    this.then(ActionModifier(action = onClick, rippleOverride = rippleOverride))
+
+/**
+ * Run [block] in response to a user click.
+ *
  * @param block The action to run.
+ * @param rippleOverride A drawable resource to use as the onClick ripple. Use [NoRippleOverride]
+ * if no custom behavior is needed.
  * @param key A stable and unique key that identifies the action for this element. This ensures
  * that the correct action is triggered, especially in cases of items that change order. If not
  * provided we use the key that is automatically generated by the Compose runtime, which is unique
@@ -57,13 +91,24 @@
 @Composable
 fun GlanceModifier.clickable(
     key: String? = null,
+    @DrawableRes rippleOverride: Int = NoRippleOverride,
     block: () -> Unit
 ): GlanceModifier =
-    this.then(ActionModifier(action(key, block)))
+    this.then(ActionModifier(action = action(key, block), rippleOverride = rippleOverride))
 
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class ActionModifier(val action: Action) : GlanceModifier.Element {
+class ActionModifier(
+    val action: Action,
+    @DrawableRes val rippleOverride: Int = NoRippleOverride
+) : GlanceModifier.Element {
     override fun toString(): String {
-        return "ActionModifier(action=$action)"
+        return "ActionModifier(action=$action, rippleOverride=$rippleOverride)"
     }
 }
+
+/**
+ * Constant. Tells the system that there is no ripple override. When this is passed, the system
+ * will use default behavior for the ripple.
+ */
+@DrawableRes
+const val NoRippleOverride = 0
diff --git a/glance/glance/src/main/java/androidx/glance/layout/Box.kt b/glance/glance/src/main/java/androidx/glance/layout/Box.kt
index 9392c48..acc6c2e 100644
--- a/glance/glance/src/main/java/androidx/glance/layout/Box.kt
+++ b/glance/glance/src/main/java/androidx/glance/layout/Box.kt
@@ -18,7 +18,6 @@
 
 import androidx.annotation.RestrictTo
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.util.fastMap
 import androidx.glance.Emittable
 import androidx.glance.EmittableWithChildren
 import androidx.glance.GlanceModifier
@@ -33,7 +32,7 @@
     override fun copy(): Emittable = EmittableBox().also {
         it.modifier = modifier
         it.contentAlignment = contentAlignment
-        it.children.addAll(children.fastMap { it.copy() })
+        it.children.addAll(children.map { it.copy() })
     }
 
     override fun toString(): String = "EmittableBox(" +
diff --git a/glance/glance/src/main/java/androidx/glance/layout/Column.kt b/glance/glance/src/main/java/androidx/glance/layout/Column.kt
index d489c7f..6e1e4e1 100644
--- a/glance/glance/src/main/java/androidx/glance/layout/Column.kt
+++ b/glance/glance/src/main/java/androidx/glance/layout/Column.kt
@@ -18,7 +18,6 @@
 
 import androidx.annotation.RestrictTo
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.util.fastMap
 import androidx.glance.Emittable
 import androidx.glance.EmittableWithChildren
 import androidx.glance.GlanceModifier
@@ -35,7 +34,7 @@
         it.modifier = modifier
         it.verticalAlignment = verticalAlignment
         it.horizontalAlignment = horizontalAlignment
-        it.children.addAll(children.fastMap { it.copy() })
+        it.children.addAll(children.map { it.copy() })
     }
 
     override fun toString(): String = "EmittableColumn(" +
diff --git a/glance/glance/src/main/java/androidx/glance/layout/Padding.kt b/glance/glance/src/main/java/androidx/glance/layout/Padding.kt
index 95ebe2c..a5027be 100644
--- a/glance/glance/src/main/java/androidx/glance/layout/Padding.kt
+++ b/glance/glance/src/main/java/androidx/glance/layout/Padding.kt
@@ -20,7 +20,6 @@
 import androidx.annotation.RestrictTo
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastFold
 import androidx.glance.GlanceModifier
 
 /**
@@ -194,7 +193,7 @@
     collectPadding()?.toDp(resources)
 
 private fun List<Int>.toDp(resources: Resources) =
-    fastFold(0.dp) { acc, res ->
+    fold(0.dp) { acc, res ->
         acc + (resources.getDimension(res) / resources.displayMetrics.density).dp
     }
 
diff --git a/glance/glance/src/main/java/androidx/glance/layout/Row.kt b/glance/glance/src/main/java/androidx/glance/layout/Row.kt
index fa7bb50..4fe0201 100644
--- a/glance/glance/src/main/java/androidx/glance/layout/Row.kt
+++ b/glance/glance/src/main/java/androidx/glance/layout/Row.kt
@@ -18,7 +18,6 @@
 
 import androidx.annotation.RestrictTo
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.util.fastMap
 import androidx.glance.Emittable
 import androidx.glance.EmittableWithChildren
 import androidx.glance.GlanceModifier
@@ -35,7 +34,7 @@
         it.modifier = modifier
         it.horizontalAlignment = horizontalAlignment
         it.verticalAlignment = verticalAlignment
-        it.children.addAll(children.fastMap { it.copy() })
+        it.children.addAll(children.map { it.copy() })
     }
 
     override fun toString(): String = "EmittableRow(" +
diff --git a/glance/glance/src/main/java/androidx/glance/session/IdleEventBroadcastReceiver.kt b/glance/glance/src/main/java/androidx/glance/session/IdleEventBroadcastReceiver.kt
index 5801903..7929994 100644
--- a/glance/glance/src/main/java/androidx/glance/session/IdleEventBroadcastReceiver.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/IdleEventBroadcastReceiver.kt
@@ -34,7 +34,6 @@
             PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED,
             PowerManager.ACTION_LOW_POWER_STANDBY_ENABLED_CHANGED
         )
-        @Suppress("ListIterator")
         val filter = IntentFilter().apply {
             events.forEach { addAction(it) }
         }
diff --git a/glance/glance/src/main/java/androidx/glance/session/Session.kt b/glance/glance/src/main/java/androidx/glance/session/Session.kt
index 673b47a..c7aec1e 100644
--- a/glance/glance/src/main/java/androidx/glance/session/Session.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/Session.kt
@@ -17,6 +17,7 @@
 package androidx.glance.session
 
 import android.content.Context
+import android.util.Log
 import androidx.annotation.RestrictTo
 import androidx.compose.runtime.Composable
 import androidx.glance.EmittableWithChildren
@@ -87,4 +88,12 @@
     fun close() {
         eventChannel.close()
     }
+
+    /**
+     * Called when there is an error in the composition. The session will be closed immediately
+     * after this.
+     */
+    open suspend fun onCompositionError(context: Context, throwable: Throwable) {
+        Log.e("GlanceSession", "Error running composition", throwable)
+    }
 }
diff --git a/glance/glance/src/main/java/androidx/glance/session/SessionManager.kt b/glance/glance/src/main/java/androidx/glance/session/SessionManager.kt
index 3213cd3..5b83da2 100644
--- a/glance/glance/src/main/java/androidx/glance/session/SessionManager.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/SessionManager.kt
@@ -96,7 +96,6 @@
     }
 
     override suspend fun isSessionRunning(context: Context, key: String) =
-        @Suppress("ListIterator")
         (WorkManager.getInstance(context).getWorkInfosForUniqueWork(key).await()
             .any { it.state == WorkInfo.State.RUNNING } && synchronized(sessions) {
             sessions.containsKey(key)
diff --git a/glance/glance/src/main/java/androidx/glance/session/SessionWorker.kt b/glance/glance/src/main/java/androidx/glance/session/SessionWorker.kt
index 79e6e40..72fd733 100644
--- a/glance/glance/src/main/java/androidx/glance/session/SessionWorker.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/SessionWorker.kt
@@ -27,8 +27,11 @@
 import androidx.work.WorkerParameters
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineExceptionHandler
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.collectLatest
@@ -113,14 +116,32 @@
         val frameClock = InteractiveFrameClock(this)
         val snapshotMonitor = launch { globalSnapshotMonitor() }
         val root = session.createRootEmittable()
-        val recomposer = Recomposer(coroutineContext)
-        val composition = Composition(Applier(root), recomposer).apply {
-            setContent(session.provideGlance(applicationContext))
-        }
         val uiReady = MutableStateFlow(false)
+        // Use an independent Job with a CoroutineExceptionHandler so that we can catch errors from
+        // LaunchedEffects in the composition and they won't propagate up to the Worker context.
+        val effectExceptionHandler = CoroutineExceptionHandler { _, throwable ->
+            launch {
+                session.onCompositionError(applicationContext, throwable)
+                session.close()
+                uiReady.emit(true)
+            }
+        }
+        val effectCoroutineContext = coroutineContext + Job() + effectExceptionHandler
+        val recomposer = Recomposer(effectCoroutineContext)
+        val composition = Composition(Applier(root), recomposer)
 
         launch(frameClock) {
-            recomposer.runRecomposeAndApplyChanges()
+            try {
+                composition.setContent(session.provideGlance(applicationContext))
+                recomposer.runRecomposeAndApplyChanges()
+            } catch (e: CancellationException) {
+                // do nothing if we are cancelled.
+            } catch (throwable: Throwable) {
+                session.onCompositionError(applicationContext, throwable)
+                session.close()
+                // Set uiReady to true to resume coroutine waiting on it.
+                uiReady.emit(true)
+            }
         }
         launch {
             var lastRecomposeCount = recomposer.changeCount
diff --git a/glance/glance/src/main/java/androidx/glance/text/TextDecoration.kt b/glance/glance/src/main/java/androidx/glance/text/TextDecoration.kt
index 386a8e2..6f100c0 100644
--- a/glance/glance/src/main/java/androidx/glance/text/TextDecoration.kt
+++ b/glance/glance/src/main/java/androidx/glance/text/TextDecoration.kt
@@ -17,8 +17,6 @@
 package androidx.glance.text
 
 import androidx.compose.runtime.Stable
-import androidx.compose.ui.util.fastFold
-import androidx.compose.ui.util.fastJoinToString
 
 /**
  * Defines a horizontal line to be drawn on the text.
@@ -46,7 +44,7 @@
          * @param decorations The decorations to be added
          */
         fun combine(decorations: List<TextDecoration>): TextDecoration {
-            val mask = decorations.fastFold(0) { acc, decoration ->
+            val mask = decorations.fold(0) { acc, decoration ->
                 acc or decoration.mask
             }
             return TextDecoration(mask)
@@ -83,6 +81,6 @@
         if ((values.size == 1)) {
             return "TextDecoration.${values[0]}"
         }
-        return "TextDecoration[${values.fastJoinToString(separator = ", ")}]"
+        return "TextDecoration[${values.joinToString(separator = ", ")}]"
     }
 }
diff --git a/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt b/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
index ccbd801..c62f9c0 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
@@ -18,6 +18,8 @@
 
 import android.content.Context
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.mutableStateOf
 import androidx.glance.EmittableWithChildren
 import androidx.glance.GlanceComposable
@@ -88,7 +90,7 @@
             Box {
                 Text("Hello World")
             }
-        }.first()
+        }.first().getOrThrow()
         val box = assertIs<EmittableBox>(root.children.single())
         val text = assertIs<EmittableText>(box.children.single())
         assertThat(text.text).isEqualTo("Hello World")
@@ -118,13 +120,13 @@
         val uiFlow = sessionManager.startSession(context) {
                 Text(state.value)
         }
-        uiFlow.first().let { root ->
+        uiFlow.first().getOrThrow().let { root ->
             val text = assertIs<EmittableText>(root.children.single())
             assertThat(text.text).isEqualTo("Hello World")
         }
 
         state.value = "Hello Earth"
-        uiFlow.first().let { root ->
+        uiFlow.first().getOrThrow().let { root ->
             val text = assertIs<EmittableText>(root.children.single())
             assertThat(text.text).isEqualTo("Hello Earth")
         }
@@ -142,7 +144,7 @@
         val uiFlow = sessionManager.startSession(context) {
             Text(state.value)
         }
-        uiFlow.first().let { root ->
+        uiFlow.first().getOrThrow().let { root ->
             val text = assertIs<EmittableText>(root.children.single())
             assertThat(text.text).isEqualTo("Hello World")
         }
@@ -150,7 +152,7 @@
         session.sendEvent {
             state.value = "Hello Earth"
         }
-        uiFlow.first().let { root ->
+        uiFlow.first().getOrThrow().let { root ->
             val text = assertIs<EmittableText>(root.children.single())
             assertThat(text.text).isEqualTo("Hello Earth")
         }
@@ -168,7 +170,7 @@
         val uiFlow = sessionManager.startDelayedProcessingSession(context) {
             Text(state.value)
         }
-        uiFlow.first().let { root ->
+        uiFlow.first().getOrThrow().let { root ->
             val text = assertIs<EmittableText>(root.children.single())
             assertThat(text.text).isEqualTo("Hello World")
         }
@@ -176,7 +178,7 @@
         // Changing the value triggers recomposition, which should cancel the currently running call
         // to processEmittableTree.
         state.value = "Hello Earth"
-        uiFlow.first().let { root ->
+        uiFlow.first().getOrThrow().let { root ->
             val text = assertIs<EmittableText>(root.children.single())
             assertThat(text.text).isEqualTo("Hello Earth")
         }
@@ -185,6 +187,86 @@
         assertThat(session.processEmittableTreeCancelCount).isEqualTo(1)
         sessionManager.closeSession()
     }
+
+    @Test
+    fun sessionWorkerCatchesCompositionError() = runTest {
+        launch {
+            val result = worker.doWork()
+            assertThat(result).isEqualTo(Result.success())
+        }
+
+        val cause = Throwable()
+        val exception = Exception("message", cause)
+        val result = sessionManager.startSession(context) {
+            throw exception
+        }.first().exceptionOrNull()
+        assertThat(result).hasCauseThat().isEqualTo(cause)
+        assertThat(result).hasMessageThat().isEqualTo("message")
+    }
+
+    @Test
+    fun sessionWorkerCatchesRecompositionError() = runTest {
+        launch {
+            val result = worker.doWork()
+            assertThat(result).isEqualTo(Result.success())
+        }
+
+        val runError = mutableStateOf(false)
+        val cause = Throwable()
+        val exception = Exception("message", cause)
+        val resultFlow = sessionManager.startSession(context) {
+            if (runError.value) {
+                throw exception
+            } else {
+                Text("Hello World")
+            }
+        }
+
+        resultFlow.first().getOrThrow().let { root ->
+            val text = assertIs<EmittableText>(root.children.single())
+            assertThat(text.text).isEqualTo("Hello World")
+        }
+
+        runError.value = true
+        val result = resultFlow.first().exceptionOrNull()
+        // Errors thrown on recomposition are wrapped in an identical outer exception with the
+        // original exception as the `cause`.
+        assertThat(result).hasCauseThat().isEqualTo(exception)
+        assertThat(result?.cause?.cause).isEqualTo(cause)
+        assertThat(result).hasMessageThat().isEqualTo("message")
+    }
+
+    @Test
+    fun sessionWorkerCatchesSideEffectError() = runTest {
+        launch {
+            val result = worker.doWork()
+            assertThat(result).isEqualTo(Result.success())
+        }
+
+        val cause = Throwable()
+        val exception = Exception("message", cause)
+        val result = sessionManager.startSession(context) {
+            SideEffect { throw exception }
+        }.first().exceptionOrNull()
+        assertThat(result).hasCauseThat().isEqualTo(cause)
+        assertThat(result).hasMessageThat().isEqualTo("message")
+    }
+
+    @Test
+    fun sessionWorkerCatchesLaunchedEffectError() = runTest {
+        launch {
+            val result = worker.doWork()
+            assertThat(result).isEqualTo(Result.success())
+        }
+
+        val cause = Throwable()
+        val exception = Exception("message", cause)
+        val result = sessionManager.startSession(context) {
+            LaunchedEffect(true) { throw exception }
+        }.first().exceptionOrNull()
+        assertThat(result).hasCauseThat().isEqualTo(cause)
+        assertThat(result).hasMessageThat().isEqualTo("message")
+    }
 }
 
 private const val SESSION_KEY = "123"
@@ -195,18 +277,18 @@
     suspend fun startSession(
         context: Context,
         content: @GlanceComposable @Composable () -> Unit = {}
-    ) = MutableSharedFlow<EmittableWithChildren>().also { flow ->
-        startSession(context, TestSession(onUiFlow = flow, content = content))
+    ) = MutableSharedFlow<kotlin.Result<EmittableWithChildren>>().also { flow ->
+        startSession(context, TestSession(resultFlow = flow, content = content))
     }
 
     suspend fun startDelayedProcessingSession(
         context: Context,
         content: @GlanceComposable @Composable () -> Unit = {}
-    ) = MutableSharedFlow<EmittableWithChildren>().also { flow ->
+    ) = MutableSharedFlow<kotlin.Result<EmittableWithChildren>>().also { flow ->
         startSession(
             context,
             TestSession(
-                onUiFlow = flow,
+                resultFlow = flow,
                 content = content,
                 processEmittableTreeHasInfiniteDelay = true,
             )
@@ -234,7 +316,7 @@
 
 class TestSession(
     key: String = SESSION_KEY,
-    val onUiFlow: MutableSharedFlow<EmittableWithChildren>? = null,
+    val resultFlow: MutableSharedFlow<kotlin.Result<EmittableWithChildren>>? = null,
     val content: @GlanceComposable @Composable () -> Unit = {},
     var processEmittableTreeHasInfiniteDelay: Boolean = false,
 ) : Session(key) {
@@ -257,7 +339,7 @@
         context: Context,
         root: EmittableWithChildren
     ): Boolean {
-        onUiFlow?.emit(root)
+        resultFlow?.emit(kotlin.Result.success(root))
         try {
             if (processEmittableTreeHasInfiniteDelay) {
                 delay(Duration.INFINITE)
@@ -274,4 +356,8 @@
         require(event is Function0<*>)
         event.invoke()
     }
+
+    override suspend fun onCompositionError(context: Context, throwable: Throwable) {
+        resultFlow?.emit(kotlin.Result.failure(throwable))
+    }
 }
diff --git a/gradle.properties b/gradle.properties
index bec21dd..a48d1e5 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -24,13 +24,14 @@
 android.lint.baselineOmitLineNumbers=true
 android.lint.printStackTrace=true
 android.builder.sdkDownload=false
-android.uniquePackageNames=false
+android.uniquePackageNames=true
 android.enableAdditionalTestOutput=true
 android.useAndroidX=true
 android.nonTransitiveRClass=true
 # Pending cleanup to support non-constant R class IDs b/260409846
 android.nonFinalResIds=false
 android.experimental.lint.missingBaselineIsEmptyBaseline=true
+android.experimental.lint.reservedMemoryPerTask=1g
 
 # Do generate versioned API files
 androidx.writeVersionedApiFiles=true
@@ -73,9 +74,7 @@
 kotlin.mpp.stability.nowarn=true
 # b/227307216
 kotlin.mpp.absentAndroidTarget.nowarn=true
-# b/261241595
-kotlin.mpp.androidSourceSetLayoutVersion=1
-kotlin.mpp.androidSourceSetLayoutVersion1.nowarn=true
+kotlin.mpp.androidSourceSetLayoutVersion=2
 # As of October 3 2022, AGP 7.4.0-alpha08 is higher than AGP 7.3
 # Presumably complains if using a non-stable AGP, which we are regularly doing to test pre-stable.
 kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
diff --git a/gradle/README.md b/gradle/README.md
index e945e4e..bd7a571 100644
--- a/gradle/README.md
+++ b/gradle/README.md
@@ -24,24 +24,13 @@
 
 [Configuration file for Gradle dependency verification](https://docs.gradle.org/current/userguide/dependency_verification.html#sub:verification-metadata) used by androidx to make sure dependencies are [signed with trusted signatures](https://docs.gradle.org/current/userguide/dependency_verification.html#sec:signature-verificationn) and that unsigned artifacts have [expected checksums](https://docs.gradle.org/current/userguide/dependency_verification.html#sec:checksum-verification).
 
-When adding a new artifact
-- if it is signed, then run:
+When adding a new artifact, first run:
 ```
 development/update-verification-metadata.sh
 ```
-to trust the signature of the new artifact.
+to trust the signature (or checksum) of the new artifact.
 
-- if it is not signed, then run the following to add generated checksums to `verification-metadata.xml`:
-
-```
-./gradlew -M sha256 buildOnServer --dry-run
-```
-
-Then you will want to diff `gradle/verification-metadata.dryrun.xml` and
-`gradle/verification-metadata.xml` using your favorite tool (e.g. meld) can copy over the entries
-that are relevant to your new artifacts.
-
-Each new checksum that you copy over in this way must be associated with a bug that is tracking
+Then, if any checksums were added, make sure they're associated with a bug that is tracking
 an effort to build or acquire a signed version of this dependency.  To associate with a bug,
 please add an `androidx:reason` attribute to a string that contains a URL for a bug filed
 either in buganizer or github:
@@ -57,8 +46,6 @@
 </component>
 ```
 
-After doing this, you can then delete all the `verification-*-dryrun.*` files.
-
 ### If that doesn't work.
 
 If the artifact is not signed, and does not get automatically added to
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index db17c37..b756b0c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,13 +2,13 @@
 # -----------------------------------------------------------------------------
 # All of the following should be updated in sync.
 # -----------------------------------------------------------------------------
-androidGradlePlugin = "8.2.0-beta01"
+androidGradlePlugin = "8.3.0-alpha02"
 # NOTE: When updating the lint version we also need to update the `api` version
 # supported by `IssueRegistry`'s.' For e.g. r.android.com/1331903
-androidLint = "31.2.0-beta01"
+androidLint = "31.3.0-alpha02"
 # Once you have a chosen version of AGP to upgrade to, go to
 # https://developer.android.com/studio/archive and find the matching version of Studio.
-androidStudio = "2023.1.1.17"
+androidStudio = "2023.2.1.2"
 # -----------------------------------------------------------------------------
 
 androidGradlePluginMin = "7.0.4"
@@ -20,20 +20,21 @@
 androidxTestCore = "1.6.0-alpha01"
 androidxTestExtJunit = "1.2.0-alpha01"
 androidxTestExtTruth = "1.6.0-alpha01"
+annotationVersion = "1.7.0"
 atomicFu = "0.17.0"
 autoService = "1.0-rc6"
 autoValue = "1.6.3"
 byteBuddy = "1.12.10"
 asm = "9.3"
 cmake = "3.22.1"
-dagger = "2.46.1"
+dagger = "2.48"
 dexmaker = "2.28.3"
 dokka = "1.8.20-dev-214"
 espresso = "3.6.0-alpha01"
 espressoDevice = "1.0.0-alpha05"
 grpc = "1.52.0"
 guavaJre = "31.1-jre"
-hilt = "2.46.1"
+hilt = "2.48"
 incap = "0.2"
 jcodec = "0.2.5"
 kotlin17 = "1.7.10"
@@ -57,7 +58,7 @@
 paparazzi = "1.0.0"
 paparazziNative = "2022.1.1-canary-f5f9f71"
 skiko = "0.7.7"
-spdxGradlePlugin = "0.1.0"
+spdxGradlePlugin = "0.2.0"
 sqldelight = "1.3.0"
 retrofit = "2.7.2"
 wire = "4.7.0"
@@ -82,6 +83,7 @@
 androidToolsRepository= { module = "com.android.tools:repository", version.ref = "androidLint" }
 androidToolsSdkCommon = { module = "com.android.tools:sdk-common", version.ref = "androidLint" }
 androidToolsAnalyticsProtos = { module = "com.android.tools.analytics-library:protos", version.ref = "androidLint" }
+androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "annotationVersion" }
 autoCommon = { module = "com.google.auto:auto-common", version = "0.11" }
 atomicFu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicFu" }
 atomicFuPluginz = { module = "org.jetbrains.kotlinx:atomicfu-gradle-plugin", version.ref = "atomicFu" }
@@ -230,11 +232,11 @@
 opentest4j = { module = "org.opentest4j:opentest4j", version = "1.2.0" }
 playFeatureDelivery = { module = "com.google.android.play:feature-delivery", version = "2.0.1" }
 playCore = { module = "com.google.android.play:core", version = "1.10.3" }
-playServicesAuth = {module = "com.google.android.gms:play-services-auth", version = "20.5.0"}
+playServicesAuth = {module = "com.google.android.gms:play-services-auth", version = "20.7.0"}
 playServicesBase = { module = "com.google.android.gms:play-services-base", version = "17.0.0" }
 playServicesBasement = { module = "com.google.android.gms:play-services-basement", version = "17.0.0" }
 playServicesDevicePerformance = { module = "com.google.android.gms:play-services-deviceperformance", version = "16.0.0" }
-playServicesFido = {module = "com.google.android.gms:play-services-fido", version = "20.0.1"}
+playServicesFido = {module = "com.google.android.gms:play-services-fido", version = "20.1.0"}
 playServicesWearable = { module = "com.google.android.gms:play-services-wearable", version = "17.1.0" }
 paparazzi = { module = "app.cash.paparazzi:paparazzi", version.ref = "paparazzi" }
 paparazziNativeJvm = { module = "app.cash.paparazzi:layoutlib-native-jdk11", version.ref = "paparazziNative" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 18337be..d3b3695 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -27,7 +27,7 @@
       <trusted-keys>
          <trusted-key id="00089ee8c3afa95a854d0f1df800dd0933ecf7f7" group="com.google.guava"/>
          <trusted-key id="019082bc00e0324e2aef4cf00d3b328562a119a7" group="org.openjdk.jmh"/>
-         <trusted-key id="03c123038c20aae9e286c857479d601f3a7b5c1a" group="com.github.ajalt.clikt" name="clikt-jvm" version="3.5.3"/>
+         <trusted-key id="03c123038c20aae9e286c857479d601f3a7b5c1a" group="com.github.ajalt.clikt" name="clikt-jvm" />
          <trusted-key id="042b29e928995b9db963c636c7ca19b7b620d787" group="org.apache.maven"/>
          <trusted-key id="04543577d6a9cc626239c50c7ecbd740ff06aeb5">
             <trusting group="com.sun.activation"/>
@@ -221,7 +221,7 @@
             <trusting group="com.sun.activation"/>
             <trusting group="jakarta.activation"/>
          </trusted-key>
-         <trusted-key id="6ead752b3e2b38e8e2236d7ba9321edaa5cb3202" group="ch.randelshofer" name="fastdoubleparser" version="0.8.0"/>
+         <trusted-key id="6ead752b3e2b38e8e2236d7ba9321edaa5cb3202" group="ch.randelshofer" name="fastdoubleparser" />
          <trusted-key id="6f538074ccebf35f28af9b066a0975f8b1127b83">
             <trusting group="org.jetbrains.kotlin"/>
             <trusting group="org.jetbrains.kotlin.jvm"/>
@@ -245,9 +245,9 @@
             <trusting group="org.jvnet.staxex"/>
             <trusting group="^com[.]sun($|([.].*))" regex="true"/>
          </trusted-key>
-         <trusted-key id="713da88be50911535fe716f5208b0ab1d63011c7" group="org.apache.tomcat" name="annotations-api" version="6.0.53"/>
+         <trusted-key id="713da88be50911535fe716f5208b0ab1d63011c7" group="org.apache.tomcat" name="annotations-api" />
          <trusted-key id="720746177725a89207a7075bfd5dea07fcb690a8" group="org.codehaus.mojo"/>
-         <trusted-key id="73976c9c39c1479b84e2641a5a68a2249128e2c6" group="com.google.crypto.tink" name="tink-android" version="1.8.0"/>
+         <trusted-key id="73976c9c39c1479b84e2641a5a68a2249128e2c6" group="com.google.crypto.tink" name="tink-android" />
          <trusted-key id="748f15b2cf9ba8f024155e6ed7c92b70fa1c814d" group="org.apache.logging.log4j"/>
          <trusted-key id="7615ad56144df2376f49d98b1669c4bb543e0445" group="com.google.errorprone"/>
          <trusted-key id="7616eb882daf57a11477aaf559a252fb1199d873" group="com.google.code.findbugs"/>
@@ -261,11 +261,11 @@
          <trusted-key id="7e22d50a7ebd9d2cd269b2d4056aca74d46000bf" group="io.netty"/>
          <trusted-key id="7f36e793ae3252e5d9e9b98fee9e7dc9d92fc896" group="com.google.errorprone"/>
          <trusted-key id="7faa0f2206de228f0db01ad741321490758aad6f" group="org.codehaus.groovy"/>
-         <trusted-key id="7fe5e98df3a5c0dc34663ab7c1add37ca0069309" group="org.spdx" name="spdx-gradle-plugin" version="0.1.0"/>
+         <trusted-key id="7fe5e98df3a5c0dc34663ab7c1add37ca0069309" group="org.spdx" name="spdx-gradle-plugin"/>
          <trusted-key id="808d78b17a5a2d7c3668e31fbffc9b54721244ad" group="org.apache.commons"/>
          <trusted-key id="80f6d6b0d90c6747753344cab5a9e81b565e89e0" group="org.tomlj"/>
          <trusted-key id="8254180bfc943b816e0b5e2e5e2f2b3d474efe6b" group="it.unimi.dsi"/>
-         <trusted-key id="82c9ec0e52c47a936a849e0113d979595e6d01e1" group="org.apache.maven.shared" name="maven-shared-utils" version="3.3.4"/>
+         <trusted-key id="82c9ec0e52c47a936a849e0113d979595e6d01e1" group="org.apache.maven.shared" name="maven-shared-utils" />
          <trusted-key id="82f833963889d7ed06f1e4dc6525fd70cc303655" group="org.codehaus.mojo"/>
          <trusted-key id="835a685c8c6f49c54980e5caf406f31bc1468eba" group="org.jcodec"/>
          <trusted-key id="842afb86375d805422835bfd82b5574242c20d6f" group="org.antlr"/>
@@ -274,7 +274,7 @@
             <trusting group="org.apache.maven" name="maven-parent"/>
          </trusted-key>
          <trusted-key id="8569c95cadc508b09fe90f3002216ed811210daa" group="io.github.detekt.sarif4k"/>
-         <trusted-key id="86616cd3c4f0803e73374a434dbf5995d492505d" group="org.json" name="json" version="20230227"/>
+         <trusted-key id="86616cd3c4f0803e73374a434dbf5995d492505d" group="org.json" name="json" />
          <trusted-key id="8756c4f765c9ac3cb6b85d62379ce192d401ab61">
             <trusting group="com.github.ajalt"/>
             <trusting group="com.github.javaparser"/>
@@ -342,7 +342,7 @@
          <trusted-key id="ae9e53fc28ff2ab1012273d0bf1518e0160788a2" group="org.apache" name="apache"/>
          <trusted-key id="afa2b1823fc021bfd08c211fd5f4c07a434ab3da" group="com.squareup"/>
          <trusted-key id="afcc4c7594d09e2182c60e0f7a01b0f236e5430f" group="com.google.code.gson"/>
-         <trusted-key id="b02137d875d833d9b23392ecae5a7fb608a0221c" group="org.codehaus.plexus" name="plexus-classworlds" version="2.6.0"/>
+         <trusted-key id="b02137d875d833d9b23392ecae5a7fb608a0221c" group="org.codehaus.plexus" name="plexus-classworlds" />
          <trusted-key id="b02335aa54ccf21e52bbf9abd9c565aa72ba2fdd">
             <trusting group="com.google.protobuf"/>
             <trusting group="io.grpc"/>
@@ -448,7 +448,7 @@
             <trusting group="org.codehaus.plexus"/>
          </trusted-key>
          <trusted-key id="f3184bcd55f4d016e30d4c9bf42e87f9665015c9" group="org.jsoup"/>
-         <trusted-key id="f3d15b8ff9902805de4be6b18dc6f3d0abdbd017" group="org.codehaus.plexus" name="plexus-sec-dispatcher" version="2.0"/>
+         <trusted-key id="f3d15b8ff9902805de4be6b18dc6f3d0abdbd017" group="org.codehaus.plexus" name="plexus-sec-dispatcher" />
          <trusted-key id="f42b96b8648b5c4a1c43a62fbb2914c1fa0811c3" group="net.bytebuddy"/>
          <trusted-key id="fa1703b1d287caea3a60f931e0130a3ed5a2079e" group="org.webjars"/>
          <trusted-key id="fa77dcfef2ee6eb2debedd2c012579464d01c06a">
@@ -511,6 +511,11 @@
             <sha256 value="32001db2443b339dd21f5b79ff29d1ade722d1ba080c214bde819f0f72d1604d" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="com.github.johnrengelman.shadow" name="com.github.johnrengelman.shadow.gradle.plugin" version="8.1.1">
+         <artifact name="com.github.johnrengelman.shadow.gradle.plugin-8.1.1.pom">
+            <sha256 value="3cb3886b97df6e066f108c316b219f262c97c3cb2df6da78927e645deb643cb0" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.google" name="google" version="1">
          <artifact name="google-1.pom">
             <sha256 value="cd6db17a11a31ede794ccbd1df0e4d9750f640234731f21cff885a9997277e81" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -572,6 +577,14 @@
             <sha256 value="663aed69db1623331032fe4dedee4f2b1c9decbdad0ab06a4fc0be14b3e52c7f" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="com.google.android.libraries.identity.googleid" name="googleid" version="1.1.0">
+         <artifact name="googleid-1.1.0.aar">
+             <sha256 value="194ac1fc1986dd1f62046fae37ddf77e63770fdc1f3d34baaa397cfbf4d191a2" origin="Generated by Gradle" reason="Artifact is not signed. b/300156232"/>
+         </artifact>
+         <artifact name="googleid-1.1.0.pom">
+            <sha256 value="a8475b43e61aedc85ad9f66c47278dd4aed0c8ff426961035b72c8b8395b1921" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.google.android.odml" name="image" version="1.0.0-beta1">
          <artifact name="image-1.0.0-beta1.aar">
             <sha256 value="2e71aa31f83a9415277f119de67195726f07d1760e9542c111778c320e3aa1f2" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -678,11 +691,6 @@
             </sha256>
          </artifact>
       </component>
-      <component group="com.github.johnrengelman.shadow" name="com.github.johnrengelman.shadow.gradle.plugin" version="8.1.1">
-         <artifact name="com.github.johnrengelman.shadow.gradle.plugin-8.1.1.pom">
-            <sha256 value="3cb3886b97df6e066f108c316b219f262c97c3cb2df6da78927e645deb643cb0" origin="Generated by Gradle" reason="Artifact is not signed"/>
-         </artifact>
-      </component>
       <component group="javax.annotation" name="jsr250-api" version="1.0">
          <artifact name="jsr250-api-1.0.jar">
             <sha256 value="a1a922d0d9b6d183ed3800dfac01d1e1eb159f0e8c6f94736931c1def54a941f" origin="Generated by Gradle"/>
diff --git a/graphics/graphics-shapes/api/current.txt b/graphics/graphics-shapes/api/current.txt
index ffd01a3..ae00513 100644
--- a/graphics/graphics-shapes/api/current.txt
+++ b/graphics/graphics-shapes/api/current.txt
@@ -22,7 +22,6 @@
   }
 
   public class Cubic {
-    ctor public Cubic(float anchor0X, float anchor0Y, float control0X, float control0Y, float control1X, float control1Y, float anchor1X, float anchor1Y);
     method public static final androidx.graphics.shapes.Cubic circularArc(float centerX, float centerY, float x0, float y0, float x1, float y1);
     method public final operator androidx.graphics.shapes.Cubic div(float x);
     method public final operator androidx.graphics.shapes.Cubic div(int x);
@@ -57,6 +56,10 @@
     method public androidx.graphics.shapes.Cubic straightLine(float x0, float y0, float x1, float y1);
   }
 
+  public final class CubicKt {
+    method public static androidx.graphics.shapes.Cubic Cubic(float anchor0X, float anchor0Y, float control0X, float control0Y, float control1X, float control1Y, float anchor1X, float anchor1Y);
+  }
+
   public final class Morph {
     ctor public Morph(androidx.graphics.shapes.RoundedPolygon start, androidx.graphics.shapes.RoundedPolygon end);
     method public java.util.List<androidx.graphics.shapes.Cubic> asCubics(float progress);
diff --git a/graphics/graphics-shapes/api/restricted_current.txt b/graphics/graphics-shapes/api/restricted_current.txt
index ffd01a3..ae00513 100644
--- a/graphics/graphics-shapes/api/restricted_current.txt
+++ b/graphics/graphics-shapes/api/restricted_current.txt
@@ -22,7 +22,6 @@
   }
 
   public class Cubic {
-    ctor public Cubic(float anchor0X, float anchor0Y, float control0X, float control0Y, float control1X, float control1Y, float anchor1X, float anchor1Y);
     method public static final androidx.graphics.shapes.Cubic circularArc(float centerX, float centerY, float x0, float y0, float x1, float y1);
     method public final operator androidx.graphics.shapes.Cubic div(float x);
     method public final operator androidx.graphics.shapes.Cubic div(int x);
@@ -57,6 +56,10 @@
     method public androidx.graphics.shapes.Cubic straightLine(float x0, float y0, float x1, float y1);
   }
 
+  public final class CubicKt {
+    method public static androidx.graphics.shapes.Cubic Cubic(float anchor0X, float anchor0Y, float control0X, float control0Y, float control1X, float control1Y, float anchor1X, float anchor1Y);
+  }
+
   public final class Morph {
     ctor public Morph(androidx.graphics.shapes.RoundedPolygon start, androidx.graphics.shapes.RoundedPolygon end);
     method public java.util.List<androidx.graphics.shapes.Cubic> asCubics(float progress);
diff --git a/graphics/graphics-shapes/build.gradle b/graphics/graphics-shapes/build.gradle
index db58bca..ffc867b 100644
--- a/graphics/graphics-shapes/build.gradle
+++ b/graphics/graphics-shapes/build.gradle
@@ -53,7 +53,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(commonTest)
             dependencies {
                 implementation(libs.testExtJunit)
diff --git a/graphics/graphics-shapes/src/androidAndroidTest/AndroidManifest.xml b/graphics/graphics-shapes/src/androidInstrumentedTest/AndroidManifest.xml
similarity index 100%
rename from graphics/graphics-shapes/src/androidAndroidTest/AndroidManifest.xml
rename to graphics/graphics-shapes/src/androidInstrumentedTest/AndroidManifest.xml
diff --git a/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/CornerRoundingTest.kt b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/CornerRoundingTest.kt
similarity index 100%
rename from graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/CornerRoundingTest.kt
rename to graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/CornerRoundingTest.kt
diff --git a/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/CubicTest.kt b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/CubicTest.kt
similarity index 100%
rename from graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/CubicTest.kt
rename to graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/CubicTest.kt
diff --git a/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/FloatMappingTest.kt b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/FloatMappingTest.kt
similarity index 100%
rename from graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/FloatMappingTest.kt
rename to graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/FloatMappingTest.kt
diff --git a/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/PolygonMeasureTest.kt b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/PolygonMeasureTest.kt
similarity index 100%
rename from graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/PolygonMeasureTest.kt
rename to graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/PolygonMeasureTest.kt
diff --git a/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/PolygonTest.kt b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/PolygonTest.kt
similarity index 100%
rename from graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/PolygonTest.kt
rename to graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/PolygonTest.kt
diff --git a/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/RoundedPolygonTest.kt b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/RoundedPolygonTest.kt
similarity index 100%
rename from graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/RoundedPolygonTest.kt
rename to graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/RoundedPolygonTest.kt
diff --git a/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/ShapesTest.kt b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/ShapesTest.kt
similarity index 100%
rename from graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/ShapesTest.kt
rename to graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/ShapesTest.kt
diff --git a/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/TestUtils.kt b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/TestUtils.kt
similarity index 100%
rename from graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/TestUtils.kt
rename to graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/TestUtils.kt
diff --git a/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Cubic.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Cubic.kt
index 4ab4abd..d49776a 100644
--- a/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Cubic.kt
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Cubic.kt
@@ -67,38 +67,9 @@
      */
     val anchor1Y get() = points[7]
 
-    /**
-     * This class holds the anchor and control point data for a single cubic Bézier curve,
-     * with anchor points ([anchor0X], [anchor0Y]) and ([anchor1X], [anchor1Y]) at either end
-     * and control points ([control0X], [control0Y]) and ([control1X], [control1Y]) determining
-     * the slope of the curve between the anchor points.
-     *
-     * This object is immutable.
-     *
-     * @param anchor0X the first anchor point x coordinate
-     * @param anchor0Y the first anchor point y coordinate
-     * @param control0X the first control point x coordinate
-     * @param control0Y the first control point y coordinate
-     * @param control1X the second control point x coordinate
-     * @param control1Y the second control point y coordinate
-     * @param anchor1X the second anchor point x coordinate
-     * @param anchor1Y the second anchor point y coordinate
-     */
-    constructor(
-        anchor0X: Float,
-        anchor0Y: Float,
-        control0X: Float,
-        control0Y: Float,
-        control1X: Float,
-        control1Y: Float,
-        anchor1X: Float,
-        anchor1Y: Float
-    ) : this(floatArrayOf(anchor0X, anchor0Y, control0X, control0Y,
-        control1X, control1Y, anchor1X, anchor1Y))
-
     internal constructor(anchor0: Point, control0: Point, control1: Point, anchor1: Point) :
-        this(anchor0.x, anchor0.y, control0.x, control0.y,
-            control1.x, control1.y, anchor1.x, anchor1.y)
+        this(floatArrayOf(anchor0.x, anchor0.y, control0.x, control0.y,
+            control1.x, control1.y, anchor1.x, anchor1.y))
 
     /**
      * Returns a point on the curve for parameter t, representing the proportional distance
@@ -254,6 +225,35 @@
 }
 
 /**
+ * Create a Cubic that holds the anchor and control point data for a single Bézier curve,
+ * with anchor points ([anchor0X], [anchor0Y]) and ([anchor1X], [anchor1Y]) at either end
+ * and control points ([control0X], [control0Y]) and ([control1X], [control1Y]) determining
+ * the slope of the curve between the anchor points.
+ *
+ * The returned instance is immutable.
+ *
+ * @param anchor0X the first anchor point x coordinate
+ * @param anchor0Y the first anchor point y coordinate
+ * @param control0X the first control point x coordinate
+ * @param control0Y the first control point y coordinate
+ * @param control1X the second control point x coordinate
+ * @param control1Y the second control point y coordinate
+ * @param anchor1X the second anchor point x coordinate
+ * @param anchor1Y the second anchor point y coordinate
+ */
+fun Cubic(
+    anchor0X: Float,
+    anchor0Y: Float,
+    control0X: Float,
+    control0Y: Float,
+    control1X: Float,
+    control1Y: Float,
+    anchor1X: Float,
+    anchor1Y: Float
+) = Cubic(floatArrayOf(anchor0X, anchor0Y, control0X, control0Y,
+    control1X, control1Y, anchor1X, anchor1Y))
+
+/**
  * This interface is used refer to Points that can be modified, as a scope to
  * [PointTransformer]
  */
@@ -280,7 +280,6 @@
 }
 
 /**
-
  * This is a Mutable version of [Cubic], used mostly for performance critical paths so we can
  * avoid creating new [Cubic]s
  *
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 3e9c371..7b0863a 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -9,6 +9,7 @@
     method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> recordIdsList, java.util.List<java.lang.String> clientRecordIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
     method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
+    method public static String getHealthConnectManageDataAction(android.content.Context context);
     method public static String getHealthConnectSettingsAction();
     method public static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
@@ -28,6 +29,7 @@
   }
 
   public static final class HealthConnectClient.Companion {
+    method public String getHealthConnectManageDataAction(android.content.Context context);
     method public String getHealthConnectSettingsAction();
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 7bccf0e..beb3500f 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -9,6 +9,7 @@
     method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> recordIdsList, java.util.List<java.lang.String> clientRecordIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
     method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
+    method public static String getHealthConnectManageDataAction(android.content.Context context);
     method public static String getHealthConnectSettingsAction();
     method public static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
@@ -28,6 +29,7 @@
   }
 
   public static final class HealthConnectClient.Companion {
+    method public String getHealthConnectManageDataAction(android.content.Context context);
     method public String getHealthConnectSettingsAction();
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
diff --git a/health/connect/connect-client/lint-baseline.xml b/health/connect/connect-client/lint-baseline.xml
index 1959b87c..3434d5f 100644
--- a/health/connect/connect-client/lint-baseline.xml
+++ b/health/connect/connect-client/lint-baseline.xml
@@ -1663,7 +1663,7 @@
         errorLine1="                        PermissionProto.Permission.newBuilder().setPermission(it).build()"
         errorLine2="                                                                                  ~~~~~">
         <location
-            file="src/main/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternal.kt"/>
+            file="src/main/java/androidx/health/connect/client/permission/HealthPermissionsRequestAppContract.kt"/>
     </issue>
 
     <issue
@@ -1672,7 +1672,7 @@
         errorLine1="                        PermissionProto.Permission.newBuilder().setPermission(it).build()"
         errorLine2="                                                                ~~~~~~~~~~~~~">
         <location
-            file="src/main/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternal.kt"/>
+            file="src/main/java/androidx/health/connect/client/permission/HealthPermissionsRequestAppContract.kt"/>
     </issue>
 
     <issue
@@ -1681,7 +1681,7 @@
         errorLine1="                        PermissionProto.Permission.newBuilder().setPermission(it).build()"
         errorLine2="                                                   ~~~~~~~~~~">
         <location
-            file="src/main/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternal.kt"/>
+            file="src/main/java/androidx/health/connect/client/permission/HealthPermissionsRequestAppContract.kt"/>
     </issue>
 
     <issue
@@ -1690,7 +1690,7 @@
         errorLine1="                ?.map { it.proto.permission }"
         errorLine2="                                 ~~~~~~~~~~">
         <location
-            file="src/main/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternal.kt"/>
+            file="src/main/java/androidx/health/connect/client/permission/HealthPermissionsRequestAppContract.kt"/>
     </issue>
 
     <issue
@@ -1699,7 +1699,7 @@
         errorLine1="                ?.map { it.proto.permission }"
         errorLine2="                                 ~~~~~~~~~~">
         <location
-            file="src/main/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternal.kt"/>
+            file="src/main/java/androidx/health/connect/client/permission/HealthPermissionsRequestAppContract.kt"/>
     </issue>
 
     <issue
diff --git a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt
index 95712e3..b36a5fc 100644
--- a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt
+++ b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt
@@ -20,8 +20,6 @@
 
 import androidx.annotation.Sampled
 import androidx.health.connect.client.HealthConnectClient
-import androidx.health.connect.client.records.SleepSessionRecord
-import androidx.health.connect.client.records.SleepStageRecord
 import androidx.health.connect.client.records.StepsRecord
 import androidx.health.connect.client.time.TimeRangeFilter
 import java.time.Instant
@@ -50,13 +48,3 @@
         timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
     )
 }
-
-@Sampled
-suspend fun DeleteSleepSession(
-    healthConnectClient: HealthConnectClient,
-    sleepRecord: SleepSessionRecord,
-) {
-    val timeRangeFilter = TimeRangeFilter.between(sleepRecord.startTime, sleepRecord.endTime)
-    healthConnectClient.deleteRecords(SleepSessionRecord::class, timeRangeFilter)
-    healthConnectClient.deleteRecords(SleepStageRecord::class, timeRangeFilter)
-}
diff --git a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt
index 578fd0e..e64ecd0 100644
--- a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt
+++ b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt
@@ -27,7 +27,6 @@
 import androidx.health.connect.client.records.ExerciseSessionRecord
 import androidx.health.connect.client.records.HeartRateRecord
 import androidx.health.connect.client.records.SleepSessionRecord
-import androidx.health.connect.client.records.SleepStageRecord
 import androidx.health.connect.client.records.StepsRecord
 import androidx.health.connect.client.request.ReadRecordsRequest
 import androidx.health.connect.client.time.TimeRangeFilter
@@ -128,17 +127,6 @@
             )
         )
     for (sleepRecord in response.records) {
-        // Process each exercise record
-        // Optionally pull in sleep stages of the same time range
-        val sleepStageRecords =
-            healthConnectClient
-                .readRecords(
-                    ReadRecordsRequest(
-                        SleepStageRecord::class,
-                        timeRangeFilter =
-                            TimeRangeFilter.between(sleepRecord.startTime, sleepRecord.endTime)
-                    )
-                )
-                .records
+        // Process each sleep record
     }
 }
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/ExerciseRouteRequestModuleContractTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/ExerciseRouteRequestModuleContractTest.kt
new file mode 100644
index 0000000..c19f393
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/ExerciseRouteRequestModuleContractTest.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.health.connect.client.impl
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.content.Intent
+import android.health.connect.HealthConnectManager
+import android.os.Build
+import androidx.health.connect.client.impl.platform.records.PlatformExerciseRoute
+import androidx.health.connect.client.impl.platform.records.PlatformExerciseRouteLocationBuilder
+import androidx.health.connect.client.impl.platform.records.PlatformLength
+import androidx.health.connect.client.permission.platform.ExerciseRouteRequestModuleContract
+import androidx.health.connect.client.records.ExerciseRoute
+import androidx.health.connect.client.units.Length
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import java.time.Instant
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+// Comment the SDK suppress to run on emulators lower than U.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class ExerciseRouteRequestModuleContractTest {
+
+    private lateinit var context: Context
+
+    @Before
+    fun setUp() {
+        context = ApplicationProvider.getApplicationContext()
+    }
+
+    @Test
+    fun createIntentTest() {
+        val requestRouteContract = ExerciseRouteRequestModuleContract()
+        val intent = requestRouteContract.createIntent(context, "someUid")
+        assertThat(intent.action).isEqualTo("android.health.connect.action.REQUEST_EXERCISE_ROUTE")
+        assertThat(intent.getStringExtra("android.health.connect.extra.SESSION_ID"))
+            .isEqualTo("someUid")
+    }
+
+    @Test
+    fun parseIntent_null() {
+        val requestRouteContract = ExerciseRouteRequestModuleContract()
+        val result = requestRouteContract.parseResult(0, null)
+        assertThat(result).isNull()
+    }
+
+    @Test
+    fun parseIntent_emptyIntent() {
+        val requestRouteContract = ExerciseRouteRequestModuleContract()
+        val result = requestRouteContract.parseResult(0, Intent())
+        assertThat(result).isNull()
+    }
+
+    @Test
+    fun parseIntent_emptyRoute() {
+        val requestRouteContract = ExerciseRouteRequestModuleContract()
+        val intent = Intent()
+        intent.putExtra(HealthConnectManager.EXTRA_EXERCISE_ROUTE, PlatformExerciseRoute(listOf()))
+        val result = requestRouteContract.parseResult(0, intent)
+        assertThat(result).isEqualTo(ExerciseRoute(listOf()))
+    }
+
+    @Test
+    fun parseIntent() {
+        val requestRouteContract = ExerciseRouteRequestModuleContract()
+        val intent = Intent()
+        val location1 =
+            PlatformExerciseRouteLocationBuilder(Instant.ofEpochMilli(1234L), 23.4, -23.4)
+                .setAltitude(PlatformLength.fromMeters(12.3))
+                .setHorizontalAccuracy(PlatformLength.fromMeters(0.9))
+                .setVerticalAccuracy(PlatformLength.fromMeters(0.3))
+                .build()
+        val location2 =
+            PlatformExerciseRouteLocationBuilder(Instant.ofEpochMilli(3456L), 23.45, -23.45).build()
+
+        intent.putExtra(
+            HealthConnectManager.EXTRA_EXERCISE_ROUTE,
+            PlatformExerciseRoute(listOf(location1, location2))
+        )
+        val result = requestRouteContract.parseResult(0, intent)
+        assertThat(result)
+            .isEqualTo(
+                ExerciseRoute(
+                    listOf(
+                        ExerciseRoute.Location(
+                            time = Instant.ofEpochMilli(1234L),
+                            latitude = 23.4,
+                            longitude = -23.4,
+                            horizontalAccuracy = Length.meters(0.9),
+                            verticalAccuracy = Length.meters(0.3),
+                            altitude = Length.meters(12.3)
+                        ),
+                        ExerciseRoute.Location(
+                            time = Instant.ofEpochMilli(3456L),
+                            latitude = 23.45,
+                            longitude = -23.45,
+                        )
+                    )
+                )
+            )
+    }
+}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/RequestExerciseRouteUpsideDownCakeTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/RequestExerciseRouteUpsideDownCakeTest.kt
deleted file mode 100644
index ea4548d..0000000
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/RequestExerciseRouteUpsideDownCakeTest.kt
+++ /dev/null
@@ -1,125 +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.health.connect.client.impl
-
-import android.annotation.TargetApi
-import android.content.Context
-import android.content.Intent
-import android.health.connect.HealthConnectManager
-import android.os.Build
-import androidx.health.connect.client.impl.platform.records.PlatformExerciseRoute
-import androidx.health.connect.client.impl.platform.records.PlatformExerciseRouteLocationBuilder
-import androidx.health.connect.client.impl.platform.records.PlatformLength
-import androidx.health.connect.client.permission.platform.RequestExerciseRouteUpsideDownCake
-import androidx.health.connect.client.records.ExerciseRoute
-import androidx.health.connect.client.units.Length
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth.assertThat
-import java.time.Instant
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@MediumTest
-@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-// Comment the SDK suppress to run on emulators lower than U.
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
-class RequestExerciseRouteUpsideDownCakeTest {
-
-    private lateinit var context: Context
-
-    @Before
-    fun setUp() {
-        context = ApplicationProvider.getApplicationContext()
-    }
-
-    @Test
-    fun createIntentTest() {
-        val requestRouteContract = RequestExerciseRouteUpsideDownCake()
-        val intent = requestRouteContract.createIntent(context, "someUid")
-        assertThat(intent.action).isEqualTo("android.health.connect.action.REQUEST_EXERCISE_ROUTE")
-        assertThat(intent.getStringExtra("android.health.connect.extra.SESSION_ID"))
-            .isEqualTo("someUid")
-    }
-
-    @Test
-    fun parseIntent_null() {
-        val requestRouteContract = RequestExerciseRouteUpsideDownCake()
-        val result = requestRouteContract.parseResult(0, null)
-        assertThat(result).isNull()
-    }
-
-    @Test
-    fun parseIntent_emptyIntent() {
-        val requestRouteContract = RequestExerciseRouteUpsideDownCake()
-        val result = requestRouteContract.parseResult(0, Intent())
-        assertThat(result).isNull()
-    }
-
-    @Test
-    fun parseIntent_emptyRoute() {
-        val requestRouteContract = RequestExerciseRouteUpsideDownCake()
-        val intent = Intent()
-        intent.putExtra(HealthConnectManager.EXTRA_EXERCISE_ROUTE, PlatformExerciseRoute(listOf()))
-        val result = requestRouteContract.parseResult(0, intent)
-        assertThat(result).isEqualTo(ExerciseRoute(listOf()))
-    }
-
-    @Test
-    fun parseIntent() {
-        val requestRouteContract = RequestExerciseRouteUpsideDownCake()
-        val intent = Intent()
-        val location1 =
-            PlatformExerciseRouteLocationBuilder(Instant.ofEpochMilli(1234L), 23.4, -23.4)
-                .setAltitude(PlatformLength.fromMeters(12.3))
-                .setHorizontalAccuracy(PlatformLength.fromMeters(0.9))
-                .setVerticalAccuracy(PlatformLength.fromMeters(0.3))
-                .build()
-        val location2 =
-            PlatformExerciseRouteLocationBuilder(Instant.ofEpochMilli(3456L), 23.45, -23.45).build()
-
-        intent.putExtra(
-            HealthConnectManager.EXTRA_EXERCISE_ROUTE,
-            PlatformExerciseRoute(listOf(location1, location2))
-        )
-        val result = requestRouteContract.parseResult(0, intent)
-        assertThat(result)
-            .isEqualTo(
-                ExerciseRoute(
-                    listOf(
-                        ExerciseRoute.Location(
-                            time = Instant.ofEpochMilli(1234L),
-                            latitude = 23.4,
-                            longitude = -23.4,
-                            horizontalAccuracy = Length.meters(0.9),
-                            verticalAccuracy = Length.meters(0.3),
-                            altitude = Length.meters(12.3)
-                        ),
-                        ExerciseRoute.Location(
-                            time = Instant.ofEpochMilli(3456L),
-                            latitude = 23.45,
-                            longitude = -23.45,
-                        )
-                    )
-                )
-            )
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
index fce572a..4f74132 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
@@ -320,6 +320,13 @@
             "androidx.health.ACTION_HEALTH_CONNECT_SETTINGS"
 
         /**
+         * The minimum version code of the default provider APK that supports manage data intent
+         * action.
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        internal const val ACTION_MANAGE_DATA_MIN_SUPPORTED_VERSION_CODE = 82932
+
+        /**
          * Intent action to open Health Connect settings on this phone. Developers should use this
          * if they want to re-direct the user to Health Connect.
          */
@@ -461,6 +468,29 @@
             )
         }
 
+        /**
+         * Intent action to open Health Connect data management screen on this phone. Developers
+         * should use this if they want to re-direct the user to Health Connect data management.
+         *
+         * @param context the context
+         * @return Intent action to open Health Connect data management screen.
+         */
+        @JvmStatic
+        fun getHealthConnectManageDataAction(context: Context): String {
+            val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+                "android.health.connect.action.MANAGE_HEALTH_DATA"
+            } else if (isProviderAvailable(
+                    context = context,
+                    providerVersionCode = ACTION_MANAGE_DATA_MIN_SUPPORTED_VERSION_CODE
+                )
+            ) {
+                "androidx.health.ACTION_MANAGE_HEALTH_DATA"
+            } else {
+                ACTION_HEALTH_CONNECT_SETTINGS
+            }
+            return action
+        }
+
         @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P)
         internal fun isSdkVersionSufficient() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
 
@@ -471,11 +501,16 @@
         internal fun isProviderAvailable(
             context: Context,
             providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME,
+            providerVersionCode: Int = DEFAULT_PROVIDER_MIN_VERSION_CODE
         ): Boolean {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
                 return true
             }
-            return isPackageInstalled(context.packageManager, providerPackageName)
+            return isPackageInstalled(
+                context.packageManager,
+                providerPackageName,
+                providerVersionCode
+            )
         }
 
         internal fun isProviderAvailableLegacy(
@@ -488,6 +523,7 @@
         private fun isPackageInstalled(
             packageManager: PackageManager,
             packageName: String,
+            versionCode: Int = DEFAULT_PROVIDER_MIN_VERSION_CODE
         ): Boolean {
             val packageInfo: PackageInfo =
                 try {
@@ -498,8 +534,7 @@
                 }
             return packageInfo.applicationInfo.enabled &&
                 (packageName != DEFAULT_PROVIDER_PACKAGE_NAME ||
-                    PackageInfoCompat.getLongVersionCode(packageInfo) >=
-                        DEFAULT_PROVIDER_MIN_VERSION_CODE) &&
+                    PackageInfoCompat.getLongVersionCode(packageInfo) >= versionCode) &&
                 hasBindableService(packageManager, packageName)
         }
 
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
index 1a840d2..bf1963e 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
@@ -19,8 +19,8 @@
 import androidx.annotation.RestrictTo
 import androidx.health.connect.client.HealthConnectClient.Companion.DEFAULT_PROVIDER_PACKAGE_NAME
 import androidx.health.connect.client.contracts.HealthPermissionsRequestContract
-import androidx.health.connect.client.permission.HealthDataRequestPermissionsInternal
 import androidx.health.connect.client.permission.HealthPermission
+import androidx.health.connect.client.permission.HealthPermissionsRequestAppContract
 
 @JvmDefaultWithCompatibility
 /** Interface for operations related to permissions. */
@@ -54,7 +54,7 @@
         fun createRequestPermissionResultContractLegacy(
             providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME
         ): ActivityResultContract<Set<String>, Set<String>> {
-            return HealthDataRequestPermissionsInternal(providerPackageName = providerPackageName)
+            return HealthPermissionsRequestAppContract(providerPackageName = providerPackageName)
         }
 
         /**
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContract.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContract.kt
index e936736..84a81bc 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContract.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContract.kt
@@ -20,8 +20,8 @@
 import android.content.Intent
 import android.os.Build
 import androidx.activity.result.contract.ActivityResultContract
-import androidx.health.connect.client.permission.RequestExerciseRouteInternal
-import androidx.health.connect.client.permission.platform.RequestExerciseRouteUpsideDownCake
+import androidx.health.connect.client.permission.ExerciseRouteRequestAppContract
+import androidx.health.connect.client.permission.platform.ExerciseRouteRequestModuleContract
 import androidx.health.connect.client.records.ExerciseRoute
 
 /**
@@ -36,9 +36,9 @@
 
     private val delegate: ActivityResultContract<String, ExerciseRoute?> =
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
-            RequestExerciseRouteUpsideDownCake()
+            ExerciseRouteRequestModuleContract()
         } else {
-            RequestExerciseRouteInternal()
+            ExerciseRouteRequestAppContract()
         }
 
     /**
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContract.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContract.kt
index 679412f..1b49a42 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContract.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContract.kt
@@ -21,9 +21,9 @@
 import android.os.Build
 import androidx.activity.result.contract.ActivityResultContract
 import androidx.health.connect.client.HealthConnectClient
-import androidx.health.connect.client.permission.HealthDataRequestPermissionsInternal
 import androidx.health.connect.client.permission.HealthPermission
-import androidx.health.connect.client.permission.platform.HealthDataRequestPermissionsUpsideDownCake
+import androidx.health.connect.client.permission.HealthPermissionsRequestAppContract
+import androidx.health.connect.client.permission.platform.HealthPermissionsRequestModuleContract
 
 /**
  * An [ActivityResultContract] to request Health permissions.
@@ -37,9 +37,9 @@
 
     private val delegate: ActivityResultContract<Set<String>, Set<String>> =
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
-            HealthDataRequestPermissionsUpsideDownCake()
+            HealthPermissionsRequestModuleContract()
         } else {
-            HealthDataRequestPermissionsInternal(providerPackageName)
+            HealthPermissionsRequestAppContract(providerPackageName)
         }
 
     /**
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/ExerciseRouteRequestAppContract.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/ExerciseRouteRequestAppContract.kt
new file mode 100644
index 0000000..2a1c981
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/ExerciseRouteRequestAppContract.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.health.connect.client.permission
+
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.impl.converters.records.toExerciseRouteData
+import androidx.health.connect.client.records.ExerciseRoute
+import androidx.health.platform.client.impl.logger.Logger
+import androidx.health.platform.client.service.HealthDataServiceConstants
+
+/**
+ * An [ActivityResultContract] to request a route associated with an {@code ExerciseSessionRecord}
+ * from the HealthConnect APK.
+ *
+ * @see androidx.activity.ComponentActivity.registerForActivityResult
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+internal class ExerciseRouteRequestAppContract : ActivityResultContract<String, ExerciseRoute?>() {
+    override fun createIntent(context: Context, input: String): Intent {
+        return Intent(HealthDataServiceConstants.ACTION_REQUEST_ROUTE).apply {
+            putExtra(HealthDataServiceConstants.EXTRA_SESSION_ID, input)
+        }
+    }
+
+    @Suppress("DEPRECATION") // getParcelableExtra
+    override fun parseResult(resultCode: Int, intent: Intent?): ExerciseRoute? {
+        val route =
+            intent?.getParcelableExtra<androidx.health.platform.client.exerciseroute.ExerciseRoute>(
+                HealthDataServiceConstants.EXTRA_EXERCISE_ROUTE
+            )
+        if (route == null) {
+            Logger.debug(HealthConnectClient.HEALTH_CONNECT_CLIENT_TAG, "No route returned.")
+            return null
+        }
+        Logger.debug(HealthConnectClient.HEALTH_CONNECT_CLIENT_TAG, "Returned a route.")
+        return toExerciseRouteData(route)
+    }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternal.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternal.kt
deleted file mode 100644
index 6ddad08..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternal.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.permission
-
-import android.content.Context
-import android.content.Intent
-import androidx.activity.result.contract.ActivityResultContract
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.HealthConnectClient.Companion.DEFAULT_PROVIDER_PACKAGE_NAME
-import androidx.health.connect.client.HealthConnectClient.Companion.HEALTH_CONNECT_CLIENT_TAG
-import androidx.health.platform.client.impl.logger.Logger
-import androidx.health.platform.client.permission.Permission as ParcelablePermission
-import androidx.health.platform.client.proto.PermissionProto
-import androidx.health.platform.client.service.HealthDataServiceConstants.ACTION_REQUEST_PERMISSIONS
-import androidx.health.platform.client.service.HealthDataServiceConstants.KEY_GRANTED_PERMISSIONS_STRING
-import androidx.health.platform.client.service.HealthDataServiceConstants.KEY_REQUESTED_PERMISSIONS_STRING
-
-/**
- * An [ActivityResultContract] to request Health Connect permissions.
- *
- * @param providerPackageName Optional provider package name for the backing implementation of
- *   choice.
- * @see androidx.activity.ComponentActivity.registerForActivityResult
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-internal class HealthDataRequestPermissionsInternal(
-    private val providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME,
-) : ActivityResultContract<Set<String>, Set<String>>() {
-
-    override fun createIntent(context: Context, input: Set<String>): Intent {
-        val protoPermissionList =
-            input
-                .asSequence()
-                .map {
-                    ParcelablePermission(
-                        PermissionProto.Permission.newBuilder().setPermission(it).build()
-                    )
-                }
-                .toCollection(ArrayList())
-        Logger.debug(HEALTH_CONNECT_CLIENT_TAG, "Requesting ${input.size} permissions.")
-        return Intent(ACTION_REQUEST_PERMISSIONS).apply {
-            putParcelableArrayListExtra(KEY_REQUESTED_PERMISSIONS_STRING, protoPermissionList)
-            if (providerPackageName.isNotEmpty()) {
-                setPackage(providerPackageName)
-            }
-        }
-    }
-
-    @Suppress("Deprecation")
-    override fun parseResult(resultCode: Int, intent: Intent?): Set<String> {
-        val grantedPermissions =
-            intent
-                ?.getParcelableArrayListExtra<ParcelablePermission>(KEY_GRANTED_PERMISSIONS_STRING)
-                ?.asSequence()
-                ?.map { it.proto.permission }
-                ?.toSet()
-                ?: emptySet()
-        Logger.debug(HEALTH_CONNECT_CLIENT_TAG, "Granted ${grantedPermissions.size} permissions.")
-        return grantedPermissions
-    }
-
-    override fun getSynchronousResult(
-        context: Context,
-        input: Set<String>,
-    ): SynchronousResult<Set<String>>? {
-        return null
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermissionsRequestAppContract.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermissionsRequestAppContract.kt
new file mode 100644
index 0000000..004c375
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermissionsRequestAppContract.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.health.connect.client.permission
+
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.HealthConnectClient.Companion.DEFAULT_PROVIDER_PACKAGE_NAME
+import androidx.health.connect.client.HealthConnectClient.Companion.HEALTH_CONNECT_CLIENT_TAG
+import androidx.health.platform.client.impl.logger.Logger
+import androidx.health.platform.client.permission.Permission as ParcelablePermission
+import androidx.health.platform.client.proto.PermissionProto
+import androidx.health.platform.client.service.HealthDataServiceConstants.ACTION_REQUEST_PERMISSIONS
+import androidx.health.platform.client.service.HealthDataServiceConstants.KEY_GRANTED_PERMISSIONS_STRING
+import androidx.health.platform.client.service.HealthDataServiceConstants.KEY_REQUESTED_PERMISSIONS_STRING
+
+/**
+ * An [ActivityResultContract] to request Health Connect permissions from the HealthConnect APK.
+ *
+ * @param providerPackageName Optional provider package name for the backing implementation of
+ *   choice.
+ * @see androidx.activity.ComponentActivity.registerForActivityResult
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+internal class HealthPermissionsRequestAppContract(
+    private val providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME,
+) : ActivityResultContract<Set<String>, Set<String>>() {
+
+    override fun createIntent(context: Context, input: Set<String>): Intent {
+        val protoPermissionList =
+            input
+                .asSequence()
+                .map {
+                    ParcelablePermission(
+                        PermissionProto.Permission.newBuilder().setPermission(it).build()
+                    )
+                }
+                .toCollection(ArrayList())
+        Logger.debug(HEALTH_CONNECT_CLIENT_TAG, "Requesting ${input.size} permissions.")
+        return Intent(ACTION_REQUEST_PERMISSIONS).apply {
+            putParcelableArrayListExtra(KEY_REQUESTED_PERMISSIONS_STRING, protoPermissionList)
+            if (providerPackageName.isNotEmpty()) {
+                setPackage(providerPackageName)
+            }
+        }
+    }
+
+    @Suppress("Deprecation")
+    override fun parseResult(resultCode: Int, intent: Intent?): Set<String> {
+        val grantedPermissions =
+            intent
+                ?.getParcelableArrayListExtra<ParcelablePermission>(KEY_GRANTED_PERMISSIONS_STRING)
+                ?.asSequence()
+                ?.map { it.proto.permission }
+                ?.toSet()
+                ?: emptySet()
+        Logger.debug(HEALTH_CONNECT_CLIENT_TAG, "Granted ${grantedPermissions.size} permissions.")
+        return grantedPermissions
+    }
+
+    override fun getSynchronousResult(
+        context: Context,
+        input: Set<String>,
+    ): SynchronousResult<Set<String>>? {
+        return null
+    }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/RequestExerciseRouteInternal.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/RequestExerciseRouteInternal.kt
deleted file mode 100644
index a21c0a3..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/RequestExerciseRouteInternal.kt
+++ /dev/null
@@ -1,55 +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.health.connect.client.permission
-
-import android.content.Context
-import android.content.Intent
-import androidx.activity.result.contract.ActivityResultContract
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.HealthConnectClient
-import androidx.health.connect.client.impl.converters.records.toExerciseRouteData
-import androidx.health.connect.client.records.ExerciseRoute
-import androidx.health.platform.client.impl.logger.Logger
-import androidx.health.platform.client.service.HealthDataServiceConstants
-
-/**
- * An [ActivityResultContract] to request a route associated with an {@code ExerciseSessionRecord}.
- *
- * @see androidx.activity.ComponentActivity.registerForActivityResult
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-internal class RequestExerciseRouteInternal : ActivityResultContract<String, ExerciseRoute?>() {
-    override fun createIntent(context: Context, input: String): Intent {
-        return Intent(HealthDataServiceConstants.ACTION_REQUEST_ROUTE).apply {
-            putExtra(HealthDataServiceConstants.EXTRA_SESSION_ID, input)
-        }
-    }
-
-    @Suppress("DEPRECATION") // getParcelableExtra
-    override fun parseResult(resultCode: Int, intent: Intent?): ExerciseRoute? {
-        val route =
-            intent?.getParcelableExtra<androidx.health.platform.client.exerciseroute.ExerciseRoute>(
-                HealthDataServiceConstants.EXTRA_EXERCISE_ROUTE
-            )
-        if (route == null) {
-            Logger.debug(HealthConnectClient.HEALTH_CONNECT_CLIENT_TAG, "No route returned.")
-            return null
-        }
-        Logger.debug(HealthConnectClient.HEALTH_CONNECT_CLIENT_TAG, "Returned a route.")
-        return toExerciseRouteData(route)
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/ExerciseRouteRequestModuleContract.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/ExerciseRouteRequestModuleContract.kt
new file mode 100644
index 0000000..363ba7c
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/ExerciseRouteRequestModuleContract.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.health.connect.client.permission.platform
+
+import android.content.Context
+import android.content.Intent
+import android.health.connect.HealthConnectManager
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.impl.platform.records.PlatformExerciseRoute
+import androidx.health.connect.client.impl.platform.records.toSdkExerciseRoute
+import androidx.health.connect.client.records.ExerciseRoute
+import androidx.health.platform.client.impl.logger.Logger
+
+/**
+ * An [ActivityResultContract] to request a route associated with an {@code ExerciseSessionRecord}
+ * from HealthConnect in the Android Platform.
+ *
+ * @see androidx.activity.ComponentActivity.registerForActivityResult
+ */
+@RequiresApi(34)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+internal class ExerciseRouteRequestModuleContract :
+    ActivityResultContract<String, ExerciseRoute?>() {
+    override fun createIntent(context: Context, input: String): Intent {
+        return Intent(HealthConnectManager.ACTION_REQUEST_EXERCISE_ROUTE).apply {
+            putExtra(HealthConnectManager.EXTRA_SESSION_ID, input)
+        }
+    }
+
+    override fun parseResult(resultCode: Int, intent: Intent?): ExerciseRoute? {
+        val route =
+            intent?.getParcelableExtra(
+                HealthConnectManager.EXTRA_EXERCISE_ROUTE,
+                PlatformExerciseRoute::class.java
+            )
+        if (route == null) {
+            Logger.debug(HealthConnectClient.HEALTH_CONNECT_CLIENT_TAG, "No route returned.")
+            return null
+        }
+        Logger.debug(HealthConnectClient.HEALTH_CONNECT_CLIENT_TAG, "Returned a route.")
+        return route.toSdkExerciseRoute()
+    }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCake.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCake.kt
deleted file mode 100644
index e4dd720..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCake.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.permission.platform
-
-import android.content.Context
-import android.content.Intent
-import androidx.activity.result.contract.ActivityResultContract
-import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
-import androidx.annotation.RestrictTo
-
-/**
- * An [ActivityResultContract] to request Health Connect system permissions.
- *
- * @see androidx.activity.ComponentActivity.registerForActivityResult
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-internal class HealthDataRequestPermissionsUpsideDownCake :
-    ActivityResultContract<Set<String>, Set<String>>() {
-
-    private val requestPermissions = RequestMultiplePermissions()
-
-    override fun createIntent(context: Context, input: Set<String>): Intent =
-        requestPermissions.createIntent(context, input.toTypedArray())
-
-    override fun parseResult(resultCode: Int, intent: Intent?): Set<String> =
-        requestPermissions.parseResult(resultCode, intent).filterValues { it }.keys
-
-    override fun getSynchronousResult(
-        context: Context,
-        input: Set<String>,
-    ): SynchronousResult<Set<String>>? =
-        requestPermissions.getSynchronousResult(context, input.toTypedArray())?.let { result ->
-            SynchronousResult(result.value.filterValues { it }.keys)
-        }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthPermissionsRequestModuleContract.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthPermissionsRequestModuleContract.kt
new file mode 100644
index 0000000..41091f9
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthPermissionsRequestModuleContract.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.health.connect.client.permission.platform
+
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
+import androidx.annotation.RestrictTo
+
+/**
+ * An [ActivityResultContract] to request Health Connect system permissions.
+ *
+ * @see androidx.activity.ComponentActivity.registerForActivityResult
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+internal class HealthPermissionsRequestModuleContract :
+    ActivityResultContract<Set<String>, Set<String>>() {
+
+    private val requestPermissions = RequestMultiplePermissions()
+
+    override fun createIntent(context: Context, input: Set<String>): Intent =
+        requestPermissions.createIntent(context, input.toTypedArray())
+
+    override fun parseResult(resultCode: Int, intent: Intent?): Set<String> =
+        requestPermissions.parseResult(resultCode, intent).filterValues { it }.keys
+
+    override fun getSynchronousResult(
+        context: Context,
+        input: Set<String>,
+    ): SynchronousResult<Set<String>>? =
+        requestPermissions.getSynchronousResult(context, input.toTypedArray())?.let { result ->
+            SynchronousResult(result.value.filterValues { it }.keys)
+        }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/RequestExerciseRouteUpsideDownCake.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/RequestExerciseRouteUpsideDownCake.kt
deleted file mode 100644
index b0c7a42..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/RequestExerciseRouteUpsideDownCake.kt
+++ /dev/null
@@ -1,59 +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.health.connect.client.permission.platform
-
-import android.content.Context
-import android.content.Intent
-import android.health.connect.HealthConnectManager
-import androidx.activity.result.contract.ActivityResultContract
-import androidx.annotation.RequiresApi
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.HealthConnectClient
-import androidx.health.connect.client.impl.platform.records.PlatformExerciseRoute
-import androidx.health.connect.client.impl.platform.records.toSdkExerciseRoute
-import androidx.health.connect.client.records.ExerciseRoute
-import androidx.health.platform.client.impl.logger.Logger
-
-/**
- * An [ActivityResultContract] to request a route associated with an {@code ExerciseSessionRecord}.
- *
- * @see androidx.activity.ComponentActivity.registerForActivityResult
- */
-@RequiresApi(34)
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-internal class RequestExerciseRouteUpsideDownCake :
-    ActivityResultContract<String, ExerciseRoute?>() {
-    override fun createIntent(context: Context, input: String): Intent {
-        return Intent(HealthConnectManager.ACTION_REQUEST_EXERCISE_ROUTE).apply {
-            putExtra(HealthConnectManager.EXTRA_SESSION_ID, input)
-        }
-    }
-
-    override fun parseResult(resultCode: Int, intent: Intent?): ExerciseRoute? {
-        val route =
-            intent?.getParcelableExtra(
-                HealthConnectManager.EXTRA_EXERCISE_ROUTE,
-                PlatformExerciseRoute::class.java
-            )
-        if (route == null) {
-            Logger.debug(HealthConnectClient.HEALTH_CONNECT_CLIENT_TAG, "No route returned.")
-            return null
-        }
-        Logger.debug(HealthConnectClient.HEALTH_CONNECT_CLIENT_TAG, "Returned a route.")
-        return route.toSdkExerciseRoute()
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRoute.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRoute.kt
index 679eddb..1bafdd3 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRoute.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRoute.kt
@@ -35,12 +35,6 @@
         }
     }
 
-    internal fun isWithin(startTime: Instant, endTime: Instant): Boolean {
-        val minTime = route.minBy { it.time }.time
-        val maxTime = route.maxBy { it.time }.time
-        return !minTime.isBefore(startTime) && maxTime.isBefore(endTime)
-    }
-
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is ExerciseRoute) return false
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
index ecd1acf..bfca023 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
@@ -130,8 +130,14 @@
                 "laps can not be out of parent time range."
             }
         }
-        if (exerciseRouteResult is ExerciseRouteResult.Data) {
-            require(exerciseRouteResult.exerciseRoute.isWithin(startTime, endTime)) {
+        if (
+            exerciseRouteResult is ExerciseRouteResult.Data &&
+                exerciseRouteResult.exerciseRoute.route.isNotEmpty()
+        ) {
+            val route = exerciseRouteResult.exerciseRoute.route
+            val minTime = route.minBy { it.time }.time
+            val maxTime = route.maxBy { it.time }.time
+            require(!minTime.isBefore(startTime) && maxTime.isBefore(endTime)) {
                 "route can not be out of parent time range."
             }
         }
@@ -351,9 +357,7 @@
             EXERCISE_TYPE_STRING_TO_INT_MAP.entries.associateBy({ it.value }, { it.key })
     }
 
-    /**
-     * List of supported activities on Health Platform.
-     */
+    /** List of supported activities on Health Platform. */
     @Retention(AnnotationRetention.SOURCE)
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @IntDef(
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepSessionRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepSessionRecord.kt
index d2a6b91..916fefc 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepSessionRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepSessionRecord.kt
@@ -24,32 +24,26 @@
 import java.time.ZoneOffset
 
 /**
- * Captures the user's length and type of sleep. Each record represents a time interval for a stage
- * of sleep.
+ * Captures the user's sleep length and its stages. Each record represents a time interval for a
+ * full sleep session.
  *
- * The start time of the record represents the start of the sleep stage and always needs to be
- * included. The timestamp represents the end of the sleep stage. Time intervals don't need to be
- * continuous but shouldn't overlap.
+ * All sleep stage time intervals should fall within the sleep session interval. Time intervals for
+ * stages don't need to be continuous but shouldn't overlap.
  *
- * Example code demonstrate how to read sleep session with stages:
+ * Example code demonstrate how to read sleep session:
  *
  * @sample androidx.health.connect.client.samples.ReadSleepSessions
- *
- * When deleting a session, associated sleep stage records need to be deleted separately:
- *
- * @sample androidx.health.connect.client.samples.DeleteSleepSession
- * @see SleepStageRecord
  */
-public class SleepSessionRecord(
+class SleepSessionRecord(
     override val startTime: Instant,
     override val startZoneOffset: ZoneOffset?,
     override val endTime: Instant,
     override val endZoneOffset: ZoneOffset?,
     /** Title of the session. Optional field. */
-    public val title: String? = null,
+    val title: String? = null,
     /** Additional notes for the session. Optional field. */
-    public val notes: String? = null,
-    public val stages: List<Stage> = emptyList(),
+    val notes: String? = null,
+    val stages: List<Stage> = emptyList(),
     override val metadata: Metadata = Metadata.EMPTY,
 ) : IntervalRecord {
 
@@ -150,9 +144,7 @@
             STAGE_TYPE_STRING_TO_INT_MAP.entries.associateBy({ it.value }, { it.key })
     }
 
-    /**
-     * Type of sleep stage.
-     */
+    /** Type of sleep stage. */
     @Retention(AnnotationRetention.SOURCE)
     @IntDef(
         value =
@@ -175,7 +167,7 @@
      *
      * @see SleepSessionRecord
      */
-    public class Stage(
+    class Stage(
         val startTime: Instant,
         val endTime: Instant,
         @property:StageTypes val stage: Int,
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
index 2b6a910..81036fd 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
@@ -178,6 +178,45 @@
         }
     }
 
+    @Test
+    @Config(sdk = [Build.VERSION_CODES.P])
+    fun getHealthConnectManageDataAction_unsupportedClient_returnsDefaultIntent() {
+        installPackage(
+            context,
+            HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME,
+            versionCode = HealthConnectClient.DEFAULT_PROVIDER_MIN_VERSION_CODE,
+            enabled = true
+        )
+
+        assertThat(HealthConnectClient.getHealthConnectManageDataAction(context)).isEqualTo(
+            HealthConnectClient.ACTION_HEALTH_CONNECT_SETTINGS
+        )
+    }
+
+    @Test
+    @Config(sdk = [Build.VERSION_CODES.P])
+    fun getHealthConnectManageDataAction_supportedClient() {
+        installPackage(
+            context,
+            HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME,
+            versionCode = HealthConnectClient.ACTION_MANAGE_DATA_MIN_SUPPORTED_VERSION_CODE,
+            enabled = true
+        )
+        installService(context, HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME)
+
+        assertThat(HealthConnectClient.getHealthConnectManageDataAction(context)).isEqualTo(
+            "androidx.health.ACTION_MANAGE_HEALTH_DATA"
+        )
+    }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun getHealthConnectManageDataAction_platformSupported() {
+        assertThat(HealthConnectClient.getHealthConnectManageDataAction(context)).isEqualTo(
+            "android.health.connect.action.MANAGE_HEALTH_DATA"
+        )
+    }
+
     private fun installPackage(
         context: Context,
         packageName: String,
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/ExerciseRouteRequestAppContractTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/ExerciseRouteRequestAppContractTest.kt
new file mode 100644
index 0000000..b3cfbc9
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/ExerciseRouteRequestAppContractTest.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.health.connect.client.permission
+
+import android.content.Context
+import android.content.Intent
+import androidx.health.connect.client.records.ExerciseRoute
+import androidx.health.connect.client.units.Length
+import androidx.health.platform.client.proto.DataProto
+import androidx.health.platform.client.service.HealthDataServiceConstants
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import java.time.Instant
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ExerciseRouteRequestAppContractTest {
+
+    private lateinit var context: Context
+
+    @Before
+    fun setUp() {
+        context = ApplicationProvider.getApplicationContext()
+    }
+
+    @Test
+    fun createIntentTest() {
+        val requestRouteContract = ExerciseRouteRequestAppContract()
+        val intent = requestRouteContract.createIntent(context, "someUid")
+        assertThat(intent.action).isEqualTo("androidx.health.action.REQUEST_EXERCISE_ROUTE")
+        assertThat(intent.getStringExtra(HealthDataServiceConstants.EXTRA_SESSION_ID))
+            .isEqualTo("someUid")
+    }
+
+    @Test
+    fun parseIntent_null() {
+        val requestRouteContract = ExerciseRouteRequestAppContract()
+        val result = requestRouteContract.parseResult(0, null)
+        assertThat(result).isNull()
+    }
+
+    @Test
+    fun parseIntent_emptyIntent() {
+        val requestRouteContract = ExerciseRouteRequestAppContract()
+        val result = requestRouteContract.parseResult(0, Intent())
+        assertThat(result).isNull()
+    }
+
+    @Test
+    fun parseIntent_emptyRoute() {
+        val requestRouteContract = ExerciseRouteRequestAppContract()
+        val intent = Intent()
+        intent.putExtra(
+            HealthDataServiceConstants.EXTRA_EXERCISE_ROUTE,
+            androidx.health.platform.client.exerciseroute.ExerciseRoute(
+                DataProto.DataPoint.SubTypeDataList.newBuilder().build()
+            )
+        )
+        val result = requestRouteContract.parseResult(0, intent)
+        assertThat(result).isEqualTo(ExerciseRoute(listOf()))
+    }
+
+    @Test
+    fun parseIntent() {
+        val requestRouteContract = ExerciseRouteRequestAppContract()
+        val intent = Intent()
+        val protoLocation1 =
+            DataProto.SubTypeDataValue.newBuilder()
+                .setStartTimeMillis(1234L)
+                .setEndTimeMillis(2345L)
+                .putValues("latitude", DataProto.Value.newBuilder().setDoubleVal(23.4).build())
+                .putValues("longitude", DataProto.Value.newBuilder().setDoubleVal(-23.4).build())
+                .putValues("altitude", DataProto.Value.newBuilder().setDoubleVal(12.3).build())
+                .putValues(
+                    "horizontal_accuracy",
+                    DataProto.Value.newBuilder().setDoubleVal(0.9).build()
+                )
+                .putValues(
+                    "vertical_accuracy",
+                    DataProto.Value.newBuilder().setDoubleVal(0.3).build()
+                )
+                .build()
+        val protoLocation2 =
+            DataProto.SubTypeDataValue.newBuilder()
+                .setStartTimeMillis(3456L)
+                .setEndTimeMillis(4567L)
+                .putValues("latitude", DataProto.Value.newBuilder().setDoubleVal(23.45).build())
+                .putValues("longitude", DataProto.Value.newBuilder().setDoubleVal(-23.45).build())
+                .build()
+        intent.putExtra(
+            HealthDataServiceConstants.EXTRA_EXERCISE_ROUTE,
+            androidx.health.platform.client.exerciseroute.ExerciseRoute(
+                DataProto.DataPoint.SubTypeDataList.newBuilder()
+                    .addAllValues(listOf(protoLocation1, protoLocation2))
+                    .build()
+            )
+        )
+        val result = requestRouteContract.parseResult(0, intent)
+        assertThat(result)
+            .isEqualTo(
+                ExerciseRoute(
+                    listOf(
+                        ExerciseRoute.Location(
+                            time = Instant.ofEpochMilli(1234L),
+                            latitude = 23.4,
+                            longitude = -23.4,
+                            horizontalAccuracy = Length.meters(0.9),
+                            verticalAccuracy = Length.meters(0.3),
+                            altitude = Length.meters(12.3)
+                        ),
+                        ExerciseRoute.Location(
+                            time = Instant.ofEpochMilli(3456L),
+                            latitude = 23.45,
+                            longitude = -23.45,
+                        )
+                    )
+                )
+            )
+    }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternalTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternalTest.kt
deleted file mode 100644
index df6fd02..0000000
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternalTest.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.health.connect.client.permission
-
-import android.content.Context
-import android.content.Intent
-import androidx.health.connect.client.HealthConnectClient
-import androidx.health.platform.client.permission.Permission
-import androidx.health.platform.client.proto.PermissionProto
-import androidx.health.platform.client.service.HealthDataServiceConstants
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-private const val TEST_PACKAGE = "com.test.app"
-
-@RunWith(AndroidJUnit4::class)
-class HealthDataRequestPermissionsInternalTest {
-
-    private lateinit var context: Context
-
-    @Before
-    fun setUp() {
-        context = ApplicationProvider.getApplicationContext()
-    }
-
-    @Test
-    fun createIntentTest() {
-        val requestPermissionContract = HealthDataRequestPermissionsInternal(TEST_PACKAGE)
-        val intent =
-            requestPermissionContract.createIntent(
-                context,
-                setOf(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)
-            )
-
-        Truth.assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
-        Truth.assertThat(intent.`package`).isEqualTo(TEST_PACKAGE)
-        Truth.assertThat(intent.`package`).isEqualTo(TEST_PACKAGE)
-
-        @Suppress("Deprecation")
-        Truth.assertThat(
-                intent.getParcelableArrayListExtra<Permission>(
-                    HealthDataServiceConstants.KEY_REQUESTED_PERMISSIONS_STRING
-                )
-            )
-            .isEqualTo(
-                arrayListOf(
-                    Permission(
-                        PermissionProto.Permission.newBuilder()
-                            .setPermission(HealthPermission.READ_STEPS)
-                            .build()
-                    ),
-                    Permission(
-                        PermissionProto.Permission.newBuilder()
-                            .setPermission(HealthPermission.WRITE_DISTANCE)
-                            .build()
-                    )
-                )
-            )
-    }
-
-    @Test
-    fun createIntent_defaultPackage() {
-        val requestPermissionContract = HealthDataRequestPermissionsInternal()
-        val intent =
-            requestPermissionContract.createIntent(context, setOf(HealthPermission.READ_STEPS))
-
-        Truth.assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
-        Truth.assertThat(intent.`package`)
-            .isEqualTo(HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME)
-    }
-
-    @Test
-    fun parseIntent_null_fallback() {
-        val requestPermissionContract = HealthDataRequestPermissionsInternal(TEST_PACKAGE)
-        val result = requestPermissionContract.parseResult(0, null)
-
-        Truth.assertThat(result).isEmpty()
-    }
-
-    @Test
-    fun parseIntent_emptyIntent() {
-        val requestPermissionContract = HealthDataRequestPermissionsInternal(TEST_PACKAGE)
-        val result = requestPermissionContract.parseResult(0, Intent())
-
-        Truth.assertThat(result).isEmpty()
-    }
-
-    @Test
-    fun parseIntent() {
-        val requestPermissionContract = HealthDataRequestPermissionsInternal(TEST_PACKAGE)
-        val intent = Intent()
-        intent.putParcelableArrayListExtra(
-            HealthDataServiceConstants.KEY_GRANTED_PERMISSIONS_STRING,
-            arrayListOf(
-                Permission(
-                    PermissionProto.Permission.newBuilder()
-                        .setPermission(HealthPermission.READ_STEPS)
-                        .build()
-                ),
-                Permission(
-                    PermissionProto.Permission.newBuilder()
-                        .setPermission(HealthPermission.WRITE_DISTANCE)
-                        .build()
-                )
-            )
-        )
-        val result = requestPermissionContract.parseResult(0, intent)
-
-        Truth.assertThat(result)
-            .containsExactly(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)
-    }
-
-    @Test
-    fun synchronousResult_null() {
-        val requestPermissionContract = HealthDataRequestPermissionsInternal(TEST_PACKAGE)
-        val result =
-            requestPermissionContract.getSynchronousResult(
-                context,
-                setOf(HealthPermission.READ_STEPS)
-            )
-
-        Truth.assertThat(result).isNull()
-    }
-}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionsRequestAppContractTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionsRequestAppContractTest.kt
new file mode 100644
index 0000000..961e65b
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionsRequestAppContractTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.permission
+
+import android.content.Context
+import android.content.Intent
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.platform.client.permission.Permission
+import androidx.health.platform.client.proto.PermissionProto
+import androidx.health.platform.client.service.HealthDataServiceConstants
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_PACKAGE = "com.test.app"
+
+@RunWith(AndroidJUnit4::class)
+class HealthPermissionsRequestAppContractTest {
+
+    private lateinit var context: Context
+
+    @Before
+    fun setUp() {
+        context = ApplicationProvider.getApplicationContext()
+    }
+
+    @Test
+    fun createIntentTest() {
+        val requestPermissionContract = HealthPermissionsRequestAppContract(TEST_PACKAGE)
+        val intent =
+            requestPermissionContract.createIntent(
+                context,
+                setOf(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)
+            )
+
+        Truth.assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
+        Truth.assertThat(intent.`package`).isEqualTo(TEST_PACKAGE)
+        Truth.assertThat(intent.`package`).isEqualTo(TEST_PACKAGE)
+
+        @Suppress("Deprecation")
+        Truth.assertThat(
+                intent.getParcelableArrayListExtra<Permission>(
+                    HealthDataServiceConstants.KEY_REQUESTED_PERMISSIONS_STRING
+                )
+            )
+            .isEqualTo(
+                arrayListOf(
+                    Permission(
+                        PermissionProto.Permission.newBuilder()
+                            .setPermission(HealthPermission.READ_STEPS)
+                            .build()
+                    ),
+                    Permission(
+                        PermissionProto.Permission.newBuilder()
+                            .setPermission(HealthPermission.WRITE_DISTANCE)
+                            .build()
+                    )
+                )
+            )
+    }
+
+    @Test
+    fun createIntent_defaultPackage() {
+        val requestPermissionContract = HealthPermissionsRequestAppContract()
+        val intent =
+            requestPermissionContract.createIntent(context, setOf(HealthPermission.READ_STEPS))
+
+        Truth.assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
+        Truth.assertThat(intent.`package`)
+            .isEqualTo(HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME)
+    }
+
+    @Test
+    fun parseIntent_null_fallback() {
+        val requestPermissionContract = HealthPermissionsRequestAppContract(TEST_PACKAGE)
+        val result = requestPermissionContract.parseResult(0, null)
+
+        Truth.assertThat(result).isEmpty()
+    }
+
+    @Test
+    fun parseIntent_emptyIntent() {
+        val requestPermissionContract = HealthPermissionsRequestAppContract(TEST_PACKAGE)
+        val result = requestPermissionContract.parseResult(0, Intent())
+
+        Truth.assertThat(result).isEmpty()
+    }
+
+    @Test
+    fun parseIntent() {
+        val requestPermissionContract = HealthPermissionsRequestAppContract(TEST_PACKAGE)
+        val intent = Intent()
+        intent.putParcelableArrayListExtra(
+            HealthDataServiceConstants.KEY_GRANTED_PERMISSIONS_STRING,
+            arrayListOf(
+                Permission(
+                    PermissionProto.Permission.newBuilder()
+                        .setPermission(HealthPermission.READ_STEPS)
+                        .build()
+                ),
+                Permission(
+                    PermissionProto.Permission.newBuilder()
+                        .setPermission(HealthPermission.WRITE_DISTANCE)
+                        .build()
+                )
+            )
+        )
+        val result = requestPermissionContract.parseResult(0, intent)
+
+        Truth.assertThat(result)
+            .containsExactly(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)
+    }
+
+    @Test
+    fun synchronousResult_null() {
+        val requestPermissionContract = HealthPermissionsRequestAppContract(TEST_PACKAGE)
+        val result =
+            requestPermissionContract.getSynchronousResult(
+                context,
+                setOf(HealthPermission.READ_STEPS)
+            )
+
+        Truth.assertThat(result).isNull()
+    }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/RequestExerciseRouteInternalTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/RequestExerciseRouteInternalTest.kt
deleted file mode 100644
index b650485..0000000
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/RequestExerciseRouteInternalTest.kt
+++ /dev/null
@@ -1,136 +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.health.connect.client.permission
-
-import android.content.Context
-import android.content.Intent
-import androidx.health.connect.client.records.ExerciseRoute
-import androidx.health.connect.client.units.Length
-import androidx.health.platform.client.proto.DataProto
-import androidx.health.platform.client.service.HealthDataServiceConstants
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import java.time.Instant
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class RequestExerciseRouteInternalTest {
-
-    private lateinit var context: Context
-
-    @Before
-    fun setUp() {
-        context = ApplicationProvider.getApplicationContext()
-    }
-
-    @Test
-    fun createIntentTest() {
-        val requestRouteContract = RequestExerciseRouteInternal()
-        val intent = requestRouteContract.createIntent(context, "someUid")
-        assertThat(intent.action).isEqualTo("androidx.health.action.REQUEST_EXERCISE_ROUTE")
-        assertThat(intent.getStringExtra(HealthDataServiceConstants.EXTRA_SESSION_ID))
-            .isEqualTo("someUid")
-    }
-
-    @Test
-    fun parseIntent_null() {
-        val requestRouteContract = RequestExerciseRouteInternal()
-        val result = requestRouteContract.parseResult(0, null)
-        assertThat(result).isNull()
-    }
-
-    @Test
-    fun parseIntent_emptyIntent() {
-        val requestRouteContract = RequestExerciseRouteInternal()
-        val result = requestRouteContract.parseResult(0, Intent())
-        assertThat(result).isNull()
-    }
-
-    @Test
-    fun parseIntent_emptyRoute() {
-        val requestRouteContract = RequestExerciseRouteInternal()
-        val intent = Intent()
-        intent.putExtra(
-            HealthDataServiceConstants.EXTRA_EXERCISE_ROUTE,
-            androidx.health.platform.client.exerciseroute.ExerciseRoute(
-                DataProto.DataPoint.SubTypeDataList.newBuilder().build()
-            )
-        )
-        val result = requestRouteContract.parseResult(0, intent)
-        assertThat(result).isEqualTo(ExerciseRoute(listOf()))
-    }
-
-    @Test
-    fun parseIntent() {
-        val requestRouteContract = RequestExerciseRouteInternal()
-        val intent = Intent()
-        val protoLocation1 =
-            DataProto.SubTypeDataValue.newBuilder()
-                .setStartTimeMillis(1234L)
-                .setEndTimeMillis(2345L)
-                .putValues("latitude", DataProto.Value.newBuilder().setDoubleVal(23.4).build())
-                .putValues("longitude", DataProto.Value.newBuilder().setDoubleVal(-23.4).build())
-                .putValues("altitude", DataProto.Value.newBuilder().setDoubleVal(12.3).build())
-                .putValues(
-                    "horizontal_accuracy",
-                    DataProto.Value.newBuilder().setDoubleVal(0.9).build()
-                )
-                .putValues(
-                    "vertical_accuracy",
-                    DataProto.Value.newBuilder().setDoubleVal(0.3).build()
-                )
-                .build()
-        val protoLocation2 =
-            DataProto.SubTypeDataValue.newBuilder()
-                .setStartTimeMillis(3456L)
-                .setEndTimeMillis(4567L)
-                .putValues("latitude", DataProto.Value.newBuilder().setDoubleVal(23.45).build())
-                .putValues("longitude", DataProto.Value.newBuilder().setDoubleVal(-23.45).build())
-                .build()
-        intent.putExtra(
-            HealthDataServiceConstants.EXTRA_EXERCISE_ROUTE,
-            androidx.health.platform.client.exerciseroute.ExerciseRoute(
-                DataProto.DataPoint.SubTypeDataList.newBuilder()
-                    .addAllValues(listOf(protoLocation1, protoLocation2))
-                    .build()
-            )
-        )
-        val result = requestRouteContract.parseResult(0, intent)
-        assertThat(result)
-            .isEqualTo(
-                ExerciseRoute(
-                    listOf(
-                        ExerciseRoute.Location(
-                            time = Instant.ofEpochMilli(1234L),
-                            latitude = 23.4,
-                            longitude = -23.4,
-                            horizontalAccuracy = Length.meters(0.9),
-                            verticalAccuracy = Length.meters(0.3),
-                            altitude = Length.meters(12.3)
-                        ),
-                        ExerciseRoute.Location(
-                            time = Instant.ofEpochMilli(3456L),
-                            latitude = 23.45,
-                            longitude = -23.45,
-                        )
-                    )
-                )
-            )
-    }
-}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCakeTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCakeTest.kt
deleted file mode 100644
index f284bd6a..0000000
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCakeTest.kt
+++ /dev/null
@@ -1,86 +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.health.connect.client.permission.platform
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
-import androidx.health.connect.client.permission.HealthPermission
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class HealthDataRequestPermissionsUpsideDownCakeTest {
-
-    private lateinit var context: Context
-
-    @Before
-    fun setUp() {
-        context = ApplicationProvider.getApplicationContext()
-    }
-
-    @Test
-    fun createIntent() {
-        val requestPermissionContract = HealthDataRequestPermissionsUpsideDownCake()
-        val intent =
-            requestPermissionContract.createIntent(
-                context,
-                setOf(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)
-            )
-
-        assertThat(intent.action).isEqualTo(RequestMultiplePermissions.ACTION_REQUEST_PERMISSIONS)
-        assertThat(intent.getStringArrayExtra(RequestMultiplePermissions.EXTRA_PERMISSIONS))
-            .asList()
-            .containsExactly(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)
-    }
-
-    @Test
-    fun parseIntent() {
-        val requestPermissionContract = HealthDataRequestPermissionsUpsideDownCake()
-
-        val intent = Intent()
-        intent.putExtra(
-            RequestMultiplePermissions.EXTRA_PERMISSIONS,
-            arrayOf(
-                HealthPermission.READ_STEPS,
-                HealthPermission.WRITE_STEPS,
-                HealthPermission.WRITE_DISTANCE,
-                HealthPermission.READ_HEART_RATE
-            )
-        )
-        intent.putExtra(
-            RequestMultiplePermissions.EXTRA_PERMISSION_GRANT_RESULTS,
-            intArrayOf(
-                PackageManager.PERMISSION_GRANTED,
-                PackageManager.PERMISSION_DENIED,
-                PackageManager.PERMISSION_GRANTED,
-                PackageManager.PERMISSION_DENIED
-            )
-        )
-
-        val result = requestPermissionContract.parseResult(Activity.RESULT_OK, intent)
-
-        assertThat(result)
-            .containsExactly(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)
-    }
-}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthPermissionsRequestModuleContractTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthPermissionsRequestModuleContractTest.kt
new file mode 100644
index 0000000..b6d7cb2
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthPermissionsRequestModuleContractTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.health.connect.client.permission.platform
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
+import androidx.health.connect.client.permission.HealthPermission
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class HealthPermissionsRequestModuleContractTest {
+
+    private lateinit var context: Context
+
+    @Before
+    fun setUp() {
+        context = ApplicationProvider.getApplicationContext()
+    }
+
+    @Test
+    fun createIntent() {
+        val requestPermissionContract = HealthPermissionsRequestModuleContract()
+        val intent =
+            requestPermissionContract.createIntent(
+                context,
+                setOf(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)
+            )
+
+        assertThat(intent.action).isEqualTo(RequestMultiplePermissions.ACTION_REQUEST_PERMISSIONS)
+        assertThat(intent.getStringArrayExtra(RequestMultiplePermissions.EXTRA_PERMISSIONS))
+            .asList()
+            .containsExactly(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)
+    }
+
+    @Test
+    fun parseIntent() {
+        val requestPermissionContract = HealthPermissionsRequestModuleContract()
+
+        val intent = Intent()
+        intent.putExtra(
+            RequestMultiplePermissions.EXTRA_PERMISSIONS,
+            arrayOf(
+                HealthPermission.READ_STEPS,
+                HealthPermission.WRITE_STEPS,
+                HealthPermission.WRITE_DISTANCE,
+                HealthPermission.READ_HEART_RATE
+            )
+        )
+        intent.putExtra(
+            RequestMultiplePermissions.EXTRA_PERMISSION_GRANT_RESULTS,
+            intArrayOf(
+                PackageManager.PERMISSION_GRANTED,
+                PackageManager.PERMISSION_DENIED,
+                PackageManager.PERMISSION_GRANTED,
+                PackageManager.PERMISSION_DENIED
+            )
+        )
+
+        val result = requestPermissionContract.parseResult(Activity.RESULT_OK, intent)
+
+        assertThat(result)
+            .containsExactly(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)
+    }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
index 73af184..f4b9255 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
@@ -121,6 +121,99 @@
     }
 
     @Test
+    fun validRecord_emptyRoute_equals() {
+        assertThat(
+                ExerciseSessionRecord(
+                    startTime = Instant.ofEpochMilli(1234L),
+                    startZoneOffset = null,
+                    endTime = Instant.ofEpochMilli(1236L),
+                    endZoneOffset = null,
+                    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_BIKING,
+                    title = "title",
+                    notes = "notes",
+                    segments =
+                        listOf(
+                            ExerciseSegment(
+                                startTime = Instant.ofEpochMilli(1234L),
+                                endTime = Instant.ofEpochMilli(1235L),
+                                segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
+                            )
+                        ),
+                    laps =
+                        listOf(
+                            ExerciseLap(
+                                startTime = Instant.ofEpochMilli(1235L),
+                                endTime = Instant.ofEpochMilli(1236L),
+                                length = 10.meters,
+                            )
+                        ),
+                    exerciseRoute = ExerciseRoute(route = listOf()),
+                )
+            )
+            .isEqualTo(
+                ExerciseSessionRecord(
+                    startTime = Instant.ofEpochMilli(1234L),
+                    startZoneOffset = null,
+                    endTime = Instant.ofEpochMilli(1236L),
+                    endZoneOffset = null,
+                    exerciseType = EXERCISE_TYPE_BIKING,
+                    title = "title",
+                    notes = "notes",
+                    segments =
+                        listOf(
+                            ExerciseSegment(
+                                startTime = Instant.ofEpochMilli(1234L),
+                                endTime = Instant.ofEpochMilli(1235L),
+                                segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
+                            )
+                        ),
+                    laps =
+                        listOf(
+                            ExerciseLap(
+                                startTime = Instant.ofEpochMilli(1235L),
+                                endTime = Instant.ofEpochMilli(1236L),
+                                length = 10.meters,
+                            )
+                        ),
+                    exerciseRoute = ExerciseRoute(route = listOf()),
+                )
+            )
+    }
+
+    @Test
+    fun validRecord_emptyRoute_hasExerciseRouteData() {
+        val record =
+            ExerciseSessionRecord(
+                startTime = Instant.ofEpochMilli(1234L),
+                startZoneOffset = null,
+                endTime = Instant.ofEpochMilli(1236L),
+                endZoneOffset = null,
+                exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_BIKING,
+                title = "title",
+                notes = "notes",
+                segments =
+                    listOf(
+                        ExerciseSegment(
+                            startTime = Instant.ofEpochMilli(1234L),
+                            endTime = Instant.ofEpochMilli(1235L),
+                            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
+                        )
+                    ),
+                laps =
+                    listOf(
+                        ExerciseLap(
+                            startTime = Instant.ofEpochMilli(1235L),
+                            endTime = Instant.ofEpochMilli(1236L),
+                            length = 10.meters,
+                        )
+                    ),
+                exerciseRoute = ExerciseRoute(route = listOf()),
+            )
+        assertThat((record.exerciseRouteResult as ExerciseRouteResult.Data))
+            .isEqualTo(ExerciseRouteResult.Data(ExerciseRoute(listOf())))
+    }
+
+    @Test
     fun invalidTimes_throws() {
         assertFailsWith<IllegalArgumentException> {
             ExerciseSessionRecord(
diff --git a/hilt/integration-tests/viewmodelapp/lint-baseline.xml b/hilt/integration-tests/viewmodelapp/lint-baseline.xml
new file mode 100644
index 0000000..18c2ee5
--- /dev/null
+++ b/hilt/integration-tests/viewmodelapp/lint-baseline.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
+
+    <issue
+        id="MissingClass"
+        message="Class referenced in the manifest, `androidx.hilt.integration.viewmodelapp.ActivityInjectionTest$TestActivity`, was not found in the project or the libraries"
+        errorLine1="        &lt;activity android:name=&quot;.ActivityInjectionTest$TestActivity&quot;/>"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/debug/AndroidManifest.xml"/>
+    </issue>
+
+    <issue
+        id="MissingClass"
+        message="Class referenced in the manifest, `androidx.hilt.integration.viewmodelapp.BaseActivityInjectionTest$TestActivity`, was not found in the project or the libraries"
+        errorLine1="        &lt;activity android:name=&quot;.BaseActivityInjectionTest$TestActivity&quot;/>"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/debug/AndroidManifest.xml"/>
+    </issue>
+
+    <issue
+        id="MissingClass"
+        message="Class referenced in the manifest, `androidx.hilt.integration.viewmodelapp.FragmentInjectionTest$TestActivity`, was not found in the project or the libraries"
+        errorLine1="        &lt;activity android:name=&quot;.FragmentInjectionTest$TestActivity&quot;/>"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/debug/AndroidManifest.xml"/>
+    </issue>
+
+    <issue
+        id="MissingClass"
+        message="Class referenced in the manifest, `androidx.hilt.integration.viewmodelapp.BaseFragmentInjectionTest$TestActivity`, was not found in the project or the libraries"
+        errorLine1="        &lt;activity android:name=&quot;.BaseFragmentInjectionTest$TestActivity&quot;/>"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/debug/AndroidManifest.xml"/>
+    </issue>
+
+</issues>
diff --git a/javascriptengine/javascriptengine/api/aidlRelease/current/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl b/javascriptengine/javascriptengine/api/aidlRelease/current/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl
new file mode 100644
index 0000000..b00a1ca
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/aidlRelease/current/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package org.chromium.android_webview.js_sandbox.common;
+/* @hide */
+interface IJsSandboxIsolateClient {
+  void onTerminated(int status, String message) = 1;
+  const int TERMINATE_UNKNOWN_ERROR = 1;
+  const int TERMINATE_SANDBOX_DEAD = 2;
+  const int TERMINATE_MEMORY_LIMIT_EXCEEDED = 3;
+}
diff --git a/javascriptengine/javascriptengine/api/aidlRelease/current/org/chromium/android_webview/js_sandbox/common/IJsSandboxService.aidl b/javascriptengine/javascriptengine/api/aidlRelease/current/org/chromium/android_webview/js_sandbox/common/IJsSandboxService.aidl
index cd3aae0..78a0831 100644
--- a/javascriptengine/javascriptengine/api/aidlRelease/current/org/chromium/android_webview/js_sandbox/common/IJsSandboxService.aidl
+++ b/javascriptengine/javascriptengine/api/aidlRelease/current/org/chromium/android_webview/js_sandbox/common/IJsSandboxService.aidl
@@ -37,9 +37,11 @@
   org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolate createIsolate() = 0;
   List<String> getSupportedFeatures() = 1;
   org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolate createIsolateWithMaxHeapSizeBytes(long maxHeapSize) = 2;
+  org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolate createIsolate2(long maxHeapSize, org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolateClient isolateClient) = 3;
   const String ISOLATE_TERMINATION = "ISOLATE_TERMINATION";
   const String ISOLATE_MAX_HEAP_SIZE_LIMIT = "ISOLATE_MAX_HEAP_SIZE_LIMIT";
   const String WASM_FROM_ARRAY_BUFFER = "WASM_FROM_ARRAY_BUFFER";
   const String EVALUATE_WITHOUT_TRANSACTION_LIMIT = "EVALUATE_WITHOUT_TRANSACTION_LIMIT";
   const String CONSOLE_MESSAGING = "CONSOLE_MESSAGING";
+  const String ISOLATE_CLIENT = "ISOLATE_CLIENT";
 }
diff --git a/javascriptengine/javascriptengine/api/current.txt b/javascriptengine/javascriptengine/api/current.txt
index e523e0b..77b4206c 100644
--- a/javascriptengine/javascriptengine/api/current.txt
+++ b/javascriptengine/javascriptengine/api/current.txt
@@ -20,7 +20,7 @@
     field public static final int DEFAULT_MAX_EVALUATION_RETURN_SIZE_BYTES = 20971520; // 0x1400000
   }
 
-  public final class IsolateTerminatedException extends androidx.javascriptengine.JavaScriptException {
+  public class IsolateTerminatedException extends androidx.javascriptengine.JavaScriptException {
     ctor public IsolateTerminatedException();
   }
 
@@ -74,12 +74,12 @@
     field public static final String JS_FEATURE_WASM_COMPILATION = "JS_FEATURE_WASM_COMPILATION";
   }
 
-  public final class MemoryLimitExceededException extends androidx.javascriptengine.JavaScriptException {
+  public final class MemoryLimitExceededException extends androidx.javascriptengine.IsolateTerminatedException {
     ctor public MemoryLimitExceededException();
     ctor public MemoryLimitExceededException(String);
   }
 
-  public final class SandboxDeadException extends androidx.javascriptengine.JavaScriptException {
+  public final class SandboxDeadException extends androidx.javascriptengine.IsolateTerminatedException {
     ctor public SandboxDeadException();
   }
 
diff --git a/javascriptengine/javascriptengine/api/restricted_current.txt b/javascriptengine/javascriptengine/api/restricted_current.txt
index e523e0b..77b4206c 100644
--- a/javascriptengine/javascriptengine/api/restricted_current.txt
+++ b/javascriptengine/javascriptengine/api/restricted_current.txt
@@ -20,7 +20,7 @@
     field public static final int DEFAULT_MAX_EVALUATION_RETURN_SIZE_BYTES = 20971520; // 0x1400000
   }
 
-  public final class IsolateTerminatedException extends androidx.javascriptengine.JavaScriptException {
+  public class IsolateTerminatedException extends androidx.javascriptengine.JavaScriptException {
     ctor public IsolateTerminatedException();
   }
 
@@ -74,12 +74,12 @@
     field public static final String JS_FEATURE_WASM_COMPILATION = "JS_FEATURE_WASM_COMPILATION";
   }
 
-  public final class MemoryLimitExceededException extends androidx.javascriptengine.JavaScriptException {
+  public final class MemoryLimitExceededException extends androidx.javascriptengine.IsolateTerminatedException {
     ctor public MemoryLimitExceededException();
     ctor public MemoryLimitExceededException(String);
   }
 
-  public final class SandboxDeadException extends androidx.javascriptengine.JavaScriptException {
+  public final class SandboxDeadException extends androidx.javascriptengine.IsolateTerminatedException {
     ctor public SandboxDeadException();
   }
 
diff --git a/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java b/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java
index 8252afd..d1f5af0 100644
--- a/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java
+++ b/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java
@@ -17,12 +17,9 @@
 package androidx.javascriptengine;
 
 import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.webkit.WebView;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
-import androidx.core.content.pm.PackageInfoCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
@@ -58,18 +55,6 @@
         Assume.assumeTrue(JavaScriptSandbox.isSupported());
     }
 
-    // Get the current WebView provider version. In a versionCode of AAAABBBCD, AAAA is the build
-    // number and BBB is the patch number. C and D may usually be ignored.
-    //
-    // Strongly prefer using feature flags over version checks if possible.
-    public long getWebViewVersion() {
-        PackageInfo systemWebViewPackage = WebView.getCurrentWebViewPackage();
-        if (systemWebViewPackage == null) {
-            Assert.fail("No current WebView provider");
-        }
-        return PackageInfoCompat.getLongVersionCode(systemWebViewPackage);
-    }
-
     @Test
     @MediumTest
     public void testSimpleJsEvaluation() throws Throwable {
@@ -584,14 +569,6 @@
     @Test
     @LargeTest
     public void testHeapSizeEnforced() throws Throwable {
-        // WebView versions < 110.0.5438.0 do not contain OOM crashes to a single isolate and
-        // instead crash the whole sandbox process. This change is not tracked in a feature flag.
-        // Versions < 110.0.5438.0 are not considered to be broken, but their behavior is not
-        // of interest for this test.
-        // See Chromium change: https://chromium-review.googlesource.com/c/chromium/src/+/4047785
-        Assume.assumeTrue("WebView version does not support per-isolate OOM handling",
-                getWebViewVersion() >= 5438_000_00L);
-
         final long maxHeapSize = REASONABLE_HEAP_SIZE;
         // We need to beat the v8 optimizer to ensure it really allocates the required memory. Note
         // that we're allocating an array of elements - not bytes. Filling will ensure that the
@@ -608,9 +585,11 @@
         try (JavaScriptSandbox jsSandbox = jsSandboxFuture1.get(5, TimeUnit.SECONDS)) {
             Assume.assumeTrue(jsSandbox.isFeatureSupported(
                     JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
-
             Assume.assumeTrue(
                     jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
+            Assume.assumeTrue(
+                    jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_CLIENT));
+
             IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
             isolateStartupParameters.setMaxHeapSizeBytes(maxHeapSize);
             try (JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate(isolateStartupParameters);
@@ -625,8 +604,7 @@
                 // Wait for jsIsolate2 to fully initialize before using jsIsolate1.
                 jsIsolate2.evaluateJavaScriptAsync(stableCode).get(5, TimeUnit.SECONDS);
 
-                // Check that the heap limit is enforced and that it reports this was the evaluation
-                // that exceeded the limit.
+                // Check that the heap limit is enforced.
                 try {
                     // Use a generous timeout for OOM, as it may involve multiple rounds of garbage
                     // collection.
@@ -638,13 +616,22 @@
                     }
                 }
 
+                // Wait for termination, but don't close the isolate.
+                final CountDownLatch latch = new CountDownLatch(1);
+                jsIsolate1.addOnTerminatedCallback(Runnable::run, info -> {
+                    Assert.assertEquals(TerminationInfo.STATUS_MEMORY_LIMIT_EXCEEDED,
+                            info.getStatus());
+                    latch.countDown();
+                });
+                Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
+
                 // Check that the previously submitted (but unresolved) promise evaluation reports a
                 // crash
                 try {
                     earlyUnresolvedResultFuture.get(5, TimeUnit.SECONDS);
                     Assert.fail("Should have thrown.");
                 } catch (ExecutionException e) {
-                    if (!(e.getCause() instanceof IsolateTerminatedException)) {
+                    if (!(e.getCause() instanceof MemoryLimitExceededException)) {
                         throw e;
                     }
                 }
@@ -667,11 +654,18 @@
                     }
                 }
 
-                // Check that other pre-existing isolates can still be used.
+                // Check that other pre-existing isolate in the same sandbox can no longer be used.
+                // (That the sandbox as a whole is dead.)
                 ListenableFuture<String> otherIsolateResultFuture =
                         jsIsolate2.evaluateJavaScriptAsync(stableCode);
-                String otherIsolateResult = otherIsolateResultFuture.get(5, TimeUnit.SECONDS);
-                Assert.assertEquals(stableExpected, otherIsolateResult);
+                try {
+                    otherIsolateResultFuture.get(5, TimeUnit.SECONDS);
+                    Assert.fail("Should have thrown.");
+                } catch (ExecutionException e) {
+                    if (!(e.getCause() instanceof SandboxDeadException)) {
+                        throw e;
+                    }
+                }
             }
         }
     }
@@ -679,14 +673,6 @@
     @Test
     @LargeTest
     public void testIsolateCreationAfterCrash() throws Throwable {
-        // WebView versions < 110.0.5438.0 do not contain OOM crashes to a single isolate and
-        // instead crash the whole sandbox process. This change is not tracked in a feature flag.
-        // Versions < 110.0.5438.0 are not considered to be broken, but their behavior is not
-        // of interest for this test.
-        // See Chromium change: https://chromium-review.googlesource.com/c/chromium/src/+/4047785
-        Assume.assumeTrue("WebView version does not support per-isolate OOM handling",
-                getWebViewVersion() >= 5438_000_00L);
-
         final long maxHeapSize = REASONABLE_HEAP_SIZE;
         // We need to beat the v8 optimizer to ensure it really allocates the required memory. Note
         // that we're allocating an array of elements - not bytes. Filling will ensure that the
@@ -704,14 +690,15 @@
                     JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
             Assume.assumeTrue(
                     jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
+            Assume.assumeTrue(
+                    jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_CLIENT));
             IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
             isolateStartupParameters.setMaxHeapSizeBytes(maxHeapSize);
             try (JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate(isolateStartupParameters)) {
                 ListenableFuture<String> oomResultFuture =
                         jsIsolate1.evaluateJavaScriptAsync(oomingCode);
 
-                // Check that the heap limit is enforced and that it reports this was the evaluation
-                // that exceeded the limit.
+                // Check that the heap limit is enforced.
                 try {
                     // Use a generous timeout for OOM, as it may involve multiple rounds of garbage
                     // collection.
@@ -723,28 +710,22 @@
                     }
                 }
 
-                // Check that other isolates can still be created and used (without closing
-                // jsIsolate1).
-                try (JavaScriptIsolate jsIsolate2 =
-                             jsSandbox.createIsolate(isolateStartupParameters)) {
-                    ListenableFuture<String> resultFuture =
-                            jsIsolate2.evaluateJavaScriptAsync(stableCode);
-                    String result = resultFuture.get(5, TimeUnit.SECONDS);
-                    Assert.assertEquals(stableExpected, result);
-                }
-            }
+                final CountDownLatch latch = new CountDownLatch(1);
+                jsIsolate1.addOnTerminatedCallback(Runnable::run, info -> {
+                    Assert.assertEquals(TerminationInfo.STATUS_MEMORY_LIMIT_EXCEEDED,
+                            info.getStatus());
+                    latch.countDown();
+                });
+                Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
 
-            // Check that other isolates can still be created and used (after closing jsIsolate1).
-            try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(isolateStartupParameters)) {
-                ListenableFuture<String> resultFuture =
-                        jsIsolate.evaluateJavaScriptAsync(stableCode);
-                String result = resultFuture.get(5, TimeUnit.SECONDS);
-                Assert.assertEquals(stableExpected, result);
+                // Check that new isolates can no longer be created in the same sandbox.
+                Assert.assertThrows(IllegalStateException.class,
+                        () -> jsSandbox.createIsolate(isolateStartupParameters));
             }
         }
 
-        // Check that the old sandbox with the "crashed" isolate can be torn down and that a new
-        // sandbox and isolate can be spun up.
+        // Check that after the old OOMed sandbox is closed and torn down that a new sandbox and
+        // isolate can be spun up.
         ListenableFuture<JavaScriptSandbox> jsSandboxFuture2 =
                 JavaScriptSandbox.createConnectedInstanceAsync(context);
         try (JavaScriptSandbox jsSandbox = jsSandboxFuture2.get(5, TimeUnit.SECONDS);
@@ -1144,4 +1125,80 @@
             Assert.assertEquals(expected, result);
         }
     }
+
+    @Test
+    @LargeTest
+    public void testTerminationNotificationForSandboxDeath() throws Throwable {
+        final Context context = ApplicationProvider.getApplicationContext();
+        final ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
+                JavaScriptSandbox.createConnectedInstanceAsync(context);
+        try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
+            try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
+                final ListenableFuture<String> loopFuture =
+                        jsIsolate.evaluateJavaScriptAsync("while(true);");
+
+                final CountDownLatch latch = new CountDownLatch(2);
+
+                final Runnable futureCallback = () -> {
+                    try {
+                        loopFuture.get();
+                        Assert.fail("Should have thrown.");
+                    } catch (ExecutionException e) {
+                        if (!(e.getCause() instanceof SandboxDeadException)) {
+                            Assert.fail("Wrong exception for evaluation: " + e);
+                        }
+                    } catch (InterruptedException e) {
+                        Assert.fail("Interrupted: " + e);
+                    }
+                    latch.countDown();
+                };
+                loopFuture.addListener(futureCallback, Runnable::run);
+
+                jsIsolate.addOnTerminatedCallback(Runnable::run, info -> {
+                    Assert.assertEquals(TerminationInfo.STATUS_SANDBOX_DEAD, info.getStatus());
+                    latch.countDown();
+                });
+
+                jsSandbox.close();
+
+                Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
+            }
+        }
+    }
+
+    @Test
+    @LargeTest
+    public void testOomOutsideOfEvaluation() throws Throwable {
+        final Context context = ApplicationProvider.getApplicationContext();
+        final ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
+                JavaScriptSandbox.createConnectedInstanceAsync(context);
+        try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
+            Assume.assumeTrue(jsSandbox.isFeatureSupported(
+                    JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
+            Assume.assumeTrue(
+                    jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_CLIENT));
+            IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
+            isolateStartupParameters.setMaxHeapSizeBytes(REASONABLE_HEAP_SIZE);
+            try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(isolateStartupParameters)) {
+                // OOM should occur in a microtask, not during this evaluation, so we should
+                // never get a MemoryLimitExceededException from the evaluation future.
+                final String code = ""
+                        + "const bytes = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];"
+                        + "WebAssembly.compile(new Uint8Array(bytes)).then(() => {"
+                        + "  this.array ="
+                        + "    Array(" + REASONABLE_HEAP_SIZE + ").fill(Math.random(), 0);"
+                        + "});"
+                        + "'PASS'";
+                jsIsolate.evaluateJavaScriptAsync(code).get(5, TimeUnit.SECONDS);
+
+                final CountDownLatch latch = new CountDownLatch(1);
+                jsIsolate.addOnTerminatedCallback(Runnable::run, info -> {
+                    Assert.assertEquals(
+                            TerminationInfo.STATUS_MEMORY_LIMIT_EXCEEDED, info.getStatus());
+                    latch.countDown();
+                });
+                Assert.assertTrue(latch.await(60, TimeUnit.SECONDS));
+            }
+        }
+    }
 }
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/EnvironmentDeadState.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/EnvironmentDeadState.java
index 0c99deb..c10d56fd 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/EnvironmentDeadState.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/EnvironmentDeadState.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.core.util.Consumer;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -25,15 +26,19 @@
 
 /**
  * Covers the case where the environment is dead.
- *
+ * <p>
  * This state covers cases where the developer explicitly closes the sandbox or sandbox/isolate
  * being dead outside of the control of the developer.
+ * <p>
+ * Although being in this state is considered terminated from the app perspective, the service
+ * side may still technically be running.
  */
 final class EnvironmentDeadState implements IsolateState {
-    private final JavaScriptException mException;
+    @NonNull
+    private final TerminationInfo mTerminationInfo;
 
-    EnvironmentDeadState(JavaScriptException e) {
-        mException = e;
+    EnvironmentDeadState(@NonNull TerminationInfo terminationInfo) {
+        mTerminationInfo = terminationInfo;
     }
 
     @NonNull
@@ -41,7 +46,7 @@
     public ListenableFuture<String> evaluateJavaScriptAsync(@NonNull String code) {
         return CallbackToFutureAdapter.getFuture(completer -> {
             final String futureDebugMessage = "evaluateJavascript Future";
-            completer.setException(mException);
+            completer.setException(mTerminationInfo.toJavaScriptException());
             return futureDebugMessage;
         });
     }
@@ -69,12 +74,16 @@
     }
 
     @Override
-    public IsolateState setSandboxDead() {
-        return new EnvironmentDeadState(new SandboxDeadException());
+    public boolean canDie() {
+        return false;
     }
 
     @Override
-    public IsolateState setIsolateDead() {
-        return this;
+    public void addOnTerminatedCallback(@NonNull Executor executor,
+            @NonNull Consumer<TerminationInfo> callback) {
+        executor.execute(() -> callback.accept(mTerminationInfo));
     }
+
+    @Override
+    public void removeOnTerminatedCallback(@NonNull Consumer<TerminationInfo> callback) {}
 }
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateClosedState.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateClosedState.java
index 927bbe0..ba944b1 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateClosedState.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateClosedState.java
@@ -17,48 +17,55 @@
 package androidx.javascriptengine;
 
 import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.concurrent.Executor;
 
 /**
- * Covers the case where the isolate is explicitly closed by the developer.
+ * Covers cases where the isolate is explicitly closed or uninitialized.
+ * <p>
+ * Although being in this state is considered terminated from the app perspective, the service
+ * side may still technically be running.
  */
 final class IsolateClosedState implements IsolateState {
-    IsolateClosedState() {
+    @NonNull
+    private final String mDescription;
+    IsolateClosedState(@NonNull String description) {
+        mDescription = description;
     }
 
     @NonNull
     @Override
     public ListenableFuture<String> evaluateJavaScriptAsync(@NonNull String code) {
-        throw new IllegalStateException("Calling evaluateJavaScriptAsync() after closing the"
-                + "Isolate");
+        throw new IllegalStateException(
+                "Calling evaluateJavaScriptAsync() when " + mDescription);
     }
 
     @Override
     public void setConsoleCallback(@NonNull Executor executor,
             @NonNull JavaScriptConsoleCallback callback) {
         throw new IllegalStateException(
-                "Calling setConsoleCallback() after closing the Isolate");
+                "Calling setConsoleCallback() when " + mDescription);
     }
 
     @Override
     public void setConsoleCallback(@NonNull JavaScriptConsoleCallback callback) {
         throw new IllegalStateException(
-                "Calling setConsoleCallback() after closing the Isolate");
+                "Calling setConsoleCallback() when " + mDescription);
     }
 
     @Override
     public void clearConsoleCallback() {
         throw new IllegalStateException(
-                "Calling clearConsoleCallback() after closing the Isolate");
+                "Calling clearConsoleCallback() when " + mDescription);
     }
 
     @Override
     public boolean provideNamedData(@NonNull String name, @NonNull byte[] inputBytes) {
         throw new IllegalStateException(
-                "Calling provideNamedData() after closing the Isolate");
+                "Calling provideNamedData() when " + mDescription);
     }
 
     @Override
@@ -66,12 +73,20 @@
     }
 
     @Override
-    public IsolateState setSandboxDead() {
-        return this;
+    public boolean canDie() {
+        return false;
     }
 
     @Override
-    public IsolateState setIsolateDead() {
-        return this;
+    public void addOnTerminatedCallback(@NonNull Executor executor,
+            @NonNull Consumer<TerminationInfo> callback) {
+        throw new IllegalStateException(
+                "Calling addOnTerminatedCallback() when " + mDescription);
+    }
+
+    @Override
+    public void removeOnTerminatedCallback(@NonNull Consumer<TerminationInfo> callback) {
+        throw new IllegalStateException(
+                "Calling removeOnTerminatedCallback() when " + mDescription);
     }
 }
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateStartupParameters.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateStartupParameters.java
index a1f5013..fe7714f 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateStartupParameters.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateStartupParameters.java
@@ -47,10 +47,17 @@
      * some multiple of bytes, be increased to some minimum value, or reduced to some maximum
      * supported value.
      * <p>
-     * Exceeding this limit will usually result in a {@link MemoryLimitExceededException},
-     * but beware that not all JavaScript sandbox service implementations (particularly older ones)
-     * handle memory exhaustion equally gracefully, and may crash the entire sandbox (see
-     * {@link SandboxDeadException}).
+     * Exceeding this limit will usually result in all unfinished and future evaluations failing
+     * with {@link MemoryLimitExceededException} and the isolate terminating with a status of
+     * {@link TerminationInfo#STATUS_MEMORY_LIMIT_EXCEEDED}. Note that exceeding the memory limit
+     * will take down the entire sandbox - not just the responsible isolate - and all other
+     * isolates will receive generic {@link SandboxDeadException} and
+     * {@link TerminationInfo#STATUS_SANDBOX_DEAD} errors.
+     * <p>
+     * Not all JavaScript sandbox service implementations (particularly older ones) handle memory
+     * exhaustion equally, and may crash the sandbox without attributing the failure to memory
+     * exhaustion in a particular isolate.
+     *
      * @param size heap size in bytes
      */
     @RequiresFeature(name = JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE,
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateState.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateState.java
index 5a44f48..6188bc9 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateState.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateState.java
@@ -17,6 +17,7 @@
 package androidx.javascriptengine;
 
 import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -24,17 +25,16 @@
 
 /**
  * Interface for State design pattern.
- *
+ * <p>
  * Isolates can be in different states due to events within/outside the control of the developer.
  * This pattern allows us to extract out the state related behaviour without maintaining it all in
  * the JavaScriptIsolate class which proved to be error-prone and hard to read.
- *
+ * <p>
  * State specific behaviour are implemented in concrete classes that implements this interface.
- *
+ * <p>
  * Refer: https://en.wikipedia.org/wiki/State_pattern
  */
 interface IsolateState {
-
     @NonNull
     ListenableFuture<String> evaluateJavaScriptAsync(@NonNull String code);
 
@@ -49,7 +49,22 @@
 
     void close();
 
-    IsolateState setIsolateDead();
+    /**
+     * Check whether the current state is permitted to transition to a dead state
+     *
+     * @return true iff a transition to a dead state is permitted.
+     */
+    boolean canDie();
 
-    IsolateState setSandboxDead();
+    /**
+     * Method to run after this state has been replaced by a dead state.
+     *
+     * @param terminationInfo The termination info describing the death.
+     */
+    default void onDied(@NonNull TerminationInfo terminationInfo) {}
+
+    void addOnTerminatedCallback(@NonNull Executor executor,
+            @NonNull Consumer<TerminationInfo> callback);
+
+    void removeOnTerminatedCallback(@NonNull Consumer<TerminationInfo> callback);
 }
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateTerminatedException.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateTerminatedException.java
index b7547ecb..8548c19 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateTerminatedException.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateTerminatedException.java
@@ -16,27 +16,42 @@
 
 package androidx.javascriptengine;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Consumer;
+
+import java.util.concurrent.Executor;
+
 /**
  * Exception thrown when evaluation is terminated due to the {@link JavaScriptIsolate} being closed
- * or crashing.
+ * or due to some crash.
  * <p>
  * Calling {@link JavaScriptIsolate#close()} will cause this exception to be thrown for all
  * previously requested but pending evaluations.
  * <p>
- * If the individual isolate has crashed, for example, due to exceeding a memory limit, this
- * exception will also be thrown for all pending and future evaluations (until
- * {@link JavaScriptIsolate#close()} is called).
- * <p>
- * Note that if the sandbox as a whole has crashed or been closed, {@link SandboxDeadException} will
- * be thrown instead.
+ * If an isolate has crashed (but not been closed), subsequently requested evaluations will fail
+ * immediately with an IsolateTerminatedException (or a subclass) consistent with that
+ * used for evaluations submitted before the crash.
  * <p>
  * Note that this exception will not be thrown if the isolate has been explicitly closed before a
  * call to {@link JavaScriptIsolate#evaluateJavaScriptAsync(String)}, which will instead immediately
  * throw an IllegalStateException (and not asynchronously via a future). This applies even if the
  * isolate was closed following a crash.
+ * <p>
+ * Do not attempt to parse the information in this exception's message as it may change between
+ * JavaScriptEngine versions.
+ * <p>
+ * Note that it is possible for an isolate to crash outside of submitted evaluations, in which
+ * case an IsolateTerminatedException may not be observed. Consider instead using
+ * {@link JavaScriptIsolate#addOnTerminatedCallback(Executor, Consumer)} if you need to reliably
+ * or immediately detect isolate crashes rather than evaluation failures.
  */
-public final class IsolateTerminatedException extends JavaScriptException {
+public class IsolateTerminatedException extends JavaScriptException {
     public IsolateTerminatedException() {
         super();
     }
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public IsolateTerminatedException(@NonNull String message) {
+        super(message);
+    }
 }
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateUsableState.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateUsableState.java
index e253c86..8f7f2f9 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateUsableState.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/IsolateUsableState.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.core.util.Consumer;
 import androidx.javascriptengine.common.LengthLimitExceededException;
 import androidx.javascriptengine.common.Utils;
 
@@ -37,6 +38,7 @@
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Objects;
 import java.util.Set;
@@ -65,6 +67,11 @@
     @GuardedBy("mLock")
     private Set<CallbackToFutureAdapter.Completer<String>> mPendingCompleterSet =
             new HashSet<>();
+    // mOnTerminatedCallbacks does not require this.mLock, as all accesses should be performed
+    // whilst holding the mLock of the JavaScriptIsolate that owns this state object.
+    @NonNull
+    private final HashMap<Consumer<TerminationInfo>, Executor> mOnTerminatedCallbacks =
+            new HashMap<>();
 
     private class IJsSandboxIsolateSyncCallbackStubWrapper extends
             IJsSandboxIsolateSyncCallback.Stub {
@@ -79,6 +86,9 @@
         @Override
         public void reportResultWithFd(AssetFileDescriptor afd) {
             Objects.requireNonNull(afd);
+            // The completer needs to be removed before offloading to the executor, otherwise there
+            // is a race to complete it if all evaluations are cancelled.
+            removePending(mCompleter);
             mJsIsolate.mJsSandbox.mThreadPoolTaskExecutor.execute(
                     () -> {
                         String result;
@@ -87,13 +97,11 @@
                                     mMaxEvaluationReturnSizeBytes,
                                     /*truncate=*/false);
                         } catch (IOException | UnsupportedOperationException ex) {
-                            removePending(mCompleter);
                             mCompleter.setException(
                                     new JavaScriptException(
                                             "Retrieving result failed: " + ex.getMessage()));
                             return;
                         } catch (LengthLimitExceededException ex) {
-                            removePending(mCompleter);
                             if (ex.getMessage() != null) {
                                 mCompleter.setException(
                                         new EvaluationResultSizeLimitExceededException(
@@ -111,6 +119,9 @@
         @Override
         public void reportErrorWithFd(@ExecutionErrorTypes int type, AssetFileDescriptor afd) {
             Objects.requireNonNull(afd);
+            // The completer needs to be removed before offloading to the executor, otherwise there
+            // is a race to complete it if all evaluations are cancelled.
+            removePending(mCompleter);
             mJsIsolate.mJsSandbox.mThreadPoolTaskExecutor.execute(
                     () -> {
                         String error;
@@ -119,7 +130,6 @@
                                     mMaxEvaluationReturnSizeBytes,
                                     /*truncate=*/true);
                         } catch (IOException | UnsupportedOperationException ex) {
-                            removePending(mCompleter);
                             mCompleter.setException(
                                     new JavaScriptException(
                                             "Retrieving error failed: " + ex.getMessage()));
@@ -144,6 +154,7 @@
         @Override
         public void reportResult(String result) {
             Objects.requireNonNull(result);
+            removePending(mCompleter);
             final long identityToken = Binder.clearCallingIdentity();
             try {
                 handleEvaluationResult(mCompleter, result);
@@ -155,6 +166,7 @@
         @Override
         public void reportError(@ExecutionErrorTypes int type, String error) {
             Objects.requireNonNull(error);
+            removePending(mCompleter);
             final long identityToken = Binder.clearCallingIdentity();
             try {
                 handleEvaluationError(mCompleter, type, error);
@@ -239,11 +251,12 @@
                     new IJsSandboxIsolateCallbackStubWrapper(completer);
             try {
                 mJsIsolateStub.evaluateJavascript(code, callbackStub);
-                addToPendingCompleterSet(completer);
+                addPending(completer);
             } catch (DeadObjectException e) {
                 // The sandbox process has died.
-                mJsIsolate.maybeSetSandboxDead();
-                completer.setException(new SandboxDeadException());
+                final TerminationInfo terminationInfo = mJsIsolate.maybeSetSandboxDead();
+                Objects.requireNonNull(terminationInfo);
+                completer.setException(terminationInfo.toJavaScriptException());
             } catch (RemoteException e) {
                 completer.setException(new RuntimeException(e));
             }
@@ -309,49 +322,52 @@
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException was thrown during close()", e);
         }
-        cancelAllPendingEvaluations(new IsolateTerminatedException());
+        cancelAllPendingEvaluations(new IsolateTerminatedException("isolate closed"));
     }
 
     @Override
-    public IsolateState setIsolateDead() {
-        IsolateTerminatedException exception = new IsolateTerminatedException();
-        cancelAllPendingEvaluations(exception);
-        return new EnvironmentDeadState(exception);
+    public boolean canDie() {
+        return true;
     }
 
     @Override
-    public IsolateState setSandboxDead() {
-        SandboxDeadException exception = new SandboxDeadException();
-        cancelAllPendingEvaluations(exception);
-        return new EnvironmentDeadState(exception);
+    public void onDied(@NonNull TerminationInfo terminationInfo) {
+        cancelAllPendingEvaluations(terminationInfo.toJavaScriptException());
+        mOnTerminatedCallbacks.forEach(
+                (callback, executor) -> executor.execute(() -> callback.accept(terminationInfo)));
     }
 
+    // Caller should call mJsIsolate.removePending(mCompleter) first
     void handleEvaluationError(@NonNull CallbackToFutureAdapter.Completer<String> completer,
             int type, @NonNull String error) {
-        removePending(completer);
-        boolean crashing = false;
         switch (type) {
             case IJsSandboxIsolateSyncCallback.JS_EVALUATION_ERROR:
                 completer.setException(new EvaluationFailedException(error));
                 break;
             case IJsSandboxIsolateSyncCallback.MEMORY_LIMIT_EXCEEDED:
-                completer.setException(new MemoryLimitExceededException(error));
-                crashing = true;
+                // Note that we won't ever receive a MEMORY_LIMIT_EXCEEDED evaluation error if
+                // the service side supports termination notifications, so this only handles the
+                // case where it doesn't.
+                final TerminationInfo terminationInfo =
+                        new TerminationInfo(TerminationInfo.STATUS_MEMORY_LIMIT_EXCEEDED, error);
+                mJsIsolate.maybeSetIsolateDead(terminationInfo);
+                // The completer was already removed from the set, so we're responsible for it.
+                // Use our exception even if the isolate was already dead or closed. This might
+                // result in an exception which is inconsistent with everything else if there was
+                // a death or close before we called maybeSetIsolateDead above, but that requires
+                // the app to have already set up a race condition.
+                completer.setException(terminationInfo.toJavaScriptException());
                 break;
             default:
                 completer.setException(new JavaScriptException(
-                        "Crashing due to unknown JavaScriptException: " + error));
-                // Assume the worst
-                crashing = true;
-        }
-        if (crashing) {
-            mJsIsolate.maybeSetIsolateDead();
+                        "Unknown error: code " + type + ": " + error));
+                break;
         }
     }
 
+    // Caller should call mJsIsolate.removePending(mCompleter) first
     void handleEvaluationResult(@NonNull CallbackToFutureAdapter.Completer<String> completer,
             @NonNull String result) {
-        removePending(completer);
         completer.set(result);
     }
 
@@ -361,7 +377,7 @@
         }
     }
 
-    void addToPendingCompleterSet(@NonNull CallbackToFutureAdapter.Completer<String> completer) {
+    void addPending(@NonNull CallbackToFutureAdapter.Completer<String> completer) {
         synchronized (mLock) {
             mPendingCompleterSet.add(completer);
         }
@@ -394,11 +410,12 @@
                     mJsIsolateStub.evaluateJavascriptWithFd(codeAfd,
                             callbackStub);
                 }
-                addToPendingCompleterSet(completer);
+                addPending(completer);
             } catch (DeadObjectException e) {
                 // The sandbox process has died.
-                mJsIsolate.maybeSetSandboxDead();
-                completer.setException(new SandboxDeadException());
+                final TerminationInfo terminationInfo = mJsIsolate.maybeSetSandboxDead();
+                Objects.requireNonNull(terminationInfo);
+                completer.setException(terminationInfo.toJavaScriptException());
             } catch (RemoteException | IOException e) {
                 completer.setException(new RuntimeException(e));
             }
@@ -406,4 +423,19 @@
             return futureDebugMessage;
         });
     }
+
+    @Override
+    public void addOnTerminatedCallback(@NonNull Executor executor,
+            @NonNull Consumer<TerminationInfo> callback) {
+        if (mOnTerminatedCallbacks.putIfAbsent(callback, executor) != null) {
+            throw new IllegalStateException("Termination callback already registered");
+        }
+    }
+
+    @Override
+    public void removeOnTerminatedCallback(@NonNull Consumer<TerminationInfo> callback) {
+        synchronized (mLock) {
+            mOnTerminatedCallbacks.remove(callback);
+        }
+    }
 }
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
index df63307..ab19538 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
@@ -16,12 +16,20 @@
 
 package androidx.javascriptengine;
 
+import android.os.Binder;
+import android.os.RemoteException;
+import android.util.Log;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresFeature;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Consumer;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
 import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolate;
+import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolateClient;
 
 import java.util.Objects;
 import java.util.concurrent.Executor;
@@ -49,44 +57,106 @@
     private final Object mLock = new Object();
     private final CloseGuardHelper mGuard = CloseGuardHelper.create();
 
+    @NonNull
     final JavaScriptSandbox mJsSandbox;
 
     @GuardedBy("mLock")
     @NonNull
     private IsolateState mIsolateState;
 
-    JavaScriptIsolate(@NonNull IJsSandboxIsolate jsIsolateStub, @NonNull JavaScriptSandbox sandbox,
-            @NonNull IsolateStartupParameters settings) {
+    private final class JsSandboxIsolateClient extends IJsSandboxIsolateClient.Stub {
+        JsSandboxIsolateClient() {}
+
+        @Override
+        public void onTerminated(int status, String message) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                // If we're already closed, this will do nothing
+                maybeSetIsolateDead(new TerminationInfo(status, message));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
+    @NonNull
+    static JavaScriptIsolate create(@NonNull JavaScriptSandbox sandbox,
+            IsolateStartupParameters settings) throws RemoteException {
+        final JavaScriptIsolate isolate = new JavaScriptIsolate(sandbox);
+        isolate.initialize(settings);
+        return isolate;
+    }
+
+    private JavaScriptIsolate(@NonNull JavaScriptSandbox sandbox) {
+        mJsSandbox = sandbox;
         synchronized (mLock) {
+            mIsolateState = new IsolateClosedState("isolate not initialized");
+        }
+    }
+
+    // Create an isolate on the service side and complete initialization.
+    // This is done outside of the constructor to avoid leaking a partially constructed
+    // JavaScriptIsolate to the service (which would complicate thread-safety).
+    private void initialize(@NonNull IsolateStartupParameters settings) throws RemoteException {
+        synchronized (mLock) {
+            final IJsSandboxIsolateClient instanceCallback;
+            if (mJsSandbox.isFeatureSupported(
+                    JavaScriptSandbox.JS_FEATURE_ISOLATE_CLIENT)) {
+                instanceCallback = new JsSandboxIsolateClient();
+            } else {
+                instanceCallback = null;
+            }
+            IJsSandboxIsolate jsIsolateStub = mJsSandbox.createIsolateOnService(settings,
+                    instanceCallback);
             mIsolateState = new IsolateUsableState(this, jsIsolateStub,
                     settings.getMaxEvaluationReturnSizeBytes());
+            mGuard.open("close");
         }
-        mJsSandbox = sandbox;
-        mGuard.open("close");
-        // This should be at the end of the constructor.
     }
 
     /**
      * Changes the state to denote that the isolate is dead.
-     *
+     * <p>
      * {@link IsolateClosedState} takes precedence so it will not change state if the current state
-     * is {@link IsolateClosedState}
+     * is {@link IsolateClosedState}.
+     * <p>
+     * If the isolate is already dead, the existing dead state is preserved.
+     *
+     * @return true iff the state was changed to a new EnvironmentDeadState
      */
-    void maybeSetIsolateDead() {
+    boolean maybeSetIsolateDead(@NonNull TerminationInfo terminationInfo) {
         synchronized (mLock) {
-            mIsolateState = mIsolateState.setIsolateDead();
+            if (terminationInfo.getStatus() == TerminationInfo.STATUS_MEMORY_LIMIT_EXCEEDED) {
+                Log.e(TAG, "isolate exceeded its heap memory limit - killing sandbox");
+                mJsSandbox.kill();
+            }
+            final IsolateState oldState = mIsolateState;
+            if (oldState.canDie()) {
+                mIsolateState = new EnvironmentDeadState(terminationInfo);
+                oldState.onDied(terminationInfo);
+                return true;
+            }
         }
+        return false;
     }
 
     /**
      * Changes the state to denote that the sandbox is dead.
+     * <p>
+     * See {@link #maybeSetIsolateDead(TerminationInfo)} for additional information.
      *
-     * {@link IsolateClosedState} takes precedence so it will not change state if the current state
-     * is {@link IsolateClosedState}
+     * @return the generated termination info if it was set, or null if the state did not change.
      */
-    void maybeSetSandboxDead() {
+    @Nullable
+    TerminationInfo maybeSetSandboxDead() {
         synchronized (mLock) {
-            mIsolateState = mIsolateState.setSandboxDead();
+            final TerminationInfo terminationInfo =
+                    new TerminationInfo(TerminationInfo.STATUS_SANDBOX_DEAD, "sandbox dead");
+            if (maybeSetIsolateDead(terminationInfo)) {
+                return terminationInfo;
+            } else {
+                return null;
+            }
         }
     }
 
@@ -143,23 +213,28 @@
     /**
      * Closes the {@link JavaScriptIsolate} object and renders it unusable.
      * <p>
-     * Once closed, no more method calls should be made. Pending evaluations resolve with
-     * {@link IsolateTerminatedException} immediately.
+     * Once closed, no more method calls should be made. Pending evaluations will reject with
+     * an {@link IsolateTerminatedException} immediately.
      * <p>
      * If {@link JavaScriptSandbox#isFeatureSupported(String)} is {@code true} for {@link
-     * JavaScriptSandbox#JS_FEATURE_ISOLATE_TERMINATION}, then any pending evaluation is immediately
-     * terminated and memory is freed. If it is {@code false}, the isolate will not get cleaned
+     * JavaScriptSandbox#JS_FEATURE_ISOLATE_TERMINATION}, then any pending evaluations are
+     * terminated. If it is {@code false}, the isolate will not get cleaned
      * up until the pending evaluations have run to completion and will consume resources until
      * then.
+     * <p>
+     * Closing an isolate via this method does not wait on the isolate to clean up. Resources
+     * held by the isolate may remain in use for a duration after this method returns.
      */
     @Override
     public void close() {
         synchronized (mLock) {
             mIsolateState.close();
-            mIsolateState = new IsolateClosedState();
-            mJsSandbox.removeFromIsolateSet(this);
-            mGuard.close();
+            mIsolateState = new IsolateClosedState("isolate closed");
         }
+        // Do not hold mLock whilst calling into JavaScriptSandbox, as JavaScriptSandbox also has
+        // its own lock and may want to call into JavaScriptIsolate from another thread.
+        mJsSandbox.removeFromIsolateSet(this);
+        mGuard.close();
     }
 
     /**
@@ -296,4 +371,58 @@
             mIsolateState.clearConsoleCallback();
         }
     }
+
+    /**
+     * Add a callback to listen for isolate crashes.
+     * <p>
+     * There is no guaranteed order to when these callbacks are triggered and unfinished
+     * evaluations' futures are rejected.
+     * <p>
+     * Registering a callback after the isolate has crashed will result in it being executed
+     * immediately on the supplied executor with the isolate's {@link TerminationInfo} as an
+     * argument.
+     * <p>
+     * Closing an isolate via {@link #close()} is not considered a crash, even if there are
+     * unresolved evaluations, and will not trigger termination callbacks.
+     *
+     * @param executor Executor with which to run callback.
+     * @param callback Consumer to be called with TerminationInfo when a crash occurs.
+     * @throws IllegalStateException if the callback is already registered (using any executor).
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public void addOnTerminatedCallback(@NonNull Executor executor,
+            @NonNull Consumer<TerminationInfo> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+        synchronized (mLock) {
+            mIsolateState.addOnTerminatedCallback(executor, callback);
+        }
+    }
+
+    /**
+     * Add a callback to listen for isolate crashes.
+     * <p>
+     * This is the same as calling {@link #addOnTerminatedCallback(Executor, Consumer)} using the
+     * main executor of the context used to create the {@link JavaScriptSandbox} object.
+     *
+     * @param callback Consumer to be called with TerminationInfo when a crash occurs.
+     * @throws IllegalStateException if the callback is already registered (using any executor).
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public void addOnTerminatedCallback(@NonNull Consumer<TerminationInfo> callback) {
+        addOnTerminatedCallback(mJsSandbox.getMainExecutor(), callback);
+    }
+
+    /**
+     * Remove a callback previously registered with addOnTerminatedCallback.
+     *
+     * @param callback The callback to unregister, if currently registered.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public void removeOnTerminatedCallback(@NonNull Consumer<TerminationInfo> callback) {
+        Objects.requireNonNull(callback);
+        synchronized (mLock) {
+            mIsolateState.removeOnTerminatedCallback(callback);
+        }
+    }
 }
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptSandbox.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptSandbox.java
index 834502a..8a5c048 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptSandbox.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptSandbox.java
@@ -37,21 +37,25 @@
 import com.google.common.util.concurrent.ListenableFuture;
 
 import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolate;
+import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolateClient;
 import org.chromium.android_webview.js_sandbox.common.IJsSandboxService;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 
 import javax.annotation.concurrent.GuardedBy;
 
@@ -88,14 +92,24 @@
     private final Object mLock = new Object();
     private final CloseGuardHelper mGuard = CloseGuardHelper.create();
 
+    // This is null iff the sandbox is closed.
     @Nullable
     @GuardedBy("mLock")
     private IJsSandboxService mJsSandboxService;
 
-    private final ConnectionSetup mConnection;
+    // Don't use mLock for the connection, allowing it to be severed at any time, regardless of
+    // the status of the main mLock. Use an AtomicReference instead.
+    //
+    // The underlying ConnectionSetup is nullable, and is null iff the service has been unbound
+    // (which should also imply dead or closed).
+    @NonNull
+    private final AtomicReference<ConnectionSetup> mConnection;
+    @NonNull
+    private final Context mContext;
 
     @GuardedBy("mLock")
-    private final HashSet<JavaScriptIsolate> mActiveIsolateSet = new HashSet<>();
+    @NonNull
+    private Set<JavaScriptIsolate> mActiveIsolateSet;
 
     final ExecutorService mThreadPoolTaskExecutor =
             Executors.newCachedThreadPool(new ThreadFactory() {
@@ -120,6 +134,7 @@
                     JS_FEATURE_ISOLATE_MAX_HEAP_SIZE,
                     JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT,
                     JS_FEATURE_CONSOLE_MESSAGING,
+                    JS_FEATURE_ISOLATE_CLIENT,
             })
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.PARAMETER, ElementType.METHOD})
@@ -198,6 +213,15 @@
      */
     public static final String JS_FEATURE_CONSOLE_MESSAGING = "JS_FEATURE_CONSOLE_MESSAGING";
 
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * <p>
+     * When this feature is present, the service can be provided with a Binder interface for
+     * calling into the client, independent of callbacks.
+     */
+    static final String JS_FEATURE_ISOLATE_CLIENT =
+            "JS_FEATURE_ISOLATE_CLIENT";
+
     // This set must not be modified after JavaScriptSandbox construction.
     @NonNull
     private final HashSet<String> mClientSideFeatureSet;
@@ -207,7 +231,8 @@
         private CallbackToFutureAdapter.Completer<JavaScriptSandbox> mCompleter;
         @Nullable
         private JavaScriptSandbox mJsSandbox;
-        final Context mContext;
+        @NonNull
+        private final Context mContext;
 
         @Override
         @SuppressWarnings("NullAway")
@@ -222,7 +247,7 @@
             IJsSandboxService jsSandboxService =
                     IJsSandboxService.Stub.asInterface(service);
             try {
-                mJsSandbox = new JavaScriptSandbox(this, jsSandboxService);
+                mJsSandbox = new JavaScriptSandbox(mContext, this, jsSandboxService);
             } catch (RemoteException e) {
                 runShutdownTasks(e);
                 return;
@@ -386,12 +411,14 @@
 
     // We prevent direct initializations of this class.
     // Use JavaScriptSandbox.createConnectedInstance().
-    JavaScriptSandbox(@NonNull ConnectionSetup connectionSetup,
+    JavaScriptSandbox(@NonNull Context context, @NonNull ConnectionSetup connectionSetup,
             @NonNull IJsSandboxService jsSandboxService) throws RemoteException {
-        mConnection = connectionSetup;
+        mContext = context;
+        mConnection = new AtomicReference<>(connectionSetup);
         mJsSandboxService = jsSandboxService;
         final List<String> features = mJsSandboxService.getSupportedFeatures();
         mClientSideFeatureSet = buildClientSideFeatureSet(features);
+        mActiveIsolateSet = new HashSet<>();
         mGuard.open("close");
         // This should be at the end of the constructor.
     }
@@ -415,27 +442,39 @@
     public JavaScriptIsolate createIsolate(@NonNull IsolateStartupParameters settings) {
         Objects.requireNonNull(settings);
         synchronized (mLock) {
-            if (mJsSandboxService == null) {
+            // TODO(b/290750354, b/290749782): Implement separate closed vs dead state and use
+            //  that instead of a mConnection nullness check.
+            if (mJsSandboxService == null || mConnection.get() == null) {
                 throw new IllegalStateException(
                         "Attempting to createIsolate on a service that isn't connected");
             }
-            IJsSandboxIsolate isolateStub;
+            final JavaScriptIsolate isolate;
             try {
-                if (settings.getMaxHeapSizeBytes()
-                        == IsolateStartupParameters.DEFAULT_ISOLATE_HEAP_SIZE) {
-                    isolateStub = mJsSandboxService.createIsolate();
-                } else {
-                    isolateStub = mJsSandboxService.createIsolateWithMaxHeapSizeBytes(
-                            settings.getMaxHeapSizeBytes());
-                    if (isolateStub == null) {
-                        throw new RuntimeException(
-                                "Service implementation doesn't support setting maximum heap size");
-                    }
-                }
+                isolate = JavaScriptIsolate.create(this, settings);
             } catch (RemoteException e) {
+                // TODO(b/286055647): Handle sandbox dying during createIsolate more sensibly.
                 throw new RuntimeException(e);
             }
-            return createJsIsolateLocked(isolateStub, settings);
+            mActiveIsolateSet.add(isolate);
+            return isolate;
+        }
+    }
+
+    // In practice, this method should only be called whilst already holding mLock, but it is
+    // called via JavaScriptIsolate and this constraint cannot be cleanly expressed via GuardedBy.
+    IJsSandboxIsolate createIsolateOnService(@NonNull IsolateStartupParameters settings,
+            @Nullable IJsSandboxIsolateClient isolateInstanceCallback) throws RemoteException {
+        synchronized (mLock) {
+            Objects.requireNonNull(mJsSandboxService);
+            if (isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_CLIENT)) {
+                return mJsSandboxService.createIsolate2(settings.getMaxHeapSizeBytes(),
+                        isolateInstanceCallback);
+            } else if (isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE)) {
+                return mJsSandboxService.createIsolateWithMaxHeapSizeBytes(
+                        settings.getMaxHeapSizeBytes());
+            } else {
+                return mJsSandboxService.createIsolate();
+            }
         }
     }
 
@@ -459,19 +498,12 @@
         if (features.contains(IJsSandboxService.CONSOLE_MESSAGING)) {
             featureSet.add(JS_FEATURE_CONSOLE_MESSAGING);
         }
+        if (features.contains(IJsSandboxService.ISOLATE_CLIENT + ":DEV")) {
+            featureSet.add(JS_FEATURE_ISOLATE_CLIENT);
+        }
         return featureSet;
     }
 
-    @GuardedBy("mLock")
-    @NonNull
-    @SuppressWarnings("NullAway")
-    private JavaScriptIsolate createJsIsolateLocked(@NonNull IJsSandboxIsolate isolateStub,
-            @NonNull IsolateStartupParameters settings) {
-        JavaScriptIsolate isolate = new JavaScriptIsolate(isolateStub, this, settings);
-        mActiveIsolateSet.add(isolate);
-        return isolate;
-    }
-
     /**
      * Checks whether a given feature is supported by the JS Sandbox implementation.
      * <p>
@@ -512,28 +544,51 @@
             if (mJsSandboxService == null) {
                 return;
             }
-            // This is the closest thing to a .close() method for ExecutorServices. This doesn't
-            // force the threads or their Runnables to immediately terminate, but will ensure
-            // that once the
-            // worker threads finish their current runnable (if any) that the thread pool terminates
-            // them, preventing a leak of threads.
-            mThreadPoolTaskExecutor.shutdownNow();
-            notifyIsolatesAboutClosureLocked();
-            mConnection.mContext.unbindService(mConnection);
+            unbindService();
             // Currently we consider that we are ready for a new connection once we unbind. This
             // might not be true if the process is not immediately killed by ActivityManager once it
             // is unbound.
             sIsReadyToConnect.set(true);
             mJsSandboxService = null;
         }
+        notifyIsolatesAboutClosure();
+        // This is the closest thing to a .close() method for ExecutorServices. This doesn't
+        // force the threads or their Runnables to immediately terminate, but will ensure
+        // that once the worker threads finish their current runnable (if any) that the thread
+        // pool terminates them, preventing a leak of threads.
+        mThreadPoolTaskExecutor.shutdownNow();
     }
 
-    @GuardedBy("mLock")
-    private void notifyIsolatesAboutClosureLocked() {
-        for (JavaScriptIsolate ele : mActiveIsolateSet) {
-            ele.maybeSetSandboxDead();
+    // Unbind the service if it hasn't been unbound already.
+    private void unbindService() {
+        final ConnectionSetup connection = mConnection.getAndSet(null);
+        if (connection != null) {
+            mContext.unbindService(connection);
         }
-        mActiveIsolateSet.clear();
+    }
+
+    // Kill the sandbox immediately.
+    //
+    // This will unbind the sandbox service so that any future IPC will fail immediately.
+    // However, isolates will be notified asynchronously, from the main thread.
+    void kill() {
+        unbindService();
+        // TODO(b/290750354, b/290749782): Implement separate closed vs dead state.
+        getMainExecutor().execute(this::close);
+    }
+
+    private void notifyIsolatesAboutClosure() {
+        // Do not hold mLock whilst calling into JavaScriptIsolate, as JavaScriptIsolate also has
+        // its own lock and may want to call into JavaScriptSandbox from another thread.
+        final Set<JavaScriptIsolate> activeIsolateSet;
+        synchronized (mLock) {
+            activeIsolateSet = mActiveIsolateSet;
+            mActiveIsolateSet = Collections.emptySet();
+        }
+        for (JavaScriptIsolate isolate : activeIsolateSet) {
+            isolate.maybeSetIsolateDead(new TerminationInfo(TerminationInfo.STATUS_SANDBOX_DEAD,
+                    "sandbox closed"));
+        }
     }
 
     @Override
@@ -553,6 +608,6 @@
 
     @NonNull
     Executor getMainExecutor() {
-        return ContextCompat.getMainExecutor(mConnection.mContext);
+        return ContextCompat.getMainExecutor(mContext);
     }
 }
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/MemoryLimitExceededException.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/MemoryLimitExceededException.java
index f4330cc..415b2d7 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/MemoryLimitExceededException.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/MemoryLimitExceededException.java
@@ -17,31 +17,34 @@
 package androidx.javascriptengine;
 
 import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+
+import java.util.concurrent.Executor;
 
 /**
- * Indicates that a JavaScriptIsolate's evaluation failed due to exceeding its heap size limit.
+ * Indicates that a JavaScriptIsolate's evaluation failed due to the isolate exceeding its heap
+ * size limit.
  * <p>
  * This exception may be thrown when exceeding the heap size limit configured for the isolate via
  * {@link IsolateStartupParameters}, or the default limit. Beware that it will not be thrown if the
  * Android system as a whole has run out of memory before the JavaScript environment has reached
  * its configured heap limit.
  * <p>
- * The isolate may not continue to be used after this exception has been thrown, and other pending
- * evaluations for the isolate will fail. The isolate may continue to hold onto resources (even if
- * explicitly closed) until the sandbox has been shutdown. Therefore, it is recommended that the
- * sandbox be restarted at the earliest opportunity in order to reclaim these resources.
+ * If an evaluation fails with a MemoryLimitExceededException, it does not imply that that
+ * particular evaluation was in any way responsible for any excessive memory usage.
+ * MemoryLimitExceededException will be raised for all unresolved and future requested evaluations
+ * regardless of their culpability.
  * <p>
- * Other isolates within the same sandbox may continue to be used, created, and closed as normal.
- * <p>
- * Beware that not all JavaScript sandbox service implementations (particularly older ones)
- * handle memory exhaustion equally gracefully, and may instead crash the entire sandbox (see
- * {@link SandboxDeadException}).
+ * An isolate may run out of memory outside of an explicit evaluation (such as in a microtask), so
+ * you should generally not use this exception to detect out of memory issues - instead, use
+ * {@link JavaScriptIsolate#addOnTerminatedCallback(Executor, Consumer)} and check
+ * for an isolate termination status of {@link TerminationInfo#STATUS_MEMORY_LIMIT_EXCEEDED}.
  */
-public final class MemoryLimitExceededException extends JavaScriptException {
-    public MemoryLimitExceededException(@NonNull String error) {
-        super(error);
-    }
+public final class MemoryLimitExceededException extends IsolateTerminatedException {
     public MemoryLimitExceededException() {
         super();
     }
+    public MemoryLimitExceededException(@NonNull String error) {
+        super(error);
+    }
 }
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/SandboxDeadException.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/SandboxDeadException.java
index 229dee08..f3439b7 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/SandboxDeadException.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/SandboxDeadException.java
@@ -16,19 +16,20 @@
 
 package androidx.javascriptengine;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
 /**
  * Exception thrown when evaluation is terminated due the {@link JavaScriptSandbox} being dead.
  * This can happen when {@link JavaScriptSandbox#close()} is called or when the sandbox process
  * is killed by the framework.
- * <p>
- * This is different from {@link IsolateTerminatedException} which occurs when the
- * {@link JavaScriptIsolate} within a {@link JavaScriptSandbox} is terminated.
- * <p>
- * This exception will continue to be thrown for all future evaluation requests on unclosed
- * isolates.
  */
-public final class SandboxDeadException extends JavaScriptException {
+public final class SandboxDeadException extends IsolateTerminatedException {
     public SandboxDeadException() {
         super();
     }
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public SandboxDeadException(@NonNull String message) {
+        super(message);
+    }
 }
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/TerminationInfo.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/TerminationInfo.java
new file mode 100644
index 0000000..fc828de
--- /dev/null
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/TerminationInfo.java
@@ -0,0 +1,138 @@
+/*
+ * 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.javascriptengine;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolateClient;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Information about how and why an isolate has terminated.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class TerminationInfo {
+    /**
+     * Termination status code for an isolate.
+     */
+    @IntDef({STATUS_UNKNOWN_ERROR, STATUS_SANDBOX_DEAD, STATUS_MEMORY_LIMIT_EXCEEDED})
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Status {
+    }
+
+    /**
+     * The isolate (but not necessarily the sandbox) has crashed for an unknown reason.
+     */
+    public static final int STATUS_UNKNOWN_ERROR = IJsSandboxIsolateClient.TERMINATE_UNKNOWN_ERROR;
+    /**
+     * The whole sandbox died (or was closed), taking this isolate with it.
+     */
+    public static final int STATUS_SANDBOX_DEAD = IJsSandboxIsolateClient.TERMINATE_SANDBOX_DEAD;
+    /**
+     * The isolate exceeded its heap size limit.
+     * <p>
+     * The isolate may continue to hold onto resources (even if explicitly closed) until the
+     * sandbox has been shutdown. If necessary, restart the sandbox at the earliest opportunity in
+     * order to reclaim these resources.
+     * <p>
+     * Note that memory exhaustion will kill the whole sandbox, so any other isolates within the
+     * same sandbox will be terminated with {@link #STATUS_SANDBOX_DEAD}.
+     */
+    public static final int STATUS_MEMORY_LIMIT_EXCEEDED =
+            IJsSandboxIsolateClient.TERMINATE_MEMORY_LIMIT_EXCEEDED;
+    @Status
+    private final int mStatus;
+    @NonNull
+    private final String mMessage;
+
+    TerminationInfo(@Status int status, @NonNull String message) {
+        mStatus = status;
+        mMessage = message;
+    }
+
+    /**
+     * Get the status code of the termination.
+     * <p>
+     * New status codes may be added with new JavaScriptEngine versions.
+     *
+     * @return status code of the termination.
+     */
+    @Status
+    public int getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Describe the status code of the termination.
+     * These strings are not stable between JavaScriptEngine versions.
+     *
+     * @return description of status code of the termination.
+     */
+    @NonNull
+    public String getStatusString() {
+        switch (mStatus) {
+            case STATUS_UNKNOWN_ERROR:
+                return "unknown error";
+            case STATUS_SANDBOX_DEAD:
+                return "sandbox dead";
+            case STATUS_MEMORY_LIMIT_EXCEEDED:
+                return "memory limit exceeded";
+            default:
+                return "unknown error code " + mStatus;
+        }
+    }
+
+    /**
+     * Get the message associated with this termination.
+     * The content or format of these messages is not stable between JavaScriptEngine versions.
+     *
+     * @return Human-readable message about the termination.
+     */
+    @NonNull
+    public String getMessage() {
+        return mMessage;
+    }
+
+    /**
+     * Describe the termination.
+     * The content or format of this description is not stable between JavaScriptEngine versions.
+     *
+     * @return Human-readable description of the termination.
+     */
+    @NonNull
+    @Override
+    public String toString() {
+        return getStatusString() + ": " + getMessage();
+    }
+
+    @NonNull
+    IsolateTerminatedException toJavaScriptException() {
+        switch (mStatus) {
+            case STATUS_SANDBOX_DEAD:
+                return new SandboxDeadException(this.toString());
+            case STATUS_MEMORY_LIMIT_EXCEEDED:
+                return new MemoryLimitExceededException(this.toString());
+            default:
+                return new IsolateTerminatedException(this.toString());
+        }
+    }
+}
diff --git a/javascriptengine/javascriptengine/src/main/stableAidl/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl b/javascriptengine/javascriptengine/src/main/stableAidl/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl
new file mode 100644
index 0000000..6574ae4
--- /dev/null
+++ b/javascriptengine/javascriptengine/src/main/stableAidl/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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 org.chromium.android_webview.js_sandbox.common;
+
+/**
+ * Callbacks for isolate events, not specific to evaluations.
+ * @hide
+ */
+interface IJsSandboxIsolateClient {
+    // These crash codes may be generated on either the client or service side.
+
+    // The isolate terminated for an unknown reason.
+    const int TERMINATE_UNKNOWN_ERROR = 1;
+    // The sandbox died.
+    //
+    // This is typically generated client-side as the service may die before it gets a chance to
+    // send a message to the client.
+    const int TERMINATE_SANDBOX_DEAD = 2;
+    // The isolate exceeded its heap size limit.
+    const int TERMINATE_MEMORY_LIMIT_EXCEEDED = 3;
+
+    /**
+     * Informs the client that the isolate should now be considered terminated.
+     *
+     * @param status  A status code describing the reason for the termination. Must be one of the
+     *                constants beginning "TERMINATE_".
+     * @param message Unstructured information about the termination. May be null.
+     */
+    void onTerminated(int status, String message) = 1;
+}
diff --git a/javascriptengine/javascriptengine/src/main/stableAidl/org/chromium/android_webview/js_sandbox/common/IJsSandboxService.aidl b/javascriptengine/javascriptengine/src/main/stableAidl/org/chromium/android_webview/js_sandbox/common/IJsSandboxService.aidl
index 0453988..8afe48c 100644
--- a/javascriptengine/javascriptengine/src/main/stableAidl/org/chromium/android_webview/js_sandbox/common/IJsSandboxService.aidl
+++ b/javascriptengine/javascriptengine/src/main/stableAidl/org/chromium/android_webview/js_sandbox/common/IJsSandboxService.aidl
@@ -16,6 +16,7 @@
 
 package org.chromium.android_webview.js_sandbox.common;
 import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolate;
+import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolateClient;
 
 /**
  * Used by the embedding app to execute JavaScript in a sandboxed environment.
@@ -62,9 +63,21 @@
     const String CONSOLE_MESSAGING = "CONSOLE_MESSAGING";
 
     /**
+     * Feature flag indicating that the client may provide the service side with an
+     * IJsSandboxIsolateClient, allowing the service to call into the client regardless of ongoing
+     * evaluations.
+     */
+    const String ISOLATE_CLIENT = "ISOLATE_CLIENT";
+
+    /**
      * @return A list of feature names supported by this implementation.
      */
     List<String> getSupportedFeatures() = 1;
 
     IJsSandboxIsolate createIsolateWithMaxHeapSizeBytes(long maxHeapSize) = 2;
+
+    /**
+     * Create an isolate with a given heap size and service-to-client interface.
+     */
+    IJsSandboxIsolate createIsolate2(long maxHeapSize, IJsSandboxIsolateClient isolateClient) = 3;
 }
diff --git a/leanback/leanback/lint-baseline.xml b/leanback/leanback/lint-baseline.xml
index 6b97ced..d5e9e66 100644
--- a/leanback/leanback/lint-baseline.xml
+++ b/leanback/leanback/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-beta05" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta05)" variant="all" version="8.1.0-beta05">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="NewApi"
@@ -2494,42 +2494,6 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="           ~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/BrowseSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="                             ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/BrowseSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="                                                      ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/BrowseSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            Bundle savedInstanceState) {"
-        errorLine2="            ~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/BrowseSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {"
         errorLine2="                                           ~~~~~~~~~~~~~~~~~">
         <location
@@ -3070,42 +3034,6 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="           ~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/DetailsSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="                             ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/DetailsSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="                                                      ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/DetailsSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            Bundle savedInstanceState) {"
-        errorLine2="            ~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/DetailsSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    protected void setupPresenter(Presenter rowPresenter) {"
         errorLine2="                                  ~~~~~~~~~">
         <location
@@ -6472,42 +6400,6 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="           ~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/PlaybackSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="                             ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/PlaybackSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="                                                      ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/PlaybackSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="                             Bundle savedInstanceState) {"
-        errorLine2="                             ~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/PlaybackSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public void setHostCallback(PlaybackGlueHost.HostCallback hostCallback) {"
         errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -8029,42 +7921,6 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="           ~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/SearchSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="                             ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/SearchSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="                                                      ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/SearchSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="                             Bundle savedInstanceState) {"
-        errorLine2="                             ~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/SearchSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public RowsSupportFragment getRowsSupportFragment() {"
         errorLine2="           ~~~~~~~~~~~~~~~~~~~">
         <location
@@ -9010,42 +8866,6 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public View onCreateView("
-        errorLine2="           ~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/VideoSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {"
-        errorLine2="            ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/VideoSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {"
-        errorLine2="                                     ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/VideoSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {"
-        errorLine2="                                                          ~~~~~~">
-        <location
-            file="src/main/java/androidx/leanback/app/VideoSupportFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public void setSurfaceHolderCallback(SurfaceHolder.Callback callback) {"
         errorLine2="                                         ~~~~~~~~~~~~~~~~~~~~~~">
         <location
diff --git a/libraryversions.toml b/libraryversions.toml
index f93a377..1f53b93 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,5 +1,5 @@
 [versions]
-ACTIVITY = "1.8.0-beta01"
+ACTIVITY = "1.8.0-rc01"
 ANNOTATION = "1.8.0-alpha01"
 ANNOTATION_EXPERIMENTAL = "1.4.0-alpha01"
 APPACTIONS_BUILTINTYPES = "1.0.0-alpha01"
@@ -9,7 +9,7 @@
 ARCH_CORE = "2.3.0-alpha01"
 ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
 AUTOFILL = "1.3.0-alpha02"
-BENCHMARK = "1.2.0-beta05"
+BENCHMARK = "1.3.0-alpha01"
 BIOMETRIC = "1.2.0-alpha06"
 BLUETOOTH = "1.0.0-alpha01"
 BROWSER = "1.7.0-alpha01"
@@ -19,9 +19,9 @@
 CARDVIEW = "1.1.0-alpha01"
 CAR_APP = "1.4.0-beta02"
 COLLECTION = "1.4.0-alpha01"
-COMPOSE = "1.6.0-alpha05"
+COMPOSE = "1.6.0-alpha06"
 COMPOSE_COMPILER = "1.5.3"
-COMPOSE_MATERIAL3 = "1.2.0-alpha07"
+COMPOSE_MATERIAL3 = "1.2.0-alpha08"
 COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha01"
 COMPOSE_RUNTIME_TRACING = "1.0.0-alpha05"
 CONSTRAINTLAYOUT = "2.2.0-alpha13"
@@ -37,13 +37,14 @@
 CORE_HAPTICS = "1.0.0-alpha01"
 CORE_I18N = "1.0.0-alpha02"
 CORE_LOCATION_ALTITUDE = "1.0.0-alpha02"
-CORE_PERFORMANCE = "1.0.0-alpha04"
+CORE_PERFORMANCE = "1.0.0-beta01"
 CORE_REMOTEVIEWS = "1.1.0-alpha01"
 CORE_ROLE = "1.2.0-alpha01"
 CORE_SPLASHSCREEN = "1.1.0-alpha02"
 CORE_TELECOM = "1.0.0-alpha01"
 CORE_UWB = "1.0.0-alpha08"
 CREDENTIALS = "1.2.0-beta04"
+CREDENTIALS_FIDO_QUARANTINE = "1.0.0-alpha01"
 CURSORADAPTER = "1.1.0-alpha01"
 CUSTOMVIEW = "1.2.0-alpha03"
 CUSTOMVIEW_POOLINGCONTAINER = "1.1.0-alpha01"
@@ -57,7 +58,7 @@
 EMOJI2 = "1.5.0-alpha01"
 ENTERPRISE = "1.1.0-rc01"
 EXIFINTERFACE = "1.4.0-alpha01"
-FRAGMENT = "1.7.0-alpha04"
+FRAGMENT = "1.7.0-alpha05"
 FUTURES = "1.2.0-alpha02"
 GLANCE = "1.1.0-alpha01"
 GLANCE_PREVIEW = "1.0.0-alpha06"
@@ -68,7 +69,7 @@
 GRAPHICS_PATH = "1.0.0-alpha02"
 GRAPHICS_SHAPES = "1.0.0-alpha03"
 GRIDLAYOUT = "1.1.0-beta02"
-HEALTH_CONNECT = "1.1.0-alpha04"
+HEALTH_CONNECT = "1.1.0-alpha05"
 HEALTH_SERVICES_CLIENT = "1.1.0-alpha02"
 HEIFWRITER = "1.1.0-alpha03"
 HILT = "1.1.0-alpha02"
@@ -110,7 +111,7 @@
 RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
 REMOTECALLBACK = "1.0.0-alpha02"
 RESOURCEINSPECTION = "1.1.0-alpha01"
-ROOM = "2.6.0-beta02"
+ROOM = "2.7.0-alpha01"
 SAFEPARCEL = "1.0.0-alpha01"
 SAVEDSTATE = "1.3.0-alpha01"
 SECURITY = "1.1.0-alpha07"
@@ -124,7 +125,7 @@
 SLICE_BUILDERS_KTX = "1.0.0-alpha08"
 SLICE_REMOTECALLBACK = "1.0.0-alpha01"
 SLIDINGPANELAYOUT = "1.3.0-alpha01"
-SQLITE = "2.4.0-beta02"
+SQLITE = "2.5.0-alpha01"
 SQLITE_INSPECTOR = "2.1.0-alpha01"
 STABLE_AIDL = "1.0.0-alpha01"
 STARTUP = "1.2.0-alpha03"
@@ -145,8 +146,8 @@
 VIEWPAGER = "1.1.0-alpha02"
 VIEWPAGER2 = "1.1.0-beta03"
 WEAR = "1.4.0-alpha01"
-WEAR_COMPOSE = "1.3.0-alpha05"
-WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha11"
+WEAR_COMPOSE = "1.3.0-alpha06"
+WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha12"
 WEAR_INPUT = "1.2.0-alpha03"
 WEAR_INPUT_TESTING = "1.2.0-alpha03"
 WEAR_ONGOING = "1.1.0-alpha02"
@@ -201,6 +202,7 @@
 CORE_HAPTICS = { group = "androidx.core.haptics", atomicGroupVersion = "versions.CORE_HAPTICS" }
 CORE_UWB = { group = "androidx.core.uwb", atomicGroupVersion = "versions.CORE_UWB" }
 CREDENTIALS = { group = "androidx.credentials", atomicGroupVersion = "versions.CREDENTIALS" }
+CREDENTIALS_FIDO = { group = "androidx.credentials.credentials-fido", atomicGroupVersion = "versions.CREDENTIALS_FIDO_QUARANTINE" }
 CURSORADAPTER = { group = "androidx.cursoradapter", atomicGroupVersion = "versions.CURSORADAPTER" }
 CUSTOMVIEW = { group = "androidx.customview" }
 DATASTORE = { group = "androidx.datastore", atomicGroupVersion = "versions.DATASTORE" }
diff --git a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
index c8f3cc7..2d0872e 100644
--- a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
+++ b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
@@ -45,10 +45,14 @@
 @JvmName("map")
 @MainThread
 @CheckResult
+@Suppress("UNCHECKED_CAST")
 fun <X, Y> LiveData<X>.map(
     transform: (@JvmSuppressWildcards X) -> (@JvmSuppressWildcards Y)
 ): LiveData<Y> {
     val result = MediatorLiveData<Y>()
+    if (isInitialized) {
+        result.value = transform(value as X)
+    }
     result.addSource(this) { x -> result.value = transform(x) }
     return result
 }
@@ -113,18 +117,21 @@
 @JvmName("switchMap")
 @MainThread
 @CheckResult
+@Suppress("UNCHECKED_CAST")
 fun <X, Y> LiveData<X>.switchMap(
     transform: (@JvmSuppressWildcards X) -> (@JvmSuppressWildcards LiveData<Y>)?
 ): LiveData<Y> {
     val result = MediatorLiveData<Y>()
-    result.addSource(this, object : Observer<X> {
-        var liveData: LiveData<Y>? = null
-
-        override fun onChanged(value: X) {
-            val newLiveData = transform(value)
-            if (liveData === newLiveData) {
-                return
-            }
+    var liveData: LiveData<Y>? = null
+    if (isInitialized) {
+        liveData = transform(value as X)
+        if (liveData != null && liveData.isInitialized) {
+            result.value = liveData.value
+        }
+    }
+    result.addSource(this) { value: X ->
+        val newLiveData = transform(value)
+        if (liveData !== newLiveData) {
             if (liveData != null) {
                 result.removeSource(liveData!!)
             }
@@ -133,7 +140,7 @@
                 result.addSource(liveData!!) { y -> result.setValue(y) }
             }
         }
-    })
+    }
     return result
 }
 
diff --git a/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.kt b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.kt
index 737abdb..e343219 100644
--- a/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.kt
+++ b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.kt
@@ -64,6 +64,26 @@
     }
 
     @Test
+    fun testMap_initialValueIsSet() {
+        val initialValue = "value"
+        val source = MutableLiveData(initialValue)
+        val mapped = source.map { it }
+        assertThat(mapped.isInitialized, `is`(true))
+        assertThat(source.value, `is`(initialValue))
+        assertThat(mapped.value, `is`(initialValue))
+    }
+
+    @Test
+    fun testMap_initialValueNull() {
+        val source = MutableLiveData<String?>(null)
+        val output = "testOutput"
+        val mapped: LiveData<String?> = source.map { output }
+        assertThat(mapped.isInitialized, `is`(true))
+        assertThat(source.value, nullValue())
+        assertThat(mapped.value, `is`(output))
+    }
+
+    @Test
     fun testSwitchMap() {
         val trigger: LiveData<Int> = MutableLiveData()
         val first: LiveData<String> = MutableLiveData()
@@ -123,6 +143,36 @@
     }
 
     @Test
+    fun testSwitchMap_initialValueSet() {
+        val initialValue1 = "value1"
+        val original = MutableLiveData(true)
+        val source1 = MutableLiveData(initialValue1)
+
+        val switched = original.switchMap { source1 }
+        assertThat(switched.isInitialized, `is`(true))
+        assertThat(source1.value, `is`(initialValue1))
+        assertThat(switched.value, `is`(initialValue1))
+    }
+
+    @Test
+    fun testSwitchMap_noInitialValue_notInitialized() {
+        val original = MutableLiveData(true)
+        val source = MutableLiveData<String>()
+
+        val switched = original.switchMap { source }
+        assertThat(switched.isInitialized, `is`(false))
+    }
+
+    @Test
+    fun testSwitchMap_initialValueNull() {
+        val original = MutableLiveData<String?>(null)
+        val source = MutableLiveData<String?>()
+
+        val switched = original.switchMap { source }
+        assertThat(switched.isInitialized, `is`(false))
+    }
+
+    @Test
     fun testNoRedispatchSwitchMap() {
         val trigger: LiveData<Int> = MutableLiveData()
         val first: LiveData<String> = MutableLiveData()
diff --git a/lint-checks/src/main/java/androidx/build/lint/TestSizeAnnotationEnforcer.kt b/lint-checks/src/main/java/androidx/build/lint/TestSizeAnnotationEnforcer.kt
index 0777c76..d068661 100644
--- a/lint-checks/src/main/java/androidx/build/lint/TestSizeAnnotationEnforcer.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/TestSizeAnnotationEnforcer.kt
@@ -168,7 +168,7 @@
          */
         private val ANDROID_TEST_DIRS = listOf(
             "androidTest",
-            "androidAndroidTest",
+            "androidInstrumentedTest",
             "androidDeviceTest",
             "androidDeviceTestDebug",
             "androidDeviceTestRelease"
diff --git a/lint-checks/src/main/java/androidx/build/lint/UnstableAidlAnnotationDetector.kt b/lint-checks/src/main/java/androidx/build/lint/UnstableAidlAnnotationDetector.kt
index 637bee0..8a9be96 100644
--- a/lint-checks/src/main/java/androidx/build/lint/UnstableAidlAnnotationDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/UnstableAidlAnnotationDetector.kt
@@ -40,6 +40,9 @@
  */
 private const val JAVA_PASSTHROUGH = "JavaPassthrough"
 
+private const val ANDROIDX_REQUIRESOPTIN = "androidx.annotation.RequiresOptIn"
+private const val KOTLIN_REQUIRESOPTIN = "kotlin.RequiresOptIn"
+
 class UnstableAidlAnnotationDetector : AidlDefinitionDetector() {
 
     override fun visitAidlParcelableDeclaration(context: Context, node: AidlParcelableDeclaration) {
@@ -84,8 +87,8 @@
                 )
                 // Determine if the class is annotated with RequiresOptIn.
                 psiClass?.annotations?.any { psiAnnotation ->
-                    // Either androidx.annotation or kotlin version is fine here.
-                    psiAnnotation.hasQualifiedName("RequiresOptIn")
+                    psiAnnotation.hasQualifiedName(ANDROIDX_REQUIRESOPTIN) ||
+                        psiAnnotation.hasQualifiedName(KOTLIN_REQUIRESOPTIN)
                 } ?: false
             } else {
                 false
diff --git a/lint-checks/src/test/java/androidx/build/lint/UnstableAidlAnnotationDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/UnstableAidlAnnotationDetectorTest.kt
index 8789d3b..c8c9885 100644
--- a/lint-checks/src/test/java/androidx/build/lint/UnstableAidlAnnotationDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/UnstableAidlAnnotationDetectorTest.kt
@@ -131,6 +131,8 @@
             java(
                 "src/androidx/core/UnstableAidlDefinition.java",
                 """
+                    import androidx.annotation.RequiresOptIn;
+
                     @RequiresOptIn
                     public @interface UnstableAidlDefinition {}
                 """.trimIndent()
@@ -168,7 +170,7 @@
             java(
                 "src/androidx/core/UnstableAidlDefinition.java",
                 """
-                    @RequiresOptIn
+                    @androidx.annotation.RequiresOptIn
                     public @interface UnstableAidlDefinition {}
                 """.trimIndent()
             ),
diff --git a/media2/integration-tests/testapp/build.gradle b/media2/integration-tests/testapp/build.gradle
index 3c9fbfe..ef3049b 100644
--- a/media2/integration-tests/testapp/build.gradle
+++ b/media2/integration-tests/testapp/build.gradle
@@ -14,13 +14,6 @@
  * limitations under the License.
  */
 
-buildscript {
-    // TODO: Remove this when this test app no longer depends on 1.0.0 of vectordrawable-animated.
-    // vectordrawable and vectordrawable-animated were accidentally using the same package name
-    // which is no longer valid in namespaced resource world.
-    project.ext["android.uniquePackageNames"] = false
-}
-
 plugins {
     id("AndroidXPlugin")
     id("com.android.application")
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
index cb66015..231c532 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
@@ -115,6 +115,7 @@
             };
 
     private boolean mTransferReceiverDeclared;
+    private boolean mUseMediaRouter2ForSystemRouting;
     private MediaRoute2Provider mMr2Provider;
     private SystemMediaRouteProvider mSystemProvider;
     private DisplayManagerCompat mDisplayManager;
@@ -142,6 +143,15 @@
         mTransferReceiverDeclared =
                 Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
                         && MediaTransferReceiver.isDeclared(mApplicationContext);
+        mUseMediaRouter2ForSystemRouting =
+                SystemRoutingUsingMediaRouter2Receiver.isDeclared(mApplicationContext);
+
+        if (DEBUG && mUseMediaRouter2ForSystemRouting) {
+            // This is only added to skip the presubmit check for UnusedVariable
+            // TODO: Remove it once mUseMediaRouter2ForSystemRouting is actually used
+            Log.d(TAG, "Using MediaRouter2 for system routing");
+        }
+
         mMr2Provider =
                 Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && mTransferReceiverDeclared
                         ? new MediaRoute2Provider(mApplicationContext, new Mr2ProviderCallback())
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/SystemRoutingUsingMediaRouter2Receiver.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/SystemRoutingUsingMediaRouter2Receiver.java
new file mode 100644
index 0000000..ecc6df6
--- /dev/null
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/SystemRoutingUsingMediaRouter2Receiver.java
@@ -0,0 +1,57 @@
+/*
+ * 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.mediarouter.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.util.List;
+
+/**
+ * A {@link BroadcastReceiver} class for enabling apps to get SystemRoutes using
+ * {@link android.media.MediaRouter2}.
+ */
+@RestrictTo(LIBRARY)
+final class SystemRoutingUsingMediaRouter2Receiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+        // Do nothing for now.
+    }
+
+    /**
+     * Checks whether the {@link SystemRoutingUsingMediaRouter2Receiver} is declared in the app's
+     * manifest.
+     */
+    @RestrictTo(LIBRARY)
+    public static boolean isDeclared(@NonNull Context applicationContext) {
+        Intent queryIntent = new Intent(applicationContext,
+                SystemRoutingUsingMediaRouter2Receiver.class);
+        queryIntent.setPackage(applicationContext.getPackageName());
+        PackageManager pm = applicationContext.getPackageManager();
+        List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers(queryIntent, 0);
+
+        return resolveInfos.size() > 0;
+    }
+}
diff --git a/mediarouter/mediarouter/src/main/res/values-ne/strings.xml b/mediarouter/mediarouter/src/main/res/values-ne/strings.xml
index 490503a..7cdafb4 100644
--- a/mediarouter/mediarouter/src/main/res/values-ne/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values-ne/strings.xml
@@ -39,7 +39,7 @@
     <string name="mr_controller_no_info_available" msgid="855271725131981086">"कुनै पनि जानकारी उपलब्ध छैन"</string>
     <string name="mr_controller_casting_screen" msgid="9171231064758955152">"स्क्रिन Cast गरिँदै छ"</string>
     <string name="mr_dialog_default_group_name" msgid="4115858704575247342">"समूह"</string>
-    <string name="mr_dialog_groupable_header" msgid="4307018456678388936">"डिभाइस थप्नुहोस्"</string>
+    <string name="mr_dialog_groupable_header" msgid="4307018456678388936">"डिभाइस कनेक्ट गर्नुहोस्"</string>
     <string name="mr_dialog_transferable_header" msgid="6068257520605505468">"कुनै समूहमा प्ले गर्नुहोस्"</string>
     <string name="mr_cast_dialog_title_view_placeholder" msgid="2175930138959078155">"कुनै पनि जानकारी उपलब्ध छैन"</string>
     <string name="mr_chooser_zero_routes_found_title" msgid="5213435473397442608">"कुनै पनि डिभाइस उपलब्ध छैन"</string>
diff --git a/metrics/integration-tests/janktest/build.gradle b/metrics/integration-tests/janktest/build.gradle
index 476a3ae..3aa5881 100644
--- a/metrics/integration-tests/janktest/build.gradle
+++ b/metrics/integration-tests/janktest/build.gradle
@@ -25,6 +25,9 @@
         viewBinding true
     }
     namespace "androidx.metrics.performance.janktest"
+    defaultConfig {
+        multiDexEnabled true
+    }
 }
 
 dependencies {
diff --git a/navigation/integration-tests/testapp/build.gradle b/navigation/integration-tests/testapp/build.gradle
index 97a9bec..f235d73 100644
--- a/navigation/integration-tests/testapp/build.gradle
+++ b/navigation/integration-tests/testapp/build.gradle
@@ -14,13 +14,6 @@
  * limitations under the License.
  */
 
-buildscript {
-    // TODO: Remove this when this test app no longer depends on 1.0.0 of vectordrawable-animated.
-    // vectordrawable and vectordrawable-animated were accidentally using the same package name
-    // which is no longer valid in namespaced resource world.
-    project.ext["android.uniquePackageNames"] = false
-}
-
 plugins {
     id("AndroidXPlugin")
     id("com.android.application")
@@ -37,10 +30,14 @@
     implementation(project(":internal-testutils-navigation"), {
         exclude group: "androidx.navigation", module: "navigation-common"
     })
+    implementation(libs.multidex)
 }
 
 android {
     namespace "androidx.navigation.testapp"
+    defaultConfig {
+        multiDexEnabled true
+    }
 }
 
 tasks["check"].dependsOn(tasks["connectedCheck"])
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index c549a7d..7102eda 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -34,11 +34,11 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    api("androidx.lifecycle:lifecycle-common:2.6.1")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
+    api("androidx.lifecycle:lifecycle-common:2.6.2")
+    api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
+    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
     api("androidx.savedstate:savedstate-ktx:1.2.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1")
+    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2")
     implementation("androidx.core:core-ktx:1.1.0")
     implementation("androidx.collection:collection-ktx:1.1.0")
     implementation("androidx.profileinstaller:profileinstaller:1.3.0")
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 30482d2..519c0c9 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -33,7 +33,7 @@
     api("androidx.compose.runtime:runtime:1.5.1")
     api("androidx.compose.runtime:runtime-saveable:1.5.1")
     api("androidx.compose.ui:ui:1.5.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
+    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
     api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
 
     androidTestImplementation(projectOrArtifact(":compose:material:material"))
diff --git a/navigation/navigation-fragment/lint-baseline.xml b/navigation/navigation-fragment/lint-baseline.xml
index f8e3e41..54ccdb0 100644
--- a/navigation/navigation-fragment/lint-baseline.xml
+++ b/navigation/navigation-fragment/lint-baseline.xml
@@ -1,11 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="RestrictedApiAndroidX"
         message="FragmentManager.isLoggingEnabled can only be called from within the same library (androidx.fragment:fragment)"
-        errorLine1="                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {"
-        errorLine2="                                        ~~~~~~~~~~~~~~~~">
+        errorLine1="                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/navigation/fragment/FragmentNavigator.kt"/>
     </issue>
@@ -22,8 +22,8 @@
     <issue
         id="RestrictedApiAndroidX"
         message="FragmentManager.isLoggingEnabled can only be called from within the same library (androidx.fragment:fragment)"
-        errorLine1="                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {"
-        errorLine2="                                        ~~~~~~~~~~~~~~~~">
+        errorLine1="                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/navigation/fragment/FragmentNavigator.kt"/>
     </issue>
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/DialogFragmentNavigatorTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/DialogFragmentNavigatorTest.kt
index b434bcf..dfc8e5d 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/DialogFragmentNavigatorTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/DialogFragmentNavigatorTest.kt
@@ -303,6 +303,193 @@
             .doesNotContain(dialogFragments[1])
     }
 
+    @UiThreadTest
+    @Test
+    fun testPop_transitioningDialogStaysInTransition() {
+        val dialogFragments = mutableListOf<DialogFragment>()
+        fragmentManager.fragmentFactory = object : FragmentFactory() {
+            override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+                return super.instantiate(classLoader, className).also { fragment ->
+                    if (fragment is DialogFragment) {
+                        dialogFragments += fragment
+                    }
+                }
+            }
+        }
+
+        val firstEntry = createBackStackEntry()
+        val secondEntry = createBackStackEntry(2)
+        val thirdEntry = createBackStackEntry(3)
+
+        dialogNavigator.navigate(listOf(firstEntry), null, null)
+        dialogNavigator.navigate(listOf(secondEntry), null, null)
+        dialogNavigator.navigate(listOf(thirdEntry), null, null)
+        assertThat(navigatorState.backStack.value).containsExactly(
+            firstEntry, secondEntry, thirdEntry
+        ).inOrder()
+
+        dialogNavigator.popBackStack(secondEntry, false)
+        // should contain all entries as they have not moved to RESUMED state yet
+        assertThat(navigatorState.transitionsInProgress.value).containsExactly(
+            firstEntry, secondEntry, thirdEntry
+        )
+        fragmentManager.executePendingTransactions()
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPop_nonTransitioningDialogMarkedComplete() {
+        val dialogFragments = mutableListOf<DialogFragment>()
+        fragmentManager.fragmentFactory = object : FragmentFactory() {
+            override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+                return super.instantiate(classLoader, className).also { fragment ->
+                    if (fragment is DialogFragment) {
+                        dialogFragments += fragment
+                    }
+                }
+            }
+        }
+
+        val firstEntry = createBackStackEntry()
+        val secondEntry = createBackStackEntry(2)
+        val thirdEntry = createBackStackEntry(3)
+
+        dialogNavigator.navigate(listOf(firstEntry), null, null)
+        dialogNavigator.navigate(listOf(secondEntry), null, null)
+        dialogNavigator.navigate(listOf(thirdEntry), null, null)
+        fragmentManager.executePendingTransactions()
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
+
+        dialogNavigator.popBackStack(secondEntry, false)
+        // firstEntry was moved to RESUMED so it is no longer transitioning. It should not
+        // be in transition when dialog above it is getting popped.
+        assertThat(navigatorState.transitionsInProgress.value).doesNotContain(firstEntry)
+        fragmentManager.executePendingTransactions()
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPush_transitioningDialogStaysInTransition() {
+        val dialogFragments = mutableListOf<DialogFragment>()
+        fragmentManager.fragmentFactory = object : FragmentFactory() {
+            override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+                return super.instantiate(classLoader, className).also { fragment ->
+                    if (fragment is DialogFragment) {
+                        dialogFragments += fragment
+                    }
+                }
+            }
+        }
+
+        val entry = createBackStackEntry()
+        val secondEntry = createBackStackEntry(SECOND_FRAGMENT)
+
+        dialogNavigator.navigate(listOf(entry), null, null)
+        dialogNavigator.navigate(listOf(secondEntry), null, null)
+        // Both entries have not reached RESUME and should be in transition
+        assertThat(navigatorState.transitionsInProgress.value).containsExactly(
+            entry, secondEntry
+        ).inOrder()
+        fragmentManager.executePendingTransactions()
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPush_nonTransitioningDialogMarkedComplete() {
+        val dialogFragments = mutableListOf<DialogFragment>()
+        fragmentManager.fragmentFactory = object : FragmentFactory() {
+            override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+                return super.instantiate(classLoader, className).also { fragment ->
+                    if (fragment is DialogFragment) {
+                        dialogFragments += fragment
+                    }
+                }
+            }
+        }
+
+        val entry = createBackStackEntry()
+
+        dialogNavigator.navigate(listOf(entry), null, null)
+        fragmentManager.executePendingTransactions()
+        assertThat(dialogFragments[0].requireDialog().isShowing).isTrue()
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
+
+        val secondEntry = createBackStackEntry(SECOND_FRAGMENT)
+
+        dialogNavigator.navigate(listOf(secondEntry), null, null)
+        fragmentManager.executePendingTransactions()
+        assertThat(dialogFragments[1].requireDialog().isShowing).isTrue()
+        // ensure outgoing entry (first entry) is not transitioning
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testConsecutiveNavigateLifecycle() {
+        val dialogFragments = mutableListOf<DialogFragment>()
+        fragmentManager.fragmentFactory = object : FragmentFactory() {
+            override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+                return super.instantiate(classLoader, className).also { fragment ->
+                    if (fragment is DialogFragment) {
+                        dialogFragments += fragment
+                    }
+                }
+            }
+        }
+
+        val entry = createBackStackEntry()
+        dialogNavigator.navigate(listOf(entry), null, null)
+        fragmentManager.executePendingTransactions()
+
+        assertThat(entry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+        val secondEntry = createBackStackEntry(SECOND_FRAGMENT)
+        dialogNavigator.navigate(listOf(secondEntry), null, null)
+        fragmentManager.executePendingTransactions()
+
+        assertThat(navigatorState.backStack.value).containsExactly(entry, secondEntry).inOrder()
+        assertThat(entry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testConsecutiveNavigateThenPopLifecycle() {
+        val dialogFragments = mutableListOf<DialogFragment>()
+        fragmentManager.fragmentFactory = object : FragmentFactory() {
+            override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+                return super.instantiate(classLoader, className).also { fragment ->
+                    if (fragment is DialogFragment) {
+                        dialogFragments += fragment
+                    }
+                }
+            }
+        }
+
+        val entry = createBackStackEntry()
+        val secondEntry = createBackStackEntry(SECOND_FRAGMENT)
+
+        dialogNavigator.navigate(listOf(entry), null, null)
+        dialogNavigator.navigate(listOf(secondEntry), null, null)
+        fragmentManager.executePendingTransactions()
+
+        assertThat(navigatorState.backStack.value).containsExactly(entry, secondEntry).inOrder()
+        assertThat(entry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+        // pop top dialog
+        dialogNavigator.popBackStack(secondEntry, false)
+        fragmentManager.executePendingTransactions()
+
+        assertThat(navigatorState.backStack.value).containsExactly(entry)
+        assertThat(entry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(dialogFragments[0].lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+    }
+
     private fun createBackStackEntry(
         destId: Int = INITIAL_FRAGMENT,
         clazz: KClass<out Fragment> = EmptyDialogFragment::class
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
index dc60ee0..5846324 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
@@ -171,6 +171,7 @@
         assertWithMessage("Replacement Fragment should be the primary navigation Fragment")
             .that(fragmentManager.primaryNavigationFragment)
             .isSameInstanceAs(replacementFragment)
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
     }
 
     @UiThreadTest
@@ -359,6 +360,7 @@
         assertWithMessage("Fragment should be the primary navigation Fragment after pop")
             .that(fragmentManager.primaryNavigationFragment)
             .isSameInstanceAs(fragment)
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
     }
 
     @UiThreadTest
@@ -528,6 +530,26 @@
         assertWithMessage("PrimaryFragment should be the correct type")
             .that(fragmentManager.primaryNavigationFragment)
             .isNotInstanceOf(EmptyFragment::class.java)
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testMultipleNavigateFragmentTransactionsThenPopMultiple() {
+        val entry = createBackStackEntry()
+        val secondEntry = createBackStackEntry(SECOND_FRAGMENT, clazz = Fragment::class)
+        val thirdEntry = createBackStackEntry(THIRD_FRAGMENT)
+
+        // Push 3 fragments
+        fragmentNavigator.navigate(listOf(entry, secondEntry, thirdEntry), null, null)
+        fragmentManager.executePendingTransactions()
+
+        // Now pop multiple fragments with savedState so that the secondEntry does not get
+        // marked complete by clear viewModel
+        fragmentNavigator.popBackStack(secondEntry, true)
+        fragmentManager.executePendingTransactions()
+        assertThat(navigatorState.backStack.value).containsExactly(entry)
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
     }
 
     @UiThreadTest
@@ -1596,10 +1618,23 @@
             emptyActivity.onBackPressed()
         }
 
+        val countDownLatch3 = CountDownLatch(1)
+        activityRule.runOnUiThread {
+            entry1.lifecycle.addObserver(object : LifecycleEventObserver {
+                override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+                    if (event == Lifecycle.Event.ON_RESUME) {
+                        // wait for animator to finish
+                        countDownLatch3.countDown()
+                    }
+                }
+            })
+        }
+        assertThat(countDownLatch3.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
         // assert exit from entry2 and enter entry1
         assertThat(fragmentNavigator.backStack.value).containsExactly(entry1)
-        assertThat(entry1.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
-        assertThat(entry2.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+        assertThat(entry1.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(entry2.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
     }
 
     @LargeTest
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
index 52c924c..b7e0009 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
@@ -77,6 +77,9 @@
         assertWithMessage("New Entry should be RESUMED")
             .that(navController.currentBackStackEntry!!.lifecycle.currentState)
             .isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry!!
+        )
     }
 
     @Test
@@ -217,6 +220,7 @@
         // ensure original Fragment is dismissed and backStacks are in sync
         assertThat(navigator.backStack.value.size).isEqualTo(1)
         assertThat(fm.fragments.size).isEqualTo(2) // start + dialog fragment
+        assertThat(navController.visibleEntries.value.size).isEqualTo(2)
     }
 
     @Test
@@ -310,6 +314,9 @@
         fm?.executePendingTransactions()
 
         assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("first")
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @LargeTest
@@ -335,6 +342,9 @@
         onBackPressedDispatcher.onBackPressed()
 
         assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("third")
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry!!
+        )
     }
 
     @LargeTest
@@ -364,6 +374,9 @@
         onBackPressedDispatcher.onBackPressed()
 
         assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("fourth")
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry!!
+        )
     }
 
     private fun withNavigationActivity(
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.kt
index 55defac..b7732f7 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.kt
@@ -121,10 +121,15 @@
         }
         val beforePopList = state.backStack.value
         // Get the set of entries that are going to be popped
+        val popUpToIndex = beforePopList.indexOf(popUpTo)
         val poppedList = beforePopList.subList(
-            beforePopList.indexOf(popUpTo),
+            popUpToIndex,
             beforePopList.size
         )
+        // track transitioning state of incoming entry
+        val incomingEntry = beforePopList.elementAtOrNull(popUpToIndex - 1)
+        val incomingEntryTransitioning = state.transitionsInProgress.value.contains(incomingEntry)
+
         // Now go through the list in reversed order (i.e., starting from the most recently added)
         // and dismiss each dialog
         for (entry in poppedList.reversed()) {
@@ -134,6 +139,10 @@
             }
         }
         state.popWithTransition(popUpTo, savedState)
+        // if incoming entry was marked as transitioning by popWithTransition, mark it as complete
+        if (incomingEntry != null && !incomingEntryTransitioning) {
+            state.markTransitionComplete(incomingEntry)
+        }
     }
 
     public override fun createDestination(): Destination {
@@ -159,7 +168,13 @@
     ) {
         val dialogFragment = createDialogFragment(entry)
         dialogFragment.show(fragmentManager, entry.id)
+        val outGoingEntry = state.backStack.value.lastOrNull()
+        val outGoingEntryTransitioning = state.transitionsInProgress.value.contains(outGoingEntry)
         state.pushWithTransition(entry)
+        // if outgoing entry was put in Transition by push, mark complete here
+        if (outGoingEntry != null && !outGoingEntryTransitioning) {
+            state.markTransitionComplete(outGoingEntry)
+        }
     }
 
     override fun onLaunchSingleTop(backStackEntry: NavBackStackEntry) {
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
index 3fb3248..0c8b113 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
@@ -85,16 +85,14 @@
                 entry.id == fragment.tag
             }
             if (entry != null) {
-                if (!state.backStack.value.contains(entry)) {
-                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
-                        Log.v(
-                            TAG,
-                            "Marking transition complete for entry $entry " +
-                                "due to fragment $source lifecycle reaching DESTROYED"
-                        )
-                    }
-                    state.markTransitionComplete(entry)
+                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                    Log.v(
+                        TAG,
+                        "Marking transition complete for entry $entry " +
+                            "due to fragment $source lifecycle reaching DESTROYED"
+                    )
                 }
+                state.markTransitionComplete(entry)
             }
         }
     }
@@ -113,19 +111,16 @@
                 }
                 state.markTransitionComplete(entry)
             }
-            // Once the lifecycle reaches DESTROYED, if the entry is not in the back stack, we can
-            // mark the transition complete
+            // Once the lifecycle reaches DESTROYED, we can mark the transition complete
             if (event == Lifecycle.Event.ON_DESTROY) {
-                if (!state.backStack.value.contains(entry)) {
-                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
-                        Log.v(
-                            TAG,
-                            "Marking transition complete for entry $entry due " +
-                                "to fragment $owner view lifecycle reaching DESTROYED"
-                        )
-                    }
-                    state.markTransitionComplete(entry)
+                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                    Log.v(
+                        TAG,
+                        "Marking transition complete for entry $entry due " +
+                            "to fragment $owner view lifecycle reaching DESTROYED"
+                    )
                 }
+                state.markTransitionComplete(entry)
             }
         }
     }
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 265df05..59fd673 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -26,8 +26,8 @@
 dependencies {
     api(project(":navigation:navigation-common"))
     api("androidx.activity:activity-ktx:1.7.1")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
+    api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
+    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
     api("androidx.annotation:annotation-experimental:1.1.0")
     implementation('androidx.collection:collection:1.0.0')
 
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
index 3b662ee..2ddc413 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
@@ -186,6 +186,9 @@
         assertThat(navigator.backStack.size)
             .isEqualTo(1)
         assertThat(originalViewModel.isCleared).isTrue()
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
@@ -215,6 +218,7 @@
         val newViewModel = ViewModelProvider(newBackStackEntry).get<TestAndroidViewModel>()
         assertThat(newBackStackEntry.id).isSameInstanceAs(originalBackStackEntry.id)
         assertThat(newViewModel).isSameInstanceAs(originalViewModel)
+        assertThat(navController.visibleEntries.value).containsExactly(newBackStackEntry)
     }
 
     @UiThreadTest
@@ -606,6 +610,9 @@
         navController.navigate(R.id.second_test)
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
         assertThat(navigator.backStack.size).isEqualTo(2)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
@@ -2028,6 +2035,7 @@
             .isFalse()
         assertThat(navController.currentDestination).isNull()
         assertThat(navigator.backStack.size).isEqualTo(0)
+        assertThat(navController.visibleEntries.value).isEmpty()
     }
 
     @UiThreadTest
@@ -2074,6 +2082,9 @@
             .isTrue()
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.start_test)
         assertThat(navigator.backStack.size).isEqualTo(1)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
@@ -2092,6 +2103,9 @@
         navigator.popCurrent()
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.start_test)
         assertThat(navigator.backStack.size).isEqualTo(1)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
@@ -2132,6 +2146,9 @@
         )
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
         assertThat(navigator.backStack.size).isEqualTo(1)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
@@ -2172,6 +2189,9 @@
             .isTrue()
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.start_test)
         assertThat(navigator.backStack.size).isEqualTo(1)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
@@ -2229,6 +2249,9 @@
         navController.navigate(R.id.self)
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
         assertThat(navigator.backStack.size).isEqualTo(2)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 51d0426..87d7074 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -126,8 +126,9 @@
      * whenever they change. If there is no visible [NavBackStackEntry], this will be set to an
      * empty list.
      *
-     * - `CREATED` entries are listed first and include all entries that have been popped from
-     * the back stack and are in the process of completing their exit transition
+     * - `CREATED` entries are listed first and include all entries that are in the process of
+     * completing their exit transition. Note that this can include entries that have been
+     * popped off the Navigation back stack.
      * - `STARTED` entries on the back stack are next and include all entries that are running
      * their enter transition and entries whose destination is partially covered by a
      * `FloatingWindow` destination
diff --git a/paging/paging-common/build.gradle b/paging/paging-common/build.gradle
index ffd08fb..0e62fb1 100644
--- a/paging/paging-common/build.gradle
+++ b/paging/paging-common/build.gradle
@@ -87,7 +87,7 @@
             dependsOn(commonJvmAndroidTest)
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(commonJvmAndroidTest)
             dependencies {
                 implementation(libs.testRunner)
diff --git a/paging/paging-compose/build.gradle b/paging/paging-compose/build.gradle
index 98319a3..eb8a9ea 100644
--- a/paging/paging-compose/build.gradle
+++ b/paging/paging-compose/build.gradle
@@ -59,7 +59,7 @@
             }
         }
 
-        androidAndroidTest {
+        androidInstrumentedTest {
             dependsOn(jvmTest)
             dependencies {
                 implementation(libs.testRunner)
diff --git a/paging/paging-compose/src/androidAndroidTest/kotlin/androidx/paging/compose/LazyPagingItemsPreviewTest.kt b/paging/paging-compose/src/androidInstrumentedTest/kotlin/androidx/paging/compose/LazyPagingItemsPreviewTest.kt
similarity index 100%
rename from paging/paging-compose/src/androidAndroidTest/kotlin/androidx/paging/compose/LazyPagingItemsPreviewTest.kt
rename to paging/paging-compose/src/androidInstrumentedTest/kotlin/androidx/paging/compose/LazyPagingItemsPreviewTest.kt
diff --git a/paging/paging-compose/src/androidAndroidTest/kotlin/androidx/paging/compose/LazyPagingItemsTest.kt b/paging/paging-compose/src/androidInstrumentedTest/kotlin/androidx/paging/compose/LazyPagingItemsTest.kt
similarity index 100%
rename from paging/paging-compose/src/androidAndroidTest/kotlin/androidx/paging/compose/LazyPagingItemsTest.kt
rename to paging/paging-compose/src/androidInstrumentedTest/kotlin/androidx/paging/compose/LazyPagingItemsTest.kt
diff --git a/paging/paging-testing/build.gradle b/paging/paging-testing/build.gradle
index f605fc4..a17e9a1 100644
--- a/paging/paging-testing/build.gradle
+++ b/paging/paging-testing/build.gradle
@@ -15,14 +15,20 @@
  */
 
 import androidx.build.LibraryType
+import androidx.build.KmpPlatformsKt
 import androidx.build.PlatformIdentifier
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 import androidx.build.Publish
+import org.jetbrains.kotlin.konan.target.Family
 
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
 }
 
+def macEnabled = KmpPlatformsKt.enableMac(project)
+def linuxEnabled = KmpPlatformsKt.enableLinux(project)
+
 androidXMultiplatform {
     jvm()
     mac()
@@ -33,20 +39,71 @@
     defaultPlatform(PlatformIdentifier.ANDROID)
 
     sourceSets {
-        androidMain {
+        commonMain {
             dependencies {
                 api(libs.kotlinStdlib)
                 implementation(project(":paging:paging-common"))
             }
         }
-        androidTest {
+
+        androidMain {
+            dependsOn(jvmMain)
+        }
+
+        commonTest {
+            dependencies {
+                implementation(libs.kotlinTest)
+                implementation(libs.kotlinCoroutinesTest)
+                implementation(project(":internal-testutils-paging"))
+                implementation(project(":kruth:kruth"))
+            }
+        }
+
+        jvmTest {
+            dependsOn(commonTest)
             dependencies {
                 implementation(libs.junit)
-                implementation(libs.kotlinCoroutinesTest)
-                implementation((libs.kotlinCoroutinesAndroid))
-                implementation(project(":internal-testutils-paging"))
-                implementation(libs.kotlinTest)
-                implementation(libs.truth)
+            }
+        }
+
+        androidInstrumentedTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRunner)
+            }
+        }
+
+        if (macEnabled || linuxEnabled) {
+            nativeMain {
+                dependsOn(commonMain)
+                dependencies {
+                    implementation(libs.atomicFu)
+                }
+            }
+        }
+        if (macEnabled) {
+            darwinMain {
+                dependsOn(nativeMain)
+            }
+        }
+        if (linuxEnabled) {
+            linuxMain {
+                dependsOn(nativeMain)
+            }
+        }
+
+        targets.all { target ->
+            if (target.platformType == KotlinPlatformType.native) {
+                target.compilations["main"].defaultSourceSet {
+                    def konanTargetFamily = target.konanTarget.family
+                    if (konanTargetFamily == Family.OSX || konanTargetFamily == Family.IOS) {
+                        dependsOn(darwinMain)
+                    } else if (konanTargetFamily == Family.LINUX) {
+                        dependsOn(linuxMain)
+                    } else {
+                        throw new GradleException("unknown native target ${target}")
+                    }
+                }
             }
         }
     }
diff --git a/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/PagerFlowSnapshot.kt b/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/PagerFlowSnapshot.kt
deleted file mode 100644
index 825b27b..0000000
--- a/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/PagerFlowSnapshot.kt
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.paging.testing
-
-import androidx.annotation.VisibleForTesting
-import androidx.paging.CombinedLoadStates
-import androidx.paging.DifferCallback
-import androidx.paging.ItemSnapshotList
-import androidx.paging.LoadState
-import androidx.paging.LoadStates
-import androidx.paging.NullPaddedList
-import androidx.paging.Pager
-import androidx.paging.PagingData
-import androidx.paging.PagingDataDiffer
-import androidx.paging.testing.ErrorRecovery.RETRY
-import androidx.paging.testing.ErrorRecovery.RETURN_CURRENT_SNAPSHOT
-import androidx.paging.testing.ErrorRecovery.THROW
-import androidx.paging.testing.LoaderCallback.CallbackType.ON_CHANGED
-import androidx.paging.testing.LoaderCallback.CallbackType.ON_INSERTED
-import androidx.paging.testing.LoaderCallback.CallbackType.ON_REMOVED
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.launch
-
-/**
- * Runs the [SnapshotLoader] load operations that are passed in and returns a List of data
- * that would be presented to the UI after all load operations are complete.
- *
- * @param onError The error recovery strategy when PagingSource returns LoadResult.Error. A lambda
- * that returns an [ErrorRecovery] value. The default strategy is [ErrorRecovery.THROW].
- *
- * @param loadOperations The block containing [SnapshotLoader] load operations.
- */
-@VisibleForTesting
-public suspend fun <Value : Any> Flow<PagingData<Value>>.asSnapshot(
-    onError: LoadErrorHandler = LoadErrorHandler { THROW },
-    loadOperations: suspend SnapshotLoader<Value>.() -> @JvmSuppressWildcards Unit = { }
-): @JvmSuppressWildcards List<Value> = coroutineScope {
-
-    lateinit var loader: SnapshotLoader<Value>
-
-    val callback = object : DifferCallback {
-        override fun onChanged(position: Int, count: Int) {
-            loader.onDataSetChanged(
-                loader.generations.value,
-                LoaderCallback(ON_CHANGED, position, count)
-            )
-        }
-        override fun onInserted(position: Int, count: Int) {
-            loader.onDataSetChanged(
-                loader.generations.value,
-                LoaderCallback(ON_INSERTED, position, count)
-            )
-        }
-        override fun onRemoved(position: Int, count: Int) {
-            loader.onDataSetChanged(
-                loader.generations.value,
-                LoaderCallback(ON_REMOVED, position, count)
-            )
-        }
-    }
-
-    // PagingDataDiffer will collect from coroutineContext instead of main dispatcher
-    val differ = object : CompletablePagingDataDiffer<Value>(callback, coroutineContext) {
-        override suspend fun presentNewList(
-            previousList: NullPaddedList<Value>,
-            newList: NullPaddedList<Value>,
-            lastAccessedIndex: Int,
-            onListPresentable: () -> Unit
-        ): Int? {
-            onListPresentable()
-            /**
-             * On new generation, SnapshotLoader needs the latest [ItemSnapshotList]
-             * state so that it can initialize lastAccessedIndex to prepend/append from onwards.
-             *
-             * This initial lastAccessedIndex is necessary because initial load
-             * key may not be 0, for example when [Pager].initialKey != 0. It is calculated
-             * based on [ItemSnapshotList.placeholdersBefore] + [1/2 initial load size] to match
-             * the initial ViewportHint that [PagingDataDiffer.presentNewList] sends on
-             * first generation to auto-trigger prefetches on either direction.
-             *
-             * Any subsequent SnapshotLoader loads are based on the index tracked by
-             * [SnapshotLoader] internally.
-             */
-            val lastLoadedIndex = snapshot().placeholdersBefore + (snapshot().items.size / 2)
-            loader.generations.value.lastAccessedIndex.set(lastLoadedIndex)
-            return null
-        }
-    }
-
-    loader = SnapshotLoader(differ, onError)
-
-    /**
-     * Launches collection on this [Pager.flow].
-     *
-     * The collection job is cancelled automatically after [loadOperations] completes.
-      */
-    val collectPagingData = launch {
-        this@asSnapshot.collectLatest {
-            incrementGeneration(loader)
-            differ.collectFrom(it)
-        }
-        differ.hasCompleted.value = true
-    }
-
-    /**
-     * Runs the input [loadOperations].
-     *
-     * Awaits for initial refresh to complete before invoking [loadOperations]. Automatically
-     * cancels the collection on this [Pager.flow] after [loadOperations] completes and Paging
-     * is idle.
-     */
-    try {
-        differ.awaitNotLoading(onError)
-        loader.loadOperations()
-        differ.awaitNotLoading(onError)
-    } catch (stub: ReturnSnapshotStub) {
-        // we just want to stub and return snapshot early
-    } catch (throwable: Throwable) {
-        throw throwable
-    } finally {
-        collectPagingData.cancelAndJoin()
-    }
-
-    differ.snapshot().items
-}
-
-internal abstract class CompletablePagingDataDiffer<Value : Any>(
-    differCallback: DifferCallback,
-    mainContext: CoroutineContext,
-) : PagingDataDiffer<Value>(differCallback, mainContext) {
-    /**
-     * Marker that the underlying Flow<PagingData> has completed - e.g., every possible generation
-     * of data has been loaded completely.
-     */
-    val hasCompleted = MutableStateFlow(false)
-
-    val completableLoadStateFlow = loadStateFlow.combine(
-        hasCompleted
-    ) { loadStates, hasCompleted ->
-        if (hasCompleted) {
-            CombinedLoadStates(
-                refresh = LoadState.NotLoading(true),
-                prepend = LoadState.NotLoading(true),
-                append = LoadState.NotLoading(true),
-                source = LoadStates(
-                    refresh = LoadState.NotLoading(true),
-                    prepend = LoadState.NotLoading(true),
-                    append = LoadState.NotLoading(true)
-                )
-            )
-        } else {
-            loadStates
-        }
-    }
-}
-
-/**
- * Awaits until both source and mediator states are NotLoading. We do not care about the state of
- * endOfPaginationReached. Source and mediator states need to be checked individually because
- * the aggregated LoadStates can reflect `NotLoading` when source states are `Loading`.
- *
- * We debounce(1ms) to prevent returning too early if this collected a `NotLoading` from the
- * previous load. Without a way to determine whether the `NotLoading` it collected was from
- * a previous operation or current operation, we debounce 1ms to allow collection on a potential
- * incoming `Loading` state.
- */
-@OptIn(kotlinx.coroutines.FlowPreview::class)
-internal suspend fun <Value : Any> CompletablePagingDataDiffer<Value>.awaitNotLoading(
-    errorHandler: LoadErrorHandler
-) {
-    val state = completableLoadStateFlow.filterNotNull().debounce(1).filter {
-        it.isIdle() || it.hasError()
-    }.firstOrNull()
-
-    if (state != null && state.hasError()) {
-        handleLoadError(state, errorHandler)
-    }
-}
-
-internal fun <Value : Any> PagingDataDiffer<Value>.handleLoadError(
-    state: CombinedLoadStates,
-    errorHandler: LoadErrorHandler
-) {
-    val recovery = errorHandler.onError(state)
-    when (recovery) {
-        THROW -> throw (state.getErrorState()).error
-        RETRY -> retry()
-        RETURN_CURRENT_SNAPSHOT -> throw ReturnSnapshotStub()
-    }
-}
-private class ReturnSnapshotStub : Exception()
-
-private fun CombinedLoadStates?.isIdle(): Boolean {
-    if (this == null) return false
-    return source.isIdle() && mediator?.isIdle() ?: true
-}
-
-private fun LoadStates.isIdle(): Boolean {
-    return refresh is LoadState.NotLoading && append is LoadState.NotLoading &&
-        prepend is LoadState.NotLoading
-}
-
-private fun CombinedLoadStates?.hasError(): Boolean {
-    if (this == null) return false
-    return source.hasError() || mediator?.hasError() ?: false
-}
-
-private fun LoadStates.hasError(): Boolean {
-    return refresh is LoadState.Error || append is LoadState.Error ||
-        prepend is LoadState.Error
-}
-
-private fun CombinedLoadStates.getErrorState(): LoadState.Error {
-    return if (refresh is LoadState.Error) {
-        refresh as LoadState.Error
-    } else if (append is LoadState.Error) {
-        append as LoadState.Error
-    } else {
-        prepend as LoadState.Error
-    }
-}
-
-private fun <Value : Any> incrementGeneration(loader: SnapshotLoader<Value>) {
-    val currGen = loader.generations.value
-    if (currGen.id == loader.generations.value.id) {
-        loader.generations.value = Generation(
-            id = currGen.id + 1
-        )
-    }
-}
diff --git a/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/SnapshotLoader.kt b/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/SnapshotLoader.kt
deleted file mode 100644
index 1ebbdbf..0000000
--- a/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/SnapshotLoader.kt
+++ /dev/null
@@ -1,472 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.paging.testing
-
-import androidx.annotation.VisibleForTesting
-import androidx.paging.DifferCallback
-import androidx.paging.LoadType.APPEND
-import androidx.paging.LoadType.PREPEND
-import androidx.paging.PagingConfig
-import androidx.paging.PagingData
-import androidx.paging.PagingDataDiffer
-import androidx.paging.PagingSource
-import androidx.paging.testing.LoaderCallback.CallbackType.ON_INSERTED
-import java.util.concurrent.atomic.AtomicInteger
-import java.util.concurrent.atomic.AtomicReference
-import kotlin.math.abs
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-
-/**
- * Contains the public APIs for load operations in tests.
- *
- * Tracks generational information and provides the listener to [DifferCallback] on
- * [PagingDataDiffer] operations.
- */
-@VisibleForTesting
-public class SnapshotLoader<Value : Any> internal constructor(
-    private val differ: CompletablePagingDataDiffer<Value>,
-    private val errorHandler: LoadErrorHandler,
-) {
-    internal val generations = MutableStateFlow(Generation())
-
-    /**
-     * Refresh the data that is presented on the UI.
-     *
-     * [refresh] triggers a new generation of [PagingData] / [PagingSource]
-     * to represent an updated snapshot of the backing dataset.
-     *
-     * This fake paging operation mimics UI-driven refresh signals such as swipe-to-refresh.
-     */
-    public suspend fun refresh(): @JvmSuppressWildcards Unit {
-        differ.awaitNotLoading(errorHandler)
-        differ.refresh()
-        differ.awaitNotLoading(errorHandler)
-    }
-
-    /**
-     * Imitates scrolling down paged items, [appending][APPEND] data until the given
-     * predicate returns false.
-     *
-     * Note: This API loads an item before passing it into the predicate. This means the
-     * loaded pages may include the page which contains the item that does not match the
-     * predicate. For example, if pageSize = 2, the predicate
-     * {item: Int -> item < 3 } will return items [[1, 2],[3, 4]] where [3, 4] is the page
-     * containing the boundary item[3] not matching the predicate.
-     *
-     * The loaded pages are also dependent on [PagingConfig] settings such as
-     * [PagingConfig.prefetchDistance]:
-     * - if `prefetchDistance` > 0, the resulting appends will include prefetched items.
-     * For example, if pageSize = 2 and prefetchDistance = 2, the predicate
-     * {item: Int -> item < 3 } will load items [[1, 2], [3, 4], [5, 6]] where [5, 6] is the
-     * prefetched page.
-     *
-     * @param [predicate] the predicate to match (return true) to continue append scrolls
-     */
-    public suspend fun appendScrollWhile(
-        predicate: (item: @JvmSuppressWildcards Value) -> @JvmSuppressWildcards Boolean
-    ): @JvmSuppressWildcards Unit {
-        differ.awaitNotLoading(errorHandler)
-        appendOrPrependScrollWhile(LoadType.APPEND, predicate)
-        differ.awaitNotLoading(errorHandler)
-    }
-
-    /**
-     * Imitates scrolling up paged items, [prepending][PREPEND] data until the given
-     * predicate returns false.
-     *
-     * Note: This API loads an item before passing it into the predicate. This means the
-     * loaded pages may include the page which contains the item that does not match the
-     * predicate. For example, if pageSize = 2, initialKey = 3, the predicate
-     * {item: Int -> item >= 3 } will return items [[1, 2],[3, 4]] where [1, 2] is the page
-     * containing the boundary item[2] not matching the predicate.
-     *
-     * The loaded pages are also dependent on [PagingConfig] settings such as
-     * [PagingConfig.prefetchDistance]:
-     * - if `prefetchDistance` > 0, the resulting prepends will include prefetched items.
-     * For example, if pageSize = 2, initialKey = 3, and prefetchDistance = 2, the predicate
-     * {item: Int -> item > 4 } will load items [[1, 2], [3, 4], [5, 6]] where both [1,2] and
-     * [5, 6] are the prefetched pages.
-     *
-     * @param [predicate] the predicate to match (return true) to continue prepend scrolls
-     */
-    public suspend fun prependScrollWhile(
-        predicate: (item: @JvmSuppressWildcards Value) -> @JvmSuppressWildcards Boolean
-    ): @JvmSuppressWildcards Unit {
-        differ.awaitNotLoading(errorHandler)
-        appendOrPrependScrollWhile(LoadType.PREPEND, predicate)
-        differ.awaitNotLoading(errorHandler)
-    }
-
-    private suspend fun appendOrPrependScrollWhile(
-        loadType: LoadType,
-        predicate: (item: Value) -> Boolean
-    ) {
-        do {
-            // awaits for next item where the item index is determined based on
-            // this generation's lastAccessedIndex. If null, it means there are no more
-            // items to load for this loadType.
-            val item = awaitNextItem(loadType) ?: return
-        } while (predicate(item))
-    }
-
-    /**
-     * Imitates scrolling from current index to the target index. It waits for an item to be loaded
-     * in before triggering load on next item. Returns all available data that has been scrolled
-     * through.
-     *
-     * The scroll direction (prepend or append) is dependent on current index and target index. In
-     * general, scrolling to a smaller index triggers [PREPEND] while scrolling to a larger
-     * index triggers [APPEND].
-     *
-     * When [PagingConfig.enablePlaceholders] is false, the [index] is scoped within currently
-     * loaded items. For example, in a list of items(0-20) with currently loaded items(10-15),
-     * index[0] = item(10), index[4] = item(15).
-     *
-     * Supports [index] beyond currently loaded items when [PagingConfig.enablePlaceholders]
-     * is false:
-     * 1. For prepends, it supports negative indices for as long as there are still available
-     * data to load from. For example, take a list of items(0-20), pageSize = 1, with currently
-     * loaded items(10-15). With index[0] = item(10), a `scrollTo(-4)` will scroll to item(6) and
-     * update index[0] = item(6).
-     * 2. For appends, it supports indices >= loadedDataSize. For example, take a list of
-     * items(0-20), pageSize = 1, with currently loaded items(10-15). With
-     * index[4] = item(15), a `scrollTo(7)` will scroll to item(18) and update
-     * index[7] = item(18).
-     * Note that both examples does not account for prefetches.
-
-     * The [index] accounts for separators/headers/footers where each one of those consumes one
-     * scrolled index.
-     *
-     * For both append/prepend, this function stops loading prior to fulfilling requested scroll
-     * distance if there are no more data to load from.
-     *
-     * @param [index] The target index to scroll to
-     *
-     * @see [flingTo] for faking a scroll that continues scrolling without waiting for items to
-     * be loaded in. Supports jumping.
-     */
-    public suspend fun scrollTo(index: Int): @JvmSuppressWildcards Unit {
-        differ.awaitNotLoading(errorHandler)
-        appendOrPrependScrollTo(index)
-        differ.awaitNotLoading(errorHandler)
-    }
-
-    /**
-     * Scrolls from current index to targeted [index].
-     *
-     * Internally this method scrolls until it fulfills requested index
-     * differential (Math.abs(requested index - current index)) rather than scrolling
-     * to the exact requested index. This is because item indices can shift depending on scroll
-     * direction and placeholders. Therefore we try to fulfill the expected amount of scrolling
-     * rather than the actual requested index.
-     */
-    private suspend fun appendOrPrependScrollTo(index: Int) {
-        val startIndex = generations.value.lastAccessedIndex.get()
-        val loadType = if (startIndex > index) LoadType.PREPEND else LoadType.APPEND
-        val scrollCount = abs(startIndex - index)
-        awaitScroll(loadType, scrollCount)
-    }
-
-    /**
-     * Imitates flinging from current index to the target index. It will continue scrolling
-     * even as data is being loaded in. Returns all available data that has been scrolled
-     * through.
-     *
-     * The scroll direction (prepend or append) is dependent on current index and target index. In
-     * general, scrolling to a smaller index triggers [PREPEND] while scrolling to a larger
-     * index triggers [APPEND].
-     *
-     * This function will scroll into placeholders. This means jumping is supported when
-     * [PagingConfig.enablePlaceholders] is true and the amount of placeholders traversed
-     * has reached [PagingConfig.jumpThreshold]. Jumping is disabled when
-     * [PagingConfig.enablePlaceholders] is false.
-     *
-     * When [PagingConfig.enablePlaceholders] is false, the [index] is scoped within currently
-     * loaded items. For example, in a list of items(0-20) with currently loaded items(10-15),
-     * index[0] = item(10), index[4] = item(15).
-     *
-     * Supports [index] beyond currently loaded items when [PagingConfig.enablePlaceholders]
-     * is false:
-     * 1. For prepends, it supports negative indices for as long as there are still available
-     * data to load from. For example, take a list of items(0-20), pageSize = 1, with currently
-     * loaded items(10-15). With index[0] = item(10), a `scrollTo(-4)` will scroll to item(6) and
-     * update index[0] = item(6).
-     * 2. For appends, it supports indices >= loadedDataSize. For example, take a list of
-     * items(0-20), pageSize = 1, with currently loaded items(10-15). With
-     * index[4] = item(15), a `scrollTo(7)` will scroll to item(18) and update
-     * index[7] = item(18).
-     * Note that both examples does not account for prefetches.
-
-     * The [index] accounts for separators/headers/footers where each one of those consumes one
-     * scrolled index.
-     *
-     * For both append/prepend, this function stops loading prior to fulfilling requested scroll
-     * distance if there are no more data to load from.
-     *
-     * @param [index] The target index to scroll to
-     *
-     * @see [scrollTo] for faking scrolls that awaits for placeholders to load before continuing
-     * to scroll.
-     */
-    public suspend fun flingTo(index: Int): @JvmSuppressWildcards Unit {
-        differ.awaitNotLoading(errorHandler)
-        appendOrPrependFlingTo(index)
-        differ.awaitNotLoading(errorHandler)
-    }
-
-    /**
-     * We start scrolling from startIndex +/- 1 so we don't accidentally trigger
-     * a prefetch on the opposite direction.
-     */
-    private suspend fun appendOrPrependFlingTo(index: Int) {
-        val startIndex = generations.value.lastAccessedIndex.get()
-        val loadType = if (startIndex > index) LoadType.PREPEND else LoadType.APPEND
-
-        when (loadType) {
-            LoadType.PREPEND -> prependFlingTo(startIndex, index)
-            LoadType.APPEND -> appendFlingTo(startIndex, index)
-        }
-    }
-
-    /**
-     * Prepend flings to target index.
-     *
-     * If target index is negative, from index[0] onwards it will normal scroll until it fulfills
-     * remaining distance.
-     */
-    private suspend fun prependFlingTo(startIndex: Int, index: Int) {
-        var lastAccessedIndex = startIndex
-        val endIndex = maxOf(0, index)
-        // first, fast scroll to index or zero
-        for (i in startIndex - 1 downTo endIndex) {
-            differ[i]
-            lastAccessedIndex = i
-        }
-        setLastAccessedIndex(lastAccessedIndex)
-        // for negative indices, we delegate remainder of scrolling (distance below zero)
-        // to the awaiting version.
-        if (index < 0) {
-            val scrollCount = abs(index)
-            flingToOutOfBounds(LoadType.PREPEND, lastAccessedIndex, scrollCount)
-        }
-    }
-
-    /**
-     * Append flings to target index.
-     *
-     * If target index is beyond [PagingDataDiffer.size] - 1, from index(differ.size) and onwards,
-     * it will normal scroll until it fulfills remaining distance.
-     */
-    private suspend fun appendFlingTo(startIndex: Int, index: Int) {
-        var lastAccessedIndex = startIndex
-        val endIndex = minOf(index, differ.size - 1)
-        // first, fast scroll to endIndex
-        for (i in startIndex + 1..endIndex) {
-            differ[i]
-            lastAccessedIndex = i
-        }
-        setLastAccessedIndex(lastAccessedIndex)
-        // for indices at or beyond differ.size, we delegate remainder of scrolling (distance
-        // beyond differ.size) to the awaiting version.
-        if (index >= differ.size) {
-            val scrollCount = index - lastAccessedIndex
-            flingToOutOfBounds(LoadType.APPEND, lastAccessedIndex, scrollCount)
-        }
-    }
-
-    /**
-     * Delegated work from [flingTo] that is responsible for scrolling to indices that is
-     * beyond the range of [0 to differ.size-1].
-     *
-     * When [PagingConfig.enablePlaceholders] is true, this function is no-op because
-     * there is no more data to load from.
-     *
-     * When [PagingConfig.enablePlaceholders] is false, its delegated work to [awaitScroll]
-     * essentially loops (trigger next page --> await for next page) until
-     * it fulfills remaining (out of bounds) requested scroll distance.
-     */
-    private suspend fun flingToOutOfBounds(
-        loadType: LoadType,
-        lastAccessedIndex: Int,
-        scrollCount: Int
-    ) {
-        // Wait for the page triggered by differ[lastAccessedIndex] to load in. This gives us the
-        // offsetIndex for next differ.get() because the current lastAccessedIndex is already the
-        // boundary index, such that differ[lastAccessedIndex +/- 1] will throw IndexOutOfBounds.
-        val (_, offsetIndex) = awaitLoad(lastAccessedIndex)
-        setLastAccessedIndex(offsetIndex)
-        // starts loading from the offsetIndex and scrolls the remaining requested distance
-        awaitScroll(loadType, scrollCount)
-    }
-
-    private suspend fun awaitScroll(loadType: LoadType, scrollCount: Int) {
-        repeat(scrollCount) {
-            awaitNextItem(loadType) ?: return
-        }
-    }
-
-    /**
-     * Triggers load for next item, awaits for it to be loaded and returns the loaded item.
-     *
-     * It calculates the next load index based on loadType and this generation's
-     * [Generation.lastAccessedIndex]. The lastAccessedIndex is updated when item is loaded in.
-     */
-    private suspend fun awaitNextItem(loadType: LoadType): Value? {
-        // Get the index to load from. Return if index is invalid.
-        val index = nextIndexOrNull(loadType) ?: return null
-        // OffsetIndex accounts for items that are prepended when placeholders are disabled,
-        // as the new items shift the position of existing items. The offsetIndex (which may
-        // or may not be the same as original index) is stored as lastAccessedIndex after load and
-        // becomes the basis for next load index.
-        val (item, offsetIndex) = awaitLoad(index)
-        setLastAccessedIndex(offsetIndex)
-        return item
-    }
-
-    /**
-     * Get and update the index to load from. Returns null if next index is out of bounds.
-     *
-     * This method computes the next load index based on the [LoadType] and
-     * [Generation.lastAccessedIndex]
-     */
-    private fun nextIndexOrNull(loadType: LoadType): Int? {
-        val currIndex = generations.value.lastAccessedIndex.get()
-        return when (loadType) {
-            LoadType.PREPEND -> {
-                if (currIndex <= 0) {
-                    return null
-                }
-                currIndex - 1
-            }
-            LoadType.APPEND -> {
-                if (currIndex >= differ.size - 1) {
-                    return null
-                }
-                currIndex + 1
-            }
-        }
-    }
-
-    // Executes actual loading by accessing the PagingDataDiffer
-    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
-    private suspend fun awaitLoad(index: Int): Pair<Value, Int> {
-        differ[index]
-        differ.awaitNotLoading(errorHandler)
-        var offsetIndex = index
-
-        // awaits for the item to be loaded
-        return generations.map { generation ->
-            // reset callbackState to null so it doesn't get applied on the next load
-            val callbackState = generation.callbackState.getAndSet(null)
-            // offsetIndex accounts for items prepended when placeholders are disabled. This
-            // is necessary because the new items shift the position of existing items, and
-            // the index no longer tracks the correct item.
-            offsetIndex += callbackState?.computeIndexOffset() ?: 0
-            differ.peek(offsetIndex)
-        }.filterNotNull().first() to offsetIndex
-    }
-
-    /**
-     * Computes the offset to add to the index when loading items from differ.
-     *
-     * The purpose of this is to address shifted item positions when new items are prepended
-     * with placeholders disabled. For example, loaded items(10-12) in the NullPaddedList
-     * would have item(12) at differ[2]. If we prefetched items(7-9), item(12) would now be in
-     * differ[5].
-     *
-     * Without the offset, [PREPEND] operations would call differ[1] to load next item(11)
-     * which would now yield item(8) instead of item(11). The offset would add the
-     * inserted count to the next load index such that after prepending 3 new items(7-9), the next
-     * [PREPEND] operation would call differ[1+3 = 4] to properly load next item(11).
-     *
-     * This method is essentially no-op unless the callback meets three conditions:
-     * - is type [DifferCallback.onChanged]
-     * - position is 0 as we only care about item prepended to front of list
-     * - inserted count > 0
-     */
-    private fun LoaderCallback.computeIndexOffset(): Int {
-        return if (type == ON_INSERTED && position == 0) count else 0
-    }
-
-    private fun setLastAccessedIndex(index: Int) {
-        generations.value.lastAccessedIndex.set(index)
-    }
-
-    /**
-     * The callback to be invoked by DifferCallback on a single generation.
-     * Increase the callbackCount to notify SnapshotLoader that the dataset has updated
-     */
-    internal fun onDataSetChanged(gen: Generation, callback: LoaderCallback) {
-        val currGen = generations.value
-        // we make sure the generation with the dataset change is still valid because we
-        // want to disregard callbacks on stale generations
-        if (gen.id == currGen.id) {
-            generations.value = gen.copy(
-                callbackCount = currGen.callbackCount + 1,
-                callbackState = currGen.callbackState.apply { set(callback) }
-            )
-        }
-    }
-
-    private enum class LoadType {
-        PREPEND,
-        APPEND
-    }
-}
-
-internal data class Generation(
-    /**
-     * Id of the current Paging generation. Incremented on each new generation (when a new
-     * PagingData is received).
-     */
-    val id: Int = -1,
-
-    /**
-     * A count of the number of times Paging invokes a [DifferCallback] callback within a single
-     * generation. Incremented on each [DifferCallback] callback invoked, i.e. on item inserted.
-     *
-     * The callbackCount enables [SnapshotLoader] to await for a requested item and continue
-     * loading next item only after a callback is invoked.
-     */
-    val callbackCount: Int = 0,
-
-    /**
-     * Temporarily stores the latest [DifferCallback] to track prepends to the beginning of list.
-     * Value is reset to null once read.
-     */
-    val callbackState: AtomicReference<LoaderCallback?> = AtomicReference(null),
-
-    /**
-     * Tracks the last accessed(peeked) index on the differ for this generation
-      */
-    var lastAccessedIndex: AtomicInteger = AtomicInteger()
-)
-
-internal data class LoaderCallback(
-    val type: CallbackType,
-    val position: Int,
-    val count: Int,
-) {
-    internal enum class CallbackType {
-        ON_CHANGED,
-        ON_INSERTED,
-        ON_REMOVED,
-    }
-}
diff --git a/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/StaticListPagingSourceFactory.kt b/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/StaticListPagingSourceFactory.kt
deleted file mode 100644
index 000e2a1..0000000
--- a/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/StaticListPagingSourceFactory.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.paging.testing
-
-import androidx.annotation.VisibleForTesting
-import androidx.paging.InvalidatingPagingSourceFactory
-import androidx.paging.LoadType.REFRESH
-import androidx.paging.Pager
-import androidx.paging.PagingSource
-import androidx.paging.PagingSourceFactory
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.launch
-
-/**
- * Returns a [PagingSourceFactory] that creates [PagingSource] instances.
- *
- * Can be used as the pagingSourceFactory when constructing a [Pager] in tests. The same factory
- * should be reused within the lifetime of a ViewModel.
- *
- * Extension method on a [Flow] of list that represents the data source, with each static list
- * representing a generation of data from which a [PagingSource] will load from. With every
- * emission to the flow, the current [PagingSource] will be invalidated, thereby triggering
- * a new generation of Paged data.
- *
- * Supports multiple factories and thus multiple collection on the same flow.
- *
- * @param coroutineScope the CoroutineScope to collect from the Flow of list.
- */
-@VisibleForTesting
-public fun <Value : Any> Flow<@JvmSuppressWildcards List<Value>>.asPagingSourceFactory(
-    coroutineScope: CoroutineScope
-): PagingSourceFactory<Int, Value> {
-
-    var data: List<Value>? = null
-
-    val factory = InvalidatingPagingSourceFactory {
-        val dataSource = data ?: emptyList()
-
-        @Suppress("UNCHECKED_CAST")
-        StaticListPagingSource(dataSource)
-    }
-
-    coroutineScope.launch {
-        collect { list ->
-            data = list
-            factory.invalidate()
-        }
-    }
-
-    return factory
-}
-
-/**
- * Returns a [PagingSourceFactory] that creates [PagingSource] instances.
- *
- * Can be used as the pagingSourceFactory when constructing a [Pager] in tests. The same factory
- * should be reused within the lifetime of a ViewModel.
- *
- * Extension method on a [List] of data from which a [PagingSource] will load from. While this
- * factory supports multi-generational operations such as [REFRESH], it does not support updating
- * the data source. This means any PagingSources generated by the same factory will load from
- * the exact same list of data.
- */
-@VisibleForTesting
-public fun <Value : Any> List<Value>.asPagingSourceFactory(): PagingSourceFactory<Int, Value> =
-    PagingSourceFactory { StaticListPagingSource(this) }
diff --git a/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/TestPager.kt b/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/TestPager.kt
deleted file mode 100644
index 35b14ad..0000000
--- a/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/TestPager.kt
+++ /dev/null
@@ -1,392 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.paging.testing
-
-import androidx.annotation.VisibleForTesting
-import androidx.paging.LoadType
-import androidx.paging.LoadType.APPEND
-import androidx.paging.LoadType.PREPEND
-import androidx.paging.LoadType.REFRESH
-import androidx.paging.Pager
-import androidx.paging.PagingConfig
-import androidx.paging.PagingSource
-import androidx.paging.PagingSource.LoadParams
-import androidx.paging.PagingSource.LoadResult
-import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
-import androidx.paging.PagingState
-import java.util.concurrent.atomic.AtomicBoolean
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-
-/**
- * A fake [Pager] class to simulate how a real Pager and UI would load data from a PagingSource.
- *
- * As Paging's first load is always of type [LoadType.REFRESH], the first load operation of
- * the [TestPager] must be a call to [refresh].
- *
- * This class only supports loads from a single instance of PagingSource. To simulate
- * multi-generational Paging behavior, you must create a new [TestPager] by supplying a
- * new instance of [PagingSource].
- *
- * @param config the [PagingConfig] to configure this TestPager's loading behavior.
- * @param pagingSource the [PagingSource] to load data from.
- */
-@VisibleForTesting
-public class TestPager<Key : Any, Value : Any>(
-    private val config: PagingConfig,
-    private val pagingSource: PagingSource<Key, Value>,
-) {
-    private val hasRefreshed = AtomicBoolean(false)
-
-    private val lock = Mutex()
-
-    private val pages = ArrayDeque<LoadResult.Page<Key, Value>>()
-
-    /**
-     * Performs a load of [LoadType.REFRESH] on the PagingSource.
-     *
-     * If initialKey != null, refresh will start loading from the supplied key.
-     *
-     * Since Paging's first load is always of [LoadType.REFRESH], this method must be the very
-     * first load operation to be called on the TestPager before either [append] or [prepend]
-     * can be called. However, other non-loading operations can still be invoked. For example,
-     * you can call [getLastLoadedPage] before any load operations.
-     *
-     * Returns the LoadResult upon refresh on the [PagingSource].
-     *
-     * @param initialKey the [Key] to start loading data from on initial refresh.
-     *
-     * @throws IllegalStateException TestPager does not support multi-generational paging behavior.
-     * As such, multiple calls to refresh() on this TestPager is illegal. The [PagingSource] passed
-     * in to this [TestPager] will also be invalidated to prevent reuse of this pager for loads.
-     * However, other [TestPager] methods that does not invoke loads can still be called,
-     * such as [getLastLoadedPage].
-     */
-    public suspend fun refresh(
-        initialKey: Key? = null
-    ): @JvmSuppressWildcards LoadResult<Key, Value> {
-        if (!hasRefreshed.compareAndSet(false, true)) {
-            pagingSource.invalidate()
-            throw IllegalStateException("TestPager does not support multi-generational access " +
-                "and refresh() can only be called once per TestPager. To start a new generation," +
-                "create a new TestPager with a new PagingSource.")
-        }
-        return doInitialLoad(initialKey)
-    }
-
-    /**
-     * Performs a load of [LoadType.APPEND] on the PagingSource.
-     *
-     * Since Paging's first load is always of [LoadType.REFRESH], [refresh] must always be called
-     * first before this [append] is called.
-     *
-     * If [PagingConfig.maxSize] is implemented, [append] loads that exceed [PagingConfig.maxSize]
-     * will cause pages to be dropped from the front of loaded pages.
-     *
-     * Returns the [LoadResult] from calling [PagingSource.load]. If the [LoadParams.key] is null,
-     * such as when there is no more data to append, this append will be no-op by returning null.
-     */
-    public suspend fun append(): @JvmSuppressWildcards LoadResult<Key, Value>? {
-        return doLoad(APPEND)
-    }
-
-    /**
-     * Performs a load of [LoadType.PREPEND] on the PagingSource.
-     *
-     * Since Paging's first load is always of [LoadType.REFRESH], [refresh] must always be called
-     * first before this [prepend] is called.
-     *
-     * If [PagingConfig.maxSize] is implemented, [prepend] loads that exceed [PagingConfig.maxSize]
-     * will cause pages to be dropped from the end of loaded pages.
-     *
-     * Returns the [LoadResult] from calling [PagingSource.load]. If the [LoadParams.key] is null,
-     * such as when there is no more data to prepend, this prepend will be no-op by returning null.
-     */
-    public suspend fun prepend(): @JvmSuppressWildcards LoadResult<Key, Value>? {
-        return doLoad(PREPEND)
-    }
-
-    /**
-     * Helper to perform REFRESH loads.
-     */
-    private suspend fun doInitialLoad(
-        initialKey: Key?
-    ): @JvmSuppressWildcards LoadResult<Key, Value> {
-        return lock.withLock {
-            pagingSource.load(
-                LoadParams.Refresh(initialKey, config.initialLoadSize, config.enablePlaceholders)
-            ).also { result ->
-                if (result is LoadResult.Page) {
-                    pages.addLast(result)
-                }
-            }
-        }
-    }
-
-    /**
-     * Helper to perform APPEND or PREPEND loads.
-     */
-    private suspend fun doLoad(loadType: LoadType): LoadResult<Key, Value>? {
-        return lock.withLock {
-            if (!hasRefreshed.get()) {
-                throw IllegalStateException("TestPager's first load operation must be a refresh. " +
-                    "Please call refresh() once before calling ${loadType.name.lowercase()}().")
-            }
-            when (loadType) {
-                REFRESH -> throw IllegalArgumentException(
-                    "For LoadType.REFRESH use doInitialLoad()"
-                )
-                APPEND -> {
-                    val key = pages.lastOrNull()?.nextKey ?: return null
-                    pagingSource.load(
-                        LoadParams.Append(key, config.pageSize, config.enablePlaceholders)
-                    ).also { result ->
-                        if (result is LoadResult.Page) {
-                            pages.addLast(result)
-                        }
-                        dropPagesOrNoOp(PREPEND)
-                    }
-                } PREPEND -> {
-                    val key = pages.firstOrNull()?.prevKey ?: return null
-                    pagingSource.load(
-                        LoadParams.Prepend(key, config.pageSize, config.enablePlaceholders)
-                    ).also { result ->
-                        if (result is LoadResult.Page) {
-                            pages.addFirst(result)
-                        }
-                        dropPagesOrNoOp(APPEND)
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Returns the most recent [LoadResult.Page] loaded from the [PagingSource]. Null if
-     * no pages have been returned from [PagingSource]. For example, if PagingSource has
-     * only returned [LoadResult.Error] or [LoadResult.Invalid].
-     */
-    public suspend fun getLastLoadedPage(): @JvmSuppressWildcards LoadResult.Page<Key, Value>? {
-        return lock.withLock {
-            pages.lastOrNull()
-        }
-    }
-
-    /**
-     * Returns the current list of [LoadResult.Page] loaded so far from the [PagingSource].
-     */
-    public suspend fun getPages(): @JvmSuppressWildcards List<LoadResult.Page<Key, Value>> {
-        return lock.withLock {
-            pages.toList()
-        }
-    }
-
-    /**
-     * Returns a [PagingState] to generate a [LoadParams.key] by supplying it to
-     * [PagingSource.getRefreshKey]. The key returned from [PagingSource.getRefreshKey]
-     * should be used as the [LoadParams.Refresh.key] when calling [refresh] on a new generation of
-     * TestPager.
-     *
-     * The anchorPosition must be within index of loaded items, which can include
-     * placeholders if [PagingConfig.enablePlaceholders] is true. For example:
-     * - No placeholders: If 40 items have been loaded so far , anchorPosition must be
-     * in [0 .. 39].
-     * - With placeholders: If there are a total of 100 loadable items, the anchorPosition
-     * must be in [0..99].
-     *
-     * The [anchorPosition] should be the index that the user has hypothetically
-     * scrolled to on the UI. Since the [PagingState.anchorPosition] in Paging can be based
-     * on any item or placeholder currently visible on the screen, the actual
-     * value of [PagingState.anchorPosition] may not exactly match the [anchorPosition] passed
-     * to this function even if viewing the same page of data.
-     *
-     * Note that when `[PagingConfig.enablePlaceholders] = false`, the
-     * [PagingState.anchorPosition] returned from this function references the absolute index
-     * within all loadable data. For example, with items[0 - 99]:
-     * If items[20 - 30] were loaded without placeholders, anchorPosition 0 references item[20].
-     * But once translated into [PagingState.anchorPosition], anchorPosition 0 references item[0].
-     * The [PagingSource] is expected to handle this correctly within [PagingSource.getRefreshKey]
-     * when [PagingConfig.enablePlaceholders] = false.
-     *
-     * @param anchorPosition the index representing the last accessed item within the
-     * items presented on the UI, which may be a placeholder if
-     * [PagingConfig.enablePlaceholders] is true.
-     *
-     * @throws IllegalStateException if anchorPosition is out of bounds.
-     */
-    public suspend fun getPagingState(
-        anchorPosition: Int
-    ): @JvmSuppressWildcards PagingState<Key, Value> {
-        lock.withLock {
-            checkWithinBoundary(anchorPosition)
-            return PagingState(
-                pages = pages.toList(),
-                anchorPosition = anchorPosition,
-                config = config,
-                leadingPlaceholderCount = getLeadingPlaceholderCount()
-            )
-        }
-    }
-
-    /**
-     * Returns a [PagingState] to generate a [LoadParams.key] by supplying it to
-     * [PagingSource.getRefreshKey]. The key returned from [PagingSource.getRefreshKey]
-     * should be used as the [LoadParams.Refresh.key] when calling [refresh] on a new generation of
-     * TestPager.
-     *
-     * The [anchorPositionLookup] lambda should return an item that the user has hypothetically
-     * scrolled to on the UI. The item must have already been loaded prior to using this helper.
-     * To generate a PagingState anchored to a placeholder, use the overloaded [getPagingState]
-     * function instead. Since the [PagingState.anchorPosition] in Paging can be based
-     * on any item or placeholder currently visible on the screen, the actual
-     * value of [PagingState.anchorPosition] may not exactly match the anchorPosition returned
-     * from this function even if viewing the same page of data.
-     *
-     * Note that when `[PagingConfig.enablePlaceholders] = false`, the
-     * [PagingState.anchorPosition] returned from this function references the absolute index
-     * within all loadable data. For example, with items[0 - 99]:
-     * If items[20 - 30] were loaded without placeholders, anchorPosition 0 references item[20].
-     * But once translated into [PagingState.anchorPosition], anchorPosition 0 references item[0].
-     * The [PagingSource] is expected to handle this correctly within [PagingSource.getRefreshKey]
-     * when [PagingConfig.enablePlaceholders] = false.
-     *
-     * @param anchorPositionLookup the predicate to match with an item which will serve as the basis
-     * for generating the [PagingState].
-     *
-     * @throws IllegalArgumentException if the given predicate fails to match with an item.
-     */
-    public suspend fun getPagingState(
-        anchorPositionLookup: (item: @JvmSuppressWildcards Value) -> Boolean
-    ): @JvmSuppressWildcards PagingState<Key, Value> {
-        lock.withLock {
-            val indexInPages = pages.flatten().indexOfFirst {
-                anchorPositionLookup(it)
-            }
-            return when {
-                indexInPages < 0 -> throw IllegalArgumentException(
-                    "The given predicate has returned false for every loaded item. To generate a" +
-                        "PagingState anchored to an item, the expected item must have already " +
-                        "been loaded."
-                )
-                else -> {
-                    val finalIndex = if (config.enablePlaceholders) {
-                        indexInPages + (pages.firstOrNull()?.itemsBefore ?: 0)
-                    } else {
-                        indexInPages
-                    }
-                    PagingState(
-                        pages = pages.toList(),
-                        anchorPosition = finalIndex,
-                        config = config,
-                        leadingPlaceholderCount = getLeadingPlaceholderCount()
-                    )
-                }
-            }
-        }
-    }
-
-    /**
-     * Ensures the anchorPosition is within boundary of loaded data.
-     *
-     * If placeholders are enabled, the provided anchorPosition must be within boundaries of
-     * [0 .. itemCount - 1], which includes placeholders before and after loaded data.
-     *
-     * If placeholders are disabled, the provided anchorPosition must be within boundaries of
-     * [0 .. loaded data size - 1].
-     *
-     * @throws IllegalStateException if anchorPosition is out of bounds
-     */
-    private fun checkWithinBoundary(anchorPosition: Int) {
-        val loadedSize = pages.flatten().size
-        val maxBoundary = if (config.enablePlaceholders) {
-            (pages.firstOrNull()?.itemsBefore ?: 0) + loadedSize +
-                (pages.lastOrNull()?.itemsAfter ?: 0) - 1
-        } else {
-            loadedSize - 1
-        }
-        check(anchorPosition in 0..maxBoundary) {
-            "anchorPosition $anchorPosition is out of bounds between [0..$maxBoundary]. Please " +
-                "provide a valid anchorPosition."
-        }
-    }
-
-    // Number of placeholders before the first loaded item if placeholders are enabled, otherwise 0.
-    private fun getLeadingPlaceholderCount(): Int {
-        return if (config.enablePlaceholders) {
-            // itemsBefore represents placeholders before first loaded item, and can be
-            // one of three.
-            // 1. valid int if implemented
-            // 2. null if pages empty
-            // 3. COUNT_UNDEFINED if not implemented
-            val itemsBefore: Int? = pages.firstOrNull()?.itemsBefore
-            // finalItemsBefore is `null` if it is either case 2. or 3.
-            val finalItemsBefore = if (itemsBefore == null || itemsBefore == COUNT_UNDEFINED) {
-                null
-            } else {
-                itemsBefore
-            }
-            // This will ultimately return 0 if user didn't implement itemsBefore or if pages
-            // are empty, i.e. user called getPagingState before any loads.
-            finalItemsBefore ?: 0
-        } else {
-            0
-        }
-    }
-
-    private fun dropPagesOrNoOp(dropType: LoadType) {
-        require(dropType != REFRESH) {
-            "Drop loadType must be APPEND or PREPEND but got $dropType"
-        }
-
-        // check if maxSize has been set
-        if (config.maxSize == PagingConfig.MAX_SIZE_UNBOUNDED) return
-
-        var itemCount = pages.flatten().size
-        if (itemCount < config.maxSize) return
-
-        // represents the max droppable amount of items
-        val presentedItemsBeforeOrAfter = when (dropType) {
-            PREPEND -> pages.take(pages.lastIndex)
-            else -> pages.takeLast(pages.lastIndex)
-        }.fold(0) { acc, page ->
-            acc + page.data.size
-        }
-
-        var itemsDropped = 0
-
-        // mirror Paging requirement to never drop below 2 pages
-        while (pages.size > 2 && itemCount - itemsDropped > config.maxSize) {
-            val pageSize = when (dropType) {
-                PREPEND -> pages.first().data.size
-                else -> pages.last().data.size
-            }
-
-            val itemsAfterDrop = presentedItemsBeforeOrAfter - itemsDropped - pageSize
-
-            // mirror Paging behavior of ensuring prefetchDistance is fulfilled in dropped
-            // direction
-            if (itemsAfterDrop < config.prefetchDistance) break
-
-            when (dropType) {
-                PREPEND -> pages.removeFirst()
-                else -> pages.removeLast()
-            }
-
-            itemsDropped += pageSize
-        }
-    }
-}
diff --git a/paging/paging-testing/src/androidTest/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt b/paging/paging-testing/src/androidTest/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
deleted file mode 100644
index d4bd050..0000000
--- a/paging/paging-testing/src/androidTest/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
+++ /dev/null
@@ -1,2585 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.paging.testing
-
-import androidx.paging.LoadState
-import androidx.paging.LoadStates
-import androidx.paging.Pager
-import androidx.paging.PagingConfig
-import androidx.paging.PagingData
-import androidx.paging.PagingSource
-import androidx.paging.PagingSource.LoadParams
-import androidx.paging.PagingSourceFactory
-import androidx.paging.PagingState
-import androidx.paging.cachedIn
-import androidx.paging.insertSeparators
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlin.test.assertFailsWith
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
-@RunWith(Parameterized::class)
-class PagerFlowSnapshotTest(
-    private val loadDelay: Long
-) {
-    companion object {
-        @Parameterized.Parameters
-        @JvmStatic
-        fun withLoadDelay(): Array<Long> {
-            return arrayOf(0, 10000)
-        }
-    }
-
-    private val testScope = TestScope(UnconfinedTestDispatcher())
-
-    private fun createFactory(dataFlow: Flow<List<Int>>) = WrappedPagingSourceFactory(
-        dataFlow.asPagingSourceFactory(testScope.backgroundScope),
-        loadDelay
-    )
-
-    private fun createSingleGenFactory(data: List<Int>) = WrappedPagingSourceFactory(
-        data.asPagingSourceFactory(),
-        loadDelay
-    )
-
-    @Test
-    fun initialRefresh() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot()
-            // first page + prefetched page
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7)
-            )
-        }
-    }
-
-    @Test
-    fun initialRefreshSingleGen() {
-        val data = List(30) { it }
-        val pager = createPager(data)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot()
-            // first page + prefetched page
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7)
-            )
-        }
-    }
-
-    @Test
-    fun initialRefresh_emptyOperations() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {}
-            // first page + prefetched page
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7)
-            )
-        }
-    }
-
-    @Test
-    fun initialRefresh_withSeparators() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow).map { pagingData ->
-            pagingData.insertSeparators { before: Int?, after: Int? ->
-                if (before != null && after != null) "sep" else null
-            }
-        }
-        testScope.runTest {
-            val snapshot = pager.asSnapshot()
-            // loads 8[initial 5 + prefetch 3] items total, including separators
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, "sep", 1, "sep", 2, "sep", 3, "sep", 4)
-            )
-        }
-    }
-
-    @Test
-    fun initialRefresh_withoutPrefetch() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPagerNoPrefetch(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot()
-
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4)
-            )
-        }
-    }
-
-    @Test
-    fun initialRefresh_withInitialKey() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow, 10)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot()
-
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
-            )
-        }
-    }
-
-    @Test
-    fun initialRefresh_withInitialKey_withoutPrefetch() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPagerNoPrefetch(dataFlow, 10)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot()
-
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(10, 11, 12, 13, 14)
-            )
-        }
-    }
-
-    @Test
-    fun initialRefresh_PagingDataFrom_withoutLoadStates() {
-        val data = List(10) { it }
-        val pager = flowOf(PagingData.from(data))
-        testScope.runTest {
-            val snapshot = pager.asSnapshot()
-            // first page + prefetched page
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
-            )
-        }
-    }
-
-    @Test
-    fun initialRefresh_PagingDataFrom_withLoadStates() {
-        val data = List(10) { it }
-        val pager = flowOf(PagingData.from(data, LoadStates(
-            refresh = LoadState.NotLoading(true),
-            prepend = LoadState.NotLoading(true),
-            append = LoadState.NotLoading(true)
-        )))
-        testScope.runTest {
-            val snapshot = pager.asSnapshot()
-            // first page + prefetched page
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
-            )
-        }
-    }
-
-    @Test
-    fun emptyInitialRefresh() {
-        val dataFlow = emptyFlow<List<Int>>()
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot()
-
-            assertThat(snapshot).containsExactlyElementsIn(
-                emptyList<Int>()
-            )
-        }
-    }
-
-    @Test
-    fun emptyInitialRefreshSingleGen() {
-        val data = emptyList<Int>()
-        val pager = createPager(data)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot()
-
-            assertThat(snapshot).containsExactlyElementsIn(
-                emptyList<Int>()
-            )
-        }
-    }
-
-    @Test
-    fun emptyInitialRefresh_emptyOperations() {
-        val dataFlow = emptyFlow<List<Int>>()
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot()
-
-            assertThat(snapshot).containsExactlyElementsIn(
-                emptyList<Int>()
-            )
-        }
-    }
-
-    @Test
-    fun manualRefresh() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPagerNoPrefetch(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                refresh()
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4),
-            )
-        }
-    }
-
-    @Test
-    fun manualRefreshSingleGen() {
-        val data = List(30) { it }
-        val pager = createPager(data)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                refresh()
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7),
-            )
-        }
-    }
-
-    @Test
-    fun manualRefreshSingleGen_pagingSourceInvalidated() {
-        val data = List(30) { it }
-        val sources = mutableListOf<PagingSource<Int, Int>>()
-        val factory = data.asPagingSourceFactory()
-        val pager = Pager(
-            config = PagingConfig(pageSize = 3, initialLoadSize = 5),
-            pagingSourceFactory = { factory().also { sources.add(it) } },
-        ).flow
-        testScope.runTest {
-            pager.asSnapshot {
-                refresh()
-            }
-            assertThat(sources.first().invalid).isTrue()
-        }
-    }
-
-    @Test
-    fun manualRefresh_PagingDataFrom_withoutLoadStates() {
-        val data = List(10) { it }
-        val pager = flowOf(PagingData.from(data))
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                refresh()
-            }
-            // first page + prefetched page
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
-            )
-        }
-    }
-
-    @Test
-    fun manualRefresh_PagingDataFrom_withLoadStates() {
-        val data = List(10) { it }
-        val pager = flowOf(PagingData.from(data, LoadStates(
-            refresh = LoadState.NotLoading(true),
-            prepend = LoadState.NotLoading(true),
-            append = LoadState.NotLoading(true)
-        )))
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                refresh()
-            }
-            // first page + prefetched page
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
-            )
-        }
-    }
-
-    @Test
-    fun manualEmptyRefresh() {
-        val dataFlow = emptyFlow<List<Int>>()
-        val pager = createPagerNoPrefetch(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                refresh()
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                emptyList<Int>()
-            )
-        }
-    }
-
-    @Test
-    fun appendWhile() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                appendScrollWhile { item: Int ->
-                    item < 14
-                }
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                // initial load [0-4]
-                // prefetched [5-7]
-                // appended [8-16]
-                // prefetched [17-19]
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
-            )
-        }
-    }
-
-    @Test
-    fun appendWhile_withDrops() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPagerWithDrops(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                appendScrollWhile { item ->
-                    item < 14
-                }
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                // dropped [0-10]
-                listOf(11, 12, 13, 14, 15, 16, 17, 18, 19)
-            )
-        }
-    }
-
-    @Test
-    fun appendWhile_withSeparators() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow).map { pagingData ->
-            pagingData.insertSeparators { before: Int?, _ ->
-                if (before == 9 || before == 12) "sep" else null
-            }
-        }
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                appendScrollWhile { item ->
-                    item !is Int || item < 14
-                }
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                // initial load [0-4]
-                // prefetched [5-7]
-                // appended [8-16]
-                // prefetched [17-19]
-                listOf(
-                    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "sep", 10, 11, 12, "sep", 13, 14, 15,
-                    16, 17, 18, 19
-                )
-            )
-        }
-    }
-
-    @Test
-    fun appendWhile_withoutPrefetch() {
-        val dataFlow = flowOf(List(50) { it })
-        val pager = createPagerNoPrefetch(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                appendScrollWhile { item: Int ->
-                    item < 14
-                }
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                // initial load [0-4]
-                // appended [5-16]
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
-            )
-        }
-    }
-
-    @Test
-    fun appendWhile_withoutPlaceholders() {
-        val dataFlow = flowOf(List(50) { it })
-        val pager = createPagerNoPlaceholders(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                appendScrollWhile { item: Int ->
-                    item != 14
-                }
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                // initial load [0-4]
-                // prefetched [5-7]
-                // appended [8-16]
-                // prefetched [17-19]
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
-            )
-        }
-    }
-
-    @Test
-    fun prependWhile() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow, 20)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                prependScrollWhile { item: Int ->
-                    item > 14
-                }
-            }
-            // initial load [20-24]
-            // prefetched [17-19], [25-27]
-            // prepended [14-16]
-            // prefetched [11-13]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27)
-            )
-        }
-    }
-
-    @Test
-    fun prependWhile_withDrops() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPagerWithDrops(dataFlow, 20)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                prependScrollWhile { item: Int ->
-                    item > 14
-                }
-            }
-            // dropped [20-27]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(11, 12, 13, 14, 15, 16, 17, 18, 19)
-            )
-        }
-    }
-
-    @Test
-    fun prependWhile_withSeparators() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow, 20).map { pagingData ->
-            pagingData.insertSeparators { before: Int?, _ ->
-                if (before == 14 || before == 18) "sep" else null
-            }
-        }
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                prependScrollWhile { item ->
-                    item !is Int || item > 14
-                }
-            }
-            // initial load [20-24]
-            // prefetched [17-19], no append prefetch because separator fulfilled prefetchDistance
-            // prepended [14-16]
-            // prefetched [11-13]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    11, 12, 13, 14, "sep", 15, 16, 17, 18, "sep", 19, 20, 21, 22, 23,
-                    24, 25, 26, 27
-                )
-            )
-        }
-    }
-
-    @Test
-    fun prependWhile_withoutPrefetch() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPagerNoPrefetch(dataFlow, 20)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                prependScrollWhile { item: Int ->
-                    item > 14
-                }
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                // initial load [20-24]
-                // prepended [14-19]
-                listOf(14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)
-            )
-        }
-    }
-
-    @Test
-    fun prependWhile_withoutPlaceholders() {
-        val dataFlow = flowOf(List(50) { it })
-        val pager = createPagerNoPlaceholders(dataFlow, 30)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                prependScrollWhile { item: Int ->
-                    item != 22
-                }
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                // initial load [30-34]
-                // prefetched [27-29], [35-37]
-                // prepended [21-26]
-                // prefetched [18-20]
-                listOf(
-                    18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37
-                )
-            )
-        }
-    }
-
-    @Test
-    fun appendWhile_withInitialKey() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow, 10)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                appendScrollWhile { item: Int ->
-                    item < 18
-                }
-            }
-            // initial load [10-14]
-            // prefetched [7-9], [15-17]
-            // appended [18-20]
-            // prefetched [21-23]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
-            )
-        }
-    }
-
-    @Test
-    fun appendWhile_withInitialKey_withoutPlaceholders() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPagerNoPlaceholders(dataFlow, 10)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                appendScrollWhile { item: Int ->
-                    item != 19
-                }
-            }
-            // initial load [10-14]
-            // prefetched [7-9], [15-17]
-            // appended [18-20]
-            // prefetched [21-23]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
-            )
-        }
-    }
-
-    @Test
-    fun appendWhile_withInitialKey_withoutPrefetch() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPagerNoPrefetch(dataFlow, 10)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                appendScrollWhile { item: Int ->
-                    item < 18
-                }
-            }
-            // initial load [10-14]
-            // appended [15-20]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
-            )
-        }
-    }
-
-    @Test
-    fun prependWhile_withoutInitialKey() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                prependScrollWhile { item: Int ->
-                    item > -3
-                }
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveAppendWhile() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot1 = pager.asSnapshot {
-                appendScrollWhile { item: Int ->
-                    item < 7
-                }
-            }
-
-            val snapshot2 = pager.asSnapshot {
-                appendScrollWhile { item: Int ->
-                    item < 22
-                }
-            }
-
-            // includes initial load, 1st page, 2nd page (from prefetch)
-            assertThat(snapshot1).containsExactlyElementsIn(
-                List(11) { it }
-            )
-
-            // includes extra page from prefetch
-            assertThat(snapshot2).containsExactlyElementsIn(
-                List(26) { it }
-            )
-        }
-    }
-
-    @Test
-    fun consecutivePrependWhile() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPagerNoPrefetch(dataFlow, 20).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot1 = pager.asSnapshot {
-                prependScrollWhile { item: Int ->
-                    item > 17
-                }
-            }
-            assertThat(snapshot1).containsExactlyElementsIn(
-                listOf(17, 18, 19, 20, 21, 22, 23, 24)
-            )
-            val snapshot2 = pager.asSnapshot {
-                prependScrollWhile { item: Int ->
-                    item > 11
-                }
-            }
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)
-            )
-        }
-    }
-
-    @Test
-    fun appendWhile_outOfBounds_returnsCurrentlyLoadedItems() {
-        val dataFlow = flowOf(List(10) { it })
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                appendScrollWhile { item: Int ->
-                    // condition scrolls till end of data since we only have 10 items
-                    item < 18
-                }
-            }
-
-            // returns the items loaded before index becomes out of bounds
-            assertThat(snapshot).containsExactlyElementsIn(
-                List(10) { it }
-            )
-        }
-    }
-
-    @Test
-    fun prependWhile_outOfBounds_returnsCurrentlyLoadedItems() {
-        val dataFlow = flowOf(List(20) { it })
-        val pager = createPager(dataFlow, 10)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                prependScrollWhile { item: Int ->
-                    // condition scrolls till index = 0
-                    item > -3
-                }
-            }
-            // returns the items loaded before index becomes out of bounds
-            assertThat(snapshot).containsExactlyElementsIn(
-                // initial load [10-14]
-                // prefetched [7-9], [15-17]
-                // prepended [0-6]
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
-            )
-        }
-    }
-
-    @Test
-    fun refreshAndAppendWhile() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                refresh() // triggers second gen
-                appendScrollWhile { item: Int ->
-                    item < 10
-                }
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
-            )
-        }
-    }
-
-    @Test
-    fun refreshAndPrependWhile() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow, 20).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // this prependScrollWhile does not cause paging to load more items
-                // but it helps this test register a non-null anchorPosition so the upcoming
-                // refresh doesn't start at index 0
-                prependScrollWhile { item -> item > 20 }
-                // triggers second gen
-                refresh()
-                prependScrollWhile { item: Int ->
-                    item > 12
-                }
-            }
-            // second gen initial load, anchorPos = 20, refreshKey = 18, loaded
-            // initial load [18-22]
-            // prefetched [15-17], [23-25]
-            // prepended [12-14]
-            // prefetched [9-11]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25)
-            )
-        }
-    }
-
-    @Test
-    fun appendWhileAndRefresh() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                appendScrollWhile { item: Int ->
-                    item < 10
-                }
-                refresh()
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                // second gen initial load, anchorPos = 10, refreshKey = 8
-                // initial load [8-12]
-                // prefetched [5-7], [13-15]
-                listOf(5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
-            )
-        }
-    }
-
-    @Test
-    fun prependWhileAndRefresh() {
-        val dataFlow = flowOf(List(30) { it })
-        val pager = createPager(dataFlow, 15).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                prependScrollWhile { item: Int ->
-                    item > 8
-                }
-                refresh()
-            }
-            assertThat(snapshot).containsExactlyElementsIn(
-                // second gen initial load, anchorPos = 8, refreshKey = 6
-                // initial load [6-10]
-                // prefetched [3-5], [11-13]
-                listOf(3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveGenerations_fromFlow() {
-        val loadDelay = 500 + loadDelay
-        // wait for 500 + loadDelay between each emission
-        val dataFlow = flow {
-            emit(emptyList())
-            delay(loadDelay)
-
-            emit(List(30) { it })
-            delay(loadDelay)
-
-            emit(List(30) { it + 30 })
-        }
-        val pager = createPagerNoPrefetch(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot1 = pager.asSnapshot()
-            assertThat(snapshot1).containsExactlyElementsIn(
-                emptyList<Int>()
-            )
-
-            delay(500)
-
-            val snapshot2 = pager.asSnapshot()
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4)
-            )
-
-            delay(500)
-
-            val snapshot3 = pager.asSnapshot()
-            assertThat(snapshot3).containsExactlyElementsIn(
-                listOf(30, 31, 32, 33, 34)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveGenerations_PagingDataFrom_withoutLoadStates() {
-        val loadDelay = 500 + loadDelay
-        // wait for 500 + loadDelay between each emission
-        val pager = flow {
-            emit(PagingData.empty())
-            delay(loadDelay)
-
-            emit(PagingData.from(List(10) { it }))
-            delay(loadDelay)
-
-            emit(PagingData.from(List(10) { it + 30 }))
-        }
-        testScope.runTest {
-            val snapshot1 = pager.asSnapshot()
-            assertWithMessage("Only the last generation should be loaded without LoadStates")
-                .that(snapshot1).containsExactlyElementsIn(
-                listOf(30, 31, 32, 33, 34, 35, 36, 37, 38, 39)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveGenerations_PagingDataFrom_withLoadStates() {
-        val loadDelay = 500 + loadDelay
-        // wait for 500 + loadDelay between each emission
-        val pager = flow {
-            emit(PagingData.empty(LoadStates(
-                refresh = LoadState.NotLoading(true),
-                prepend = LoadState.NotLoading(true),
-                append = LoadState.NotLoading(true)
-            )))
-            delay(loadDelay)
-
-            emit(PagingData.from(List(10) { it }, LoadStates(
-                refresh = LoadState.NotLoading(true),
-                prepend = LoadState.NotLoading(true),
-                append = LoadState.NotLoading(true)
-            )))
-            delay(loadDelay)
-
-            emit(PagingData.from(List(10) { it + 30 }, LoadStates(
-                refresh = LoadState.NotLoading(true),
-                prepend = LoadState.NotLoading(true),
-                append = LoadState.NotLoading(true)
-            )))
-        }.cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot1 = pager.asSnapshot()
-            assertThat(snapshot1).containsExactlyElementsIn(
-                emptyList<Int>()
-            )
-
-            delay(loadDelay)
-
-            val snapshot2 = pager.asSnapshot()
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
-            )
-
-            delay(loadDelay)
-
-            val snapshot3 = pager.asSnapshot()
-            assertThat(snapshot3).containsExactlyElementsIn(
-                listOf(30, 31, 32, 33, 34, 35, 36, 37, 38, 39)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveGenerations_fromSharedFlow_emitAfterRefresh() {
-        val dataFlow = MutableSharedFlow<List<Int>>()
-        val pager = createPagerNoPrefetch(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot1 = pager.asSnapshot()
-            assertThat(snapshot1).containsExactlyElementsIn(
-                emptyList<Int>()
-            )
-
-            val snapshot2 = pager.asSnapshot {
-                dataFlow.emit(List(30) { it })
-            }
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4)
-            )
-
-            val snapshot3 = pager.asSnapshot {
-                dataFlow.emit(List(30) { it + 30 })
-            }
-            assertThat(snapshot3).containsExactlyElementsIn(
-                listOf(30, 31, 32, 33, 34)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveGenerations_fromSharedFlow_emitBeforeRefresh() {
-        val dataFlow = MutableSharedFlow<List<Int>>()
-        val pager = createPagerNoPrefetch(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            dataFlow.emit(emptyList())
-            val snapshot1 = pager.asSnapshot()
-            assertThat(snapshot1).containsExactlyElementsIn(
-                emptyList<Int>()
-            )
-
-            dataFlow.emit(List(30) { it })
-            val snapshot2 = pager.asSnapshot()
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4)
-            )
-
-            dataFlow.emit(List(30) { it + 30 })
-            val snapshot3 = pager.asSnapshot()
-            assertThat(snapshot3).containsExactlyElementsIn(
-                listOf(30, 31, 32, 33, 34)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveGenerations_nonNullRefreshKey() {
-        val loadDelay = 500 + loadDelay
-        val dataFlow = flow {
-            // first gen
-            emit(List(20) { it })
-            // wait for refresh + append
-            delay(loadDelay * 2)
-            // second gen
-            emit(List(20) { it })
-        }
-        val pager = createPagerNoPrefetch(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot1 = pager.asSnapshot {
-                // we scroll to register a non-null anchorPos
-                appendScrollWhile { item: Int ->
-                    item < 5
-                }
-            }
-            assertThat(snapshot1).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7)
-            )
-
-            delay(1000)
-            val snapshot2 = pager.asSnapshot()
-            // anchorPos = 5, refreshKey = 3
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(3, 4, 5, 6, 7)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveGenerations_withInitialKey_nullRefreshKey() {
-        val loadDelay = 500 + loadDelay
-        // wait for 500 + loadDelay between each emission
-        val dataFlow = flow {
-            // first gen
-            emit(List(20) { it })
-            delay(loadDelay)
-            // second gen
-            emit(List(20) { it })
-        }
-        val pager = createPagerNoPrefetch(dataFlow, 10).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot1 = pager.asSnapshot()
-            assertThat(snapshot1).containsExactlyElementsIn(
-                listOf(10, 11, 12, 13, 14)
-            )
-
-            delay(500)
-            val snapshot2 = pager.asSnapshot()
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveGenerations_withInitialKey_nonNullRefreshKey() {
-        val loadDelay = 500 + loadDelay
-        val dataFlow = flow {
-            // first gen
-            emit(List(20) { it })
-            // wait for refresh + append
-            delay(loadDelay * 2)
-            // second gen
-            emit(List(20) { it })
-        }
-        val pager = createPagerNoPrefetch(dataFlow, 10).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot1 = pager.asSnapshot {
-                // we scroll to register a non-null anchorPos
-                appendScrollWhile { item: Int ->
-                    item < 15
-                }
-            }
-            assertThat(snapshot1).containsExactlyElementsIn(
-                listOf(10, 11, 12, 13, 14, 15, 16, 17)
-            )
-
-            delay(1000)
-            val snapshot2 = pager.asSnapshot()
-            // anchorPos = 15, refreshKey = 13
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(13, 14, 15, 16, 17)
-            )
-        }
-    }
-
-    @Test
-    fun prependScroll() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(42)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // prepended [41-46]
-            // prefetched [38-40]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun prependScroll_withDrops() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerWithDrops(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(42)
-            }
-            // dropped [47-57]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(38, 39, 40, 41, 42, 43, 44, 45, 46)
-            )
-        }
-    }
-
-    @Test
-    fun prependScroll_withSeparators() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow, 50).map { pagingData ->
-            pagingData.insertSeparators { before: Int?, _ ->
-                if (before == 42 || before == 49) "sep" else null
-            }
-        }
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(42)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // prepended [41-46]
-            // prefetched [38-40]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    38, 39, 40, 41, 42, "sep", 43, 44, 45, 46, 47, 48, 49, "sep", 50, 51, 52,
-                    53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun consecutivePrependScroll() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(42)
-                scrollTo(38)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // prepended [38-46]
-            // prefetched [35-37]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
-                    51, 52, 53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun consecutivePrependScroll_multiSnapshots() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(42)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // prepended [41-46]
-            // prefetched [38-40]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
-                )
-            )
-
-            val snapshot2 = pager.asSnapshot {
-                scrollTo(38)
-            }
-            // prefetched [35-37]
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(
-                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
-                    51, 52, 53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun prependScroll_indexOutOfBounds() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow, 5).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(-5)
-            }
-            // ensure index is capped when no more data to load
-            // initial load [5-9]
-            // prefetched [2-4], [10-12]
-            // scrollTo prepended [0-1]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
-            )
-        }
-    }
-
-    @Test
-    fun prependScroll_accessPageBoundary() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow, 50).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(47)
-            }
-            // ensure that SnapshotLoader waited for last prefetch before returning
-            // initial load [50-54]
-            // prefetched [47-49], [55-57] - expect only one extra page to be prefetched after this
-            // scrollTo prepended [44-46]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
-            )
-        }
-    }
-
-    @Test
-    fun prependScroll_withoutPrefetch() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPrefetch(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(42)
-            }
-            // initial load [50-54]
-            // prepended [41-49]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54)
-            )
-        }
-    }
-
-    @Test
-    fun prependScroll_withoutPlaceholders() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // Without placeholders, first loaded page always starts at index[0]
-                scrollTo(0)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // prepended [44-46]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
-            )
-        }
-    }
-
-    @Test
-    fun prependScroll_withoutPlaceholders_indexOutOfBounds() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(-5)
-            }
-            // ensure it honors negative indices starting with index[0] = item[47]
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // scrollTo prepended [41-46]
-            // prefetched [38-40]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun prependScroll_withoutPlaceholders_indexOutOfBoundsIsCapped() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow, 5).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(-5)
-            }
-            // ensure index is capped when no more data to load
-            // initial load [5-9]
-            // prefetched [2-4], [10-12]
-            // scrollTo prepended [0-1]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
-            )
-        }
-    }
-
-    @Test
-    fun consecutivePrependScroll_withoutPlaceholders() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // Without placeholders, first loaded page always starts at index[0]
-                scrollTo(-1)
-                // Without placeholders, first loaded page always starts at index[0]
-                scrollTo(-5)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // first scrollTo prepended [41-46]
-            // index[0] is now anchored to [41]
-            // second scrollTo prepended [35-40]
-            // prefetched [32-34]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
-                    50, 51, 52, 53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun consecutivePrependScroll_withoutPlaceholders_multiSnapshot() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // Without placeholders, first loaded page always starts at index[0]
-                scrollTo(-1)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // scrollTo prepended [44-46]
-            // prefetched [41-43]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
-            )
-
-            val snapshot2 = pager.asSnapshot {
-                // Without placeholders, first loaded page always starts at index[0]
-                scrollTo(-5)
-            }
-            // scrollTo prepended [35-40]
-            // prefetched [32-34]
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(
-                    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
-                    50, 51, 52, 53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun prependScroll_withoutPlaceholders_noPrefetchTriggered() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = Pager(
-            config = PagingConfig(
-                pageSize = 4,
-                initialLoadSize = 8,
-                enablePlaceholders = false,
-                // a small prefetchDistance to prevent prefetch until we scroll to boundary
-                prefetchDistance = 1
-            ),
-            initialKey = 50,
-            pagingSourceFactory = createFactory(dataFlow),
-        ).flow.cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // Without placeholders, first loaded page always starts at index[0]
-                scrollTo(0)
-            }
-            // initial load [50-57]
-            // no prefetch after initial load because it didn't hit prefetch distance
-            // scrollTo prepended [46-49]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
-            )
-        }
-    }
-
-    @Test
-    fun appendScroll() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(12)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // appended [8-13]
-            // prefetched [14-16]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
-            )
-        }
-    }
-
-    @Test
-    fun appendScroll_withDrops() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerWithDrops(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(12)
-            }
-            // dropped [0-7]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(8, 9, 10, 11, 12, 13, 14, 15, 16)
-            )
-        }
-    }
-
-    @Test
-    fun appendScroll_withSeparators() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow).map { pagingData ->
-            pagingData.insertSeparators { before: Int?, _ ->
-                if (before == 0 || before == 14) "sep" else null
-            }
-        }
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(12)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // appended [8-13]
-            // prefetched [14-16]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, "sep", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, "sep", 15, 16)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveAppendScroll() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(12)
-                scrollTo(18)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // appended [8-19]
-            // prefetched [20-22]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
-                21, 22)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveAppendScroll_multiSnapshots() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(12)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // appended [8-13]
-            // prefetched [14-16]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
-            )
-
-            val snapshot2 = pager.asSnapshot {
-                scrollTo(18)
-            }
-            // appended [17-19]
-            // prefetched [20-22]
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
-                    18, 19, 20, 21, 22
-                )
-            )
-        }
-    }
-
-    @Test
-    fun appendScroll_indexOutOfBounds() {
-        val dataFlow = flowOf(List(15) { it })
-        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // index out of bounds
-                scrollTo(50)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // scrollTo appended [8-10]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
-            )
-        }
-    }
-
-    @Test
-    fun appendScroll_accessPageBoundary() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // after initial Load and prefetch, max loaded index is 7
-                scrollTo(7)
-            }
-            // ensure that SnapshotLoader waited for last prefetch before returning
-            // initial load [0-4]
-            // prefetched [5-7] - expect only one extra page to be prefetched after this
-            // scrollTo appended [8-10]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
-            )
-        }
-    }
-
-    @Test
-    fun appendScroll_withoutPrefetch() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPrefetch(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(10)
-            }
-            // initial load [0-4]
-            // appended [5-10]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
-            )
-        }
-    }
-
-    @Test
-    fun appendScroll_withoutPlaceholders() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // scroll to max loaded index
-                scrollTo(7)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // scrollTo appended [8-10]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
-            )
-        }
-    }
-
-    @Test
-    fun appendScroll_withoutPlaceholders_indexOutOfBounds() {
-        val dataFlow = flowOf(List(20) { it })
-        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // 12 is larger than differ.size = 8 after initial refresh
-                scrollTo(12)
-            }
-            // ensure it honors scrollTo indices >= differ.size
-            // initial load [0-4]
-            // prefetched [5-7]
-            // scrollTo appended [8-13]
-            // prefetched [14-16]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
-            )
-        }
-    }
-
-    @Test
-    fun appendToScroll_withoutPlaceholders_indexOutOfBoundsIsCapped() {
-        val dataFlow = flowOf(List(20) { it })
-        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(50)
-            }
-            // ensure index is still capped to max index available
-            // initial load [0-4]
-            // prefetched [5-7]
-            // scrollTo appended [8-19]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveAppendScroll_withoutPlaceholders() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(12)
-                scrollTo(17)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // first scrollTo appended [8-13]
-            // second scrollTo appended [14-19]
-            // prefetched [19-22]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
-                21, 22)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveAppendScroll_withoutPlaceholders_multiSnapshot() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(12)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // scrollTo appended [8-13]
-            // prefetched [14-16]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
-            )
-
-            val snapshot2 = pager.asSnapshot {
-                scrollTo(17)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // first scrollTo appended [8-13]
-            // second scrollTo appended [14-19]
-            // prefetched [19-22]
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
-                    21, 22)
-            )
-        }
-    }
-
-    @Test
-    fun scrollTo_indexAccountsForSeparators() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow)
-        val pagerWithSeparator = pager.map { pagingData ->
-            pagingData.insertSeparators { before: Int?, _ ->
-                if (before == 6) "sep" else null
-            }
-        }
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(8)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // appended [8-10]
-            // prefetched [11-13]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
-            )
-
-            val snapshotWithSeparator = pagerWithSeparator.asSnapshot {
-                scrollTo(8)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // appended [8-10]
-            // no prefetch on [11-13] because separator fulfilled prefetchDistance
-            assertThat(snapshotWithSeparator).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, "sep", 7, 8, 9, 10)
-            )
-        }
-    }
-
-    @Test
-    fun prependFling() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(42)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // prepended [41-46]
-            // prefetched [38-40]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun prependFling_withDrops() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerWithDrops(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(42)
-            }
-            // dropped [47-57]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(38, 39, 40, 41, 42, 43, 44, 45, 46)
-            )
-        }
-    }
-
-    @Test
-    fun prependFling_withSeparators() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow, 50).map { pagingData ->
-            pagingData.insertSeparators { before: Int?, _ ->
-                if (before == 42 || before == 49) "sep" else null
-            }
-        }
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(42)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // prepended [41-46]
-            // prefetched [38-40]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    38, 39, 40, 41, 42, "sep", 43, 44, 45, 46, 47, 48, 49, "sep", 50, 51, 52,
-                    53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun consecutivePrependFling() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(42)
-                flingTo(38)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // prepended [38-46]
-            // prefetched [35-37]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
-                    51, 52, 53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun consecutivePrependFling_multiSnapshots() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(42)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // prepended [41-46]
-            // prefetched [38-40]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
-                )
-            )
-
-            val snapshot2 = pager.asSnapshot {
-                flingTo(38)
-            }
-            // prefetched [35-37]
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(
-                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
-                    51, 52, 53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-    @Test
-    fun prependFling_jump() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerWithJump(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(30)
-                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
-            }
-            // initial load [28-32]
-            // prefetched [25-27], [33-35]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35)
-            )
-        }
-    }
-
-    @Test
-    fun prependFling_scrollThenJump() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerWithJump(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(43)
-                flingTo(30)
-                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
-            }
-            // initial load [28-32]
-            // prefetched [25-27], [33-35]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35)
-            )
-        }
-    }
-
-    @Test
-    fun prependFling_jumpThenFling() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerWithJump(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(30)
-                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
-                flingTo(22)
-            }
-            // initial load [28-32]
-            // prefetched [25-27], [33-35]
-            // flingTo prepended [22-24]
-            // prefetched [19-21]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35)
-            )
-        }
-    }
-
-    @Test
-    fun prependFling_indexOutOfBounds() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow, 10)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(-3)
-            }
-            // initial load [10-14]
-            // prefetched [7-9], [15-17]
-            // flingTo prepended [0-6]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
-            )
-        }
-    }
-
-    @Test
-    fun prependFling_accessPageBoundary() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // page boundary
-                flingTo(44)
-            }
-            // ensure that SnapshotLoader waited for last prefetch before returning
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // prepended [44-46] - expect only one extra page to be prefetched after this
-            // prefetched [41-43]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
-            )
-        }
-    }
-
-    @Test
-    fun prependFling_withoutPlaceholders() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // Without placeholders, first loaded page always starts at index[0]
-                flingTo(0)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // prepended [44-46]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
-            )
-        }
-    }
-
-    @Test
-    fun prependFling_withoutPlaceholders_indexOutOfBounds() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow, 50)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(-8)
-            }
-            // ensure we honor negative indices if there is data to load
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // prepended [38-46]
-            // prefetched [35-37]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53,
-                    54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun prependFling_withoutPlaceholders_indexOutOfBoundsIsCapped() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow, 5).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(-20)
-            }
-            // ensure index is capped when no more data to load
-            // initial load [5-9]
-            // prefetched [2-4], [10-12]
-            // flingTo prepended [0-1]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
-            )
-        }
-    }
-
-    @Test
-    fun consecutivePrependFling_withoutPlaceholders() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // Without placeholders, first loaded page always starts at index[0]
-                flingTo(-1)
-                // Without placeholders, first loaded page always starts at index[0]
-                flingTo(-5)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // first flingTo prepended [41-46]
-            // index[0] is now anchored to [41]
-            // second flingTo prepended [35-40]
-            // prefetched [32-34]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(
-                    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
-                    50, 51, 52, 53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun consecutivePrependFling_withoutPlaceholders_multiSnapshot() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // Without placeholders, first loaded page always starts at index[0]
-                flingTo(-1)
-            }
-            // initial load [50-54]
-            // prefetched [47-49], [55-57]
-            // flingTo prepended [44-46]
-            // prefetched [41-43]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
-            )
-
-            val snapshot2 = pager.asSnapshot {
-                // Without placeholders, first loaded page always starts at index[0]
-                flingTo(-5)
-            }
-            // flingTo prepended [35-40]
-            // prefetched [32-34]
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(
-                    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
-                    50, 51, 52, 53, 54, 55, 56, 57
-                )
-            )
-        }
-    }
-
-    @Test
-    fun prependFling_withoutPlaceholders_indexPrecision() {
-        val dataFlow = flowOf(List(100) { it })
-        // load sizes and prefetch set to 1 to test precision of flingTo indexing
-        val pager = Pager(
-            config = PagingConfig(
-                pageSize = 1,
-                initialLoadSize = 1,
-                enablePlaceholders = false,
-                prefetchDistance = 1
-            ),
-            initialKey = 50,
-            pagingSourceFactory = createFactory(dataFlow),
-        )
-        testScope.runTest {
-            val snapshot = pager.flow.asSnapshot {
-                // after refresh, lastAccessedIndex == index[2] == item(9)
-                flingTo(-1)
-            }
-            // initial load [50]
-            // prefetched [49], [51]
-            // prepended [48]
-            // prefetched [47]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(47, 48, 49, 50, 51)
-            )
-        }
-    }
-
-    @Test
-    fun appendFling() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(12)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // appended [8-13]
-            // prefetched [14-16]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
-            )
-        }
-    }
-
-    @Test
-    fun appendFling_withDrops() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerWithDrops(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(12)
-            }
-            // dropped [0-7]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(8, 9, 10, 11, 12, 13, 14, 15, 16)
-            )
-        }
-    }
-
-    @Test
-    fun appendFling_withSeparators() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow).map { pagingData ->
-            pagingData.insertSeparators { before: Int?, _ ->
-                if (before == 0 || before == 14) "sep" else null
-            }
-        }
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(12)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // appended [8-13]
-            // prefetched [14-16]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, "sep", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, "sep", 15, 16)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveAppendFling() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(12)
-                flingTo(18)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // appended [8-19]
-            // prefetched [20-22]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
-                    21, 22)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveAppendFling_multiSnapshots() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(12)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // appended [8-13]
-            // prefetched [14-16]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
-            )
-
-            val snapshot2 = pager.asSnapshot {
-                flingTo(18)
-            }
-            // appended [17-19]
-            // prefetched [20-22]
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
-                    18, 19, 20, 21, 22
-                )
-            )
-        }
-    }
-
-    @Test
-    fun appendFling_jump() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerWithJump(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(30)
-                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
-            }
-            // initial load [28-32]
-            // prefetched [25-27], [33-35]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35)
-            )
-        }
-    }
-
-    @Test
-    fun appendFling_scrollThenJump() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerWithJump(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                scrollTo(30)
-                flingTo(43)
-                // jump triggered when flingTo registered lastAccessedIndex[43], refreshKey[41]
-            }
-            // initial load [41-45]
-            // prefetched [38-40], [46-48]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48)
-            )
-        }
-    }
-
-    @Test
-    fun appendFling_jumpThenFling() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerWithJump(dataFlow)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(30)
-                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
-                flingTo(38)
-            }
-            // initial load [28-32]
-            // prefetched [25-27], [33-35]
-            // flingTo appended [36-38]
-            // prefetched [39-41]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41)
-            )
-        }
-    }
-
-    @Test
-    fun appendFling_indexOutOfBounds() {
-        val dataFlow = flowOf(List(15) { it })
-        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // index out of bounds
-                flingTo(50)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // flingTo appended [8-10]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
-            )
-        }
-    }
-
-    @Test
-    fun appendFling_accessPageBoundary() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // after initial Load and prefetch, max loaded index is 7
-                flingTo(7)
-            }
-            // ensure that SnapshotLoader waited for last prefetch before returning
-            // initial load [0-4]
-            // prefetched [5-7] - expect only one extra page to be prefetched after this
-            // flingTo appended [8-10]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
-            )
-        }
-    }
-
-    @Test
-    fun appendFling_withoutPlaceholders() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // scroll to max loaded index
-                flingTo(7)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // flingTo appended [8-10]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
-            )
-        }
-    }
-
-    @Test
-    fun appendFling_withoutPlaceholders_indexOutOfBounds() {
-        val dataFlow = flowOf(List(20) { it })
-        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                // 12 is larger than differ.size = 8 after initial refresh
-                flingTo(12)
-            }
-            // ensure it honors scrollTo indices >= differ.size
-            // initial load [0-4]
-            // prefetched [5-7]
-            // flingTo appended [8-13]
-            // prefetched [14-16]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
-            )
-        }
-    }
-
-    @Test
-    fun appendFling_withoutPlaceholders_indexOutOfBoundsIsCapped() {
-        val dataFlow = flowOf(List(20) { it })
-        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(50)
-            }
-            // ensure index is still capped to max index available
-            // initial load [0-4]
-            // prefetched [5-7]
-            // flingTo appended [8-19]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveAppendFling_withoutPlaceholders() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(12)
-                flingTo(17)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // first flingTo appended [8-13]
-            // second flingTo appended [14-19]
-            // prefetched [19-22]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
-                    21, 22)
-            )
-        }
-    }
-
-    @Test
-    fun consecutiveAppendFling_withoutPlaceholders_multiSnapshot() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(12)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // flingTo appended [8-13]
-            // prefetched [14-16]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
-            )
-
-            val snapshot2 = pager.asSnapshot {
-                flingTo(17)
-            }
-            // initial load [0-4]
-            // prefetched [5-7]
-            // first flingTo appended [8-13]
-            // second flingTo appended [14-19]
-            // prefetched [19-22]
-            assertThat(snapshot2).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
-                    21, 22)
-            )
-        }
-    }
-
-    @Test
-    fun appendFling_withoutPlaceholders_indexPrecision() {
-        val dataFlow = flowOf(List(100) { it })
-        // load sizes and prefetch set to 1 to test precision of flingTo indexing
-        val pager = Pager(
-            config = PagingConfig(
-                pageSize = 1,
-                initialLoadSize = 1,
-                enablePlaceholders = false,
-                prefetchDistance = 1
-            ),
-            pagingSourceFactory = createFactory(dataFlow),
-        )
-        testScope.runTest {
-            val snapshot = pager.flow.asSnapshot {
-                // after refresh, lastAccessedIndex == index[2] == item(9)
-                flingTo(2)
-            }
-            // initial load [0]
-            // prefetched [1]
-            // appended [2]
-            // prefetched [3]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3)
-            )
-        }
-    }
-
-    @Test
-    fun flingTo_indexAccountsForSeparators() {
-        val dataFlow = flowOf(List(100) { it })
-        val pager = createPager(
-            dataFlow,
-            PagingConfig(
-                pageSize = 1,
-                initialLoadSize = 1,
-                prefetchDistance = 1
-            ),
-            50
-        )
-        val pagerWithSeparator = pager.map { pagingData ->
-            pagingData.insertSeparators { before: Int?, _ ->
-                if (before == 49) "sep" else null
-            }
-        }
-        testScope.runTest {
-            val snapshot = pager.asSnapshot {
-                flingTo(51)
-            }
-            // initial load [50]
-            // prefetched [49], [51]
-            // flingTo [51] accessed item[51]prefetched [52]
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(49, 50, 51, 52)
-            )
-
-            val snapshotWithSeparator = pagerWithSeparator.asSnapshot {
-                flingTo(51)
-            }
-            // initial load [50]
-            // prefetched [49], [51]
-            // flingTo [51] accessed item[50], no prefetch triggered
-            assertThat(snapshotWithSeparator).containsExactlyElementsIn(
-                listOf(49, "sep", 50, 51)
-            )
-        }
-    }
-
-    @Test
-    fun errorHandler_throw() {
-        val dataFlow = flowOf(List(30) { it })
-        val factory = createFactory(dataFlow)
-        val pagingSources = mutableListOf<TestPagingSource>()
-        val pager = Pager(
-            config = PagingConfig(pageSize = 3, initialLoadSize = 5),
-            pagingSourceFactory = {
-                factory.invoke().also { pagingSources.add(it as TestPagingSource) }
-            },
-        ).flow
-        testScope.runTest {
-            val error = assertFailsWith(IllegalArgumentException::class) {
-                pager.asSnapshot(onError = { ErrorRecovery.THROW }) {
-                    val source = pagingSources.first()
-                    source.errorOnNextLoad = true
-                    scrollTo(12)
-                }
-            }
-            assertThat(error.message).isEqualTo("PagingSource load error")
-        }
-    }
-
-    @Test
-    fun errorHandler_retry() {
-        val dataFlow = flowOf(List(30) { it })
-        val factory = createFactory(dataFlow)
-        val pagingSources = mutableListOf<TestPagingSource>()
-        val pager = Pager(
-            config = PagingConfig(pageSize = 3, initialLoadSize = 5),
-            pagingSourceFactory = {
-                factory.invoke().also { pagingSources.add(it as TestPagingSource) }
-            },
-        ).flow
-        testScope.runTest {
-            val snapshot = pager.asSnapshot(onError = { ErrorRecovery.RETRY }) {
-                val source = pagingSources.first()
-                // should have two loads to far - refresh and append(prefetch)
-                assertThat(source.loads.size).isEqualTo(2)
-
-                // throw error on next load, should trigger a retry
-                source.errorOnNextLoad = true
-                scrollTo(7)
-
-                // make sure it did retry
-                assertThat(source.loads.size).isEqualTo(4)
-                // failed load
-                val failedLoad = source.loads[2]
-                assertThat(failedLoad is LoadParams.Append).isTrue()
-                assertThat(failedLoad.key).isEqualTo(8)
-                // retry load
-                val retryLoad = source.loads[3]
-                assertThat(retryLoad is LoadParams.Append).isTrue()
-                assertThat(retryLoad.key).isEqualTo(8)
-            }
-            // retry success
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
-            )
-        }
-    }
-
-    @Test
-    fun errorHandler_retryFails() {
-        val dataFlow = flowOf(List(30) { it })
-        val factory = createFactory(dataFlow)
-        val pagingSources = mutableListOf<TestPagingSource>()
-        val pager = Pager(
-            config = PagingConfig(pageSize = 3, initialLoadSize = 5),
-            pagingSourceFactory = {
-                factory.invoke().also { pagingSources.add(it as TestPagingSource) }
-            },
-        ).flow
-        var retryCount = 0
-        testScope.runTest {
-            val snapshot = pager.asSnapshot(
-                onError = {
-                    // retry twice
-                    if (retryCount < 2) {
-                        retryCount++
-                        ErrorRecovery.RETRY
-                    } else {
-                        ErrorRecovery.RETURN_CURRENT_SNAPSHOT
-                    }
-                }
-            ) {
-                val source = pagingSources.first()
-                // should have two loads to far - refresh and append(prefetch)
-                assertThat(source.loads.size).isEqualTo(2)
-
-                source.errorOnLoads = true
-                scrollTo(8)
-
-                // additional failed load + two retries
-                assertThat(source.loads.size).isEqualTo(5)
-            }
-            // retry failed, returned existing snapshot
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7)
-            )
-        }
-    }
-
-    @Test
-    fun errorHandler_returnSnapshot() {
-        val dataFlow = flowOf(List(30) { it })
-        val factory = createFactory(dataFlow)
-        val pagingSources = mutableListOf<TestPagingSource>()
-        val pager = Pager(
-            config = PagingConfig(pageSize = 3, initialLoadSize = 5),
-            pagingSourceFactory = {
-                factory.invoke().also { pagingSources.add(it as TestPagingSource) }
-            },
-        ).flow
-        testScope.runTest {
-            val snapshot = pager.asSnapshot(onError = { ErrorRecovery.RETURN_CURRENT_SNAPSHOT }) {
-                val source = pagingSources.first()
-                source.errorOnNextLoad = true
-                scrollTo(12)
-            }
-            // snapshot items before scrollTo
-            assertThat(snapshot).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4, 5, 6, 7)
-            )
-        }
-    }
-
-    private fun createPager(dataFlow: Flow<List<Int>>, initialKey: Int = 0) =
-        createPager(
-            dataFlow,
-            PagingConfig(pageSize = 3, initialLoadSize = 5),
-            initialKey
-        )
-
-    private fun createPager(data: List<Int>, initialKey: Int = 0) =
-        Pager(
-            PagingConfig(pageSize = 3, initialLoadSize = 5),
-            initialKey,
-            createSingleGenFactory(data),
-        ).flow
-
-    private fun createPagerNoPlaceholders(dataFlow: Flow<List<Int>>, initialKey: Int = 0) =
-        createPager(
-            dataFlow,
-            PagingConfig(
-                pageSize = 3,
-                initialLoadSize = 5,
-                enablePlaceholders = false,
-                prefetchDistance = 3
-            ),
-            initialKey)
-
-    private fun createPagerNoPrefetch(dataFlow: Flow<List<Int>>, initialKey: Int = 0) =
-        createPager(
-            dataFlow,
-            PagingConfig(pageSize = 3, initialLoadSize = 5, prefetchDistance = 0),
-            initialKey
-        )
-
-    private fun createPagerWithJump(dataFlow: Flow<List<Int>>, initialKey: Int = 0) =
-        createPager(
-            dataFlow,
-            PagingConfig(pageSize = 3, initialLoadSize = 5, jumpThreshold = 5),
-            initialKey
-        )
-
-    private fun createPagerWithDrops(dataFlow: Flow<List<Int>>, initialKey: Int = 0) =
-        createPager(
-            dataFlow,
-            PagingConfig(pageSize = 3, initialLoadSize = 5, maxSize = 9),
-            initialKey
-        )
-
-    private fun createPager(
-        dataFlow: Flow<List<Int>>,
-        config: PagingConfig,
-        initialKey: Int = 0,
-    ) = Pager(
-            config = config,
-            initialKey = initialKey,
-            pagingSourceFactory = createFactory(dataFlow),
-        ).flow
-}
-
-private class WrappedPagingSourceFactory(
-    private val factory: PagingSourceFactory<Int, Int>,
-    private val loadDelay: Long,
-) : PagingSourceFactory<Int, Int> {
-    override fun invoke(): PagingSource<Int, Int> = TestPagingSource(factory(), loadDelay)
-}
-
-private class TestPagingSource(
-    private val originalSource: PagingSource<Int, Int>,
-    private val loadDelay: Long,
-) : PagingSource<Int, Int>() {
-
-    var errorOnNextLoad = false
-    var errorOnLoads = false
-    private val _loads = mutableListOf<LoadParams<Int>>()
-    val loads: List<LoadParams<Int>>
-        get() = _loads.toList()
-
-    init {
-        originalSource.registerInvalidatedCallback {
-            invalidate()
-        }
-    }
-    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Int> {
-        delay(loadDelay)
-        _loads.add(params)
-        if (errorOnNextLoad) {
-            errorOnNextLoad = false
-            return LoadResult.Error(IllegalArgumentException("PagingSource load error"))
-        }
-        if (errorOnLoads) {
-            return LoadResult.Error(IllegalArgumentException("PagingSource load error"))
-        }
-        return originalSource.load(params)
-    }
-
-    override fun getRefreshKey(state: PagingState<Int, Int>) = originalSource.getRefreshKey(state)
-
-    override val jumpingSupported: Boolean = originalSource.jumpingSupported
-}
diff --git a/paging/paging-testing/src/androidTest/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt b/paging/paging-testing/src/androidTest/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
deleted file mode 100644
index b4cc8d1..0000000
--- a/paging/paging-testing/src/androidTest/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.paging.testing
-
-import androidx.paging.PagingConfig
-import androidx.paging.PagingSource.LoadResult.Page
-import androidx.paging.PagingSourceFactory
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.advanceTimeBy
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@RunWith(JUnit4::class)
-class StaticListPagingSourceFactoryTest {
-
-    private val testScope = TestScope(UnconfinedTestDispatcher())
-    private val CONFIG = PagingConfig(
-        pageSize = 3,
-        initialLoadSize = 5
-    )
-
-    @Test
-    fun emptyFlow() {
-        val factory: PagingSourceFactory<Int, Int> =
-            flowOf<List<Int>>().asPagingSourceFactory(testScope)
-        val pagingSource = factory()
-        val pager = TestPager(CONFIG, pagingSource)
-
-        runTest {
-            val result = pager.refresh() as Page
-            assertThat(result.data.isEmpty()).isTrue()
-        }
-    }
-
-    @Test
-    fun simpleCollect_singleGen() {
-        val flow = flowOf(
-            List(20) { it }
-        )
-
-        val factory: PagingSourceFactory<Int, Int> =
-            flow.asPagingSourceFactory(testScope)
-        val pagingSource = factory()
-        val pager = TestPager(CONFIG, pagingSource)
-
-        runTest {
-            val result = pager.refresh() as Page
-            assertThat(result.data).containsExactlyElementsIn(
-                listOf(0, 1, 2, 3, 4)
-            )
-        }
-    }
-
-    @Test
-    fun simpleCollect_multiGeneration() = testScope.runTest {
-        val flow = flow {
-            emit(List(20) { it }) // first gen
-            delay(1500)
-            emit(List(15) { it + 30 }) // second gen
-        }
-
-        val factory: PagingSourceFactory<Int, Int> =
-            flow.asPagingSourceFactory(testScope)
-
-        advanceTimeBy(1000)
-
-        // first gen
-        val pagingSource1 = factory()
-        val pager1 = TestPager(CONFIG, pagingSource1)
-        val result1 = pager1.refresh() as Page
-        assertThat(result1.data).containsExactlyElementsIn(
-            listOf(0, 1, 2, 3, 4)
-        )
-
-        // second list emits -- this should invalidate original pagingSource and trigger new gen
-        advanceUntilIdle()
-
-        assertThat(pagingSource1.invalid).isTrue()
-
-        // second gen
-        val pagingSource2 = factory()
-        val pager2 = TestPager(CONFIG, pagingSource2)
-        val result2 = pager2.refresh() as Page
-        assertThat(result2.data).containsExactlyElementsIn(
-            listOf(30, 31, 32, 33, 34)
-        )
-    }
-
-    @Test
-    fun collection_cancellation() = testScope.runTest {
-        val mutableFlow = MutableSharedFlow<List<Int>>()
-        val collectionScope = this.backgroundScope
-
-        val factory: PagingSourceFactory<Int, Int> =
-            mutableFlow.asPagingSourceFactory(collectionScope)
-
-        mutableFlow.emit(List(10) { it })
-
-        advanceUntilIdle()
-
-        val pagingSource = factory()
-        val pager = TestPager(CONFIG, pagingSource)
-        val result = pager.refresh() as Page
-        assertThat(result.data).containsExactlyElementsIn(
-            listOf(0, 1, 2, 3, 4)
-        )
-
-        // cancel collection scope inside the pagingSourceFactory
-        collectionScope.cancel()
-
-        mutableFlow.emit(List(10) { it })
-
-        advanceUntilIdle()
-
-        // new list should not be collected, meaning the previous generation should still be valid
-        assertThat(pagingSource.invalid).isFalse()
-    }
-
-    @Test
-    fun multipleFactories_fromSameFlow() = testScope.runTest {
-        val mutableFlow = MutableSharedFlow<List<Int>>()
-
-        val factory1: PagingSourceFactory<Int, Int> =
-            mutableFlow.asPagingSourceFactory(testScope.backgroundScope)
-
-        val factory2: PagingSourceFactory<Int, Int> =
-            mutableFlow.asPagingSourceFactory(testScope.backgroundScope)
-
-        mutableFlow.emit(List(10) { it })
-
-        advanceUntilIdle()
-
-        // factory 1 first gen
-        val pagingSource = factory1()
-        val pager = TestPager(CONFIG, pagingSource)
-        val result = pager.refresh() as Page
-        assertThat(result.data).containsExactlyElementsIn(
-            listOf(0, 1, 2, 3, 4)
-        )
-
-        // factory 2 first gen
-        val pagingSource2 = factory2()
-        val pager2 = TestPager(CONFIG, pagingSource2)
-        val result2 = pager2.refresh() as Page
-        assertThat(result2.data).containsExactlyElementsIn(
-            listOf(0, 1, 2, 3, 4)
-        )
-
-        // trigger second generation
-        mutableFlow.emit(List(10) { it + 30 })
-
-        advanceUntilIdle()
-
-        assertThat(pagingSource.invalid).isTrue()
-        assertThat(pagingSource2.invalid).isTrue()
-
-        // factory 1 second gen
-        val pagingSource3 = factory1()
-        val pager3 = TestPager(CONFIG, pagingSource3)
-        val result3 = pager3.refresh() as Page
-        assertThat(result3.data).containsExactlyElementsIn(
-            listOf(30, 31, 32, 33, 34)
-        )
-
-        // factory 2 second gen
-        val pagingSource4 = factory2()
-        val pager4 = TestPager(CONFIG, pagingSource4)
-        val result4 = pager4.refresh() as Page
-        assertThat(result4.data).containsExactlyElementsIn(
-            listOf(30, 31, 32, 33, 34)
-        )
-    }
-
-    @Test
-    fun singleListFactory_refresh() = testScope.runTest {
-        val data = List(20) { it }
-        val factory = data.asPagingSourceFactory()
-
-        val pagingSource1 = factory()
-        val pager1 = TestPager(CONFIG, pagingSource1)
-        val refresh1 = pager1.refresh() as Page
-        assertThat(refresh1.data).containsExactlyElementsIn(
-            listOf(0, 1, 2, 3, 4)
-        )
-
-        val pagingSource2 = factory()
-        val pager2 = TestPager(CONFIG, pagingSource2)
-        val refresh2 = pager2.refresh() as Page
-        assertThat(refresh2.data).containsExactlyElementsIn(
-            listOf(0, 1, 2, 3, 4)
-        )
-    }
-
-    @Test
-    fun singleListFactory_empty() = testScope.runTest {
-        val data = emptyList<Int>()
-        val factory = data.asPagingSourceFactory()
-
-        val pagingSource1 = factory()
-        val pager1 = TestPager(CONFIG, pagingSource1)
-        val refresh1 = pager1.refresh() as Page
-        assertThat(refresh1.data).isEmpty()
-
-        val pagingSource2 = factory()
-        val pager2 = TestPager(CONFIG, pagingSource2)
-        val refresh2 = pager2.refresh() as Page
-        assertThat(refresh2.data).isEmpty()
-    }
-
-    @Test
-    fun singleListFactory_append() = testScope.runTest {
-        val data = List(20) { it }
-        val factory = data.asPagingSourceFactory()
-        val pagingSource1 = factory()
-        val pager1 = TestPager(CONFIG, pagingSource1)
-
-        pager1.refresh() as Page
-        pager1.append()
-        assertThat(pager1.getPages().flatMap { it.data }).containsExactlyElementsIn(
-            listOf(0, 1, 2, 3, 4, 5, 6, 7)
-        )
-
-        pager1.append()
-        assertThat(pager1.getPages().flatMap { it.data }).containsExactlyElementsIn(
-            listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
-        )
-    }
-
-    @Test
-    fun singleListFactory_prepend() = testScope.runTest {
-        val data = List(20) { it }
-        val factory = data.asPagingSourceFactory()
-        val pagingSource1 = factory()
-        val pager1 = TestPager(CONFIG, pagingSource1)
-
-        pager1.refresh(initialKey = 10) as Page
-        pager1.prepend()
-        assertThat(pager1.getPages().flatMap { it.data }).containsExactlyElementsIn(
-            listOf(7, 8, 9, 10, 11, 12, 13, 14)
-        )
-
-        pager1.prepend()
-        assertThat(pager1.getPages().flatMap { it.data }).containsExactlyElementsIn(
-            listOf(4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
-        )
-    }
-}
diff --git a/paging/paging-testing/src/androidTest/kotlin/androidx/paging/testing/StaticListPagingSourceTest.kt b/paging/paging-testing/src/androidTest/kotlin/androidx/paging/testing/StaticListPagingSourceTest.kt
deleted file mode 100644
index dd4197f..0000000
--- a/paging/paging-testing/src/androidTest/kotlin/androidx/paging/testing/StaticListPagingSourceTest.kt
+++ /dev/null
@@ -1,297 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.paging.testing
-
-import androidx.paging.PagingConfig
-import androidx.paging.PagingSource
-import androidx.paging.PagingSource.LoadResult
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class StaticListPagingSourceTest {
-
-    private val DATA = List(100) { it }
-    private val CONFIG = PagingConfig(
-        pageSize = 3,
-        initialLoadSize = 5,
-    )
-
-    @Test
-    fun refresh() = runPagingSourceTest { _, pager ->
-        val result = pager.refresh() as LoadResult.Page
-        assertThat(result).isEqualTo(
-            LoadResult.Page(
-                data = listOf(0, 1, 2, 3, 4),
-                prevKey = null,
-                nextKey = 5,
-                itemsBefore = 0,
-                itemsAfter = 95
-            )
-        )
-    }
-
-    @Test
-    fun refresh_withEmptyData() = runPagingSourceTest(StaticListPagingSource(emptyList())) {
-            _, pager ->
-
-        val result = pager.refresh() as LoadResult.Page
-        assertThat(result).isEqualTo(
-            LoadResult.Page(
-                data = emptyList(),
-                prevKey = null,
-                nextKey = null,
-                itemsBefore = 0,
-                itemsAfter = 0
-            )
-        )
-    }
-
-    @Test
-    fun refresh_initialKey() = runPagingSourceTest { _, pager ->
-        val result = pager.refresh(initialKey = 20) as LoadResult.Page
-        assertThat(result).isEqualTo(
-            LoadResult.Page(
-                data = listOf(20, 21, 22, 23, 24),
-                prevKey = 19,
-                nextKey = 25,
-                itemsBefore = 20,
-                itemsAfter = 75
-            )
-        )
-    }
-
-    @Test
-    fun refresh_initialKey_withEmptyData() = runPagingSourceTest(
-        StaticListPagingSource(emptyList())
-    ) { _, pager ->
-
-        val result = pager.refresh(initialKey = 20) as LoadResult.Page
-        assertThat(result).isEqualTo(
-            LoadResult.Page(
-                data = emptyList(),
-                prevKey = null,
-                nextKey = null,
-                itemsBefore = 0,
-                itemsAfter = 0
-            )
-        )
-    }
-
-    @Test
-    fun refresh_negativeKeyClippedToZero() = runPagingSourceTest { _, pager ->
-        val result = pager.refresh(initialKey = -1) as LoadResult.Page
-        // loads first page
-        assertThat(result).isEqualTo(
-            listOf(0, 1, 2, 3, 4).asPage()
-        )
-    }
-
-    @Test
-    fun refresh_KeyLargerThanDataSize_loadsLastPage() = runPagingSourceTest { _, pager ->
-        val result = pager.refresh(initialKey = 140) as LoadResult.Page
-        // loads last page
-        assertThat(result).isEqualTo(
-            listOf(95, 96, 97, 98, 99).asPage()
-        )
-    }
-
-    @Test
-    fun append() = runPagingSourceTest { _, pager ->
-        pager.run {
-            refresh()
-            append()
-        }
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                LoadResult.Page(
-                    data = listOf(0, 1, 2, 3, 4),
-                    prevKey = null,
-                    nextKey = 5,
-                    itemsBefore = 0,
-                    itemsAfter = 95
-                ),
-                LoadResult.Page(
-                    data = listOf(5, 6, 7),
-                    prevKey = 4,
-                    nextKey = 8,
-                    itemsBefore = 5,
-                    itemsAfter = 92
-                )
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun append_consecutively() = runPagingSourceTest { _, pager ->
-        pager.run {
-            refresh()
-            append()
-            append()
-            append()
-        }
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                listOf(0, 1, 2, 3, 4).asPage(),
-                listOf(5, 6, 7).asPage(),
-                listOf(8, 9, 10).asPage(),
-                listOf(11, 12, 13).asPage(),
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun append_loadSizeLargerThanAvailableData() = runPagingSourceTest { _, pager ->
-        val result = pager.run {
-            refresh(initialKey = 94)
-            append() as LoadResult.Page
-        }
-        assertThat(result).isEqualTo(
-            LoadResult.Page(
-                data = listOf(99),
-                prevKey = 98,
-                nextKey = null,
-                itemsBefore = 99,
-                itemsAfter = 0
-            )
-        )
-    }
-
-    @Test
-    fun prepend() = runPagingSourceTest { _, pager ->
-        pager.run {
-            refresh(20)
-            prepend()
-        }
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                LoadResult.Page(
-                    data = listOf(17, 18, 19),
-                    prevKey = 16,
-                    nextKey = 20,
-                    itemsBefore = 17,
-                    itemsAfter = 80
-                ),
-                LoadResult.Page(
-                    data = listOf(20, 21, 22, 23, 24),
-                    prevKey = 19,
-                    nextKey = 25,
-                    itemsBefore = 20,
-                    itemsAfter = 75
-                ),
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun prepend_consecutively() = runPagingSourceTest { _, pager ->
-        pager.run {
-            refresh(initialKey = 50)
-            prepend()
-            prepend()
-            prepend()
-        }
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                listOf(41, 42, 43).asPage(),
-                listOf(44, 45, 46).asPage(),
-                listOf(47, 48, 49).asPage(),
-                listOf(50, 51, 52, 53, 54).asPage(),
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun prepend_loadSizeLargerThanAvailableData() = runPagingSourceTest { _, pager ->
-        val result = pager.run {
-            refresh(initialKey = 2)
-            prepend()
-        }
-        assertThat(result).isEqualTo(
-            LoadResult.Page(
-                data = listOf(0, 1),
-                prevKey = null,
-                nextKey = 2,
-                itemsBefore = 0,
-                itemsAfter = 98
-            )
-        )
-    }
-
-    @Test
-    fun jump_enabled() {
-        val source = StaticListPagingSource(DATA)
-        assertThat(source.jumpingSupported).isTrue()
-    }
-
-    @Test
-    fun refreshKey() = runPagingSourceTest { pagingSource, pager ->
-        val state = pager.run {
-            refresh() // [0, 1, 2, 3, 4]
-            append() // [5, 6, 7]
-            // the anchorPos should be 7
-            getPagingState(anchorPosition = 7)
-        }
-
-        val refreshKey = pagingSource.getRefreshKey(state)
-        val expected = 7 - (CONFIG.initialLoadSize / 2)
-        assertThat(expected).isEqualTo(5)
-        assertThat(refreshKey).isEqualTo(expected)
-    }
-
-    @Test
-    fun refreshKey_negativeKeyClippedToZero() = runPagingSourceTest { pagingSource, pager ->
-        val state = pager.run {
-            refresh(2) // [2, 3, 4, 5, 6]
-            prepend() // [0, 1]
-            getPagingState(anchorPosition = 1)
-        }
-        // before clipping, refreshKey = 1 - (CONFIG.initialLoadSize / 2) = -1
-        val refreshKey = pagingSource.getRefreshKey(state)
-        assertThat(refreshKey).isEqualTo(0)
-    }
-
-    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
-    private fun runPagingSourceTest(
-        source: PagingSource<Int, Int> = StaticListPagingSource(DATA),
-        pager: TestPager<Int, Int> = TestPager(CONFIG, source),
-        block: suspend (pagingSource: PagingSource<Int, Int>, pager: TestPager<Int, Int>) -> Unit
-    ) {
-        runTest {
-            block(source, pager)
-        }
-    }
-
-    private fun List<Int>.asPage(): LoadResult.Page<Int, Int> {
-        val indexStart = firstOrNull()
-        val indexEnd = lastOrNull()
-        return LoadResult.Page(
-            data = this,
-            prevKey = indexStart?.let {
-                if (indexStart <= 0 || isEmpty()) null else indexStart - 1
-            },
-            nextKey = indexEnd?.let {
-                if (indexEnd >= DATA.lastIndex || isEmpty()) null else indexEnd + 1
-            },
-            itemsBefore = indexStart ?: -1,
-            itemsAfter = if (indexEnd == null) -1 else DATA.lastIndex - indexEnd
-        )
-    }
-}
diff --git a/paging/paging-testing/src/androidTest/kotlin/androidx/paging/testing/TestPagerTest.kt b/paging/paging-testing/src/androidTest/kotlin/androidx/paging/testing/TestPagerTest.kt
deleted file mode 100644
index 477450e..0000000
--- a/paging/paging-testing/src/androidTest/kotlin/androidx/paging/testing/TestPagerTest.kt
+++ /dev/null
@@ -1,973 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.paging.testing
-
-import androidx.paging.PagingConfig
-import androidx.paging.PagingSource.LoadResult
-import androidx.paging.PagingState
-import androidx.paging.TestPagingSource
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.assertFailsWith
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@RunWith(JUnit4::class)
-class TestPagerTest {
-
-    @Test
-    fun refresh_nullKey() {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        runTest {
-            val result = pager.refresh(null) as LoadResult.Page
-
-            assertThat(result.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
-        }
-    }
-
-    @Test
-    fun refresh_withInitialKey() {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        runTest {
-            val result = pager.refresh(50) as LoadResult.Page
-
-            assertThat(result.data).containsExactlyElementsIn(listOf(50, 51, 52, 53, 54)).inOrder()
-        }
-    }
-
-    @Test
-    fun refresh_returnError() {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        runTest {
-            source.errorNextLoad = true
-            val result = pager.refresh()
-            assertTrue(result is LoadResult.Error)
-
-            val page = pager.getLastLoadedPage()
-            assertThat(page).isNull()
-        }
-    }
-
-    @Test
-    fun refresh_returnInvalid() {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        runTest {
-            source.nextLoadResult = LoadResult.Invalid()
-            val result = pager.refresh()
-            assertTrue(result is LoadResult.Invalid)
-
-            val page = pager.getLastLoadedPage()
-            assertThat(page).isNull()
-        }
-    }
-
-    @Test
-    fun refresh_invalidPagingSource() {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        runTest {
-            source.invalidate()
-            assertTrue(source.invalid)
-            // simulate a PagingSource that returns LoadResult.Invalid when it's invalidated
-            source.nextLoadResult = LoadResult.Invalid()
-
-            assertThat(pager.refresh()).isInstanceOf(LoadResult.Invalid::class.java)
-        }
-    }
-
-    @Test
-    fun refresh_getLastLoadedPage() {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        runTest {
-            val page: LoadResult.Page<Int, Int>? = pager.run {
-                refresh()
-                getLastLoadedPage()
-            }
-            assertThat(page).isNotNull()
-            assertThat(page?.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
-        }
-    }
-
-    @Test
-    fun getLastLoadedPage_afterInvalidPagingSource() {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        runTest {
-            val page = pager.run {
-                refresh()
-                append() // page should be this appended page
-                source.invalidate()
-                assertTrue(source.invalid)
-                getLastLoadedPage()
-            }
-            assertThat(page).isNotNull()
-            assertThat(page?.data).containsExactlyElementsIn(listOf(5, 6, 7)).inOrder()
-        }
-    }
-
-    @Test
-    fun refresh_getPages() {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        runTest {
-            val pages = pager.run {
-                refresh()
-                getPages()
-            }
-            assertThat(pages).hasSize(1)
-            assertThat(pages).containsExactlyElementsIn(
-                listOf(
-                    listOf(0, 1, 2, 3, 4).asPage()
-                )
-            ).inOrder()
-        }
-    }
-
-    @Test
-    fun getPages_multiplePages() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        pager.run {
-            refresh(20)
-            prepend()
-        }
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                // prepend
-                listOf(17, 18, 19).asPage(),
-                // refresh
-                listOf(20, 21, 22, 23, 24).asPage(),
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun getPages_fromEmptyList() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-        val pages = pager.getPages()
-        assertThat(pages).isEmpty()
-    }
-
-    @Test
-    fun getPages_afterInvalidPagingSource() {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        runTest {
-            val pages = pager.run {
-                refresh()
-                append()
-                source.invalidate()
-                assertTrue(source.invalid)
-                getPages()
-            }
-            assertThat(pages).containsExactlyElementsIn(
-                listOf(
-                    listOf(0, 1, 2, 3, 4).asPage(),
-                    listOf(5, 6, 7).asPage()
-                )
-            ).inOrder()
-        }
-    }
-
-    @Test
-    fun getPages_multiThread() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        var pages: List<LoadResult.Page<Int, Int>>? = null
-        val job = launch {
-            pager.run {
-                refresh(20) // first
-                pages = getPages() // third
-                prepend() // fifth
-            }
-        }
-        job.start()
-        assertTrue(job.isActive)
-        val pages2 = pager.run {
-            delay(200) // let launch start first
-            append() // second
-            prepend() // fourth
-            getPages() // sixth
-        }
-
-        advanceUntilIdle()
-        assertThat(pages).containsExactlyElementsIn(
-            listOf(
-                // should contain first and second load
-                listOf(20, 21, 22, 23, 24).asPage(), // refresh
-                listOf(25, 26, 27).asPage(), // append
-            )
-        ).inOrder()
-        assertThat(pages2).containsExactlyElementsIn(
-            // should contain all loads
-            listOf(
-                listOf(14, 15, 16).asPage(),
-                listOf(17, 18, 19).asPage(),
-                listOf(20, 21, 22, 23, 24).asPage(),
-                listOf(25, 26, 27).asPage(),
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun multipleRefresh_onSinglePager_throws() {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        runTest {
-            pager.run {
-                // second refresh should throw since testPager is not mult-generational
-                assertFailsWith<IllegalStateException> {
-                    refresh()
-                    refresh()
-                }
-            }
-            assertTrue(source.invalid)
-            // the first refresh should still have succeeded
-            assertThat(pager.getPages()).hasSize(1)
-        }
-    }
-
-    @Test
-    fun multipleRefresh_onMultiplePagers() = runTest {
-        val source1 = TestPagingSource()
-        val pager1 = TestPager(CONFIG, source1)
-
-        // first gen
-        val result1 = pager1.run {
-            refresh()
-        } as LoadResult.Page
-
-        assertThat(result1.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
-
-        // second gen
-        val source2 = TestPagingSource()
-        val pager2 = TestPager(CONFIG, source2)
-
-        val result2 = pager2.run {
-            refresh()
-        } as LoadResult.Page
-
-        assertThat(result2.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
-    }
-
-    @Test
-    fun simpleAppend() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        val result = pager.run {
-            refresh(null)
-            append()
-        } as LoadResult.Page
-
-        assertThat(result.data).containsExactlyElementsIn(listOf(5, 6, 7)).inOrder()
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                listOf(0, 1, 2, 3, 4).asPage(),
-                listOf(5, 6, 7).asPage()
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun simplePrepend() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        val result = pager.run {
-            refresh(30)
-            prepend()
-        } as LoadResult.Page
-
-        assertThat(result.data).containsExactlyElementsIn(listOf(27, 28, 29)).inOrder()
-        // prepended pages should be inserted before refresh
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                // prepend
-                listOf(27, 28, 29).asPage(),
-                // refresh
-                listOf(30, 31, 32, 33, 34).asPage()
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun append_beforeRefresh_throws() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-        assertFailsWith<IllegalStateException> {
-            pager.append()
-        }
-    }
-
-    @Test
-    fun prepend_beforeRefresh_throws() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-        assertFailsWith<IllegalStateException> {
-            pager.prepend()
-        }
-    }
-
-    @Test
-    fun append_invalidPagingSource() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        val result = pager.run {
-            refresh()
-            source.invalidate()
-            assertThat(source.invalid).isTrue()
-            // simulate a PagingSource which returns LoadResult.Invalid when it's invalidated
-            source.nextLoadResult = LoadResult.Invalid()
-            append()
-        }
-        assertThat(result).isInstanceOf(LoadResult.Invalid::class.java)
-    }
-
-    @Test
-    fun prepend_invalidPagingSource() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        val result = pager.run {
-            refresh(initialKey = 20)
-            source.invalidate()
-            assertThat(source.invalid).isTrue()
-            // simulate a PagingSource which returns LoadResult.Invalid when it's invalidated
-            source.nextLoadResult = LoadResult.Invalid()
-            prepend()
-        }
-        assertThat(result).isInstanceOf(LoadResult.Invalid::class.java)
-    }
-
-    @Test
-    fun consecutive_append() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        pager.run {
-            refresh(20)
-            append()
-            append()
-        } as LoadResult.Page
-
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                listOf(20, 21, 22, 23, 24).asPage(),
-                listOf(25, 26, 27).asPage(),
-                listOf(28, 29, 30).asPage()
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun consecutive_prepend() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        pager.run {
-            refresh(20)
-            prepend()
-            prepend()
-        } as LoadResult.Page
-
-        // prepended pages should be ordered before the refresh
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                // 2nd prepend
-                listOf(14, 15, 16).asPage(),
-                // 1st prepend
-                listOf(17, 18, 19).asPage(),
-                // refresh
-                listOf(20, 21, 22, 23, 24).asPage(),
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun append_then_prepend() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        pager.run {
-            refresh(20)
-            append()
-            prepend()
-        } as LoadResult.Page
-
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                // prepend
-                listOf(17, 18, 19).asPage(),
-                // refresh
-                listOf(20, 21, 22, 23, 24).asPage(),
-                // append
-                listOf(25, 26, 27).asPage(),
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun prepend_then_append() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        pager.run {
-            refresh(20)
-            prepend()
-            append()
-        } as LoadResult.Page
-
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                // prepend
-                listOf(17, 18, 19).asPage(),
-                // refresh
-                listOf(20, 21, 22, 23, 24).asPage(),
-                // append
-                listOf(25, 26, 27).asPage(),
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun multiThread_loads() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-        // load operations upon completion add an int to the list.
-        // after all loads complete, we assert the order that the ints were added.
-        val loadOrder = mutableListOf<Int>()
-
-        val job = launch {
-            pager.run {
-                refresh(20).also { loadOrder.add(1) } // first load
-                prepend().also { loadOrder.add(3) } // third load
-                append().also { loadOrder.add(5) } // fifth load
-            }
-        }
-        job.start()
-        assertTrue(job.isActive)
-
-        pager.run {
-            // give some time for job to start
-            delay(200)
-            append().also { loadOrder.add(2) } // second load
-            prepend().also { loadOrder.add(4) } // fourth load
-        }
-
-        advanceUntilIdle()
-        assertThat(loadOrder).containsExactlyElementsIn(listOf(1, 2, 3, 4, 5)).inOrder()
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                listOf(14, 15, 16).asPage(),
-                listOf(17, 18, 19).asPage(),
-                listOf(20, 21, 22, 23, 24).asPage(),
-                listOf(25, 26, 27).asPage(),
-                listOf(28, 29, 30).asPage(),
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun multiThread_operations() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-        // operations upon completion add an int to the list.
-        // after all operations complete, we assert the order that the ints were added.
-        val loadOrder = mutableListOf<Int>()
-
-        var lastLoadedPage: LoadResult.Page<Int, Int>? = null
-        val job = launch {
-            pager.run {
-                refresh(20).also { loadOrder.add(1) } // first operation
-                // third operation, should return first appended page
-                lastLoadedPage = getLastLoadedPage().also { loadOrder.add(3) }
-                append().also { loadOrder.add(5) } // fifth operation
-                prepend().also { loadOrder.add(7) } // last operation
-            }
-        }
-        job.start()
-        assertTrue(job.isActive)
-
-        val pages = pager.run {
-            // give some time for job to start first
-            delay(200)
-            append().also { loadOrder.add(2) } // second operation
-            prepend().also { loadOrder.add(4) } // fourth operation
-            // sixth operation, should return 4 pages
-            getPages().also { loadOrder.add(6) }
-        }
-
-        advanceUntilIdle()
-        assertThat(loadOrder).containsExactlyElementsIn(
-            listOf(1, 2, 3, 4, 5, 6, 7)
-        ).inOrder()
-        assertThat(lastLoadedPage).isEqualTo(
-            listOf(25, 26, 27).asPage(),
-        )
-        // should not contain the second prepend, with a total of 4 pages
-        assertThat(pages).containsExactlyElementsIn(
-            listOf(
-                listOf(17, 18, 19).asPage(), // first prepend
-                listOf(20, 21, 22, 23, 24).asPage(), // refresh
-                listOf(25, 26, 27).asPage(), // first append
-                listOf(28, 29, 30).asPage(), // second append
-            )
-        ).inOrder()
-    }
-
-    @Test
-    fun getPagingStateWithAnchorPosition_placeHoldersEnabled() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        val state = pager.run {
-            refresh(20)
-            prepend()
-            append()
-            getPagingState(7)
-        }
-        // in this case anchorPos is a placeholder at index 7
-        assertThat(state).isEqualTo(
-            PagingState(
-                pages = listOf(
-                    listOf(17, 18, 19).asPage(),
-                    // refresh
-                    listOf(20, 21, 22, 23, 24).asPage(),
-                    // append
-                    listOf(25, 26, 27).asPage(),
-                ),
-                anchorPosition = 7,
-                config = CONFIG,
-                leadingPlaceholderCount = 17
-            )
-        )
-        val source2 = TestPagingSource()
-        val pager2 = TestPager(CONFIG, source)
-        val page = pager2.run {
-            refresh(source2.getRefreshKey(state))
-        }
-        assertThat(page).isEqualTo(listOf(7, 8, 9, 10, 11).asPage())
-    }
-
-    @Test
-    fun getPagingStateWithAnchorPosition_placeHoldersDisabled() = runTest {
-        val source = TestPagingSource(placeholdersEnabled = false)
-        val config = PagingConfig(
-            pageSize = 3,
-            initialLoadSize = 5,
-            enablePlaceholders = false
-        )
-        val pager = TestPager(config, source)
-
-        val state = pager.run {
-            refresh(20)
-            prepend()
-            append()
-            getPagingState(7)
-        }
-        assertThat(state).isEqualTo(
-            PagingState(
-                pages = listOf(
-                    listOf(17, 18, 19).asPage(placeholdersEnabled = false),
-                    // refresh
-                    listOf(20, 21, 22, 23, 24).asPage(placeholdersEnabled = false),
-                    // append
-                    listOf(25, 26, 27).asPage(placeholdersEnabled = false),
-                ),
-                anchorPosition = 7,
-                config = config,
-                leadingPlaceholderCount = 0
-            )
-        )
-        val source2 = TestPagingSource()
-        val pager2 = TestPager(CONFIG, source)
-        val page = pager2.run {
-            refresh(source2.getRefreshKey(state))
-        }
-        // without placeholders, Paging currently has no way to translate item[7] within loaded
-        // pages into its absolute position within available data. Hence anchorPosition 7 will
-        // reference item[7] within available data.
-        assertThat(page).isEqualTo(listOf(7, 8, 9, 10, 11).asPage(placeholdersEnabled = false))
-    }
-
-    @Test
-    fun getPagingStateWithAnchorPosition_indexOutOfBoundsWithPlaceholders() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        val msg = assertFailsWith<IllegalStateException> {
-            pager.run {
-                refresh()
-                append()
-                getPagingState(-1)
-            }
-        }.localizedMessage
-        assertThat(msg).isEqualTo(
-            "anchorPosition -1 is out of bounds between [0..${ITEM_COUNT - 1}]. Please " +
-                "provide a valid anchorPosition."
-        )
-
-        val msg2 = assertFailsWith<IllegalStateException> {
-            pager.getPagingState(ITEM_COUNT)
-        }.localizedMessage
-        assertThat(msg2).isEqualTo(
-            "anchorPosition $ITEM_COUNT is out of bounds between [0..${ITEM_COUNT - 1}]. " +
-                "Please provide a valid anchorPosition."
-        )
-    }
-
-    @Test
-    fun getPagingStateWithAnchorPosition_indexOutOfBoundsWithoutPlaceholders() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(
-            PagingConfig(
-                pageSize = 3,
-                initialLoadSize = 5,
-                enablePlaceholders = false
-            ),
-            source
-        )
-
-        val msg = assertFailsWith<IllegalStateException> {
-            pager.run {
-                refresh()
-                append()
-                getPagingState(-1)
-            }
-        }.localizedMessage
-        assertThat(msg).isEqualTo(
-            "anchorPosition -1 is out of bounds between [0..7]. Please " +
-                "provide a valid anchorPosition."
-        )
-
-        // total loaded items = 8, anchorPos with index 8 should be out of bounds
-        val msg2 = assertFailsWith<IllegalStateException> {
-            pager.getPagingState(8)
-        }.localizedMessage
-        assertThat(msg2).isEqualTo(
-            "anchorPosition 8 is out of bounds between [0..7]. Please " +
-        "provide a valid anchorPosition."
-        )
-    }
-
-    @Test
-    fun getPagingStateWithAnchorLookup_placeHoldersEnabled() = runTest {
-        val source = TestPagingSource()
-        val pager = TestPager(CONFIG, source)
-
-        val state = pager.run {
-            refresh(20)
-            prepend()
-            append()
-            getPagingState { it == TestPagingSource.ITEMS[22] }
-        }
-        assertThat(state).isEqualTo(
-            PagingState(
-                pages = listOf(
-                    listOf(17, 18, 19).asPage(),
-                    // refresh
-                    listOf(20, 21, 22, 23, 24).asPage(),
-                    // append
-                    listOf(25, 26, 27).asPage(),
-                ),
-                anchorPosition = 22,
-                config = CONFIG,
-                leadingPlaceholderCount = 17
-            )
-        )
-        // use state to getRefreshKey
-        val source2 = TestPagingSource()
-        val pager2 = TestPager(CONFIG, source)
-        val page = pager2.run {
-            refresh(source2.getRefreshKey(state))
-        }
-        assertThat(page).isEqualTo(listOf(22, 23, 24, 25, 26).asPage())
-    }
-
-    @Test
-    fun getPagingStateWithAnchorLookup_placeHoldersDisabled() = runTest {
-        val source = TestPagingSource(placeholdersEnabled = false)
-        val config = PagingConfig(
-            pageSize = 3,
-            initialLoadSize = 5,
-            enablePlaceholders = false
-        )
-        val pager = TestPager(config, source)
-
-        val state = pager.run {
-            refresh(20)
-            prepend()
-            append()
-            getPagingState { it == TestPagingSource.ITEMS[22] } // item 22 in this case
-        }
-        assertThat(state).isEqualTo(
-            PagingState(
-                pages = listOf(
-                    listOf(17, 18, 19).asPage(placeholdersEnabled = false),
-                    // refresh
-                    listOf(20, 21, 22, 23, 24).asPage(placeholdersEnabled = false),
-                    // append
-                    listOf(25, 26, 27).asPage(placeholdersEnabled = false),
-                ),
-                anchorPosition = 5,
-                config = config,
-                leadingPlaceholderCount = 0
-            )
-        )
-        // use state to getRefreshKey
-        val source2 = TestPagingSource()
-        val pager2 = TestPager(CONFIG, source)
-        val page = pager2.run {
-            refresh(source2.getRefreshKey(state))
-        }
-        // without placeholders, Paging currently has no way to translate item[5] within loaded
-        // pages into its absolute position within available data. anchorPosition 5 will reference
-        // item[5] within available data.
-        assertThat(page).isEqualTo(listOf(5, 6, 7, 8, 9).asPage(placeholdersEnabled = false))
-    }
-
-    @Test
-    fun getPagingStateWithAnchorLookup_itemNotFoundThrows() = runTest {
-        val source = TestPagingSource(placeholdersEnabled = false)
-        val config = PagingConfig(
-            pageSize = 3,
-            initialLoadSize = 5,
-            enablePlaceholders = false
-        )
-        val pager = TestPager(config, source)
-
-        val msg = assertFailsWith<IllegalArgumentException> {
-            pager.run {
-                refresh(20)
-                prepend()
-                append()
-                getPagingState { it == TestPagingSource.ITEMS[10] }
-            }
-        }.message
-        assertThat(msg).isEqualTo(
-            "The given predicate has returned false for every loaded item. To generate a" +
-                "PagingState anchored to an item, the expected item must have already " +
-                "been loaded."
-        )
-    }
-
-    @Test
-    fun dropPrependedPage() = runTest {
-        val source = TestPagingSource()
-        val config = PagingConfig(
-            pageSize = 3,
-            initialLoadSize = 5,
-            enablePlaceholders = false,
-            maxSize = 10
-        )
-        val pager = TestPager(config, source)
-        pager.run {
-            refresh(20)
-            prepend()
-        }
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                listOf(17, 18, 19).asPage(),
-                // refresh
-                listOf(20, 21, 22, 23, 24).asPage(),
-            )
-        )
-
-        // this append should trigger paging to drop the prepended page
-        pager.append()
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                listOf(20, 21, 22, 23, 24).asPage(),
-                listOf(25, 26, 27).asPage(),
-            )
-        )
-    }
-
-    @Test
-    fun dropAppendedPage() = runTest {
-        val source = TestPagingSource()
-        val config = PagingConfig(
-            pageSize = 3,
-            initialLoadSize = 5,
-            enablePlaceholders = false,
-            maxSize = 10
-        )
-        val pager = TestPager(config, source)
-        pager.run {
-            refresh(20)
-            append()
-        }
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                listOf(20, 21, 22, 23, 24).asPage(),
-                listOf(25, 26, 27).asPage(),
-            )
-        )
-
-        // this prepend should trigger paging to drop the prepended page
-        pager.prepend()
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                listOf(17, 18, 19).asPage(),
-                listOf(20, 21, 22, 23, 24).asPage(),
-            )
-        )
-    }
-
-    @Test
-    fun dropInitialRefreshedPage() = runTest {
-        val source = TestPagingSource()
-        val config = PagingConfig(
-            pageSize = 3,
-            initialLoadSize = 5,
-            enablePlaceholders = false,
-            maxSize = 10
-        )
-        val pager = TestPager(config, source)
-        pager.run {
-            refresh(20)
-            append()
-        }
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                listOf(20, 21, 22, 23, 24).asPage(),
-                listOf(25, 26, 27).asPage(),
-            )
-        )
-
-        // this append should trigger paging to drop the first page which is the initial refresh
-        pager.append()
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                listOf(25, 26, 27).asPage(),
-                listOf(28, 29, 30).asPage(),
-            )
-        )
-    }
-
-    @Test
-    fun dropRespectsPrefetchDistance_InDroppedDirection() = runTest {
-        val source = TestPagingSource()
-        val config = PagingConfig(
-            pageSize = 1,
-            initialLoadSize = 10,
-            enablePlaceholders = false,
-            maxSize = 5,
-            prefetchDistance = 2
-        )
-        val pager = TestPager(config, source)
-        pager.refresh(20)
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                listOf(20, 21, 22, 23, 24, 25, 26, 27, 28, 29).asPage(),
-            )
-        )
-
-        // these appends should would normally trigger paging to drop first page, but it won't
-        // in this case due to prefetchDistance
-        pager.run {
-            append()
-            append()
-        }
-        assertThat(pager.getPages()).containsExactlyElementsIn(
-            listOf(
-                // second page counted towards prefetch distance
-                listOf(20, 21, 22, 23, 24, 25, 26, 27, 28, 29).asPage(),
-                // first page counted towards prefetch distance
-                listOf(30).asPage(),
-                listOf(31).asPage()
-            )
-        )
-    }
-
-    @Test
-    fun drop_noOpUnderTwoPages() = runTest {
-        val source = TestPagingSource()
-        val config = PagingConfig(
-            pageSize = 1,
-            initialLoadSize = 5,
-            enablePlaceholders = false,
-            maxSize = 3,
-            prefetchDistance = 1
-        )
-        val pager = TestPager(config, source)
-        val result = pager.refresh() as LoadResult.Page
-        assertThat(result.data).containsExactlyElementsIn(
-            listOf(0, 1, 2, 3, 4)
-        )
-
-        pager.append()
-        // data size exceeds maxSize but no data should be dropped
-        assertThat(pager.getPages().flatten()).containsExactlyElementsIn(
-            listOf(0, 1, 2, 3, 4, 5)
-        )
-    }
-
-    private val CONFIG = PagingConfig(
-        pageSize = 3,
-        initialLoadSize = 5,
-    )
-
-    private fun List<Int>.asPage(placeholdersEnabled: Boolean = true): LoadResult.Page<Int, Int> {
-        val itemsBefore = if (placeholdersEnabled) {
-            if (first() == 0) 0 else first()
-        } else {
-            Int.MIN_VALUE
-        }
-        val itemsAfter = if (placeholdersEnabled) {
-            if (last() == ITEM_COUNT - 1) 0 else ITEM_COUNT - 1 - last()
-        } else {
-            Int.MIN_VALUE
-        }
-        return LoadResult.Page(
-            data = this,
-            prevKey = if (first() == 0) null else first() - 1,
-            nextKey = if (last() == ITEM_COUNT - 1) null else last() + 1,
-            itemsBefore = itemsBefore,
-            itemsAfter = itemsAfter
-        )
-    }
-
-    private val ITEM_COUNT = 100
-}
diff --git a/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/LoadErrorHandler.kt b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/LoadErrorHandler.kt
similarity index 100%
rename from paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/LoadErrorHandler.kt
rename to paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/LoadErrorHandler.kt
diff --git a/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/PagerFlowSnapshot.kt b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/PagerFlowSnapshot.kt
new file mode 100644
index 0000000..c6dc443
--- /dev/null
+++ b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/PagerFlowSnapshot.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging.testing
+
+import androidx.annotation.VisibleForTesting
+import androidx.paging.CombinedLoadStates
+import androidx.paging.DifferCallback
+import androidx.paging.ItemSnapshotList
+import androidx.paging.LoadState
+import androidx.paging.LoadStates
+import androidx.paging.NullPaddedList
+import androidx.paging.Pager
+import androidx.paging.PagingData
+import androidx.paging.PagingDataDiffer
+import androidx.paging.testing.ErrorRecovery.RETRY
+import androidx.paging.testing.ErrorRecovery.RETURN_CURRENT_SNAPSHOT
+import androidx.paging.testing.ErrorRecovery.THROW
+import androidx.paging.testing.LoaderCallback.CallbackType.ON_CHANGED
+import androidx.paging.testing.LoaderCallback.CallbackType.ON_INSERTED
+import androidx.paging.testing.LoaderCallback.CallbackType.ON_REMOVED
+import kotlin.coroutines.CoroutineContext
+import kotlin.jvm.JvmSuppressWildcards
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.launch
+
+/**
+ * Runs the [SnapshotLoader] load operations that are passed in and returns a List of data
+ * that would be presented to the UI after all load operations are complete.
+ *
+ * @param onError The error recovery strategy when PagingSource returns LoadResult.Error. A lambda
+ * that returns an [ErrorRecovery] value. The default strategy is [ErrorRecovery.THROW].
+ *
+ * @param loadOperations The block containing [SnapshotLoader] load operations.
+ */
+@VisibleForTesting
+public suspend fun <Value : Any> Flow<PagingData<Value>>.asSnapshot(
+    onError: LoadErrorHandler = LoadErrorHandler { THROW },
+    loadOperations: suspend SnapshotLoader<Value>.() -> @JvmSuppressWildcards Unit = { }
+): @JvmSuppressWildcards List<Value> = coroutineScope {
+
+    lateinit var loader: SnapshotLoader<Value>
+
+    val callback = object : DifferCallback {
+        override fun onChanged(position: Int, count: Int) {
+            loader.onDataSetChanged(
+                loader.generations.value,
+                LoaderCallback(ON_CHANGED, position, count)
+            )
+        }
+        override fun onInserted(position: Int, count: Int) {
+            loader.onDataSetChanged(
+                loader.generations.value,
+                LoaderCallback(ON_INSERTED, position, count)
+            )
+        }
+        override fun onRemoved(position: Int, count: Int) {
+            loader.onDataSetChanged(
+                loader.generations.value,
+                LoaderCallback(ON_REMOVED, position, count)
+            )
+        }
+    }
+
+    // PagingDataDiffer will collect from coroutineContext instead of main dispatcher
+    val differ = object : CompletablePagingDataDiffer<Value>(callback, coroutineContext) {
+        override suspend fun presentNewList(
+            previousList: NullPaddedList<Value>,
+            newList: NullPaddedList<Value>,
+            lastAccessedIndex: Int,
+            onListPresentable: () -> Unit
+        ): Int? {
+            onListPresentable()
+            /**
+             * On new generation, SnapshotLoader needs the latest [ItemSnapshotList]
+             * state so that it can initialize lastAccessedIndex to prepend/append from onwards.
+             *
+             * This initial lastAccessedIndex is necessary because initial load
+             * key may not be 0, for example when [Pager].initialKey != 0. It is calculated
+             * based on [ItemSnapshotList.placeholdersBefore] + [1/2 initial load size] to match
+             * the initial ViewportHint that [PagingDataDiffer.presentNewList] sends on
+             * first generation to auto-trigger prefetches on either direction.
+             *
+             * Any subsequent SnapshotLoader loads are based on the index tracked by
+             * [SnapshotLoader] internally.
+             */
+            val lastLoadedIndex = snapshot().placeholdersBefore + (snapshot().items.size / 2)
+            loader.generations.value.lastAccessedIndex.set(lastLoadedIndex)
+            return null
+        }
+    }
+
+    loader = SnapshotLoader(differ, onError)
+
+    /**
+     * Launches collection on this [Pager.flow].
+     *
+     * The collection job is cancelled automatically after [loadOperations] completes.
+      */
+    val collectPagingData = launch {
+        this@asSnapshot.collectLatest {
+            incrementGeneration(loader)
+            differ.collectFrom(it)
+        }
+        differ.hasCompleted.value = true
+    }
+
+    /**
+     * Runs the input [loadOperations].
+     *
+     * Awaits for initial refresh to complete before invoking [loadOperations]. Automatically
+     * cancels the collection on this [Pager.flow] after [loadOperations] completes and Paging
+     * is idle.
+     */
+    try {
+        differ.awaitNotLoading(onError)
+        loader.loadOperations()
+        differ.awaitNotLoading(onError)
+    } catch (stub: ReturnSnapshotStub) {
+        // we just want to stub and return snapshot early
+    } catch (throwable: Throwable) {
+        throw throwable
+    } finally {
+        collectPagingData.cancelAndJoin()
+    }
+
+    differ.snapshot().items
+}
+
+internal abstract class CompletablePagingDataDiffer<Value : Any>(
+    differCallback: DifferCallback,
+    mainContext: CoroutineContext,
+) : PagingDataDiffer<Value>(differCallback, mainContext) {
+    /**
+     * Marker that the underlying Flow<PagingData> has completed - e.g., every possible generation
+     * of data has been loaded completely.
+     */
+    val hasCompleted = MutableStateFlow(false)
+
+    val completableLoadStateFlow = loadStateFlow.combine(
+        hasCompleted
+    ) { loadStates, hasCompleted ->
+        if (hasCompleted) {
+            CombinedLoadStates(
+                refresh = LoadState.NotLoading(true),
+                prepend = LoadState.NotLoading(true),
+                append = LoadState.NotLoading(true),
+                source = LoadStates(
+                    refresh = LoadState.NotLoading(true),
+                    prepend = LoadState.NotLoading(true),
+                    append = LoadState.NotLoading(true)
+                )
+            )
+        } else {
+            loadStates
+        }
+    }
+}
+
+/**
+ * Awaits until both source and mediator states are NotLoading. We do not care about the state of
+ * endOfPaginationReached. Source and mediator states need to be checked individually because
+ * the aggregated LoadStates can reflect `NotLoading` when source states are `Loading`.
+ *
+ * We debounce(1ms) to prevent returning too early if this collected a `NotLoading` from the
+ * previous load. Without a way to determine whether the `NotLoading` it collected was from
+ * a previous operation or current operation, we debounce 1ms to allow collection on a potential
+ * incoming `Loading` state.
+ */
+@OptIn(kotlinx.coroutines.FlowPreview::class)
+internal suspend fun <Value : Any> CompletablePagingDataDiffer<Value>.awaitNotLoading(
+    errorHandler: LoadErrorHandler
+) {
+    val state = completableLoadStateFlow.filterNotNull().debounce(1).filter {
+        it.isIdle() || it.hasError()
+    }.firstOrNull()
+
+    if (state != null && state.hasError()) {
+        handleLoadError(state, errorHandler)
+    }
+}
+
+internal fun <Value : Any> PagingDataDiffer<Value>.handleLoadError(
+    state: CombinedLoadStates,
+    errorHandler: LoadErrorHandler
+) {
+    val recovery = errorHandler.onError(state)
+    when (recovery) {
+        THROW -> throw (state.getErrorState()).error
+        RETRY -> retry()
+        RETURN_CURRENT_SNAPSHOT -> throw ReturnSnapshotStub()
+    }
+}
+private class ReturnSnapshotStub : Exception()
+
+private fun CombinedLoadStates?.isIdle(): Boolean {
+    if (this == null) return false
+    return source.isIdle() && mediator?.isIdle() ?: true
+}
+
+private fun LoadStates.isIdle(): Boolean {
+    return refresh is LoadState.NotLoading && append is LoadState.NotLoading &&
+        prepend is LoadState.NotLoading
+}
+
+private fun CombinedLoadStates?.hasError(): Boolean {
+    if (this == null) return false
+    return source.hasError() || mediator?.hasError() ?: false
+}
+
+private fun LoadStates.hasError(): Boolean {
+    return refresh is LoadState.Error || append is LoadState.Error ||
+        prepend is LoadState.Error
+}
+
+private fun CombinedLoadStates.getErrorState(): LoadState.Error {
+    return if (refresh is LoadState.Error) {
+        refresh as LoadState.Error
+    } else if (append is LoadState.Error) {
+        append as LoadState.Error
+    } else {
+        prepend as LoadState.Error
+    }
+}
+
+private fun <Value : Any> incrementGeneration(loader: SnapshotLoader<Value>) {
+    val currGen = loader.generations.value
+    if (currGen.id == loader.generations.value.id) {
+        loader.generations.value = Generation(
+            id = currGen.id + 1
+        )
+    }
+}
diff --git a/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/SnapshotLoader.kt b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/SnapshotLoader.kt
new file mode 100644
index 0000000..b8258d3
--- /dev/null
+++ b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/SnapshotLoader.kt
@@ -0,0 +1,473 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging.testing
+
+import androidx.annotation.VisibleForTesting
+import androidx.paging.DifferCallback
+import androidx.paging.LoadType.APPEND
+import androidx.paging.LoadType.PREPEND
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import androidx.paging.PagingDataDiffer
+import androidx.paging.PagingSource
+import androidx.paging.testing.LoaderCallback.CallbackType.ON_INSERTED
+import androidx.paging.testing.internal.AtomicInt
+import androidx.paging.testing.internal.AtomicRef
+import kotlin.jvm.JvmSuppressWildcards
+import kotlin.math.abs
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+
+/**
+ * Contains the public APIs for load operations in tests.
+ *
+ * Tracks generational information and provides the listener to [DifferCallback] on
+ * [PagingDataDiffer] operations.
+ */
+@VisibleForTesting
+public class SnapshotLoader<Value : Any> internal constructor(
+    private val differ: CompletablePagingDataDiffer<Value>,
+    private val errorHandler: LoadErrorHandler,
+) {
+    internal val generations = MutableStateFlow(Generation())
+
+    /**
+     * Refresh the data that is presented on the UI.
+     *
+     * [refresh] triggers a new generation of [PagingData] / [PagingSource]
+     * to represent an updated snapshot of the backing dataset.
+     *
+     * This fake paging operation mimics UI-driven refresh signals such as swipe-to-refresh.
+     */
+    public suspend fun refresh(): @JvmSuppressWildcards Unit {
+        differ.awaitNotLoading(errorHandler)
+        differ.refresh()
+        differ.awaitNotLoading(errorHandler)
+    }
+
+    /**
+     * Imitates scrolling down paged items, [appending][APPEND] data until the given
+     * predicate returns false.
+     *
+     * Note: This API loads an item before passing it into the predicate. This means the
+     * loaded pages may include the page which contains the item that does not match the
+     * predicate. For example, if pageSize = 2, the predicate
+     * {item: Int -> item < 3 } will return items [[1, 2],[3, 4]] where [3, 4] is the page
+     * containing the boundary item[3] not matching the predicate.
+     *
+     * The loaded pages are also dependent on [PagingConfig] settings such as
+     * [PagingConfig.prefetchDistance]:
+     * - if `prefetchDistance` > 0, the resulting appends will include prefetched items.
+     * For example, if pageSize = 2 and prefetchDistance = 2, the predicate
+     * {item: Int -> item < 3 } will load items [[1, 2], [3, 4], [5, 6]] where [5, 6] is the
+     * prefetched page.
+     *
+     * @param [predicate] the predicate to match (return true) to continue append scrolls
+     */
+    public suspend fun appendScrollWhile(
+        predicate: (item: @JvmSuppressWildcards Value) -> @JvmSuppressWildcards Boolean
+    ): @JvmSuppressWildcards Unit {
+        differ.awaitNotLoading(errorHandler)
+        appendOrPrependScrollWhile(LoadType.APPEND, predicate)
+        differ.awaitNotLoading(errorHandler)
+    }
+
+    /**
+     * Imitates scrolling up paged items, [prepending][PREPEND] data until the given
+     * predicate returns false.
+     *
+     * Note: This API loads an item before passing it into the predicate. This means the
+     * loaded pages may include the page which contains the item that does not match the
+     * predicate. For example, if pageSize = 2, initialKey = 3, the predicate
+     * {item: Int -> item >= 3 } will return items [[1, 2],[3, 4]] where [1, 2] is the page
+     * containing the boundary item[2] not matching the predicate.
+     *
+     * The loaded pages are also dependent on [PagingConfig] settings such as
+     * [PagingConfig.prefetchDistance]:
+     * - if `prefetchDistance` > 0, the resulting prepends will include prefetched items.
+     * For example, if pageSize = 2, initialKey = 3, and prefetchDistance = 2, the predicate
+     * {item: Int -> item > 4 } will load items [[1, 2], [3, 4], [5, 6]] where both [1,2] and
+     * [5, 6] are the prefetched pages.
+     *
+     * @param [predicate] the predicate to match (return true) to continue prepend scrolls
+     */
+    public suspend fun prependScrollWhile(
+        predicate: (item: @JvmSuppressWildcards Value) -> @JvmSuppressWildcards Boolean
+    ): @JvmSuppressWildcards Unit {
+        differ.awaitNotLoading(errorHandler)
+        appendOrPrependScrollWhile(LoadType.PREPEND, predicate)
+        differ.awaitNotLoading(errorHandler)
+    }
+
+    private suspend fun appendOrPrependScrollWhile(
+        loadType: LoadType,
+        predicate: (item: Value) -> Boolean
+    ) {
+        do {
+            // awaits for next item where the item index is determined based on
+            // this generation's lastAccessedIndex. If null, it means there are no more
+            // items to load for this loadType.
+            val item = awaitNextItem(loadType) ?: return
+        } while (predicate(item))
+    }
+
+    /**
+     * Imitates scrolling from current index to the target index. It waits for an item to be loaded
+     * in before triggering load on next item. Returns all available data that has been scrolled
+     * through.
+     *
+     * The scroll direction (prepend or append) is dependent on current index and target index. In
+     * general, scrolling to a smaller index triggers [PREPEND] while scrolling to a larger
+     * index triggers [APPEND].
+     *
+     * When [PagingConfig.enablePlaceholders] is false, the [index] is scoped within currently
+     * loaded items. For example, in a list of items(0-20) with currently loaded items(10-15),
+     * index[0] = item(10), index[4] = item(15).
+     *
+     * Supports [index] beyond currently loaded items when [PagingConfig.enablePlaceholders]
+     * is false:
+     * 1. For prepends, it supports negative indices for as long as there are still available
+     * data to load from. For example, take a list of items(0-20), pageSize = 1, with currently
+     * loaded items(10-15). With index[0] = item(10), a `scrollTo(-4)` will scroll to item(6) and
+     * update index[0] = item(6).
+     * 2. For appends, it supports indices >= loadedDataSize. For example, take a list of
+     * items(0-20), pageSize = 1, with currently loaded items(10-15). With
+     * index[4] = item(15), a `scrollTo(7)` will scroll to item(18) and update
+     * index[7] = item(18).
+     * Note that both examples does not account for prefetches.
+
+     * The [index] accounts for separators/headers/footers where each one of those consumes one
+     * scrolled index.
+     *
+     * For both append/prepend, this function stops loading prior to fulfilling requested scroll
+     * distance if there are no more data to load from.
+     *
+     * @param [index] The target index to scroll to
+     *
+     * @see [flingTo] for faking a scroll that continues scrolling without waiting for items to
+     * be loaded in. Supports jumping.
+     */
+    public suspend fun scrollTo(index: Int): @JvmSuppressWildcards Unit {
+        differ.awaitNotLoading(errorHandler)
+        appendOrPrependScrollTo(index)
+        differ.awaitNotLoading(errorHandler)
+    }
+
+    /**
+     * Scrolls from current index to targeted [index].
+     *
+     * Internally this method scrolls until it fulfills requested index
+     * differential (Math.abs(requested index - current index)) rather than scrolling
+     * to the exact requested index. This is because item indices can shift depending on scroll
+     * direction and placeholders. Therefore we try to fulfill the expected amount of scrolling
+     * rather than the actual requested index.
+     */
+    private suspend fun appendOrPrependScrollTo(index: Int) {
+        val startIndex = generations.value.lastAccessedIndex.get()
+        val loadType = if (startIndex > index) LoadType.PREPEND else LoadType.APPEND
+        val scrollCount = abs(startIndex - index)
+        awaitScroll(loadType, scrollCount)
+    }
+
+    /**
+     * Imitates flinging from current index to the target index. It will continue scrolling
+     * even as data is being loaded in. Returns all available data that has been scrolled
+     * through.
+     *
+     * The scroll direction (prepend or append) is dependent on current index and target index. In
+     * general, scrolling to a smaller index triggers [PREPEND] while scrolling to a larger
+     * index triggers [APPEND].
+     *
+     * This function will scroll into placeholders. This means jumping is supported when
+     * [PagingConfig.enablePlaceholders] is true and the amount of placeholders traversed
+     * has reached [PagingConfig.jumpThreshold]. Jumping is disabled when
+     * [PagingConfig.enablePlaceholders] is false.
+     *
+     * When [PagingConfig.enablePlaceholders] is false, the [index] is scoped within currently
+     * loaded items. For example, in a list of items(0-20) with currently loaded items(10-15),
+     * index[0] = item(10), index[4] = item(15).
+     *
+     * Supports [index] beyond currently loaded items when [PagingConfig.enablePlaceholders]
+     * is false:
+     * 1. For prepends, it supports negative indices for as long as there are still available
+     * data to load from. For example, take a list of items(0-20), pageSize = 1, with currently
+     * loaded items(10-15). With index[0] = item(10), a `scrollTo(-4)` will scroll to item(6) and
+     * update index[0] = item(6).
+     * 2. For appends, it supports indices >= loadedDataSize. For example, take a list of
+     * items(0-20), pageSize = 1, with currently loaded items(10-15). With
+     * index[4] = item(15), a `scrollTo(7)` will scroll to item(18) and update
+     * index[7] = item(18).
+     * Note that both examples does not account for prefetches.
+
+     * The [index] accounts for separators/headers/footers where each one of those consumes one
+     * scrolled index.
+     *
+     * For both append/prepend, this function stops loading prior to fulfilling requested scroll
+     * distance if there are no more data to load from.
+     *
+     * @param [index] The target index to scroll to
+     *
+     * @see [scrollTo] for faking scrolls that awaits for placeholders to load before continuing
+     * to scroll.
+     */
+    public suspend fun flingTo(index: Int): @JvmSuppressWildcards Unit {
+        differ.awaitNotLoading(errorHandler)
+        appendOrPrependFlingTo(index)
+        differ.awaitNotLoading(errorHandler)
+    }
+
+    /**
+     * We start scrolling from startIndex +/- 1 so we don't accidentally trigger
+     * a prefetch on the opposite direction.
+     */
+    private suspend fun appendOrPrependFlingTo(index: Int) {
+        val startIndex = generations.value.lastAccessedIndex.get()
+        val loadType = if (startIndex > index) LoadType.PREPEND else LoadType.APPEND
+
+        when (loadType) {
+            LoadType.PREPEND -> prependFlingTo(startIndex, index)
+            LoadType.APPEND -> appendFlingTo(startIndex, index)
+        }
+    }
+
+    /**
+     * Prepend flings to target index.
+     *
+     * If target index is negative, from index[0] onwards it will normal scroll until it fulfills
+     * remaining distance.
+     */
+    private suspend fun prependFlingTo(startIndex: Int, index: Int) {
+        var lastAccessedIndex = startIndex
+        val endIndex = maxOf(0, index)
+        // first, fast scroll to index or zero
+        for (i in startIndex - 1 downTo endIndex) {
+            differ[i]
+            lastAccessedIndex = i
+        }
+        setLastAccessedIndex(lastAccessedIndex)
+        // for negative indices, we delegate remainder of scrolling (distance below zero)
+        // to the awaiting version.
+        if (index < 0) {
+            val scrollCount = abs(index)
+            flingToOutOfBounds(LoadType.PREPEND, lastAccessedIndex, scrollCount)
+        }
+    }
+
+    /**
+     * Append flings to target index.
+     *
+     * If target index is beyond [PagingDataDiffer.size] - 1, from index(differ.size) and onwards,
+     * it will normal scroll until it fulfills remaining distance.
+     */
+    private suspend fun appendFlingTo(startIndex: Int, index: Int) {
+        var lastAccessedIndex = startIndex
+        val endIndex = minOf(index, differ.size - 1)
+        // first, fast scroll to endIndex
+        for (i in startIndex + 1..endIndex) {
+            differ[i]
+            lastAccessedIndex = i
+        }
+        setLastAccessedIndex(lastAccessedIndex)
+        // for indices at or beyond differ.size, we delegate remainder of scrolling (distance
+        // beyond differ.size) to the awaiting version.
+        if (index >= differ.size) {
+            val scrollCount = index - lastAccessedIndex
+            flingToOutOfBounds(LoadType.APPEND, lastAccessedIndex, scrollCount)
+        }
+    }
+
+    /**
+     * Delegated work from [flingTo] that is responsible for scrolling to indices that is
+     * beyond the range of [0 to differ.size-1].
+     *
+     * When [PagingConfig.enablePlaceholders] is true, this function is no-op because
+     * there is no more data to load from.
+     *
+     * When [PagingConfig.enablePlaceholders] is false, its delegated work to [awaitScroll]
+     * essentially loops (trigger next page --> await for next page) until
+     * it fulfills remaining (out of bounds) requested scroll distance.
+     */
+    private suspend fun flingToOutOfBounds(
+        loadType: LoadType,
+        lastAccessedIndex: Int,
+        scrollCount: Int
+    ) {
+        // Wait for the page triggered by differ[lastAccessedIndex] to load in. This gives us the
+        // offsetIndex for next differ.get() because the current lastAccessedIndex is already the
+        // boundary index, such that differ[lastAccessedIndex +/- 1] will throw IndexOutOfBounds.
+        val (_, offsetIndex) = awaitLoad(lastAccessedIndex)
+        setLastAccessedIndex(offsetIndex)
+        // starts loading from the offsetIndex and scrolls the remaining requested distance
+        awaitScroll(loadType, scrollCount)
+    }
+
+    private suspend fun awaitScroll(loadType: LoadType, scrollCount: Int) {
+        repeat(scrollCount) {
+            awaitNextItem(loadType) ?: return
+        }
+    }
+
+    /**
+     * Triggers load for next item, awaits for it to be loaded and returns the loaded item.
+     *
+     * It calculates the next load index based on loadType and this generation's
+     * [Generation.lastAccessedIndex]. The lastAccessedIndex is updated when item is loaded in.
+     */
+    private suspend fun awaitNextItem(loadType: LoadType): Value? {
+        // Get the index to load from. Return if index is invalid.
+        val index = nextIndexOrNull(loadType) ?: return null
+        // OffsetIndex accounts for items that are prepended when placeholders are disabled,
+        // as the new items shift the position of existing items. The offsetIndex (which may
+        // or may not be the same as original index) is stored as lastAccessedIndex after load and
+        // becomes the basis for next load index.
+        val (item, offsetIndex) = awaitLoad(index)
+        setLastAccessedIndex(offsetIndex)
+        return item
+    }
+
+    /**
+     * Get and update the index to load from. Returns null if next index is out of bounds.
+     *
+     * This method computes the next load index based on the [LoadType] and
+     * [Generation.lastAccessedIndex]
+     */
+    private fun nextIndexOrNull(loadType: LoadType): Int? {
+        val currIndex = generations.value.lastAccessedIndex.get()
+        return when (loadType) {
+            LoadType.PREPEND -> {
+                if (currIndex <= 0) {
+                    return null
+                }
+                currIndex - 1
+            }
+            LoadType.APPEND -> {
+                if (currIndex >= differ.size - 1) {
+                    return null
+                }
+                currIndex + 1
+            }
+        }
+    }
+
+    // Executes actual loading by accessing the PagingDataDiffer
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    private suspend fun awaitLoad(index: Int): Pair<Value, Int> {
+        differ[index]
+        differ.awaitNotLoading(errorHandler)
+        var offsetIndex = index
+
+        // awaits for the item to be loaded
+        return generations.map { generation ->
+            // reset callbackState to null so it doesn't get applied on the next load
+            val callbackState = generation.callbackState.getAndSet(null)
+            // offsetIndex accounts for items prepended when placeholders are disabled. This
+            // is necessary because the new items shift the position of existing items, and
+            // the index no longer tracks the correct item.
+            offsetIndex += callbackState?.computeIndexOffset() ?: 0
+            differ.peek(offsetIndex)
+        }.filterNotNull().first() to offsetIndex
+    }
+
+    /**
+     * Computes the offset to add to the index when loading items from differ.
+     *
+     * The purpose of this is to address shifted item positions when new items are prepended
+     * with placeholders disabled. For example, loaded items(10-12) in the NullPaddedList
+     * would have item(12) at differ[2]. If we prefetched items(7-9), item(12) would now be in
+     * differ[5].
+     *
+     * Without the offset, [PREPEND] operations would call differ[1] to load next item(11)
+     * which would now yield item(8) instead of item(11). The offset would add the
+     * inserted count to the next load index such that after prepending 3 new items(7-9), the next
+     * [PREPEND] operation would call differ[1+3 = 4] to properly load next item(11).
+     *
+     * This method is essentially no-op unless the callback meets three conditions:
+     * - is type [DifferCallback.onChanged]
+     * - position is 0 as we only care about item prepended to front of list
+     * - inserted count > 0
+     */
+    private fun LoaderCallback.computeIndexOffset(): Int {
+        return if (type == ON_INSERTED && position == 0) count else 0
+    }
+
+    private fun setLastAccessedIndex(index: Int) {
+        generations.value.lastAccessedIndex.set(index)
+    }
+
+    /**
+     * The callback to be invoked by DifferCallback on a single generation.
+     * Increase the callbackCount to notify SnapshotLoader that the dataset has updated
+     */
+    internal fun onDataSetChanged(gen: Generation, callback: LoaderCallback) {
+        val currGen = generations.value
+        // we make sure the generation with the dataset change is still valid because we
+        // want to disregard callbacks on stale generations
+        if (gen.id == currGen.id) {
+            generations.value = gen.copy(
+                callbackCount = currGen.callbackCount + 1,
+                callbackState = currGen.callbackState.apply { set(callback) }
+            )
+        }
+    }
+
+    private enum class LoadType {
+        PREPEND,
+        APPEND
+    }
+}
+
+internal data class Generation(
+    /**
+     * Id of the current Paging generation. Incremented on each new generation (when a new
+     * PagingData is received).
+     */
+    val id: Int = -1,
+
+    /**
+     * A count of the number of times Paging invokes a [DifferCallback] callback within a single
+     * generation. Incremented on each [DifferCallback] callback invoked, i.e. on item inserted.
+     *
+     * The callbackCount enables [SnapshotLoader] to await for a requested item and continue
+     * loading next item only after a callback is invoked.
+     */
+    val callbackCount: Int = 0,
+
+    /**
+     * Temporarily stores the latest [DifferCallback] to track prepends to the beginning of list.
+     * Value is reset to null once read.
+     */
+    val callbackState: AtomicRef<LoaderCallback?> = AtomicRef(null),
+
+    /**
+     * Tracks the last accessed(peeked) index on the differ for this generation
+      */
+    var lastAccessedIndex: AtomicInt = AtomicInt(0)
+)
+
+internal data class LoaderCallback(
+    val type: CallbackType,
+    val position: Int,
+    val count: Int,
+) {
+    internal enum class CallbackType {
+        ON_CHANGED,
+        ON_INSERTED,
+        ON_REMOVED,
+    }
+}
diff --git a/paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/StaticListPagingSource.kt b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/StaticListPagingSource.kt
similarity index 100%
rename from paging/paging-testing/src/androidMain/kotlin/androidx/paging/testing/StaticListPagingSource.kt
rename to paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/StaticListPagingSource.kt
diff --git a/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/StaticListPagingSourceFactory.kt b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/StaticListPagingSourceFactory.kt
new file mode 100644
index 0000000..fc5ea4d
--- /dev/null
+++ b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/StaticListPagingSourceFactory.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging.testing
+
+import androidx.annotation.VisibleForTesting
+import androidx.paging.InvalidatingPagingSourceFactory
+import androidx.paging.LoadType.REFRESH
+import androidx.paging.Pager
+import androidx.paging.PagingSource
+import androidx.paging.PagingSourceFactory
+import kotlin.jvm.JvmSuppressWildcards
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+
+/**
+ * Returns a [PagingSourceFactory] that creates [PagingSource] instances.
+ *
+ * Can be used as the pagingSourceFactory when constructing a [Pager] in tests. The same factory
+ * should be reused within the lifetime of a ViewModel.
+ *
+ * Extension method on a [Flow] of list that represents the data source, with each static list
+ * representing a generation of data from which a [PagingSource] will load from. With every
+ * emission to the flow, the current [PagingSource] will be invalidated, thereby triggering
+ * a new generation of Paged data.
+ *
+ * Supports multiple factories and thus multiple collection on the same flow.
+ *
+ * @param coroutineScope the CoroutineScope to collect from the Flow of list.
+ */
+@VisibleForTesting
+public fun <Value : Any> Flow<@JvmSuppressWildcards List<Value>>.asPagingSourceFactory(
+    coroutineScope: CoroutineScope
+): PagingSourceFactory<Int, Value> {
+
+    var data: List<Value>? = null
+
+    val factory = InvalidatingPagingSourceFactory {
+        val dataSource = data ?: emptyList()
+
+        @Suppress("UNCHECKED_CAST")
+        StaticListPagingSource(dataSource)
+    }
+
+    coroutineScope.launch {
+        collect { list ->
+            data = list
+            factory.invalidate()
+        }
+    }
+
+    return factory
+}
+
+/**
+ * Returns a [PagingSourceFactory] that creates [PagingSource] instances.
+ *
+ * Can be used as the pagingSourceFactory when constructing a [Pager] in tests. The same factory
+ * should be reused within the lifetime of a ViewModel.
+ *
+ * Extension method on a [List] of data from which a [PagingSource] will load from. While this
+ * factory supports multi-generational operations such as [REFRESH], it does not support updating
+ * the data source. This means any PagingSources generated by the same factory will load from
+ * the exact same list of data.
+ */
+@VisibleForTesting
+public fun <Value : Any> List<Value>.asPagingSourceFactory(): PagingSourceFactory<Int, Value> =
+    PagingSourceFactory { StaticListPagingSource(this) }
diff --git a/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/TestPager.kt b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/TestPager.kt
new file mode 100644
index 0000000..6cca56c
--- /dev/null
+++ b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/TestPager.kt
@@ -0,0 +1,393 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging.testing
+
+import androidx.annotation.VisibleForTesting
+import androidx.paging.LoadType
+import androidx.paging.LoadType.APPEND
+import androidx.paging.LoadType.PREPEND
+import androidx.paging.LoadType.REFRESH
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingSource
+import androidx.paging.PagingSource.LoadParams
+import androidx.paging.PagingSource.LoadResult
+import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
+import androidx.paging.PagingState
+import androidx.paging.testing.internal.AtomicBoolean
+import kotlin.jvm.JvmSuppressWildcards
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+/**
+ * A fake [Pager] class to simulate how a real Pager and UI would load data from a PagingSource.
+ *
+ * As Paging's first load is always of type [LoadType.REFRESH], the first load operation of
+ * the [TestPager] must be a call to [refresh].
+ *
+ * This class only supports loads from a single instance of PagingSource. To simulate
+ * multi-generational Paging behavior, you must create a new [TestPager] by supplying a
+ * new instance of [PagingSource].
+ *
+ * @param config the [PagingConfig] to configure this TestPager's loading behavior.
+ * @param pagingSource the [PagingSource] to load data from.
+ */
+@VisibleForTesting
+public class TestPager<Key : Any, Value : Any>(
+    private val config: PagingConfig,
+    private val pagingSource: PagingSource<Key, Value>,
+) {
+    private val hasRefreshed = AtomicBoolean(false)
+
+    private val lock = Mutex()
+
+    private val pages = ArrayDeque<LoadResult.Page<Key, Value>>()
+
+    /**
+     * Performs a load of [LoadType.REFRESH] on the PagingSource.
+     *
+     * If initialKey != null, refresh will start loading from the supplied key.
+     *
+     * Since Paging's first load is always of [LoadType.REFRESH], this method must be the very
+     * first load operation to be called on the TestPager before either [append] or [prepend]
+     * can be called. However, other non-loading operations can still be invoked. For example,
+     * you can call [getLastLoadedPage] before any load operations.
+     *
+     * Returns the LoadResult upon refresh on the [PagingSource].
+     *
+     * @param initialKey the [Key] to start loading data from on initial refresh.
+     *
+     * @throws IllegalStateException TestPager does not support multi-generational paging behavior.
+     * As such, multiple calls to refresh() on this TestPager is illegal. The [PagingSource] passed
+     * in to this [TestPager] will also be invalidated to prevent reuse of this pager for loads.
+     * However, other [TestPager] methods that does not invoke loads can still be called,
+     * such as [getLastLoadedPage].
+     */
+    public suspend fun refresh(
+        initialKey: Key? = null
+    ): @JvmSuppressWildcards LoadResult<Key, Value> {
+        if (!hasRefreshed.compareAndSet(false, true)) {
+            pagingSource.invalidate()
+            throw IllegalStateException("TestPager does not support multi-generational access " +
+                "and refresh() can only be called once per TestPager. To start a new generation," +
+                "create a new TestPager with a new PagingSource.")
+        }
+        return doInitialLoad(initialKey)
+    }
+
+    /**
+     * Performs a load of [LoadType.APPEND] on the PagingSource.
+     *
+     * Since Paging's first load is always of [LoadType.REFRESH], [refresh] must always be called
+     * first before this [append] is called.
+     *
+     * If [PagingConfig.maxSize] is implemented, [append] loads that exceed [PagingConfig.maxSize]
+     * will cause pages to be dropped from the front of loaded pages.
+     *
+     * Returns the [LoadResult] from calling [PagingSource.load]. If the [LoadParams.key] is null,
+     * such as when there is no more data to append, this append will be no-op by returning null.
+     */
+    public suspend fun append(): @JvmSuppressWildcards LoadResult<Key, Value>? {
+        return doLoad(APPEND)
+    }
+
+    /**
+     * Performs a load of [LoadType.PREPEND] on the PagingSource.
+     *
+     * Since Paging's first load is always of [LoadType.REFRESH], [refresh] must always be called
+     * first before this [prepend] is called.
+     *
+     * If [PagingConfig.maxSize] is implemented, [prepend] loads that exceed [PagingConfig.maxSize]
+     * will cause pages to be dropped from the end of loaded pages.
+     *
+     * Returns the [LoadResult] from calling [PagingSource.load]. If the [LoadParams.key] is null,
+     * such as when there is no more data to prepend, this prepend will be no-op by returning null.
+     */
+    public suspend fun prepend(): @JvmSuppressWildcards LoadResult<Key, Value>? {
+        return doLoad(PREPEND)
+    }
+
+    /**
+     * Helper to perform REFRESH loads.
+     */
+    private suspend fun doInitialLoad(
+        initialKey: Key?
+    ): @JvmSuppressWildcards LoadResult<Key, Value> {
+        return lock.withLock {
+            pagingSource.load(
+                LoadParams.Refresh(initialKey, config.initialLoadSize, config.enablePlaceholders)
+            ).also { result ->
+                if (result is LoadResult.Page) {
+                    pages.addLast(result)
+                }
+            }
+        }
+    }
+
+    /**
+     * Helper to perform APPEND or PREPEND loads.
+     */
+    private suspend fun doLoad(loadType: LoadType): LoadResult<Key, Value>? {
+        return lock.withLock {
+            if (!hasRefreshed.get()) {
+                throw IllegalStateException("TestPager's first load operation must be a refresh. " +
+                    "Please call refresh() once before calling ${loadType.name.lowercase()}().")
+            }
+            when (loadType) {
+                REFRESH -> throw IllegalArgumentException(
+                    "For LoadType.REFRESH use doInitialLoad()"
+                )
+                APPEND -> {
+                    val key = pages.lastOrNull()?.nextKey ?: return null
+                    pagingSource.load(
+                        LoadParams.Append(key, config.pageSize, config.enablePlaceholders)
+                    ).also { result ->
+                        if (result is LoadResult.Page) {
+                            pages.addLast(result)
+                        }
+                        dropPagesOrNoOp(PREPEND)
+                    }
+                } PREPEND -> {
+                    val key = pages.firstOrNull()?.prevKey ?: return null
+                    pagingSource.load(
+                        LoadParams.Prepend(key, config.pageSize, config.enablePlaceholders)
+                    ).also { result ->
+                        if (result is LoadResult.Page) {
+                            pages.addFirst(result)
+                        }
+                        dropPagesOrNoOp(APPEND)
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the most recent [LoadResult.Page] loaded from the [PagingSource]. Null if
+     * no pages have been returned from [PagingSource]. For example, if PagingSource has
+     * only returned [LoadResult.Error] or [LoadResult.Invalid].
+     */
+    public suspend fun getLastLoadedPage(): @JvmSuppressWildcards LoadResult.Page<Key, Value>? {
+        return lock.withLock {
+            pages.lastOrNull()
+        }
+    }
+
+    /**
+     * Returns the current list of [LoadResult.Page] loaded so far from the [PagingSource].
+     */
+    public suspend fun getPages(): @JvmSuppressWildcards List<LoadResult.Page<Key, Value>> {
+        return lock.withLock {
+            pages.toList()
+        }
+    }
+
+    /**
+     * Returns a [PagingState] to generate a [LoadParams.key] by supplying it to
+     * [PagingSource.getRefreshKey]. The key returned from [PagingSource.getRefreshKey]
+     * should be used as the [LoadParams.Refresh.key] when calling [refresh] on a new generation of
+     * TestPager.
+     *
+     * The anchorPosition must be within index of loaded items, which can include
+     * placeholders if [PagingConfig.enablePlaceholders] is true. For example:
+     * - No placeholders: If 40 items have been loaded so far , anchorPosition must be
+     * in [0 .. 39].
+     * - With placeholders: If there are a total of 100 loadable items, the anchorPosition
+     * must be in [0..99].
+     *
+     * The [anchorPosition] should be the index that the user has hypothetically
+     * scrolled to on the UI. Since the [PagingState.anchorPosition] in Paging can be based
+     * on any item or placeholder currently visible on the screen, the actual
+     * value of [PagingState.anchorPosition] may not exactly match the [anchorPosition] passed
+     * to this function even if viewing the same page of data.
+     *
+     * Note that when `[PagingConfig.enablePlaceholders] = false`, the
+     * [PagingState.anchorPosition] returned from this function references the absolute index
+     * within all loadable data. For example, with items[0 - 99]:
+     * If items[20 - 30] were loaded without placeholders, anchorPosition 0 references item[20].
+     * But once translated into [PagingState.anchorPosition], anchorPosition 0 references item[0].
+     * The [PagingSource] is expected to handle this correctly within [PagingSource.getRefreshKey]
+     * when [PagingConfig.enablePlaceholders] = false.
+     *
+     * @param anchorPosition the index representing the last accessed item within the
+     * items presented on the UI, which may be a placeholder if
+     * [PagingConfig.enablePlaceholders] is true.
+     *
+     * @throws IllegalStateException if anchorPosition is out of bounds.
+     */
+    public suspend fun getPagingState(
+        anchorPosition: Int
+    ): @JvmSuppressWildcards PagingState<Key, Value> {
+        lock.withLock {
+            checkWithinBoundary(anchorPosition)
+            return PagingState(
+                pages = pages.toList(),
+                anchorPosition = anchorPosition,
+                config = config,
+                leadingPlaceholderCount = getLeadingPlaceholderCount()
+            )
+        }
+    }
+
+    /**
+     * Returns a [PagingState] to generate a [LoadParams.key] by supplying it to
+     * [PagingSource.getRefreshKey]. The key returned from [PagingSource.getRefreshKey]
+     * should be used as the [LoadParams.Refresh.key] when calling [refresh] on a new generation of
+     * TestPager.
+     *
+     * The [anchorPositionLookup] lambda should return an item that the user has hypothetically
+     * scrolled to on the UI. The item must have already been loaded prior to using this helper.
+     * To generate a PagingState anchored to a placeholder, use the overloaded [getPagingState]
+     * function instead. Since the [PagingState.anchorPosition] in Paging can be based
+     * on any item or placeholder currently visible on the screen, the actual
+     * value of [PagingState.anchorPosition] may not exactly match the anchorPosition returned
+     * from this function even if viewing the same page of data.
+     *
+     * Note that when `[PagingConfig.enablePlaceholders] = false`, the
+     * [PagingState.anchorPosition] returned from this function references the absolute index
+     * within all loadable data. For example, with items[0 - 99]:
+     * If items[20 - 30] were loaded without placeholders, anchorPosition 0 references item[20].
+     * But once translated into [PagingState.anchorPosition], anchorPosition 0 references item[0].
+     * The [PagingSource] is expected to handle this correctly within [PagingSource.getRefreshKey]
+     * when [PagingConfig.enablePlaceholders] = false.
+     *
+     * @param anchorPositionLookup the predicate to match with an item which will serve as the basis
+     * for generating the [PagingState].
+     *
+     * @throws IllegalArgumentException if the given predicate fails to match with an item.
+     */
+    public suspend fun getPagingState(
+        anchorPositionLookup: (item: @JvmSuppressWildcards Value) -> Boolean
+    ): @JvmSuppressWildcards PagingState<Key, Value> {
+        lock.withLock {
+            val indexInPages = pages.flatten().indexOfFirst {
+                anchorPositionLookup(it)
+            }
+            return when {
+                indexInPages < 0 -> throw IllegalArgumentException(
+                    "The given predicate has returned false for every loaded item. To generate a" +
+                        "PagingState anchored to an item, the expected item must have already " +
+                        "been loaded."
+                )
+                else -> {
+                    val finalIndex = if (config.enablePlaceholders) {
+                        indexInPages + (pages.firstOrNull()?.itemsBefore ?: 0)
+                    } else {
+                        indexInPages
+                    }
+                    PagingState(
+                        pages = pages.toList(),
+                        anchorPosition = finalIndex,
+                        config = config,
+                        leadingPlaceholderCount = getLeadingPlaceholderCount()
+                    )
+                }
+            }
+        }
+    }
+
+    /**
+     * Ensures the anchorPosition is within boundary of loaded data.
+     *
+     * If placeholders are enabled, the provided anchorPosition must be within boundaries of
+     * [0 .. itemCount - 1], which includes placeholders before and after loaded data.
+     *
+     * If placeholders are disabled, the provided anchorPosition must be within boundaries of
+     * [0 .. loaded data size - 1].
+     *
+     * @throws IllegalStateException if anchorPosition is out of bounds
+     */
+    private fun checkWithinBoundary(anchorPosition: Int) {
+        val loadedSize = pages.flatten().size
+        val maxBoundary = if (config.enablePlaceholders) {
+            (pages.firstOrNull()?.itemsBefore ?: 0) + loadedSize +
+                (pages.lastOrNull()?.itemsAfter ?: 0) - 1
+        } else {
+            loadedSize - 1
+        }
+        check(anchorPosition in 0..maxBoundary) {
+            "anchorPosition $anchorPosition is out of bounds between [0..$maxBoundary]. Please " +
+                "provide a valid anchorPosition."
+        }
+    }
+
+    // Number of placeholders before the first loaded item if placeholders are enabled, otherwise 0.
+    private fun getLeadingPlaceholderCount(): Int {
+        return if (config.enablePlaceholders) {
+            // itemsBefore represents placeholders before first loaded item, and can be
+            // one of three.
+            // 1. valid int if implemented
+            // 2. null if pages empty
+            // 3. COUNT_UNDEFINED if not implemented
+            val itemsBefore: Int? = pages.firstOrNull()?.itemsBefore
+            // finalItemsBefore is `null` if it is either case 2. or 3.
+            val finalItemsBefore = if (itemsBefore == null || itemsBefore == COUNT_UNDEFINED) {
+                null
+            } else {
+                itemsBefore
+            }
+            // This will ultimately return 0 if user didn't implement itemsBefore or if pages
+            // are empty, i.e. user called getPagingState before any loads.
+            finalItemsBefore ?: 0
+        } else {
+            0
+        }
+    }
+
+    private fun dropPagesOrNoOp(dropType: LoadType) {
+        require(dropType != REFRESH) {
+            "Drop loadType must be APPEND or PREPEND but got $dropType"
+        }
+
+        // check if maxSize has been set
+        if (config.maxSize == PagingConfig.MAX_SIZE_UNBOUNDED) return
+
+        var itemCount = pages.flatten().size
+        if (itemCount < config.maxSize) return
+
+        // represents the max droppable amount of items
+        val presentedItemsBeforeOrAfter = when (dropType) {
+            PREPEND -> pages.take(pages.lastIndex)
+            else -> pages.takeLast(pages.lastIndex)
+        }.fold(0) { acc, page ->
+            acc + page.data.size
+        }
+
+        var itemsDropped = 0
+
+        // mirror Paging requirement to never drop below 2 pages
+        while (pages.size > 2 && itemCount - itemsDropped > config.maxSize) {
+            val pageSize = when (dropType) {
+                PREPEND -> pages.first().data.size
+                else -> pages.last().data.size
+            }
+
+            val itemsAfterDrop = presentedItemsBeforeOrAfter - itemsDropped - pageSize
+
+            // mirror Paging behavior of ensuring prefetchDistance is fulfilled in dropped
+            // direction
+            if (itemsAfterDrop < config.prefetchDistance) break
+
+            when (dropType) {
+                PREPEND -> pages.removeFirst()
+                else -> pages.removeLast()
+            }
+
+            itemsDropped += pageSize
+        }
+    }
+}
diff --git a/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/internal/Atomics.kt b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/internal/Atomics.kt
new file mode 100644
index 0000000..8aed900
--- /dev/null
+++ b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/internal/Atomics.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.paging.testing.internal
+
+internal expect class AtomicInt(initialValue: Int) {
+    fun get(): Int
+    fun set(value: Int)
+}
+
+internal expect class AtomicBoolean(initialValue: Boolean) {
+    fun get(): Boolean
+    fun set(value: Boolean)
+    fun compareAndSet(expect: Boolean, update: Boolean): Boolean
+}
+
+internal expect class AtomicRef<T>(initialValue: T) {
+    fun get(): T
+    fun set(value: T)
+    fun getAndSet(value: T): T
+}
diff --git a/paging/paging-testing/src/commonTest/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt b/paging/paging-testing/src/commonTest/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
new file mode 100644
index 0000000..6fd3153
--- /dev/null
+++ b/paging/paging-testing/src/commonTest/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
@@ -0,0 +1,3229 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging.testing
+
+import androidx.kruth.assertThat
+import androidx.kruth.assertWithMessage
+import androidx.paging.LoadState
+import androidx.paging.LoadStates
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import androidx.paging.PagingSource
+import androidx.paging.PagingSource.LoadParams
+import androidx.paging.PagingSourceFactory
+import androidx.paging.PagingState
+import androidx.paging.cachedIn
+import androidx.paging.insertSeparators
+import kotlin.test.Test
+import kotlin.test.assertFailsWith
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+class PagerFlowSnapshotTest {
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+
+    private fun createFactory(dataFlow: Flow<List<Int>>, loadDelay: Long) =
+        WrappedPagingSourceFactory(
+            dataFlow.asPagingSourceFactory(testScope.backgroundScope),
+            loadDelay
+        )
+
+    private fun createSingleGenFactory(data: List<Int>, loadDelay: Long) =
+        WrappedPagingSourceFactory(
+            data.asPagingSourceFactory(),
+            loadDelay
+        )
+
+    @Test
+    fun initialRefresh_loadDelay0() = initialRefresh(0)
+
+    @Test
+    fun initialRefresh_loadDelay10000() = initialRefresh(10000)
+
+    private fun initialRefresh(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot()
+            // first page + prefetched page
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7)
+            )
+        }
+    }
+
+    @Test
+    fun initialRefreshSingleGen_loadDelay0() = initialRefreshSingleGen(0)
+
+    @Test
+    fun initialRefreshSingleGen_loadDelay10000() = initialRefreshSingleGen(10000)
+
+    private fun initialRefreshSingleGen(loadDelay: Long) {
+        val data = List(30) { it }
+        val pager = createPager(data, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot()
+            // first page + prefetched page
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7)
+            )
+        }
+    }
+
+    @Test
+    fun initialRefresh_emptyOperations_loadDelay0() = initialRefresh_emptyOperations(0)
+
+    @Test
+    fun initialRefresh_emptyOperations_loadDelay10000() =
+        initialRefresh_emptyOperations(10000)
+
+    private fun initialRefresh_emptyOperations(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {}
+            // first page + prefetched page
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7)
+            )
+        }
+    }
+
+    @Test
+    fun initialRefresh_withSeparators_loadDelay0() = initialRefresh_withSeparators(0)
+
+    @Test
+    fun initialRefresh_withSeparators_loadDelay10000() = initialRefresh_withSeparators(10000)
+
+    private fun initialRefresh_withSeparators(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, after: Int? ->
+                if (before != null && after != null) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot()
+            // loads 8[initial 5 + prefetch 3] items total, including separators
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, "sep", 1, "sep", 2, "sep", 3, "sep", 4)
+            )
+        }
+    }
+
+    @Test
+    fun initialRefresh_withoutPrefetch_loadDelay0() = initialRefresh_withoutPrefetch(0)
+
+    @Test
+    fun initialRefresh_withoutPrefetch_loadDelay10000() = initialRefresh_withoutPrefetch(10000)
+
+    private fun initialRefresh_withoutPrefetch(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot()
+
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4)
+            )
+        }
+    }
+
+    @Test
+    fun initialRefresh_withInitialKey_loadDelay0() = initialRefresh_withInitialKey(0)
+
+    @Test
+    fun initialRefresh_withInitialKey_loadDelay10000() = initialRefresh_withInitialKey(10000)
+
+    private fun initialRefresh_withInitialKey(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay, 10)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot()
+
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
+            )
+        }
+    }
+
+    @Test
+    fun initialRefresh_withInitialKey_withoutPrefetch_loadDelay0() =
+        initialRefresh_withInitialKey_withoutPrefetch(0)
+
+    @Test
+    fun initialRefresh_withInitialKey_withoutPrefetch_loadDelay10000() =
+        initialRefresh_withInitialKey_withoutPrefetch(10000)
+
+    private fun initialRefresh_withInitialKey_withoutPrefetch(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay, 10)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot()
+
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(10, 11, 12, 13, 14)
+            )
+        }
+    }
+
+    @Test
+    fun initialRefresh_PagingDataFrom_withoutLoadStates() {
+        val data = List(10) { it }
+        val pager = flowOf(PagingData.from(data))
+        testScope.runTest {
+            val snapshot = pager.asSnapshot()
+            // first page + prefetched page
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
+            )
+        }
+    }
+
+    @Test
+    fun initialRefresh_PagingDataFrom_withLoadStates() {
+        val data = List(10) { it }
+        val pager = flowOf(PagingData.from(data, LoadStates(
+            refresh = LoadState.NotLoading(true),
+            prepend = LoadState.NotLoading(true),
+            append = LoadState.NotLoading(true)
+        )))
+        testScope.runTest {
+            val snapshot = pager.asSnapshot()
+            // first page + prefetched page
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
+            )
+        }
+    }
+
+    @Test
+    fun emptyInitialRefresh_loadDelay0() = emptyInitialRefresh(0)
+
+    @Test
+    fun emptyInitialRefresh_loadDelay10000() = emptyInitialRefresh(10000)
+
+    private fun emptyInitialRefresh(loadDelay: Long) {
+        val dataFlow = emptyFlow<List<Int>>()
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot()
+
+            assertThat(snapshot).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+        }
+    }
+
+    @Test
+    fun emptyInitialRefreshSingleGen_loadDelay0() = emptyInitialRefreshSingleGen(0)
+
+    @Test
+    fun emptyInitialRefreshSingleGen_loadDelay10000() = emptyInitialRefreshSingleGen(10000)
+
+    private fun emptyInitialRefreshSingleGen(loadDelay: Long) {
+        val data = emptyList<Int>()
+        val pager = createPager(data, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot()
+
+            assertThat(snapshot).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+        }
+    }
+
+    @Test
+    fun emptyInitialRefresh_emptyOperations_loadDelay0() = emptyInitialRefresh_emptyOperations(0)
+
+    @Test
+    fun emptyInitialRefresh_emptyOperations_loadDelay10000() =
+        emptyInitialRefresh_emptyOperations(10000)
+
+    private fun emptyInitialRefresh_emptyOperations(loadDelay: Long) {
+        val dataFlow = emptyFlow<List<Int>>()
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot()
+
+            assertThat(snapshot).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+        }
+    }
+
+    @Test
+    fun manualRefresh_loadDelay0() = manualRefresh(0)
+
+    @Test
+    fun manualRefresh_loadDelay10000() = manualRefresh(10000)
+
+    private fun manualRefresh(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                refresh()
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4),
+            )
+        }
+    }
+
+    @Test
+    fun manualRefreshSingleGen_loadDelay0() = manualRefreshSingleGen(0)
+
+    @Test
+    fun manualRefreshSingleGen_loadDelay10000() = manualRefreshSingleGen(10000)
+
+    private fun manualRefreshSingleGen(loadDelay: Long) {
+        val data = List(30) { it }
+        val pager = createPager(data, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                refresh()
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7),
+            )
+        }
+    }
+
+    @Test
+    fun manualRefreshSingleGen_pagingSourceInvalidated() {
+        val data = List(30) { it }
+        val sources = mutableListOf<PagingSource<Int, Int>>()
+        val factory = data.asPagingSourceFactory()
+        val pager = Pager(
+            config = PagingConfig(pageSize = 3, initialLoadSize = 5),
+            pagingSourceFactory = { factory().also { sources.add(it) } },
+        ).flow
+        testScope.runTest {
+            pager.asSnapshot {
+                refresh()
+            }
+            assertThat(sources.first().invalid).isTrue()
+        }
+    }
+
+    @Test
+    fun manualRefresh_PagingDataFrom_withoutLoadStates() {
+        val data = List(10) { it }
+        val pager = flowOf(PagingData.from(data))
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                refresh()
+            }
+            // first page + prefetched page
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
+            )
+        }
+    }
+
+    @Test
+    fun manualRefresh_PagingDataFrom_withLoadStates() {
+        val data = List(10) { it }
+        val pager = flowOf(PagingData.from(data, LoadStates(
+            refresh = LoadState.NotLoading(true),
+            prepend = LoadState.NotLoading(true),
+            append = LoadState.NotLoading(true)
+        )))
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                refresh()
+            }
+            // first page + prefetched page
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
+            )
+        }
+    }
+
+    @Test
+    fun manualEmptyRefresh_loadDelay0() = manualEmptyRefresh(0)
+
+    @Test
+    fun manualEmptyRefresh_loadDelay10000() = manualEmptyRefresh(10000)
+
+    private fun manualEmptyRefresh(loadDelay: Long) {
+        val dataFlow = emptyFlow<List<Int>>()
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                refresh()
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_loadDelay0() = appendWhile(0)
+
+    @Test
+    fun appendWhile_loadDelay10000() = appendWhile(10000)
+
+    private fun appendWhile(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                appendScrollWhile { item: Int ->
+                    item < 14
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // initial load [0-4]
+                // prefetched [5-7]
+                // appended [8-16]
+                // prefetched [17-19]
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_withDrops_loadDelay0() = appendWhile_withDrops(0)
+
+    @Test
+    fun appendWhile_withDrops_loadDelay10000() = appendWhile_withDrops(10000)
+
+    private fun appendWhile_withDrops(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPagerWithDrops(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                appendScrollWhile { item ->
+                    item < 14
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // dropped [0-10]
+                listOf(11, 12, 13, 14, 15, 16, 17, 18, 19)
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_withSeparators_loadDelay0() = appendWhile_withSeparators(0)
+
+    @Test
+    fun appendWhile_withSeparators_loadDelay10000() = appendWhile_withSeparators(10000)
+
+    private fun appendWhile_withSeparators(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 9 || before == 12) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                appendScrollWhile { item ->
+                    item !is Int || item < 14
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // initial load [0-4]
+                // prefetched [5-7]
+                // appended [8-16]
+                // prefetched [17-19]
+                listOf(
+                    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "sep", 10, 11, 12, "sep", 13, 14, 15,
+                    16, 17, 18, 19
+                )
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_withoutPrefetch_loadDelay0() = appendWhile_withoutPrefetch(0)
+
+    @Test
+    fun appendWhile_withoutPrefetch_loadDelay10000() = appendWhile_withoutPrefetch(10000)
+
+    private fun appendWhile_withoutPrefetch(loadDelay: Long) {
+        val dataFlow = flowOf(List(50) { it })
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                appendScrollWhile { item: Int ->
+                    item < 14
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // initial load [0-4]
+                // appended [5-16]
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_withoutPlaceholders_loadDelay0() = appendWhile_withoutPlaceholders(0)
+
+    @Test
+    fun appendWhile_withoutPlaceholders_loadDelay10000() = appendWhile_withoutPlaceholders(10000)
+
+    private fun appendWhile_withoutPlaceholders(loadDelay: Long) {
+        val dataFlow = flowOf(List(50) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                appendScrollWhile { item: Int ->
+                    item != 14
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // initial load [0-4]
+                // prefetched [5-7]
+                // appended [8-16]
+                // prefetched [17-19]
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
+            )
+        }
+    }
+
+    @Test
+    fun prependWhile_loadDelay0() = prependWhile(0)
+
+    @Test
+    fun prependWhile_loadDelay10000() = prependWhile(10000)
+
+    private fun prependWhile(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay, 20)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                prependScrollWhile { item: Int ->
+                    item > 14
+                }
+            }
+            // initial load [20-24]
+            // prefetched [17-19], [25-27]
+            // prepended [14-16]
+            // prefetched [11-13]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27)
+            )
+        }
+    }
+
+    @Test
+    fun prependWhile_withDrops_loadDelay0() = prependWhile_withDrops(0)
+
+    @Test
+    fun prependWhile_withDrops_loadDelay10000() = prependWhile_withDrops(10000)
+
+    private fun prependWhile_withDrops(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPagerWithDrops(dataFlow, loadDelay, 20)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                prependScrollWhile { item: Int ->
+                    item > 14
+                }
+            }
+            // dropped [20-27]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(11, 12, 13, 14, 15, 16, 17, 18, 19)
+            )
+        }
+    }
+
+    @Test
+    fun prependWhile_withSeparators_loadDelay0() = prependWhile_withSeparators(0)
+
+    @Test
+    fun prependWhile_withSeparators_loadDelay10000() = prependWhile_withSeparators(10000)
+
+    private fun prependWhile_withSeparators(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay, 20).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 14 || before == 18) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                prependScrollWhile { item ->
+                    item !is Int || item > 14
+                }
+            }
+            // initial load [20-24]
+            // prefetched [17-19], no append prefetch because separator fulfilled prefetchDistance
+            // prepended [14-16]
+            // prefetched [11-13]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    11, 12, 13, 14, "sep", 15, 16, 17, 18, "sep", 19, 20, 21, 22, 23,
+                    24, 25, 26, 27
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependWhile_withoutPrefetch_loadDelay0() = prependWhile_withoutPrefetch(0)
+
+    @Test
+    fun prependWhile_withoutPrefetch_loadDelay10000() = prependWhile_withoutPrefetch(10000)
+
+    private fun prependWhile_withoutPrefetch(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay, 20)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                prependScrollWhile { item: Int ->
+                    item > 14
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // initial load [20-24]
+                // prepended [14-19]
+                listOf(14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)
+            )
+        }
+    }
+
+    @Test
+    fun prependWhile_withoutPlaceholders_loadDelay0() = prependWhile_withoutPlaceholders(0)
+
+    @Test
+    fun prependWhile_withoutPlaceholders_loadDelay10000() = prependWhile_withoutPlaceholders(10000)
+
+    private fun prependWhile_withoutPlaceholders(loadDelay: Long) {
+        val dataFlow = flowOf(List(50) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay, 30)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                prependScrollWhile { item: Int ->
+                    item != 22
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // initial load [30-34]
+                // prefetched [27-29], [35-37]
+                // prepended [21-26]
+                // prefetched [18-20]
+                listOf(
+                    18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37
+                )
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_withInitialKey_loadDelay0() = appendWhile_withInitialKey(0)
+
+    @Test
+    fun appendWhile_withInitialKey_loadDelay10000() = appendWhile_withInitialKey(10000)
+
+    private fun appendWhile_withInitialKey(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay, 10)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                appendScrollWhile { item: Int ->
+                    item < 18
+                }
+            }
+            // initial load [10-14]
+            // prefetched [7-9], [15-17]
+            // appended [18-20]
+            // prefetched [21-23]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_withInitialKey_withoutPlaceholders_loadDelay0() =
+        appendWhile_withInitialKey_withoutPlaceholders(0)
+
+    @Test
+    fun appendWhile_withInitialKey_withoutPlaceholders_loadDelay10000() =
+        appendWhile_withInitialKey_withoutPlaceholders(10000)
+
+    private fun appendWhile_withInitialKey_withoutPlaceholders(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay, 10)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                appendScrollWhile { item: Int ->
+                    item != 19
+                }
+            }
+            // initial load [10-14]
+            // prefetched [7-9], [15-17]
+            // appended [18-20]
+            // prefetched [21-23]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_withInitialKey_withoutPrefetch_loadDelay0() =
+        appendWhile_withInitialKey_withoutPrefetch(0)
+
+    @Test
+    fun appendWhile_withInitialKey_withoutPrefetch_loadDelay10000() =
+        appendWhile_withInitialKey_withoutPrefetch(10000)
+
+    private fun appendWhile_withInitialKey_withoutPrefetch(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay, 10)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                appendScrollWhile { item: Int ->
+                    item < 18
+                }
+            }
+            // initial load [10-14]
+            // appended [15-20]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
+            )
+        }
+    }
+
+    @Test
+    fun prependWhile_withoutInitialKey_loadDelay0() = prependWhile_withoutInitialKey(0)
+
+    @Test
+    fun prependWhile_withoutInitialKey_loadDelay10000() = prependWhile_withoutInitialKey(10000)
+
+    private fun prependWhile_withoutInitialKey(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                prependScrollWhile { item: Int ->
+                    item > -3
+                }
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendWhile_loadDelay0() = consecutiveAppendWhile(0)
+
+    @Test
+    fun consecutiveAppendWhile_loadDelay10000() = consecutiveAppendWhile(10000)
+
+    private fun consecutiveAppendWhile(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot {
+                appendScrollWhile { item: Int ->
+                    item < 7
+                }
+            }
+
+            val snapshot2 = pager.asSnapshot {
+                appendScrollWhile { item: Int ->
+                    item < 22
+                }
+            }
+
+            // includes initial load, 1st page, 2nd page (from prefetch)
+            assertThat(snapshot1).containsExactlyElementsIn(
+                List(11) { it }
+            )
+
+            // includes extra page from prefetch
+            assertThat(snapshot2).containsExactlyElementsIn(
+                List(26) { it }
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependWhile_loadDelay0() = consecutivePrependWhile(0)
+
+    @Test
+    fun consecutivePrependWhile_loadDelay10000() = consecutivePrependWhile(10000)
+
+    private fun consecutivePrependWhile(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay, 20)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot {
+                prependScrollWhile { item: Int ->
+                    item > 17
+                }
+            }
+            assertThat(snapshot1).containsExactlyElementsIn(
+                listOf(17, 18, 19, 20, 21, 22, 23, 24)
+            )
+            val snapshot2 = pager.asSnapshot {
+                prependScrollWhile { item: Int ->
+                    item > 11
+                }
+            }
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_outOfBounds_returnsCurrentlyLoadedItems_loadDelay0() =
+        appendWhile_outOfBounds_returnsCurrentlyLoadedItems(0)
+
+    @Test
+    fun appendWhile_outOfBounds_returnsCurrentlyLoadedItems_loadDelay10000() =
+        appendWhile_outOfBounds_returnsCurrentlyLoadedItems(10000)
+
+    private fun appendWhile_outOfBounds_returnsCurrentlyLoadedItems(loadDelay: Long) {
+        val dataFlow = flowOf(List(10) { it })
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                appendScrollWhile { item: Int ->
+                    // condition scrolls till end of data since we only have 10 items
+                    item < 18
+                }
+            }
+
+            // returns the items loaded before index becomes out of bounds
+            assertThat(snapshot).containsExactlyElementsIn(
+                List(10) { it }
+            )
+        }
+    }
+
+    @Test
+    fun prependWhile_outOfBounds_returnsCurrentlyLoadedItems_loadDelay0() =
+        prependWhile_outOfBounds_returnsCurrentlyLoadedItems(0)
+
+    @Test
+    fun prependWhile_outOfBounds_returnsCurrentlyLoadedItems_loadDelay10000() =
+        prependWhile_outOfBounds_returnsCurrentlyLoadedItems(10000)
+
+    private fun prependWhile_outOfBounds_returnsCurrentlyLoadedItems(loadDelay: Long) {
+        val dataFlow = flowOf(List(20) { it })
+        val pager = createPager(dataFlow, loadDelay, 10)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                prependScrollWhile { item: Int ->
+                    // condition scrolls till index = 0
+                    item > -3
+                }
+            }
+            // returns the items loaded before index becomes out of bounds
+            assertThat(snapshot).containsExactlyElementsIn(
+                // initial load [10-14]
+                // prefetched [7-9], [15-17]
+                // prepended [0-6]
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
+            )
+        }
+    }
+
+    @Test
+    fun refreshAndAppendWhile_loadDelay0() = refreshAndAppendWhile(0)
+
+    @Test
+    fun refreshAndAppendWhile_loadDelay10000() = refreshAndAppendWhile(10000)
+
+    private fun refreshAndAppendWhile(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                refresh() // triggers second gen
+                appendScrollWhile { item: Int ->
+                    item < 10
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
+            )
+        }
+    }
+
+    @Test
+    fun refreshAndPrependWhile_loadDelay0() = refreshAndPrependWhile(0)
+
+    @Test
+    fun refreshAndPrependWhile_loadDelay10000() = refreshAndPrependWhile(10000)
+
+    private fun refreshAndPrependWhile(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay, 20).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // this prependScrollWhile does not cause paging to load more items
+                // but it helps this test register a non-null anchorPosition so the upcoming
+                // refresh doesn't start at index 0
+                prependScrollWhile { item -> item > 20 }
+                // triggers second gen
+                refresh()
+                prependScrollWhile { item: Int ->
+                    item > 12
+                }
+            }
+            // second gen initial load, anchorPos = 20, refreshKey = 18, loaded
+            // initial load [18-22]
+            // prefetched [15-17], [23-25]
+            // prepended [12-14]
+            // prefetched [9-11]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25)
+            )
+        }
+    }
+
+    @Test
+    fun appendWhileAndRefresh_loadDelay0() = appendWhileAndRefresh(0)
+
+    @Test
+    fun appendWhileAndRefresh_loadDelay10000() = appendWhileAndRefresh(10000)
+
+    private fun appendWhileAndRefresh(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                appendScrollWhile { item: Int ->
+                    item < 10
+                }
+                refresh()
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // second gen initial load, anchorPos = 10, refreshKey = 8
+                // initial load [8-12]
+                // prefetched [5-7], [13-15]
+                listOf(5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
+            )
+        }
+    }
+
+    @Test
+    fun prependWhileAndRefresh_loadDelay0() = prependWhileAndRefresh(0)
+
+    @Test
+    fun prependWhileAndRefresh_loadDelay10000() = prependWhileAndRefresh(10000)
+
+    private fun prependWhileAndRefresh(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, loadDelay, 15).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                prependScrollWhile { item: Int ->
+                    item > 8
+                }
+                refresh()
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // second gen initial load, anchorPos = 8, refreshKey = 6
+                // initial load [6-10]
+                // prefetched [3-5], [11-13]
+                listOf(3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_fromFlow_loadDelay0() = consecutiveGenerations_fromFlow(0)
+
+    @Test
+    fun consecutiveGenerations_fromFlow_loadDelay10000() = consecutiveGenerations_fromFlow(10000)
+
+    private fun consecutiveGenerations_fromFlow(loadDelay: Long) {
+        // wait for 500 + loadDelay between each emission
+        val dataFlow = flow {
+            emit(emptyList())
+            delay(500 + loadDelay)
+
+            emit(List(30) { it })
+            delay(500 + loadDelay)
+
+            emit(List(30) { it + 30 })
+        }
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot()
+            assertThat(snapshot1).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+
+            delay(500)
+
+            val snapshot2 = pager.asSnapshot()
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4)
+            )
+
+            delay(500)
+
+            val snapshot3 = pager.asSnapshot()
+            assertThat(snapshot3).containsExactlyElementsIn(
+                listOf(30, 31, 32, 33, 34)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_PagingDataFrom_withoutLoadStates_loadDelay0() =
+        consecutiveGenerations_PagingDataFrom_withoutLoadStates(0)
+
+    @Test
+    fun consecutiveGenerations_PagingDataFrom_withoutLoadStates_loadDelay10000() =
+        consecutiveGenerations_PagingDataFrom_withoutLoadStates(10000)
+
+    private fun consecutiveGenerations_PagingDataFrom_withoutLoadStates(loadDelay: Long) {
+        // wait for 500 + loadDelay between each emission
+        val pager = flow {
+            emit(PagingData.empty())
+            delay(500 + loadDelay)
+
+            emit(PagingData.from(List(10) { it }))
+            delay(500 + loadDelay)
+
+            emit(PagingData.from(List(10) { it + 30 }))
+        }
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot()
+            assertWithMessage("Only the last generation should be loaded without LoadStates")
+                .that(snapshot1).containsExactlyElementsIn(
+                listOf(30, 31, 32, 33, 34, 35, 36, 37, 38, 39)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_PagingDataFrom_withLoadStates_loadDelay0() =
+        consecutiveGenerations_PagingDataFrom_withLoadStates(0)
+
+    @Test
+    fun consecutiveGenerations_PagingDataFrom_withLoadStates_loadDelay10000() =
+        consecutiveGenerations_PagingDataFrom_withLoadStates(10000)
+
+    private fun consecutiveGenerations_PagingDataFrom_withLoadStates(loadDelay: Long) {
+        // wait for 500 + loadDelay between each emission
+        val pager = flow {
+            emit(PagingData.empty(LoadStates(
+                refresh = LoadState.NotLoading(true),
+                prepend = LoadState.NotLoading(true),
+                append = LoadState.NotLoading(true)
+            )))
+            delay(500 + loadDelay)
+
+            emit(PagingData.from(List(10) { it }, LoadStates(
+                refresh = LoadState.NotLoading(true),
+                prepend = LoadState.NotLoading(true),
+                append = LoadState.NotLoading(true)
+            )))
+            delay(500 + loadDelay)
+
+            emit(PagingData.from(List(10) { it + 30 }, LoadStates(
+                refresh = LoadState.NotLoading(true),
+                prepend = LoadState.NotLoading(true),
+                append = LoadState.NotLoading(true)
+            )))
+        }.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot()
+            assertThat(snapshot1).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+
+            delay(500 + loadDelay)
+
+            val snapshot2 = pager.asSnapshot()
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
+            )
+
+            delay(500 + loadDelay)
+
+            val snapshot3 = pager.asSnapshot()
+            assertThat(snapshot3).containsExactlyElementsIn(
+                listOf(30, 31, 32, 33, 34, 35, 36, 37, 38, 39)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_fromSharedFlow_emitAfterRefresh_loadDelay0() =
+        consecutiveGenerations_fromSharedFlow_emitAfterRefresh(0)
+
+    @Test
+    fun consecutiveGenerations_fromSharedFlow_emitAfterRefresh_loadDelay10000() =
+        consecutiveGenerations_fromSharedFlow_emitAfterRefresh(10000)
+
+    private fun consecutiveGenerations_fromSharedFlow_emitAfterRefresh(loadDelay: Long) {
+        val dataFlow = MutableSharedFlow<List<Int>>()
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot()
+            assertThat(snapshot1).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+
+            val snapshot2 = pager.asSnapshot {
+                dataFlow.emit(List(30) { it })
+            }
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4)
+            )
+
+            val snapshot3 = pager.asSnapshot {
+                dataFlow.emit(List(30) { it + 30 })
+            }
+            assertThat(snapshot3).containsExactlyElementsIn(
+                listOf(30, 31, 32, 33, 34)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_fromSharedFlow_emitBeforeRefresh_loadDelay0() =
+        consecutiveGenerations_fromSharedFlow_emitBeforeRefresh(0)
+
+    @Test
+    fun consecutiveGenerations_fromSharedFlow_emitBeforeRefresh_loadDelay10000() =
+        consecutiveGenerations_fromSharedFlow_emitBeforeRefresh(10000)
+
+    private fun consecutiveGenerations_fromSharedFlow_emitBeforeRefresh(loadDelay: Long) {
+        val dataFlow = MutableSharedFlow<List<Int>>()
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            dataFlow.emit(emptyList())
+            val snapshot1 = pager.asSnapshot()
+            assertThat(snapshot1).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+
+            dataFlow.emit(List(30) { it })
+            val snapshot2 = pager.asSnapshot()
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4)
+            )
+
+            dataFlow.emit(List(30) { it + 30 })
+            val snapshot3 = pager.asSnapshot()
+            assertThat(snapshot3).containsExactlyElementsIn(
+                listOf(30, 31, 32, 33, 34)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_nonNullRefreshKey_loadDelay0() =
+        consecutiveGenerations_nonNullRefreshKey(0)
+
+    @Test
+    fun consecutiveGenerations_nonNullRefreshKey_loadDelay10000() =
+        consecutiveGenerations_nonNullRefreshKey(10000)
+
+    private fun consecutiveGenerations_nonNullRefreshKey(loadDelay: Long) {
+        val dataFlow = flow {
+            // first gen
+            emit(List(20) { it })
+            // wait for refresh + append
+            delay((500 + loadDelay) * 2)
+            // second gen
+            emit(List(20) { it })
+        }
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot {
+                // we scroll to register a non-null anchorPos
+                appendScrollWhile { item: Int ->
+                    item < 5
+                }
+            }
+            assertThat(snapshot1).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7)
+            )
+
+            delay(1000)
+            val snapshot2 = pager.asSnapshot()
+            // anchorPos = 5, refreshKey = 3
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(3, 4, 5, 6, 7)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_withInitialKey_nullRefreshKey_loadDelay0() =
+        consecutiveGenerations_withInitialKey_nullRefreshKey(0)
+
+    @Test
+    fun consecutiveGenerations_withInitialKey_nullRefreshKey_loadDelay10000() =
+        consecutiveGenerations_withInitialKey_nullRefreshKey(10000)
+
+    private fun consecutiveGenerations_withInitialKey_nullRefreshKey(loadDelay: Long) {
+        // wait for 500 + loadDelay between each emission
+        val dataFlow = flow {
+            // first gen
+            emit(List(20) { it })
+            delay(500 + loadDelay)
+            // second gen
+            emit(List(20) { it })
+        }
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay, 10)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot()
+            assertThat(snapshot1).containsExactlyElementsIn(
+                listOf(10, 11, 12, 13, 14)
+            )
+
+            delay(500)
+            val snapshot2 = pager.asSnapshot()
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_withInitialKey_nonNullRefreshKey_loadDelay0() =
+        consecutiveGenerations_withInitialKey_nonNullRefreshKey(0)
+
+    @Test
+    fun consecutiveGenerations_withInitialKey_nonNullRefreshKey_loadDelay10000() =
+        consecutiveGenerations_withInitialKey_nonNullRefreshKey(10000)
+
+    private fun consecutiveGenerations_withInitialKey_nonNullRefreshKey(loadDelay: Long) {
+        val dataFlow = flow {
+            // first gen
+            emit(List(20) { it })
+            // wait for refresh + append
+            delay((500 + loadDelay) * 2)
+            // second gen
+            emit(List(20) { it })
+        }
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay, 10)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot {
+                // we scroll to register a non-null anchorPos
+                appendScrollWhile { item: Int ->
+                    item < 15
+                }
+            }
+            assertThat(snapshot1).containsExactlyElementsIn(
+                listOf(10, 11, 12, 13, 14, 15, 16, 17)
+            )
+
+            delay(1000)
+            val snapshot2 = pager.asSnapshot()
+            // anchorPos = 15, refreshKey = 13
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(13, 14, 15, 16, 17)
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_loadDelay0() = prependScroll(0)
+
+    @Test
+    fun prependScroll_loadDelay10000() = prependScroll(10000)
+
+    private fun prependScroll(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(42)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withDrops_loadDelay0() = prependScroll_withDrops(0)
+
+    @Test
+    fun prependScroll_withDrops_loadDelay10000() = prependScroll_withDrops(10000)
+
+    private fun prependScroll_withDrops(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithDrops(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(42)
+            }
+            // dropped [47-57]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(38, 39, 40, 41, 42, 43, 44, 45, 46)
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withSeparators_loadDelay0() = prependScroll_withSeparators(0)
+
+    @Test
+    fun prependScroll_withSeparators_loadDelay10000() = prependScroll_withSeparators(10000)
+
+    private fun prependScroll_withSeparators(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay, 50).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 42 || before == 49) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(42)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, "sep", 43, 44, 45, 46, 47, 48, 49, "sep", 50, 51, 52,
+                    53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependScroll_loadDelay0() = consecutivePrependScroll(0)
+
+    @Test
+    fun consecutivePrependScroll_loadDelay10000() = consecutivePrependScroll(10000)
+
+    private fun consecutivePrependScroll(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(42)
+                scrollTo(38)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [38-46]
+            // prefetched [35-37]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
+                    51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependScroll_multiSnapshots_loadDelay0() =
+        consecutivePrependScroll_multiSnapshots(0)
+
+    @Test
+    fun consecutivePrependScroll_multiSnapshots_loadDelay10000() =
+        consecutivePrependScroll_multiSnapshots(10000)
+
+    private fun consecutivePrependScroll_multiSnapshots(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(42)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+
+            val snapshot2 = pager.asSnapshot {
+                scrollTo(38)
+            }
+            // prefetched [35-37]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(
+                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
+                    51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_indexOutOfBounds_loadDelay0() = prependScroll_indexOutOfBounds(0)
+
+    @Test
+    fun prependScroll_indexOutOfBounds_loadDelay10000() = prependScroll_indexOutOfBounds(10000)
+
+    private fun prependScroll_indexOutOfBounds(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay, 5).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(-5)
+            }
+            // ensure index is capped when no more data to load
+            // initial load [5-9]
+            // prefetched [2-4], [10-12]
+            // scrollTo prepended [0-1]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_accessPageBoundary_loadDelay0() = prependScroll_accessPageBoundary(0)
+
+    @Test
+    fun prependScroll_accessPageBoundary_loadDelay10000() = prependScroll_accessPageBoundary(10000)
+
+    private fun prependScroll_accessPageBoundary(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay, 50).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(47)
+            }
+            // ensure that SnapshotLoader waited for last prefetch before returning
+            // initial load [50-54]
+            // prefetched [47-49], [55-57] - expect only one extra page to be prefetched after this
+            // scrollTo prepended [44-46]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withoutPrefetch_loadDelay0() = prependScroll_withoutPrefetch(0)
+
+    @Test
+    fun prependScroll_withoutPrefetch_loadDelay10000() = prependScroll_withoutPrefetch(10000)
+
+    private fun prependScroll_withoutPrefetch(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(42)
+            }
+            // initial load [50-54]
+            // prepended [41-49]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54)
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withoutPlaceholders_loadDelay0() = prependScroll_withoutPlaceholders(0)
+
+    @Test
+    fun prependScroll_withoutPlaceholders_loadDelay10000() =
+        prependScroll_withoutPlaceholders(10000)
+
+    private fun prependScroll_withoutPlaceholders(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay, 50)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // Without placeholders, first loaded page always starts at index[0]
+                scrollTo(0)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [44-46]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withoutPlaceholders_indexOutOfBounds_loadDelay0() =
+        prependScroll_withoutPlaceholders_indexOutOfBounds(0)
+
+    @Test
+    fun prependScroll_withoutPlaceholders_indexOutOfBounds_loadDelay10000() =
+        prependScroll_withoutPlaceholders_indexOutOfBounds(10000)
+
+    private fun prependScroll_withoutPlaceholders_indexOutOfBounds(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay, 50)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(-5)
+            }
+            // ensure it honors negative indices starting with index[0] = item[47]
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // scrollTo prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withoutPlaceholders_indexOutOfBoundsIsCapped_loadDelay0() =
+        prependScroll_withoutPlaceholders_indexOutOfBoundsIsCapped(0)
+
+    @Test
+    fun prependScroll_withoutPlaceholders_indexOutOfBoundsIsCapped_loadDelay10000() =
+        prependScroll_withoutPlaceholders_indexOutOfBoundsIsCapped(10000)
+
+    private fun prependScroll_withoutPlaceholders_indexOutOfBoundsIsCapped(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay, 5)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(-5)
+            }
+            // ensure index is capped when no more data to load
+            // initial load [5-9]
+            // prefetched [2-4], [10-12]
+            // scrollTo prepended [0-1]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependScroll_withoutPlaceholders_loadDelay0() =
+        consecutivePrependScroll_withoutPlaceholders(0)
+
+    @Test
+    fun consecutivePrependScroll_withoutPlaceholders_loadDelay10000() =
+        consecutivePrependScroll_withoutPlaceholders(10000)
+
+    private fun consecutivePrependScroll_withoutPlaceholders(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay, 50)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // Without placeholders, first loaded page always starts at index[0]
+                scrollTo(-1)
+                // Without placeholders, first loaded page always starts at index[0]
+                scrollTo(-5)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // first scrollTo prepended [41-46]
+            // index[0] is now anchored to [41]
+            // second scrollTo prepended [35-40]
+            // prefetched [32-34]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
+                    50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependScroll_withoutPlaceholders_multiSnapshot_loadDelay0() =
+        consecutivePrependScroll_withoutPlaceholders_multiSnapshot(0)
+
+    @Test
+    fun consecutivePrependScroll_withoutPlaceholders_multiSnapshot_loadDelay10000() =
+        consecutivePrependScroll_withoutPlaceholders_multiSnapshot(10000)
+
+    private fun consecutivePrependScroll_withoutPlaceholders_multiSnapshot(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay, 50)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // Without placeholders, first loaded page always starts at index[0]
+                scrollTo(-1)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // scrollTo prepended [44-46]
+            // prefetched [41-43]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+
+            val snapshot2 = pager.asSnapshot {
+                // Without placeholders, first loaded page always starts at index[0]
+                scrollTo(-5)
+            }
+            // scrollTo prepended [35-40]
+            // prefetched [32-34]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(
+                    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
+                    50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withoutPlaceholders_noPrefetchTriggered_loadDelay0() =
+        prependScroll_withoutPlaceholders_noPrefetchTriggered(0)
+
+    @Test
+    fun prependScroll_withoutPlaceholders_noPrefetchTriggered_loadDelay10000() =
+        prependScroll_withoutPlaceholders_noPrefetchTriggered(10000)
+
+    private fun prependScroll_withoutPlaceholders_noPrefetchTriggered(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = Pager(
+            config = PagingConfig(
+                pageSize = 4,
+                initialLoadSize = 8,
+                enablePlaceholders = false,
+                // a small prefetchDistance to prevent prefetch until we scroll to boundary
+                prefetchDistance = 1
+            ),
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow, loadDelay),
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // Without placeholders, first loaded page always starts at index[0]
+                scrollTo(0)
+            }
+            // initial load [50-57]
+            // no prefetch after initial load because it didn't hit prefetch distance
+            // scrollTo prepended [46-49]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_loadDelay0() = appendScroll(0)
+
+    @Test
+    fun appendScroll_loadDelay10000() = appendScroll(10000)
+
+    private fun appendScroll(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_withDrops_loadDelay0() = appendScroll_withDrops(0)
+
+    @Test
+    fun appendScroll_withDrops_loadDelay10000() = appendScroll_withDrops(10000)
+
+    private fun appendScroll_withDrops(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithDrops(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(12)
+            }
+            // dropped [0-7]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_withSeparators_loadDelay0() = appendScroll_withSeparators(0)
+
+    @Test
+    fun appendScroll_withSeparators_loadDelay10000() = appendScroll_withSeparators(10000)
+
+    private fun appendScroll_withSeparators(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 0 || before == 14) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, "sep", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, "sep", 15, 16)
+            )
+        }
+    }
+
+        @Test
+    fun consecutiveAppendScroll_loadDelay0() = consecutiveAppendScroll(0)
+
+    @Test
+    fun consecutiveAppendScroll_loadDelay10000() = consecutiveAppendScroll(10000)
+
+    private fun consecutiveAppendScroll(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(12)
+                scrollTo(18)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-19]
+            // prefetched [20-22]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                21, 22)
+            )
+        }
+    }
+
+        @Test
+    fun consecutiveAppendScroll_multiSnapshots_loadDelay0() =
+        consecutiveAppendScroll_multiSnapshots(0)
+
+    @Test
+    fun consecutiveAppendScroll_multiSnapshots_loadDelay10000() =
+        consecutiveAppendScroll_multiSnapshots(10000)
+
+    private fun consecutiveAppendScroll_multiSnapshots(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+
+            val snapshot2 = pager.asSnapshot {
+                scrollTo(18)
+            }
+            // appended [17-19]
+            // prefetched [20-22]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
+                    18, 19, 20, 21, 22
+                )
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_indexOutOfBounds_loadDelay0() = appendScroll_indexOutOfBounds(0)
+
+    @Test
+    fun appendScroll_indexOutOfBounds_loadDelay10000() = appendScroll_indexOutOfBounds(10000)
+
+    private fun appendScroll_indexOutOfBounds(loadDelay: Long) {
+        val dataFlow = flowOf(List(15) { it })
+        val pager = createPager(dataFlow, loadDelay).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // index out of bounds
+                scrollTo(50)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // scrollTo appended [8-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_accessPageBoundary_loadDelay0() = appendScroll_accessPageBoundary(0)
+
+    @Test
+    fun appendScroll_accessPageBoundary_loadDelay10000() = appendScroll_accessPageBoundary(10000)
+
+    private fun appendScroll_accessPageBoundary(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // after initial Load and prefetch, max loaded index is 7
+                scrollTo(7)
+            }
+            // ensure that SnapshotLoader waited for last prefetch before returning
+            // initial load [0-4]
+            // prefetched [5-7] - expect only one extra page to be prefetched after this
+            // scrollTo appended [8-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_withoutPrefetch_loadDelay0() = appendScroll_withoutPrefetch(0)
+
+    @Test
+    fun appendScroll_withoutPrefetch_loadDelay10000() = appendScroll_withoutPrefetch(10000)
+
+    private fun appendScroll_withoutPrefetch(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPrefetch(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(10)
+            }
+            // initial load [0-4]
+            // appended [5-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_withoutPlaceholders_loadDelay0() = appendScroll_withoutPlaceholders(0)
+
+    @Test
+    fun appendScroll_withoutPlaceholders_loadDelay10000() = appendScroll_withoutPlaceholders(10000)
+
+    private fun appendScroll_withoutPlaceholders(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // scroll to max loaded index
+                scrollTo(7)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // scrollTo appended [8-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_withoutPlaceholders_indexOutOfBounds_loadDelay0() =
+        appendScroll_withoutPlaceholders_indexOutOfBounds(0)
+
+    @Test
+    fun appendScroll_withoutPlaceholders_indexOutOfBounds_loadDelay10000() =
+        appendScroll_withoutPlaceholders_indexOutOfBounds(10000)
+
+    private fun appendScroll_withoutPlaceholders_indexOutOfBounds(loadDelay: Long) {
+        val dataFlow = flowOf(List(20) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // 12 is larger than differ.size = 8 after initial refresh
+                scrollTo(12)
+            }
+            // ensure it honors scrollTo indices >= differ.size
+            // initial load [0-4]
+            // prefetched [5-7]
+            // scrollTo appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendToScroll_withoutPlaceholders_indexOutOfBoundsIsCapped_loadDelay0() =
+        appendToScroll_withoutPlaceholders_indexOutOfBoundsIsCapped(0)
+
+    @Test
+    fun appendToScroll_withoutPlaceholders_indexOutOfBoundsIsCapped_loadDelay10000() =
+        appendToScroll_withoutPlaceholders_indexOutOfBoundsIsCapped(10000)
+
+    private fun appendToScroll_withoutPlaceholders_indexOutOfBoundsIsCapped(loadDelay: Long) {
+        val dataFlow = flowOf(List(20) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(50)
+            }
+            // ensure index is still capped to max index available
+            // initial load [0-4]
+            // prefetched [5-7]
+            // scrollTo appended [8-19]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendScroll_withoutPlaceholders_loadDelay0() =
+        consecutiveAppendScroll_withoutPlaceholders(0)
+
+    @Test
+    fun consecutiveAppendScroll_withoutPlaceholders_loadDelay10000() =
+        consecutiveAppendScroll_withoutPlaceholders(10000)
+
+    private fun consecutiveAppendScroll_withoutPlaceholders(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(12)
+                scrollTo(17)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // first scrollTo appended [8-13]
+            // second scrollTo appended [14-19]
+            // prefetched [19-22]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                21, 22)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendScroll_withoutPlaceholders_multiSnapshot_loadDelay0() =
+        consecutiveAppendScroll_withoutPlaceholders_multiSnapshot(0)
+
+    @Test
+    fun consecutiveAppendScroll_withoutPlaceholders_multiSnapshot_loadDelay10000() =
+        consecutiveAppendScroll_withoutPlaceholders_multiSnapshot(10000)
+
+    private fun consecutiveAppendScroll_withoutPlaceholders_multiSnapshot(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // scrollTo appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+
+            val snapshot2 = pager.asSnapshot {
+                scrollTo(17)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // first scrollTo appended [8-13]
+            // second scrollTo appended [14-19]
+            // prefetched [19-22]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                    21, 22)
+            )
+        }
+    }
+
+    @Test
+    fun scrollTo_indexAccountsForSeparators_loadDelay0() = scrollTo_indexAccountsForSeparators(0)
+
+    @Test
+    fun scrollTo_indexAccountsForSeparators_loadDelay10000() =
+        scrollTo_indexAccountsForSeparators(10000)
+
+    private fun scrollTo_indexAccountsForSeparators(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay)
+        val pagerWithSeparator = pager.map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 6) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(8)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-10]
+            // prefetched [11-13]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
+            )
+
+            val snapshotWithSeparator = pagerWithSeparator.asSnapshot {
+                scrollTo(8)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-10]
+            // no prefetch on [11-13] because separator fulfilled prefetchDistance
+            assertThat(snapshotWithSeparator).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, "sep", 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_loadDelay0() = prependFling(0)
+
+    @Test
+    fun prependFling_loadDelay10000() = prependFling(10000)
+
+    private fun prependFling(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(42)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_withDrops_loadDelay0() = prependFling_withDrops(0)
+
+    @Test
+    fun prependFling_withDrops_loadDelay10000() = prependFling_withDrops(10000)
+
+    private fun prependFling_withDrops(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithDrops(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(42)
+            }
+            // dropped [47-57]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(38, 39, 40, 41, 42, 43, 44, 45, 46)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_withSeparators_loadDelay0() = prependFling_withSeparators(0)
+
+    @Test
+    fun prependFling_withSeparators_loadDelay10000() = prependFling_withSeparators(10000)
+
+    private fun prependFling_withSeparators(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay, 50).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 42 || before == 49) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(42)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, "sep", 43, 44, 45, 46, 47, 48, 49, "sep", 50, 51, 52,
+                    53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependFling_loadDelay0() = consecutivePrependFling(0)
+
+    @Test
+    fun consecutivePrependFling_loadDelay10000() = consecutivePrependFling(10000)
+
+    private fun consecutivePrependFling(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(42)
+                flingTo(38)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [38-46]
+            // prefetched [35-37]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
+                    51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependFling_multiSnapshots_loadDelay0() =
+        consecutivePrependFling_multiSnapshots(0)
+
+    @Test
+    fun consecutivePrependFling_multiSnapshots_loadDelay10000() =
+        consecutivePrependFling_multiSnapshots(10000)
+
+    private fun consecutivePrependFling_multiSnapshots(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(42)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+
+            val snapshot2 = pager.asSnapshot {
+                flingTo(38)
+            }
+            // prefetched [35-37]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(
+                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
+                    51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_jump_loadDelay0() = prependFling_jump(0)
+
+    @Test
+    fun prependFling_jump_loadDelay10000() = prependFling_jump(10000)
+
+    private fun prependFling_jump(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithJump(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(30)
+                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
+            }
+            // initial load [28-32]
+            // prefetched [25-27], [33-35]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_scrollThenJump_loadDelay0() = prependFling_scrollThenJump(0)
+
+    @Test
+    fun prependFling_scrollThenJump_loadDelay10000() = prependFling_scrollThenJump(10000)
+
+    private fun prependFling_scrollThenJump(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithJump(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(43)
+                flingTo(30)
+                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
+            }
+            // initial load [28-32]
+            // prefetched [25-27], [33-35]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_jumpThenFling_loadDelay0() = prependFling_jumpThenFling(0)
+
+    @Test
+    fun prependFling_jumpThenFling_loadDelay10000() = prependFling_jumpThenFling(10000)
+
+    private fun prependFling_jumpThenFling(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithJump(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(30)
+                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
+                flingTo(22)
+            }
+            // initial load [28-32]
+            // prefetched [25-27], [33-35]
+            // flingTo prepended [22-24]
+            // prefetched [19-21]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_indexOutOfBounds_loadDelay0() = prependFling_indexOutOfBounds(0)
+
+    @Test
+    fun prependFling_indexOutOfBounds_loadDelay10000() = prependFling_indexOutOfBounds(10000)
+
+    private fun prependFling_indexOutOfBounds(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay, 10)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(-3)
+            }
+            // initial load [10-14]
+            // prefetched [7-9], [15-17]
+            // flingTo prepended [0-6]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_accessPageBoundary_loadDelay0() = prependFling_accessPageBoundary(0)
+
+    @Test
+    fun prependFling_accessPageBoundary_loadDelay10000() = prependFling_accessPageBoundary(10000)
+
+    private fun prependFling_accessPageBoundary(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // page boundary
+                flingTo(44)
+            }
+            // ensure that SnapshotLoader waited for last prefetch before returning
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [44-46] - expect only one extra page to be prefetched after this
+            // prefetched [41-43]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_withoutPlaceholders_loadDelay0() = prependFling_withoutPlaceholders(0)
+
+    @Test
+    fun prependFling_withoutPlaceholders_loadDelay10000() = prependFling_withoutPlaceholders(10000)
+
+    private fun prependFling_withoutPlaceholders(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay, 50)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // Without placeholders, first loaded page always starts at index[0]
+                flingTo(0)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [44-46]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_withoutPlaceholders_indexOutOfBounds_loadDelay0() =
+        prependFling_withoutPlaceholders_indexOutOfBounds(0)
+
+    @Test
+    fun prependFling_withoutPlaceholders_indexOutOfBounds_loadDelay10000() =
+        prependFling_withoutPlaceholders_indexOutOfBounds(10000)
+
+    private fun prependFling_withoutPlaceholders_indexOutOfBounds(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(-8)
+            }
+            // ensure we honor negative indices if there is data to load
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [38-46]
+            // prefetched [35-37]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53,
+                    54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_withoutPlaceholders_indexOutOfBoundsIsCapped_loadDelay0() =
+        prependFling_withoutPlaceholders_indexOutOfBoundsIsCapped(0)
+
+    @Test
+    fun prependFling_withoutPlaceholders_indexOutOfBoundsIsCapped_loadDelay10000() =
+        prependFling_withoutPlaceholders_indexOutOfBoundsIsCapped(10000)
+
+    private fun prependFling_withoutPlaceholders_indexOutOfBoundsIsCapped(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay, 5)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(-20)
+            }
+            // ensure index is capped when no more data to load
+            // initial load [5-9]
+            // prefetched [2-4], [10-12]
+            // flingTo prepended [0-1]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependFling_withoutPlaceholders_loadDelay0() =
+        consecutivePrependFling_withoutPlaceholders(0)
+
+    @Test
+    fun consecutivePrependFling_withoutPlaceholders_loadDelay10000() =
+        consecutivePrependFling_withoutPlaceholders(10000)
+
+    private fun consecutivePrependFling_withoutPlaceholders(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay, 50)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // Without placeholders, first loaded page always starts at index[0]
+                flingTo(-1)
+                // Without placeholders, first loaded page always starts at index[0]
+                flingTo(-5)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // first flingTo prepended [41-46]
+            // index[0] is now anchored to [41]
+            // second flingTo prepended [35-40]
+            // prefetched [32-34]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
+                    50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependFling_withoutPlaceholders_multiSnapshot_loadDelay0() =
+        consecutivePrependFling_withoutPlaceholders_multiSnapshot(0)
+
+    @Test
+    fun consecutivePrependFling_withoutPlaceholders_multiSnapshot_loadDelay10000() =
+        consecutivePrependFling_withoutPlaceholders_multiSnapshot(10000)
+
+    private fun consecutivePrependFling_withoutPlaceholders_multiSnapshot(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay, 50)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // Without placeholders, first loaded page always starts at index[0]
+                flingTo(-1)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // flingTo prepended [44-46]
+            // prefetched [41-43]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+
+            val snapshot2 = pager.asSnapshot {
+                // Without placeholders, first loaded page always starts at index[0]
+                flingTo(-5)
+            }
+            // flingTo prepended [35-40]
+            // prefetched [32-34]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(
+                    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
+                    50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_withoutPlaceholders_indexPrecision_loadDelay0() =
+        prependFling_withoutPlaceholders_indexPrecision(0)
+
+    @Test
+    fun prependFling_withoutPlaceholders_indexPrecision_loadDelay10000() =
+        prependFling_withoutPlaceholders_indexPrecision(10000)
+
+    private fun prependFling_withoutPlaceholders_indexPrecision(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        // load sizes and prefetch set to 1 to test precision of flingTo indexing
+        val pager = Pager(
+            config = PagingConfig(
+                pageSize = 1,
+                initialLoadSize = 1,
+                enablePlaceholders = false,
+                prefetchDistance = 1
+            ),
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow, loadDelay),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot {
+                // after refresh, lastAccessedIndex == index[2] == item(9)
+                flingTo(-1)
+            }
+            // initial load [50]
+            // prefetched [49], [51]
+            // prepended [48]
+            // prefetched [47]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(47, 48, 49, 50, 51)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_loadDelay0() = appendFling(0)
+
+    @Test
+    fun appendFling_loadDelay10000() = appendFling(10000)
+
+    private fun appendFling(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_withDrops_loadDelay0() = appendFling_withDrops(0)
+
+    @Test
+    fun appendFling_withDrops_loadDelay10000() = appendFling_withDrops(10000)
+
+    private fun appendFling_withDrops(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithDrops(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(12)
+            }
+            // dropped [0-7]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_withSeparators_loadDelay0() = appendFling_withSeparators(0)
+
+    @Test
+    fun appendFling_withSeparators_loadDelay10000() = appendFling_withSeparators(10000)
+
+    private fun appendFling_withSeparators(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 0 || before == 14) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, "sep", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, "sep", 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendFling_loadDelay0() = consecutiveAppendFling(0)
+
+    @Test
+    fun consecutiveAppendFling_loadDelay10000() = consecutiveAppendFling(10000)
+
+    private fun consecutiveAppendFling(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(12)
+                flingTo(18)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-19]
+            // prefetched [20-22]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                    21, 22)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendFling_multiSnapshots_loadDelay0() =
+        consecutiveAppendFling_multiSnapshots(0)
+
+    @Test
+    fun consecutiveAppendFling_multiSnapshots_loadDelay10000() =
+        consecutiveAppendFling_multiSnapshots(10000)
+
+    private fun consecutiveAppendFling_multiSnapshots(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+
+            val snapshot2 = pager.asSnapshot {
+                flingTo(18)
+            }
+            // appended [17-19]
+            // prefetched [20-22]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
+                    18, 19, 20, 21, 22
+                )
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_jump_loadDelay0() = appendFling_jump(0)
+
+    @Test
+    fun appendFling_jump_loadDelay10000() = appendFling_jump(10000)
+
+    private fun appendFling_jump(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithJump(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(30)
+                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
+            }
+            // initial load [28-32]
+            // prefetched [25-27], [33-35]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_scrollThenJump_loadDelay0() = appendFling_scrollThenJump(0)
+
+    @Test
+    fun appendFling_scrollThenJump_loadDelay10000() = appendFling_scrollThenJump(10000)
+
+    private fun appendFling_scrollThenJump(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithJump(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                scrollTo(30)
+                flingTo(43)
+                // jump triggered when flingTo registered lastAccessedIndex[43], refreshKey[41]
+            }
+            // initial load [41-45]
+            // prefetched [38-40], [46-48]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_jumpThenFling_loadDelay0() = appendFling_jumpThenFling(0)
+
+    @Test
+    fun appendFling_jumpThenFling_loadDelay10000() = appendFling_jumpThenFling(10000)
+
+    private fun appendFling_jumpThenFling(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithJump(dataFlow, loadDelay)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(30)
+                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
+                flingTo(38)
+            }
+            // initial load [28-32]
+            // prefetched [25-27], [33-35]
+            // flingTo appended [36-38]
+            // prefetched [39-41]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_indexOutOfBounds_loadDelay0() = appendFling_indexOutOfBounds(0)
+
+    @Test
+    fun appendFling_indexOutOfBounds_loadDelay10000() = appendFling_indexOutOfBounds(10000)
+
+    private fun appendFling_indexOutOfBounds(loadDelay: Long) {
+        val dataFlow = flowOf(List(15) { it })
+        val pager = createPager(dataFlow, loadDelay).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // index out of bounds
+                flingTo(50)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // flingTo appended [8-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_accessPageBoundary_loadDelay0() = appendFling_accessPageBoundary(0)
+
+    @Test
+    fun appendFling_accessPageBoundary_loadDelay10000() = appendFling_accessPageBoundary(10000)
+
+    private fun appendFling_accessPageBoundary(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, loadDelay).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // after initial Load and prefetch, max loaded index is 7
+                flingTo(7)
+            }
+            // ensure that SnapshotLoader waited for last prefetch before returning
+            // initial load [0-4]
+            // prefetched [5-7] - expect only one extra page to be prefetched after this
+            // flingTo appended [8-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_withoutPlaceholders_loadDelay0() = appendFling_withoutPlaceholders(0)
+
+    @Test
+    fun appendFling_withoutPlaceholders_loadDelay10000() = appendFling_withoutPlaceholders(10000)
+
+    private fun appendFling_withoutPlaceholders(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // scroll to max loaded index
+                flingTo(7)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // flingTo appended [8-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_withoutPlaceholders_indexOutOfBounds_loadDelay0() =
+        appendFling_withoutPlaceholders_indexOutOfBounds(0)
+
+    @Test
+    fun appendFling_withoutPlaceholders_indexOutOfBounds_loadDelay10000() =
+        appendFling_withoutPlaceholders_indexOutOfBounds(10000)
+
+    private fun appendFling_withoutPlaceholders_indexOutOfBounds(loadDelay: Long) {
+        val dataFlow = flowOf(List(20) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                // 12 is larger than differ.size = 8 after initial refresh
+                flingTo(12)
+            }
+            // ensure it honors scrollTo indices >= differ.size
+            // initial load [0-4]
+            // prefetched [5-7]
+            // flingTo appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_withoutPlaceholders_indexOutOfBoundsIsCapped_loadDelay0() =
+        appendFling_withoutPlaceholders_indexOutOfBoundsIsCapped(0)
+
+    @Test
+    fun appendFling_withoutPlaceholders_indexOutOfBoundsIsCapped_loadDelay10000() =
+        appendFling_withoutPlaceholders_indexOutOfBoundsIsCapped(10000)
+
+    private fun appendFling_withoutPlaceholders_indexOutOfBoundsIsCapped(loadDelay: Long) {
+        val dataFlow = flowOf(List(20) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(50)
+            }
+            // ensure index is still capped to max index available
+            // initial load [0-4]
+            // prefetched [5-7]
+            // flingTo appended [8-19]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendFling_withoutPlaceholders_loadDelay0() =
+        consecutiveAppendFling_withoutPlaceholders(0)
+
+    @Test
+    fun consecutiveAppendFling_withoutPlaceholders_loadDelay10000() =
+        consecutiveAppendFling_withoutPlaceholders(10000)
+
+    private fun consecutiveAppendFling_withoutPlaceholders(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(12)
+                flingTo(17)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // first flingTo appended [8-13]
+            // second flingTo appended [14-19]
+            // prefetched [19-22]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                    21, 22)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendFling_withoutPlaceholders_multiSnapshot_loadDelay0() =
+        consecutiveAppendFling_withoutPlaceholders_multiSnapshot(0)
+
+    @Test
+    fun consecutiveAppendFling_withoutPlaceholders_multiSnapshot_loadDelay10000() =
+        consecutiveAppendFling_withoutPlaceholders_multiSnapshot(10000)
+
+    private fun consecutiveAppendFling_withoutPlaceholders_multiSnapshot(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, loadDelay)
+            .cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // flingTo appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+
+            val snapshot2 = pager.asSnapshot {
+                flingTo(17)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // first flingTo appended [8-13]
+            // second flingTo appended [14-19]
+            // prefetched [19-22]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                    21, 22)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_withoutPlaceholders_indexPrecision_loadDelay0() =
+        appendFling_withoutPlaceholders_indexPrecision(0)
+
+    @Test
+    fun appendFling_withoutPlaceholders_indexPrecision_loadDelay10000() =
+        appendFling_withoutPlaceholders_indexPrecision(10000)
+
+    private fun appendFling_withoutPlaceholders_indexPrecision(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        // load sizes and prefetch set to 1 to test precision of flingTo indexing
+        val pager = Pager(
+            config = PagingConfig(
+                pageSize = 1,
+                initialLoadSize = 1,
+                enablePlaceholders = false,
+                prefetchDistance = 1
+            ),
+            pagingSourceFactory = createFactory(dataFlow, loadDelay),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot {
+                // after refresh, lastAccessedIndex == index[2] == item(9)
+                flingTo(2)
+            }
+            // initial load [0]
+            // prefetched [1]
+            // appended [2]
+            // prefetched [3]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3)
+            )
+        }
+    }
+
+    @Test
+    fun flingTo_indexAccountsForSeparators_loadDelay0() = flingTo_indexAccountsForSeparators(0)
+
+    @Test
+    fun flingTo_indexAccountsForSeparators_loadDelay10000() =
+        flingTo_indexAccountsForSeparators(10000)
+
+    private fun flingTo_indexAccountsForSeparators(loadDelay: Long) {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(
+            dataFlow,
+            PagingConfig(
+                pageSize = 1,
+                initialLoadSize = 1,
+                prefetchDistance = 1
+            ),
+            loadDelay,
+            50
+        )
+        val pagerWithSeparator = pager.map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 49) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot {
+                flingTo(51)
+            }
+            // initial load [50]
+            // prefetched [49], [51]
+            // flingTo [51] accessed item[51]prefetched [52]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(49, 50, 51, 52)
+            )
+
+            val snapshotWithSeparator = pagerWithSeparator.asSnapshot {
+                flingTo(51)
+            }
+            // initial load [50]
+            // prefetched [49], [51]
+            // flingTo [51] accessed item[50], no prefetch triggered
+            assertThat(snapshotWithSeparator).containsExactlyElementsIn(
+                listOf(49, "sep", 50, 51)
+            )
+        }
+    }
+
+    @Test
+    fun errorHandler_throw_loadDelay0() = errorHandler_throw(0)
+
+    @Test
+    fun errorHandler_throw_loadDelay10000() = errorHandler_throw(10000)
+
+    private fun errorHandler_throw(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = createFactory(dataFlow, loadDelay)
+        val pagingSources = mutableListOf<TestPagingSource>()
+        val pager = Pager(
+            config = PagingConfig(pageSize = 3, initialLoadSize = 5),
+            pagingSourceFactory = {
+                factory.invoke().also { pagingSources.add(it as TestPagingSource) }
+            },
+        ).flow
+        testScope.runTest {
+            val error = assertFailsWith(IllegalArgumentException::class) {
+                pager.asSnapshot(onError = { ErrorRecovery.THROW }) {
+                    val source = pagingSources.first()
+                    source.errorOnNextLoad = true
+                    scrollTo(12)
+                }
+            }
+            assertThat(error.message).isEqualTo("PagingSource load error")
+        }
+    }
+
+    @Test
+    fun errorHandler_retry_loadDelay0() = errorHandler_retry(0)
+
+    @Test
+    fun errorHandler_retry_loadDelay10000() = errorHandler_retry(10000)
+
+    private fun errorHandler_retry(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = createFactory(dataFlow, loadDelay)
+        val pagingSources = mutableListOf<TestPagingSource>()
+        val pager = Pager(
+            config = PagingConfig(pageSize = 3, initialLoadSize = 5),
+            pagingSourceFactory = {
+                factory.invoke().also { pagingSources.add(it as TestPagingSource) }
+            },
+        ).flow
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(onError = { ErrorRecovery.RETRY }) {
+                val source = pagingSources.first()
+                // should have two loads to far - refresh and append(prefetch)
+                assertThat(source.loads.size).isEqualTo(2)
+
+                // throw error on next load, should trigger a retry
+                source.errorOnNextLoad = true
+                scrollTo(7)
+
+                // make sure it did retry
+                assertThat(source.loads.size).isEqualTo(4)
+                // failed load
+                val failedLoad = source.loads[2]
+                assertThat(failedLoad is LoadParams.Append).isTrue()
+                assertThat(failedLoad.key).isEqualTo(8)
+                // retry load
+                val retryLoad = source.loads[3]
+                assertThat(retryLoad is LoadParams.Append).isTrue()
+                assertThat(retryLoad.key).isEqualTo(8)
+            }
+            // retry success
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun errorHandler_retryFails_loadDelay0() = errorHandler_retryFails(0)
+
+    @Test
+    fun errorHandler_retryFails_loadDelay10000() = errorHandler_retryFails(10000)
+
+    private fun errorHandler_retryFails(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = createFactory(dataFlow, loadDelay)
+        val pagingSources = mutableListOf<TestPagingSource>()
+        val pager = Pager(
+            config = PagingConfig(pageSize = 3, initialLoadSize = 5),
+            pagingSourceFactory = {
+                factory.invoke().also { pagingSources.add(it as TestPagingSource) }
+            },
+        ).flow
+        var retryCount = 0
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(
+                onError = {
+                    // retry twice
+                    if (retryCount < 2) {
+                        retryCount++
+                        ErrorRecovery.RETRY
+                    } else {
+                        ErrorRecovery.RETURN_CURRENT_SNAPSHOT
+                    }
+                }
+            ) {
+                val source = pagingSources.first()
+                // should have two loads to far - refresh and append(prefetch)
+                assertThat(source.loads.size).isEqualTo(2)
+
+                source.errorOnLoads = true
+                scrollTo(8)
+
+                // additional failed load + two retries
+                assertThat(source.loads.size).isEqualTo(5)
+            }
+            // retry failed, returned existing snapshot
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7)
+            )
+        }
+    }
+
+    @Test
+    fun errorHandler_returnSnapshot_loadDelay0() = errorHandler_returnSnapshot(0)
+
+    @Test
+    fun errorHandler_returnSnapshot_loadDelay10000() = errorHandler_returnSnapshot(10000)
+
+    private fun errorHandler_returnSnapshot(loadDelay: Long) {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = createFactory(dataFlow, loadDelay)
+        val pagingSources = mutableListOf<TestPagingSource>()
+        val pager = Pager(
+            config = PagingConfig(pageSize = 3, initialLoadSize = 5),
+            pagingSourceFactory = {
+                factory.invoke().also { pagingSources.add(it as TestPagingSource) }
+            },
+        ).flow
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(onError = { ErrorRecovery.RETURN_CURRENT_SNAPSHOT }) {
+                val source = pagingSources.first()
+                source.errorOnNextLoad = true
+                scrollTo(12)
+            }
+            // snapshot items before scrollTo
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7)
+            )
+        }
+    }
+
+    private fun createPager(dataFlow: Flow<List<Int>>, loadDelay: Long, initialKey: Int = 0) =
+        createPager(
+            dataFlow,
+            PagingConfig(pageSize = 3, initialLoadSize = 5),
+            loadDelay,
+            initialKey
+        )
+
+    private fun createPager(data: List<Int>, loadDelay: Long, initialKey: Int = 0) =
+        Pager(
+            PagingConfig(pageSize = 3, initialLoadSize = 5),
+            initialKey,
+            createSingleGenFactory(data, loadDelay),
+        ).flow
+
+    private fun createPagerNoPlaceholders(
+        dataFlow: Flow<List<Int>>,
+        loadDelay: Long,
+        initialKey: Int = 0
+    ) =
+        createPager(
+            dataFlow,
+            PagingConfig(
+                pageSize = 3,
+                initialLoadSize = 5,
+                enablePlaceholders = false,
+                prefetchDistance = 3
+            ),
+            loadDelay,
+            initialKey)
+
+    private fun createPagerNoPrefetch(
+        dataFlow: Flow<List<Int>>,
+        loadDelay: Long,
+        initialKey: Int = 0
+    ) =
+        createPager(
+            dataFlow,
+            PagingConfig(pageSize = 3, initialLoadSize = 5, prefetchDistance = 0),
+            loadDelay,
+            initialKey
+        )
+
+    private fun createPagerWithJump(
+        dataFlow: Flow<List<Int>>,
+        loadDelay: Long,
+        initialKey: Int = 0
+    ) =
+        createPager(
+            dataFlow,
+            PagingConfig(pageSize = 3, initialLoadSize = 5, jumpThreshold = 5),
+            loadDelay,
+            initialKey
+        )
+
+    private fun createPagerWithDrops(
+        dataFlow: Flow<List<Int>>,
+        loadDelay: Long,
+        initialKey: Int = 0
+    ) =
+        createPager(
+            dataFlow,
+            PagingConfig(pageSize = 3, initialLoadSize = 5, maxSize = 9),
+            loadDelay,
+            initialKey
+        )
+
+    private fun createPager(
+        dataFlow: Flow<List<Int>>,
+        config: PagingConfig,
+        loadDelay: Long,
+        initialKey: Int = 0,
+    ) = Pager(
+            config = config,
+            initialKey = initialKey,
+            pagingSourceFactory = createFactory(dataFlow, loadDelay),
+        ).flow
+}
+
+private class WrappedPagingSourceFactory(
+    private val factory: PagingSourceFactory<Int, Int>,
+    private val loadDelay: Long,
+) : PagingSourceFactory<Int, Int> {
+    override fun invoke(): PagingSource<Int, Int> = TestPagingSource(factory(), loadDelay)
+}
+
+private class TestPagingSource(
+    private val originalSource: PagingSource<Int, Int>,
+    private val loadDelay: Long,
+) : PagingSource<Int, Int>() {
+
+    var errorOnNextLoad = false
+    var errorOnLoads = false
+    private val _loads = mutableListOf<LoadParams<Int>>()
+    val loads: List<LoadParams<Int>>
+        get() = _loads.toList()
+
+    init {
+        originalSource.registerInvalidatedCallback {
+            invalidate()
+        }
+    }
+    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Int> {
+        delay(loadDelay)
+        _loads.add(params)
+        if (errorOnNextLoad) {
+            errorOnNextLoad = false
+            return LoadResult.Error(IllegalArgumentException("PagingSource load error"))
+        }
+        if (errorOnLoads) {
+            return LoadResult.Error(IllegalArgumentException("PagingSource load error"))
+        }
+        return originalSource.load(params)
+    }
+
+    override fun getRefreshKey(state: PagingState<Int, Int>) = originalSource.getRefreshKey(state)
+
+    override val jumpingSupported: Boolean = originalSource.jumpingSupported
+}
diff --git a/paging/paging-testing/src/commonTest/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt b/paging/paging-testing/src/commonTest/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
new file mode 100644
index 0000000..0419b4f
--- /dev/null
+++ b/paging/paging-testing/src/commonTest/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging.testing
+
+import androidx.kruth.assertThat
+import androidx.paging.PagingConfig
+import androidx.paging.PagingSource.LoadResult.Page
+import androidx.paging.PagingSourceFactory
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class StaticListPagingSourceFactoryTest {
+
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+    private val CONFIG = PagingConfig(
+        pageSize = 3,
+        initialLoadSize = 5
+    )
+
+    @Test
+    fun emptyFlow() {
+        val factory: PagingSourceFactory<Int, Int> =
+            flowOf<List<Int>>().asPagingSourceFactory(testScope)
+        val pagingSource = factory()
+        val pager = TestPager(CONFIG, pagingSource)
+
+        runTest {
+            val result = pager.refresh() as Page
+            assertThat(result.data.isEmpty()).isTrue()
+        }
+    }
+
+    @Test
+    fun simpleCollect_singleGen() {
+        val flow = flowOf(
+            List(20) { it }
+        )
+
+        val factory: PagingSourceFactory<Int, Int> =
+            flow.asPagingSourceFactory(testScope)
+        val pagingSource = factory()
+        val pager = TestPager(CONFIG, pagingSource)
+
+        runTest {
+            val result = pager.refresh() as Page
+            assertThat(result.data).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4)
+            )
+        }
+    }
+
+    @Test
+    fun simpleCollect_multiGeneration() = testScope.runTest {
+        val flow = flow {
+            emit(List(20) { it }) // first gen
+            delay(1500)
+            emit(List(15) { it + 30 }) // second gen
+        }
+
+        val factory: PagingSourceFactory<Int, Int> =
+            flow.asPagingSourceFactory(testScope)
+
+        advanceTimeBy(1000)
+
+        // first gen
+        val pagingSource1 = factory()
+        val pager1 = TestPager(CONFIG, pagingSource1)
+        val result1 = pager1.refresh() as Page
+        assertThat(result1.data).containsExactlyElementsIn(
+            listOf(0, 1, 2, 3, 4)
+        )
+
+        // second list emits -- this should invalidate original pagingSource and trigger new gen
+        advanceUntilIdle()
+
+        assertThat(pagingSource1.invalid).isTrue()
+
+        // second gen
+        val pagingSource2 = factory()
+        val pager2 = TestPager(CONFIG, pagingSource2)
+        val result2 = pager2.refresh() as Page
+        assertThat(result2.data).containsExactlyElementsIn(
+            listOf(30, 31, 32, 33, 34)
+        )
+    }
+
+    @Test
+    fun collection_cancellation() = testScope.runTest {
+        val mutableFlow = MutableSharedFlow<List<Int>>()
+        val collectionScope = this.backgroundScope
+
+        val factory: PagingSourceFactory<Int, Int> =
+            mutableFlow.asPagingSourceFactory(collectionScope)
+
+        mutableFlow.emit(List(10) { it })
+
+        advanceUntilIdle()
+
+        val pagingSource = factory()
+        val pager = TestPager(CONFIG, pagingSource)
+        val result = pager.refresh() as Page
+        assertThat(result.data).containsExactlyElementsIn(
+            listOf(0, 1, 2, 3, 4)
+        )
+
+        // cancel collection scope inside the pagingSourceFactory
+        collectionScope.cancel()
+
+        mutableFlow.emit(List(10) { it })
+
+        advanceUntilIdle()
+
+        // new list should not be collected, meaning the previous generation should still be valid
+        assertThat(pagingSource.invalid).isFalse()
+    }
+
+    @Test
+    fun multipleFactories_fromSameFlow() = testScope.runTest {
+        val mutableFlow = MutableSharedFlow<List<Int>>()
+
+        val factory1: PagingSourceFactory<Int, Int> =
+            mutableFlow.asPagingSourceFactory(testScope.backgroundScope)
+
+        val factory2: PagingSourceFactory<Int, Int> =
+            mutableFlow.asPagingSourceFactory(testScope.backgroundScope)
+
+        mutableFlow.emit(List(10) { it })
+
+        advanceUntilIdle()
+
+        // factory 1 first gen
+        val pagingSource = factory1()
+        val pager = TestPager(CONFIG, pagingSource)
+        val result = pager.refresh() as Page
+        assertThat(result.data).containsExactlyElementsIn(
+            listOf(0, 1, 2, 3, 4)
+        )
+
+        // factory 2 first gen
+        val pagingSource2 = factory2()
+        val pager2 = TestPager(CONFIG, pagingSource2)
+        val result2 = pager2.refresh() as Page
+        assertThat(result2.data).containsExactlyElementsIn(
+            listOf(0, 1, 2, 3, 4)
+        )
+
+        // trigger second generation
+        mutableFlow.emit(List(10) { it + 30 })
+
+        advanceUntilIdle()
+
+        assertThat(pagingSource.invalid).isTrue()
+        assertThat(pagingSource2.invalid).isTrue()
+
+        // factory 1 second gen
+        val pagingSource3 = factory1()
+        val pager3 = TestPager(CONFIG, pagingSource3)
+        val result3 = pager3.refresh() as Page
+        assertThat(result3.data).containsExactlyElementsIn(
+            listOf(30, 31, 32, 33, 34)
+        )
+
+        // factory 2 second gen
+        val pagingSource4 = factory2()
+        val pager4 = TestPager(CONFIG, pagingSource4)
+        val result4 = pager4.refresh() as Page
+        assertThat(result4.data).containsExactlyElementsIn(
+            listOf(30, 31, 32, 33, 34)
+        )
+    }
+
+    @Test
+    fun singleListFactory_refresh() = testScope.runTest {
+        val data = List(20) { it }
+        val factory = data.asPagingSourceFactory()
+
+        val pagingSource1 = factory()
+        val pager1 = TestPager(CONFIG, pagingSource1)
+        val refresh1 = pager1.refresh() as Page
+        assertThat(refresh1.data).containsExactlyElementsIn(
+            listOf(0, 1, 2, 3, 4)
+        )
+
+        val pagingSource2 = factory()
+        val pager2 = TestPager(CONFIG, pagingSource2)
+        val refresh2 = pager2.refresh() as Page
+        assertThat(refresh2.data).containsExactlyElementsIn(
+            listOf(0, 1, 2, 3, 4)
+        )
+    }
+
+    @Test
+    fun singleListFactory_empty() = testScope.runTest {
+        val data = emptyList<Int>()
+        val factory = data.asPagingSourceFactory()
+
+        val pagingSource1 = factory()
+        val pager1 = TestPager(CONFIG, pagingSource1)
+        val refresh1 = pager1.refresh() as Page
+        assertThat(refresh1.data).isEmpty()
+
+        val pagingSource2 = factory()
+        val pager2 = TestPager(CONFIG, pagingSource2)
+        val refresh2 = pager2.refresh() as Page
+        assertThat(refresh2.data).isEmpty()
+    }
+
+    @Test
+    fun singleListFactory_append() = testScope.runTest {
+        val data = List(20) { it }
+        val factory = data.asPagingSourceFactory()
+        val pagingSource1 = factory()
+        val pager1 = TestPager(CONFIG, pagingSource1)
+
+        pager1.refresh() as Page
+        pager1.append()
+        assertThat(pager1.getPages().flatMap { it.data }).containsExactlyElementsIn(
+            listOf(0, 1, 2, 3, 4, 5, 6, 7)
+        )
+
+        pager1.append()
+        assertThat(pager1.getPages().flatMap { it.data }).containsExactlyElementsIn(
+            listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+        )
+    }
+
+    @Test
+    fun singleListFactory_prepend() = testScope.runTest {
+        val data = List(20) { it }
+        val factory = data.asPagingSourceFactory()
+        val pagingSource1 = factory()
+        val pager1 = TestPager(CONFIG, pagingSource1)
+
+        pager1.refresh(initialKey = 10) as Page
+        pager1.prepend()
+        assertThat(pager1.getPages().flatMap { it.data }).containsExactlyElementsIn(
+            listOf(7, 8, 9, 10, 11, 12, 13, 14)
+        )
+
+        pager1.prepend()
+        assertThat(pager1.getPages().flatMap { it.data }).containsExactlyElementsIn(
+            listOf(4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
+        )
+    }
+}
diff --git a/paging/paging-testing/src/commonTest/kotlin/androidx/paging/testing/StaticListPagingSourceTest.kt b/paging/paging-testing/src/commonTest/kotlin/androidx/paging/testing/StaticListPagingSourceTest.kt
new file mode 100644
index 0000000..ae8ce3b
--- /dev/null
+++ b/paging/paging-testing/src/commonTest/kotlin/androidx/paging/testing/StaticListPagingSourceTest.kt
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging.testing
+
+import androidx.kruth.assertThat
+import androidx.paging.PagingConfig
+import androidx.paging.PagingSource
+import androidx.paging.PagingSource.LoadResult
+import kotlin.test.Test
+import kotlinx.coroutines.test.runTest
+
+class StaticListPagingSourceTest {
+
+    private val DATA = List(100) { it }
+    private val CONFIG = PagingConfig(
+        pageSize = 3,
+        initialLoadSize = 5,
+    )
+
+    @Test
+    fun refresh() = runPagingSourceTest { _, pager ->
+        val result = pager.refresh() as LoadResult.Page
+        assertThat(result).isEqualTo(
+            LoadResult.Page(
+                data = listOf(0, 1, 2, 3, 4),
+                prevKey = null,
+                nextKey = 5,
+                itemsBefore = 0,
+                itemsAfter = 95
+            )
+        )
+    }
+
+    @Test
+    fun refresh_withEmptyData() = runPagingSourceTest(StaticListPagingSource(emptyList())) {
+            _, pager ->
+
+        val result = pager.refresh() as LoadResult.Page
+        assertThat(result).isEqualTo(
+            LoadResult.Page(
+                data = emptyList(),
+                prevKey = null,
+                nextKey = null,
+                itemsBefore = 0,
+                itemsAfter = 0
+            )
+        )
+    }
+
+    @Test
+    fun refresh_initialKey() = runPagingSourceTest { _, pager ->
+        val result = pager.refresh(initialKey = 20) as LoadResult.Page
+        assertThat(result).isEqualTo(
+            LoadResult.Page(
+                data = listOf(20, 21, 22, 23, 24),
+                prevKey = 19,
+                nextKey = 25,
+                itemsBefore = 20,
+                itemsAfter = 75
+            )
+        )
+    }
+
+    @Test
+    fun refresh_initialKey_withEmptyData() = runPagingSourceTest(
+        StaticListPagingSource(emptyList())
+    ) { _, pager ->
+
+        val result = pager.refresh(initialKey = 20) as LoadResult.Page
+        assertThat(result).isEqualTo(
+            LoadResult.Page(
+                data = emptyList(),
+                prevKey = null,
+                nextKey = null,
+                itemsBefore = 0,
+                itemsAfter = 0
+            )
+        )
+    }
+
+    @Test
+    fun refresh_negativeKeyClippedToZero() = runPagingSourceTest { _, pager ->
+        val result = pager.refresh(initialKey = -1) as LoadResult.Page
+        // loads first page
+        assertThat(result).isEqualTo(
+            listOf(0, 1, 2, 3, 4).asPage()
+        )
+    }
+
+    @Test
+    fun refresh_KeyLargerThanDataSize_loadsLastPage() = runPagingSourceTest { _, pager ->
+        val result = pager.refresh(initialKey = 140) as LoadResult.Page
+        // loads last page
+        assertThat(result).isEqualTo(
+            listOf(95, 96, 97, 98, 99).asPage()
+        )
+    }
+
+    @Test
+    fun append() = runPagingSourceTest { _, pager ->
+        pager.run {
+            refresh()
+            append()
+        }
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                LoadResult.Page(
+                    data = listOf(0, 1, 2, 3, 4),
+                    prevKey = null,
+                    nextKey = 5,
+                    itemsBefore = 0,
+                    itemsAfter = 95
+                ),
+                LoadResult.Page(
+                    data = listOf(5, 6, 7),
+                    prevKey = 4,
+                    nextKey = 8,
+                    itemsBefore = 5,
+                    itemsAfter = 92
+                )
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun append_consecutively() = runPagingSourceTest { _, pager ->
+        pager.run {
+            refresh()
+            append()
+            append()
+            append()
+        }
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                listOf(0, 1, 2, 3, 4).asPage(),
+                listOf(5, 6, 7).asPage(),
+                listOf(8, 9, 10).asPage(),
+                listOf(11, 12, 13).asPage(),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun append_loadSizeLargerThanAvailableData() = runPagingSourceTest { _, pager ->
+        val result = pager.run {
+            refresh(initialKey = 94)
+            append() as LoadResult.Page
+        }
+        assertThat(result).isEqualTo(
+            LoadResult.Page(
+                data = listOf(99),
+                prevKey = 98,
+                nextKey = null,
+                itemsBefore = 99,
+                itemsAfter = 0
+            )
+        )
+    }
+
+    @Test
+    fun prepend() = runPagingSourceTest { _, pager ->
+        pager.run {
+            refresh(20)
+            prepend()
+        }
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                LoadResult.Page(
+                    data = listOf(17, 18, 19),
+                    prevKey = 16,
+                    nextKey = 20,
+                    itemsBefore = 17,
+                    itemsAfter = 80
+                ),
+                LoadResult.Page(
+                    data = listOf(20, 21, 22, 23, 24),
+                    prevKey = 19,
+                    nextKey = 25,
+                    itemsBefore = 20,
+                    itemsAfter = 75
+                ),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun prepend_consecutively() = runPagingSourceTest { _, pager ->
+        pager.run {
+            refresh(initialKey = 50)
+            prepend()
+            prepend()
+            prepend()
+        }
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                listOf(41, 42, 43).asPage(),
+                listOf(44, 45, 46).asPage(),
+                listOf(47, 48, 49).asPage(),
+                listOf(50, 51, 52, 53, 54).asPage(),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun prepend_loadSizeLargerThanAvailableData() = runPagingSourceTest { _, pager ->
+        val result = pager.run {
+            refresh(initialKey = 2)
+            prepend()
+        }
+        assertThat(result).isEqualTo(
+            LoadResult.Page(
+                data = listOf(0, 1),
+                prevKey = null,
+                nextKey = 2,
+                itemsBefore = 0,
+                itemsAfter = 98
+            )
+        )
+    }
+
+    @Test
+    fun jump_enabled() {
+        val source = StaticListPagingSource(DATA)
+        assertThat(source.jumpingSupported).isTrue()
+    }
+
+    @Test
+    fun refreshKey() = runPagingSourceTest { pagingSource, pager ->
+        val state = pager.run {
+            refresh() // [0, 1, 2, 3, 4]
+            append() // [5, 6, 7]
+            // the anchorPos should be 7
+            getPagingState(anchorPosition = 7)
+        }
+
+        val refreshKey = pagingSource.getRefreshKey(state)
+        val expected = 7 - (CONFIG.initialLoadSize / 2)
+        assertThat(expected).isEqualTo(5)
+        assertThat(refreshKey).isEqualTo(expected)
+    }
+
+    @Test
+    fun refreshKey_negativeKeyClippedToZero() = runPagingSourceTest { pagingSource, pager ->
+        val state = pager.run {
+            refresh(2) // [2, 3, 4, 5, 6]
+            prepend() // [0, 1]
+            getPagingState(anchorPosition = 1)
+        }
+        // before clipping, refreshKey = 1 - (CONFIG.initialLoadSize / 2) = -1
+        val refreshKey = pagingSource.getRefreshKey(state)
+        assertThat(refreshKey).isEqualTo(0)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    private fun runPagingSourceTest(
+        source: PagingSource<Int, Int> = StaticListPagingSource(DATA),
+        pager: TestPager<Int, Int> = TestPager(CONFIG, source),
+        block: suspend (pagingSource: PagingSource<Int, Int>, pager: TestPager<Int, Int>) -> Unit
+    ) {
+        runTest {
+            block(source, pager)
+        }
+    }
+
+    private fun List<Int>.asPage(): LoadResult.Page<Int, Int> {
+        val indexStart = firstOrNull()
+        val indexEnd = lastOrNull()
+        return LoadResult.Page(
+            data = this,
+            prevKey = indexStart?.let {
+                if (indexStart <= 0 || isEmpty()) null else indexStart - 1
+            },
+            nextKey = indexEnd?.let {
+                if (indexEnd >= DATA.lastIndex || isEmpty()) null else indexEnd + 1
+            },
+            itemsBefore = indexStart ?: -1,
+            itemsAfter = if (indexEnd == null) -1 else DATA.lastIndex - indexEnd
+        )
+    }
+}
diff --git a/paging/paging-testing/src/commonTest/kotlin/androidx/paging/testing/TestPagerTest.kt b/paging/paging-testing/src/commonTest/kotlin/androidx/paging/testing/TestPagerTest.kt
new file mode 100644
index 0000000..b534856
--- /dev/null
+++ b/paging/paging-testing/src/commonTest/kotlin/androidx/paging/testing/TestPagerTest.kt
@@ -0,0 +1,970 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging.testing
+
+import androidx.kruth.assertThat
+import androidx.paging.PagingConfig
+import androidx.paging.PagingSource.LoadResult
+import androidx.paging.PagingState
+import androidx.paging.TestPagingSource
+import kotlin.test.Test
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class TestPagerTest {
+
+    @Test
+    fun refresh_nullKey() {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        runTest {
+            val result = pager.refresh(null) as LoadResult.Page
+
+            assertThat(result.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
+        }
+    }
+
+    @Test
+    fun refresh_withInitialKey() {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        runTest {
+            val result = pager.refresh(50) as LoadResult.Page
+
+            assertThat(result.data).containsExactlyElementsIn(listOf(50, 51, 52, 53, 54)).inOrder()
+        }
+    }
+
+    @Test
+    fun refresh_returnError() {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        runTest {
+            source.errorNextLoad = true
+            val result = pager.refresh()
+            assertTrue(result is LoadResult.Error)
+
+            val page = pager.getLastLoadedPage()
+            assertThat(page).isNull()
+        }
+    }
+
+    @Test
+    fun refresh_returnInvalid() {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        runTest {
+            source.nextLoadResult = LoadResult.Invalid()
+            val result = pager.refresh()
+            assertTrue(result is LoadResult.Invalid)
+
+            val page = pager.getLastLoadedPage()
+            assertThat(page).isNull()
+        }
+    }
+
+    @Test
+    fun refresh_invalidPagingSource() {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        runTest {
+            source.invalidate()
+            assertTrue(source.invalid)
+            // simulate a PagingSource that returns LoadResult.Invalid when it's invalidated
+            source.nextLoadResult = LoadResult.Invalid()
+
+            assertThat(pager.refresh()).isInstanceOf<LoadResult.Invalid<Int, Int>>()
+        }
+    }
+
+    @Test
+    fun refresh_getLastLoadedPage() {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        runTest {
+            val page: LoadResult.Page<Int, Int>? = pager.run {
+                refresh()
+                getLastLoadedPage()
+            }
+            assertThat(page).isNotNull()
+            assertThat(page?.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
+        }
+    }
+
+    @Test
+    fun getLastLoadedPage_afterInvalidPagingSource() {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        runTest {
+            val page = pager.run {
+                refresh()
+                append() // page should be this appended page
+                source.invalidate()
+                assertTrue(source.invalid)
+                getLastLoadedPage()
+            }
+            assertThat(page).isNotNull()
+            assertThat(page?.data).containsExactlyElementsIn(listOf(5, 6, 7)).inOrder()
+        }
+    }
+
+    @Test
+    fun refresh_getPages() {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        runTest {
+            val pages = pager.run {
+                refresh()
+                getPages()
+            }
+            assertThat(pages).hasSize(1)
+            assertThat(pages).containsExactlyElementsIn(
+                listOf(
+                    listOf(0, 1, 2, 3, 4).asPage()
+                )
+            ).inOrder()
+        }
+    }
+
+    @Test
+    fun getPages_multiplePages() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        pager.run {
+            refresh(20)
+            prepend()
+        }
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                // prepend
+                listOf(17, 18, 19).asPage(),
+                // refresh
+                listOf(20, 21, 22, 23, 24).asPage(),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun getPages_fromEmptyList() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+        val pages = pager.getPages()
+        assertThat(pages).isEmpty()
+    }
+
+    @Test
+    fun getPages_afterInvalidPagingSource() {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        runTest {
+            val pages = pager.run {
+                refresh()
+                append()
+                source.invalidate()
+                assertTrue(source.invalid)
+                getPages()
+            }
+            assertThat(pages).containsExactlyElementsIn(
+                listOf(
+                    listOf(0, 1, 2, 3, 4).asPage(),
+                    listOf(5, 6, 7).asPage()
+                )
+            ).inOrder()
+        }
+    }
+
+    @Test
+    fun getPages_multiThread() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        var pages: List<LoadResult.Page<Int, Int>>? = null
+        val job = launch {
+            pager.run {
+                refresh(20) // first
+                pages = getPages() // third
+                prepend() // fifth
+            }
+        }
+        job.start()
+        assertTrue(job.isActive)
+        val pages2 = pager.run {
+            delay(200) // let launch start first
+            append() // second
+            prepend() // fourth
+            getPages() // sixth
+        }
+
+        advanceUntilIdle()
+        assertThat(pages).containsExactlyElementsIn(
+            listOf(
+                // should contain first and second load
+                listOf(20, 21, 22, 23, 24).asPage(), // refresh
+                listOf(25, 26, 27).asPage(), // append
+            )
+        ).inOrder()
+        assertThat(pages2).containsExactlyElementsIn(
+            // should contain all loads
+            listOf(
+                listOf(14, 15, 16).asPage(),
+                listOf(17, 18, 19).asPage(),
+                listOf(20, 21, 22, 23, 24).asPage(),
+                listOf(25, 26, 27).asPage(),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun multipleRefresh_onSinglePager_throws() {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        runTest {
+            pager.run {
+                // second refresh should throw since testPager is not mult-generational
+                assertFailsWith<IllegalStateException> {
+                    refresh()
+                    refresh()
+                }
+            }
+            assertTrue(source.invalid)
+            // the first refresh should still have succeeded
+            assertThat(pager.getPages()).hasSize(1)
+        }
+    }
+
+    @Test
+    fun multipleRefresh_onMultiplePagers() = runTest {
+        val source1 = TestPagingSource()
+        val pager1 = TestPager(CONFIG, source1)
+
+        // first gen
+        val result1 = pager1.run {
+            refresh()
+        } as LoadResult.Page
+
+        assertThat(result1.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
+
+        // second gen
+        val source2 = TestPagingSource()
+        val pager2 = TestPager(CONFIG, source2)
+
+        val result2 = pager2.run {
+            refresh()
+        } as LoadResult.Page
+
+        assertThat(result2.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
+    }
+
+    @Test
+    fun simpleAppend() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        val result = pager.run {
+            refresh(null)
+            append()
+        } as LoadResult.Page
+
+        assertThat(result.data).containsExactlyElementsIn(listOf(5, 6, 7)).inOrder()
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                listOf(0, 1, 2, 3, 4).asPage(),
+                listOf(5, 6, 7).asPage()
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun simplePrepend() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        val result = pager.run {
+            refresh(30)
+            prepend()
+        } as LoadResult.Page
+
+        assertThat(result.data).containsExactlyElementsIn(listOf(27, 28, 29)).inOrder()
+        // prepended pages should be inserted before refresh
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                // prepend
+                listOf(27, 28, 29).asPage(),
+                // refresh
+                listOf(30, 31, 32, 33, 34).asPage()
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun append_beforeRefresh_throws() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+        assertFailsWith<IllegalStateException> {
+            pager.append()
+        }
+    }
+
+    @Test
+    fun prepend_beforeRefresh_throws() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+        assertFailsWith<IllegalStateException> {
+            pager.prepend()
+        }
+    }
+
+    @Test
+    fun append_invalidPagingSource() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        val result = pager.run {
+            refresh()
+            source.invalidate()
+            assertThat(source.invalid).isTrue()
+            // simulate a PagingSource which returns LoadResult.Invalid when it's invalidated
+            source.nextLoadResult = LoadResult.Invalid()
+            append()
+        }
+        assertThat(result).isInstanceOf<LoadResult.Invalid<Int, Int>>()
+    }
+
+    @Test
+    fun prepend_invalidPagingSource() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        val result = pager.run {
+            refresh(initialKey = 20)
+            source.invalidate()
+            assertThat(source.invalid).isTrue()
+            // simulate a PagingSource which returns LoadResult.Invalid when it's invalidated
+            source.nextLoadResult = LoadResult.Invalid()
+            prepend()
+        }
+        assertThat(result).isInstanceOf<LoadResult.Invalid<Int, Int>>()
+    }
+
+    @Test
+    fun consecutive_append() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        pager.run {
+            refresh(20)
+            append()
+            append()
+        } as LoadResult.Page
+
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                listOf(20, 21, 22, 23, 24).asPage(),
+                listOf(25, 26, 27).asPage(),
+                listOf(28, 29, 30).asPage()
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun consecutive_prepend() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        pager.run {
+            refresh(20)
+            prepend()
+            prepend()
+        } as LoadResult.Page
+
+        // prepended pages should be ordered before the refresh
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                // 2nd prepend
+                listOf(14, 15, 16).asPage(),
+                // 1st prepend
+                listOf(17, 18, 19).asPage(),
+                // refresh
+                listOf(20, 21, 22, 23, 24).asPage(),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun append_then_prepend() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        pager.run {
+            refresh(20)
+            append()
+            prepend()
+        } as LoadResult.Page
+
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                // prepend
+                listOf(17, 18, 19).asPage(),
+                // refresh
+                listOf(20, 21, 22, 23, 24).asPage(),
+                // append
+                listOf(25, 26, 27).asPage(),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun prepend_then_append() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        pager.run {
+            refresh(20)
+            prepend()
+            append()
+        } as LoadResult.Page
+
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                // prepend
+                listOf(17, 18, 19).asPage(),
+                // refresh
+                listOf(20, 21, 22, 23, 24).asPage(),
+                // append
+                listOf(25, 26, 27).asPage(),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun multiThread_loads() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+        // load operations upon completion add an int to the list.
+        // after all loads complete, we assert the order that the ints were added.
+        val loadOrder = mutableListOf<Int>()
+
+        val job = launch {
+            pager.run {
+                refresh(20).also { loadOrder.add(1) } // first load
+                prepend().also { loadOrder.add(3) } // third load
+                append().also { loadOrder.add(5) } // fifth load
+            }
+        }
+        job.start()
+        assertTrue(job.isActive)
+
+        pager.run {
+            // give some time for job to start
+            delay(200)
+            append().also { loadOrder.add(2) } // second load
+            prepend().also { loadOrder.add(4) } // fourth load
+        }
+
+        advanceUntilIdle()
+        assertThat(loadOrder).containsExactlyElementsIn(listOf(1, 2, 3, 4, 5)).inOrder()
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                listOf(14, 15, 16).asPage(),
+                listOf(17, 18, 19).asPage(),
+                listOf(20, 21, 22, 23, 24).asPage(),
+                listOf(25, 26, 27).asPage(),
+                listOf(28, 29, 30).asPage(),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun multiThread_operations() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+        // operations upon completion add an int to the list.
+        // after all operations complete, we assert the order that the ints were added.
+        val loadOrder = mutableListOf<Int>()
+
+        var lastLoadedPage: LoadResult.Page<Int, Int>? = null
+        val job = launch {
+            pager.run {
+                refresh(20).also { loadOrder.add(1) } // first operation
+                // third operation, should return first appended page
+                lastLoadedPage = getLastLoadedPage().also { loadOrder.add(3) }
+                append().also { loadOrder.add(5) } // fifth operation
+                prepend().also { loadOrder.add(7) } // last operation
+            }
+        }
+        job.start()
+        assertTrue(job.isActive)
+
+        val pages = pager.run {
+            // give some time for job to start first
+            delay(200)
+            append().also { loadOrder.add(2) } // second operation
+            prepend().also { loadOrder.add(4) } // fourth operation
+            // sixth operation, should return 4 pages
+            getPages().also { loadOrder.add(6) }
+        }
+
+        advanceUntilIdle()
+        assertThat(loadOrder).containsExactlyElementsIn(
+            listOf(1, 2, 3, 4, 5, 6, 7)
+        ).inOrder()
+        assertThat(lastLoadedPage).isEqualTo(
+            listOf(25, 26, 27).asPage(),
+        )
+        // should not contain the second prepend, with a total of 4 pages
+        assertThat(pages).containsExactlyElementsIn(
+            listOf(
+                listOf(17, 18, 19).asPage(), // first prepend
+                listOf(20, 21, 22, 23, 24).asPage(), // refresh
+                listOf(25, 26, 27).asPage(), // first append
+                listOf(28, 29, 30).asPage(), // second append
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun getPagingStateWithAnchorPosition_placeHoldersEnabled() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        val state = pager.run {
+            refresh(20)
+            prepend()
+            append()
+            getPagingState(7)
+        }
+        // in this case anchorPos is a placeholder at index 7
+        assertThat(state).isEqualTo(
+            PagingState(
+                pages = listOf(
+                    listOf(17, 18, 19).asPage(),
+                    // refresh
+                    listOf(20, 21, 22, 23, 24).asPage(),
+                    // append
+                    listOf(25, 26, 27).asPage(),
+                ),
+                anchorPosition = 7,
+                config = CONFIG,
+                leadingPlaceholderCount = 17
+            )
+        )
+        val source2 = TestPagingSource()
+        val pager2 = TestPager(CONFIG, source)
+        val page = pager2.run {
+            refresh(source2.getRefreshKey(state))
+        }
+        assertThat(page).isEqualTo(listOf(7, 8, 9, 10, 11).asPage())
+    }
+
+    @Test
+    fun getPagingStateWithAnchorPosition_placeHoldersDisabled() = runTest {
+        val source = TestPagingSource(placeholdersEnabled = false)
+        val config = PagingConfig(
+            pageSize = 3,
+            initialLoadSize = 5,
+            enablePlaceholders = false
+        )
+        val pager = TestPager(config, source)
+
+        val state = pager.run {
+            refresh(20)
+            prepend()
+            append()
+            getPagingState(7)
+        }
+        assertThat(state).isEqualTo(
+            PagingState(
+                pages = listOf(
+                    listOf(17, 18, 19).asPage(placeholdersEnabled = false),
+                    // refresh
+                    listOf(20, 21, 22, 23, 24).asPage(placeholdersEnabled = false),
+                    // append
+                    listOf(25, 26, 27).asPage(placeholdersEnabled = false),
+                ),
+                anchorPosition = 7,
+                config = config,
+                leadingPlaceholderCount = 0
+            )
+        )
+        val source2 = TestPagingSource()
+        val pager2 = TestPager(CONFIG, source)
+        val page = pager2.run {
+            refresh(source2.getRefreshKey(state))
+        }
+        // without placeholders, Paging currently has no way to translate item[7] within loaded
+        // pages into its absolute position within available data. Hence anchorPosition 7 will
+        // reference item[7] within available data.
+        assertThat(page).isEqualTo(listOf(7, 8, 9, 10, 11).asPage(placeholdersEnabled = false))
+    }
+
+    @Test
+    fun getPagingStateWithAnchorPosition_indexOutOfBoundsWithPlaceholders() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        val msg = assertFailsWith<IllegalStateException> {
+            pager.run {
+                refresh()
+                append()
+                getPagingState(-1)
+            }
+        }.message
+        assertThat(msg).isEqualTo(
+            "anchorPosition -1 is out of bounds between [0..${ITEM_COUNT - 1}]. Please " +
+                "provide a valid anchorPosition."
+        )
+
+        val msg2 = assertFailsWith<IllegalStateException> {
+            pager.getPagingState(ITEM_COUNT)
+        }.message
+        assertThat(msg2).isEqualTo(
+            "anchorPosition $ITEM_COUNT is out of bounds between [0..${ITEM_COUNT - 1}]. " +
+                "Please provide a valid anchorPosition."
+        )
+    }
+
+    @Test
+    fun getPagingStateWithAnchorPosition_indexOutOfBoundsWithoutPlaceholders() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(
+            PagingConfig(
+                pageSize = 3,
+                initialLoadSize = 5,
+                enablePlaceholders = false
+            ),
+            source
+        )
+
+        val msg = assertFailsWith<IllegalStateException> {
+            pager.run {
+                refresh()
+                append()
+                getPagingState(-1)
+            }
+        }.message
+        assertThat(msg).isEqualTo(
+            "anchorPosition -1 is out of bounds between [0..7]. Please " +
+                "provide a valid anchorPosition."
+        )
+
+        // total loaded items = 8, anchorPos with index 8 should be out of bounds
+        val msg2 = assertFailsWith<IllegalStateException> {
+            pager.getPagingState(8)
+        }.message
+        assertThat(msg2).isEqualTo(
+            "anchorPosition 8 is out of bounds between [0..7]. Please " +
+        "provide a valid anchorPosition."
+        )
+    }
+
+    @Test
+    fun getPagingStateWithAnchorLookup_placeHoldersEnabled() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(CONFIG, source)
+
+        val state = pager.run {
+            refresh(20)
+            prepend()
+            append()
+            getPagingState { it == TestPagingSource.ITEMS[22] }
+        }
+        assertThat(state).isEqualTo(
+            PagingState(
+                pages = listOf(
+                    listOf(17, 18, 19).asPage(),
+                    // refresh
+                    listOf(20, 21, 22, 23, 24).asPage(),
+                    // append
+                    listOf(25, 26, 27).asPage(),
+                ),
+                anchorPosition = 22,
+                config = CONFIG,
+                leadingPlaceholderCount = 17
+            )
+        )
+        // use state to getRefreshKey
+        val source2 = TestPagingSource()
+        val pager2 = TestPager(CONFIG, source)
+        val page = pager2.run {
+            refresh(source2.getRefreshKey(state))
+        }
+        assertThat(page).isEqualTo(listOf(22, 23, 24, 25, 26).asPage())
+    }
+
+    @Test
+    fun getPagingStateWithAnchorLookup_placeHoldersDisabled() = runTest {
+        val source = TestPagingSource(placeholdersEnabled = false)
+        val config = PagingConfig(
+            pageSize = 3,
+            initialLoadSize = 5,
+            enablePlaceholders = false
+        )
+        val pager = TestPager(config, source)
+
+        val state = pager.run {
+            refresh(20)
+            prepend()
+            append()
+            getPagingState { it == TestPagingSource.ITEMS[22] } // item 22 in this case
+        }
+        assertThat(state).isEqualTo(
+            PagingState(
+                pages = listOf(
+                    listOf(17, 18, 19).asPage(placeholdersEnabled = false),
+                    // refresh
+                    listOf(20, 21, 22, 23, 24).asPage(placeholdersEnabled = false),
+                    // append
+                    listOf(25, 26, 27).asPage(placeholdersEnabled = false),
+                ),
+                anchorPosition = 5,
+                config = config,
+                leadingPlaceholderCount = 0
+            )
+        )
+        // use state to getRefreshKey
+        val source2 = TestPagingSource()
+        val pager2 = TestPager(CONFIG, source)
+        val page = pager2.run {
+            refresh(source2.getRefreshKey(state))
+        }
+        // without placeholders, Paging currently has no way to translate item[5] within loaded
+        // pages into its absolute position within available data. anchorPosition 5 will reference
+        // item[5] within available data.
+        assertThat(page).isEqualTo(listOf(5, 6, 7, 8, 9).asPage(placeholdersEnabled = false))
+    }
+
+    @Test
+    fun getPagingStateWithAnchorLookup_itemNotFoundThrows() = runTest {
+        val source = TestPagingSource(placeholdersEnabled = false)
+        val config = PagingConfig(
+            pageSize = 3,
+            initialLoadSize = 5,
+            enablePlaceholders = false
+        )
+        val pager = TestPager(config, source)
+
+        val msg = assertFailsWith<IllegalArgumentException> {
+            pager.run {
+                refresh(20)
+                prepend()
+                append()
+                getPagingState { it == TestPagingSource.ITEMS[10] }
+            }
+        }.message
+        assertThat(msg).isEqualTo(
+            "The given predicate has returned false for every loaded item. To generate a" +
+                "PagingState anchored to an item, the expected item must have already " +
+                "been loaded."
+        )
+    }
+
+    @Test
+    fun dropPrependedPage() = runTest {
+        val source = TestPagingSource()
+        val config = PagingConfig(
+            pageSize = 3,
+            initialLoadSize = 5,
+            enablePlaceholders = false,
+            maxSize = 10
+        )
+        val pager = TestPager(config, source)
+        pager.run {
+            refresh(20)
+            prepend()
+        }
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                listOf(17, 18, 19).asPage(),
+                // refresh
+                listOf(20, 21, 22, 23, 24).asPage(),
+            )
+        )
+
+        // this append should trigger paging to drop the prepended page
+        pager.append()
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                listOf(20, 21, 22, 23, 24).asPage(),
+                listOf(25, 26, 27).asPage(),
+            )
+        )
+    }
+
+    @Test
+    fun dropAppendedPage() = runTest {
+        val source = TestPagingSource()
+        val config = PagingConfig(
+            pageSize = 3,
+            initialLoadSize = 5,
+            enablePlaceholders = false,
+            maxSize = 10
+        )
+        val pager = TestPager(config, source)
+        pager.run {
+            refresh(20)
+            append()
+        }
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                listOf(20, 21, 22, 23, 24).asPage(),
+                listOf(25, 26, 27).asPage(),
+            )
+        )
+
+        // this prepend should trigger paging to drop the prepended page
+        pager.prepend()
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                listOf(17, 18, 19).asPage(),
+                listOf(20, 21, 22, 23, 24).asPage(),
+            )
+        )
+    }
+
+    @Test
+    fun dropInitialRefreshedPage() = runTest {
+        val source = TestPagingSource()
+        val config = PagingConfig(
+            pageSize = 3,
+            initialLoadSize = 5,
+            enablePlaceholders = false,
+            maxSize = 10
+        )
+        val pager = TestPager(config, source)
+        pager.run {
+            refresh(20)
+            append()
+        }
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                listOf(20, 21, 22, 23, 24).asPage(),
+                listOf(25, 26, 27).asPage(),
+            )
+        )
+
+        // this append should trigger paging to drop the first page which is the initial refresh
+        pager.append()
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                listOf(25, 26, 27).asPage(),
+                listOf(28, 29, 30).asPage(),
+            )
+        )
+    }
+
+    @Test
+    fun dropRespectsPrefetchDistance_InDroppedDirection() = runTest {
+        val source = TestPagingSource()
+        val config = PagingConfig(
+            pageSize = 1,
+            initialLoadSize = 10,
+            enablePlaceholders = false,
+            maxSize = 5,
+            prefetchDistance = 2
+        )
+        val pager = TestPager(config, source)
+        pager.refresh(20)
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                listOf(20, 21, 22, 23, 24, 25, 26, 27, 28, 29).asPage(),
+            )
+        )
+
+        // these appends should would normally trigger paging to drop first page, but it won't
+        // in this case due to prefetchDistance
+        pager.run {
+            append()
+            append()
+        }
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                // second page counted towards prefetch distance
+                listOf(20, 21, 22, 23, 24, 25, 26, 27, 28, 29).asPage(),
+                // first page counted towards prefetch distance
+                listOf(30).asPage(),
+                listOf(31).asPage()
+            )
+        )
+    }
+
+    @Test
+    fun drop_noOpUnderTwoPages() = runTest {
+        val source = TestPagingSource()
+        val config = PagingConfig(
+            pageSize = 1,
+            initialLoadSize = 5,
+            enablePlaceholders = false,
+            maxSize = 3,
+            prefetchDistance = 1
+        )
+        val pager = TestPager(config, source)
+        val result = pager.refresh() as LoadResult.Page
+        assertThat(result.data).containsExactlyElementsIn(
+            listOf(0, 1, 2, 3, 4)
+        )
+
+        pager.append()
+        // data size exceeds maxSize but no data should be dropped
+        assertThat(pager.getPages().flatten()).containsExactlyElementsIn(
+            listOf(0, 1, 2, 3, 4, 5)
+        )
+    }
+
+    private val CONFIG = PagingConfig(
+        pageSize = 3,
+        initialLoadSize = 5,
+    )
+
+    private fun List<Int>.asPage(placeholdersEnabled: Boolean = true): LoadResult.Page<Int, Int> {
+        val itemsBefore = if (placeholdersEnabled) {
+            if (first() == 0) 0 else first()
+        } else {
+            Int.MIN_VALUE
+        }
+        val itemsAfter = if (placeholdersEnabled) {
+            if (last() == ITEM_COUNT - 1) 0 else ITEM_COUNT - 1 - last()
+        } else {
+            Int.MIN_VALUE
+        }
+        return LoadResult.Page(
+            data = this,
+            prevKey = if (first() == 0) null else first() - 1,
+            nextKey = if (last() == ITEM_COUNT - 1) null else last() + 1,
+            itemsBefore = itemsBefore,
+            itemsAfter = itemsAfter
+        )
+    }
+
+    private val ITEM_COUNT = 100
+}
diff --git a/paging/paging-testing/src/jvmMain/kotlin/androidx/paging/testing/internal/Atomics.jvm.kt b/paging/paging-testing/src/jvmMain/kotlin/androidx/paging/testing/internal/Atomics.jvm.kt
new file mode 100644
index 0000000..b9532be
--- /dev/null
+++ b/paging/paging-testing/src/jvmMain/kotlin/androidx/paging/testing/internal/Atomics.jvm.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+@file:Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316
+package androidx.paging.testing.internal
+
+internal actual typealias AtomicInt = java.util.concurrent.atomic.AtomicInteger
+
+internal actual typealias AtomicBoolean = java.util.concurrent.atomic.AtomicBoolean
+
+internal actual typealias AtomicRef<T> = java.util.concurrent.atomic.AtomicReference<T>
diff --git a/paging/paging-testing/src/nativeMain/kotlin/androidx/paging/testing/internal/Atomics.native.kt b/paging/paging-testing/src/nativeMain/kotlin/androidx/paging/testing/internal/Atomics.native.kt
new file mode 100644
index 0000000..bc053e9
--- /dev/null
+++ b/paging/paging-testing/src/nativeMain/kotlin/androidx/paging/testing/internal/Atomics.native.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalForeignApi::class)
+
+package androidx.paging.testing.internal
+
+import kotlinx.atomicfu.AtomicBoolean as AtomicFuAtomicBoolean
+import kotlinx.atomicfu.AtomicInt as AtomicFuAtomicInt
+import kotlinx.atomicfu.AtomicRef as AtomicFuAtomicRef
+import kotlinx.atomicfu.atomic
+import kotlinx.cinterop.ExperimentalForeignApi
+
+internal actual class AtomicInt actual constructor(initialValue: Int) {
+    private var delegate: AtomicFuAtomicInt = atomic(initialValue)
+    private var property by delegate
+
+    actual fun get(): Int = property
+
+    actual fun set(value: Int) {
+        property = value
+    }
+}
+
+internal actual class AtomicBoolean actual constructor(initialValue: Boolean) {
+    private var delegate: AtomicFuAtomicBoolean = atomic(initialValue)
+    private var property by delegate
+
+    actual fun get(): Boolean = property
+
+    actual fun set(value: Boolean) {
+        property = value
+    }
+
+    actual fun compareAndSet(expect: Boolean, update: Boolean): Boolean {
+        return delegate.compareAndSet(expect, update)
+    }
+}
+
+internal actual class AtomicRef<T> actual constructor(initialValue: T) {
+    private var delegate: AtomicFuAtomicRef<T> = atomic(initialValue)
+    private var property by delegate
+
+    actual fun get(): T = property
+
+    actual fun set(value: T) {
+        property = value
+    }
+
+    actual fun getAndSet(value: T): T {
+        return delegate.getAndSet(value)
+    }
+}
diff --git a/playground-common/androidx-shared.properties b/playground-common/androidx-shared.properties
index 27cc998..8a53805 100644
--- a/playground-common/androidx-shared.properties
+++ b/playground-common/androidx-shared.properties
@@ -35,7 +35,7 @@
 org.gradle.configuration-cache.problems=fail
 
 android.lint.printStackTrace=true
-android.uniquePackageNames=false
+android.uniquePackageNames=true
 android.enableAdditionalTestOutput=true
 android.useAndroidX=true
 android.nonTransitiveRClass=true
@@ -78,4 +78,4 @@
 # Properties we often want to toggle
 # ksp.version.check=false
 
-kotlin.mpp.androidSourceSetLayoutVersion=1
+kotlin.mpp.androidSourceSetLayoutVersion=2
diff --git a/privacysandbox/ads/ads-adservices-java/build.gradle b/privacysandbox/ads/ads-adservices-java/build.gradle
index d65ff5c0..b3a4b71 100644
--- a/privacysandbox/ads/ads-adservices-java/build.gradle
+++ b/privacysandbox/ads/ads-adservices-java/build.gradle
@@ -36,8 +36,10 @@
 
     androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0'
     androidTestImplementation project(path: ':privacysandbox:ads:ads-adservices')
+    androidTestImplementation project(path: ':javascriptengine:javascriptengine')
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.kotlinTestJunit)
+    androidTestImplementation(libs.multidex)
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRunner)
@@ -46,9 +48,14 @@
 
     androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy) // DexMaker has it"s own MockMaker
     androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(libs.dexmakerMockitoInlineExtended)
 }
 
 android {
+    defaultConfig {
+        multiDexEnabled = true
+    }
+
     namespace "androidx.privacysandbox.ads.adservices.java"
 }
 
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml b/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml
index 90665d4..aa19a69 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml
@@ -14,11 +14,13 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
     <uses-permission android:name="android.permission.ACCESS_ADSERVICES_TOPICS" />
     <uses-permission android:name="android.permission.ACCESS_ADSERVICES_AD_ID" />
     <uses-permission android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION" />
     <uses-permission android:name="android.permission.ACCESS_ADSERVICES_CUSTOM_AUDIENCE" />
+    <uses-sdk tools:overrideLibrary="androidx.javascriptengine" />
     <application>
         <property android:name="android.adservices.AD_SERVICES_CONFIG"
             android:resource="@xml/ad_services_config" />
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/VersionCompatUtil.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/VersionCompatUtil.kt
new file mode 100644
index 0000000..d4ad1a2
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/VersionCompatUtil.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.privacysandbox.ads.adservices.java
+
+import android.os.Build
+import android.os.ext.SdkExtensions
+import androidx.test.filters.SdkSuppress
+
+@SdkSuppress(minSdkVersion = 30)
+object VersionCompatUtil {
+    fun isTPlusWithMinAdServicesVersion(minVersion: Int): Boolean {
+        return Build.VERSION.SDK_INT >= 33 &&
+            SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES) >= minVersion
+    }
+
+    fun isSWithMinExtServicesVersion(minVersion: Int): Boolean {
+        return (Build.VERSION.SDK_INT == 31 || Build.VERSION.SDK_INT == 32) &&
+            SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= minVersion
+    }
+
+    fun isTestableVersion(minAdServicesVersion: Int, minExtServicesVersion: Int): Boolean {
+        return isTPlusWithMinAdServicesVersion(minAdServicesVersion) ||
+            isSWithMinExtServicesVersion(minExtServicesVersion)
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
index 5f20ba7..35d5109 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
@@ -19,16 +19,18 @@
 import android.content.Context
 import android.os.Looper
 import android.os.OutcomeReceiver
-import android.os.ext.SdkExtensions
-import androidx.annotation.RequiresExtension
 import androidx.privacysandbox.ads.adservices.adid.AdId
+import androidx.privacysandbox.ads.adservices.java.VersionCompatUtil
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.google.common.truth.Truth
 import com.google.common.util.concurrent.ListenableFuture
 import kotlin.test.assertNotEquals
+import org.junit.After
 import org.junit.Assert
 import org.junit.Assume
 import org.junit.Before
@@ -36,6 +38,8 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers
 import org.mockito.Mockito
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.`when`
 import org.mockito.invocation.InvocationOnMock
 
 @SmallTest
@@ -44,25 +48,45 @@
 @SdkSuppress(minSdkVersion = 30)
 class AdIdManagerFuturesTest {
 
+    private var mSession: StaticMockitoSession? = null
+    private val mValidAdExtServicesSdkExtVersion = VersionCompatUtil.isSWithMinExtServicesVersion(9)
+
     @Before
     fun setUp() {
-        mContext = Mockito.spy(ApplicationProvider.getApplicationContext<Context>())
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+
+        if (mValidAdExtServicesSdkExtVersion) {
+            // setup a mockitoSession to return the mocked manager
+            // when the static method .get() is called
+            mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(android.adservices.adid.AdIdManager::class.java)
+                .startMocking()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mSession?.finishMocking()
     }
 
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testAdIdOlderVersions() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Assume.assumeFalse("maxSdkVersion = API 33 ext 3 or API 31/32 ext 8",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion=*/ 4,
+                /* minExtServicesVersion=*/ 9))
         Truth.assertThat(AdIdManagerFutures.from(mContext)).isEqualTo(null)
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testAdIdAsync() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val adIdManager = mockAdIdManager(mContext)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 4,
+                /* minExtServicesVersion=*/ 9))
+
+        val adIdManager = mockAdIdManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupResponse(adIdManager)
         val managerCompat = AdIdManagerFutures.from(mContext)
 
@@ -76,16 +100,23 @@
         Mockito.verify(adIdManager).getAdId(ArgumentMatchers.any(), ArgumentMatchers.any())
     }
 
-    @SuppressWarnings("NewApi")
     @SdkSuppress(minSdkVersion = 30)
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     companion object {
         private lateinit var mContext: Context
 
-        private fun mockAdIdManager(spyContext: Context): android.adservices.adid.AdIdManager {
+        private fun mockAdIdManager(
+            spyContext: Context,
+            isExtServices: Boolean
+        ): android.adservices.adid.AdIdManager {
             val adIdManager = Mockito.mock(android.adservices.adid.AdIdManager::class.java)
-            Mockito.`when`(spyContext.getSystemService(
-                android.adservices.adid.AdIdManager::class.java)).thenReturn(adIdManager)
+            // mock the .get() method if using extServices version, otherwise mock getSystemService
+            if (isExtServices) {
+                `when`(android.adservices.adid.AdIdManager.get(ArgumentMatchers.any()))
+                    .thenReturn(adIdManager)
+            } else {
+                `when`(spyContext.getSystemService(
+                    android.adservices.adid.AdIdManager::class.java)).thenReturn(adIdManager)
+            }
             return adIdManager
         }
 
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt
index af4ef91..afc5c68 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt
@@ -19,20 +19,22 @@
 import android.content.Context
 import android.net.Uri
 import android.os.OutcomeReceiver
-import android.os.ext.SdkExtensions
-import androidx.annotation.RequiresExtension
 import androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig
 import androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome
 import androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest
 import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
 import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.privacysandbox.ads.adservices.java.VersionCompatUtil
 import androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures.Companion.from
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.google.common.truth.Truth
 import com.google.common.util.concurrent.ListenableFuture
+import org.junit.After
 import org.junit.Assert
 import org.junit.Assume
 import org.junit.Before
@@ -46,6 +48,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.invocation.InvocationOnMock
+import org.mockito.quality.Strictness
 
 @SmallTest
 @SuppressWarnings("NewApi")
@@ -53,27 +56,46 @@
 @SdkSuppress(minSdkVersion = 30)
 class AdSelectionManagerFuturesTest {
 
+    private var mSession: StaticMockitoSession? = null
+    private val mValidAdExtServicesSdkExtVersion = VersionCompatUtil.isSWithMinExtServicesVersion(9)
+
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+
+        if (mValidAdExtServicesSdkExtVersion) {
+            // setup a mockitoSession to return the mocked manager
+            // when the static method .get() is called
+            mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(android.adservices.adselection.AdSelectionManager::class.java)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mSession?.finishMocking()
     }
 
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testAdSelectionOlderVersions() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-
-        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Assume.assumeFalse("maxSdkVersion = API 33 ext 3 or API 31/32 ext 8",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion=*/ 4,
+                /* minExtServicesVersion=*/ 9))
         Truth.assertThat(from(mContext)).isEqualTo(null)
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testSelectAds() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 4,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val adSelectionManager = mockAdSelectionManager(mContext)
+        val adSelectionManager = mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupAdSelectionResponse(adSelectionManager)
         val managerCompat = from(mContext)
 
@@ -95,12 +117,13 @@
 
     @Test
     @SuppressWarnings("NewApi")
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testReportImpression() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 4,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val adSelectionManager = mockAdSelectionManager(mContext)
+        val adSelectionManager = mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupAdSelectionResponse(adSelectionManager)
         val managerCompat = from(mContext)
         val reportImpressionRequest = ReportImpressionRequest(adSelectionId, adSelectionConfig)
@@ -119,7 +142,6 @@
 
     @SuppressWarnings("NewApi")
     @SdkSuppress(minSdkVersion = 30)
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     companion object {
         private lateinit var mContext: Context
         private const val adSelectionId = 1234L
@@ -148,13 +170,20 @@
         private val renderUri = Uri.parse("render-uri.com")
 
         private fun mockAdSelectionManager(
-            spyContext: Context
+            spyContext: Context,
+            isExtServices: Boolean
         ): android.adservices.adselection.AdSelectionManager {
             val adSelectionManager =
                 mock(android.adservices.adselection.AdSelectionManager::class.java)
-            `when`(spyContext.getSystemService(
-                android.adservices.adselection.AdSelectionManager::class.java))
-                .thenReturn(adSelectionManager)
+            // mock the .get() method if using extServices version, otherwise mock getSystemService
+            if (isExtServices) {
+                `when`(android.adservices.adselection.AdSelectionManager.get(any()))
+                    .thenReturn(adSelectionManager)
+            } else {
+                `when`(spyContext.getSystemService(
+                    android.adservices.adselection.AdSelectionManager::class.java))
+                    .thenReturn(adSelectionManager)
+            }
             return adSelectionManager
         }
 
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt
index a540f73..b92fa9a 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt
@@ -19,21 +19,24 @@
 import android.content.Context
 import android.os.Looper
 import android.os.OutcomeReceiver
-import android.os.ext.SdkExtensions
-import androidx.annotation.RequiresExtension
 import androidx.privacysandbox.ads.adservices.appsetid.AppSetId
+import androidx.privacysandbox.ads.adservices.java.VersionCompatUtil
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.ListenableFuture
 import kotlin.test.assertNotEquals
+import org.junit.After
 import org.junit.Assert
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
 import org.mockito.ArgumentMatchers.any
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.mock
@@ -43,32 +46,50 @@
 import org.mockito.invocation.InvocationOnMock
 
 @SmallTest
+@SuppressWarnings("NewApi")
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = 30)
 class AppSetIdManagerFuturesTest {
 
+    private var mSession: StaticMockitoSession? = null
+    private val mValidAdExtServicesSdkExtVersion = VersionCompatUtil.isSWithMinExtServicesVersion(9)
+
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+
+        if (mValidAdExtServicesSdkExtVersion) {
+            // setup a mockitoSession to return the mocked manager
+            // when the static method .get() is called
+            mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(android.adservices.appsetid.AppSetIdManager::class.java)
+                .startMocking()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mSession?.finishMocking()
     }
 
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testAppSetIdOlderVersions() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-
-        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Assume.assumeFalse("maxSdkVersion = API 33 ext 3 or API 31/32 ext 8",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion=*/ 4,
+                /* minExtServicesVersion=*/ 9))
         assertThat(AppSetIdManagerFutures.from(mContext)).isEqualTo(null)
     }
 
     @Test
-    @SuppressWarnings("NewApi")
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testAppSetIdAsync() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 4,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val appSetIdManager = mockAppSetIdManager(mContext)
+        val appSetIdManager = mockAppSetIdManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupResponse(appSetIdManager)
         val managerCompat = AppSetIdManagerFutures.from(mContext)
 
@@ -82,19 +103,24 @@
         verify(appSetIdManager).getAppSetId(any(), any())
     }
 
-    @SuppressWarnings("NewApi")
     @SdkSuppress(minSdkVersion = 30)
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     companion object {
         private lateinit var mContext: Context
 
         private fun mockAppSetIdManager(
-            spyContext: Context
+            spyContext: Context,
+            isExtServices: Boolean
         ): android.adservices.appsetid.AppSetIdManager {
             val appSetIdManager = mock(android.adservices.appsetid.AppSetIdManager::class.java)
-            `when`(spyContext.getSystemService(
-                android.adservices.appsetid.AppSetIdManager::class.java))
-                .thenReturn(appSetIdManager)
+            // mock the .get() method if using extServices version, otherwise mock getSystemService
+            if (isExtServices) {
+                `when`(android.adservices.appsetid.AppSetIdManager.get(ArgumentMatchers.any()))
+                    .thenReturn(appSetIdManager)
+            } else {
+                `when`(spyContext.getSystemService(
+                    android.adservices.appsetid.AppSetIdManager::class.java))
+                    .thenReturn(appSetIdManager)
+            }
             return appSetIdManager
         }
 
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt
index 31b287d..9da1419 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt
@@ -20,8 +20,6 @@
 import android.content.Context
 import android.net.Uri
 import android.os.OutcomeReceiver
-import android.os.ext.SdkExtensions
-import androidx.annotation.RequiresExtension
 import androidx.privacysandbox.ads.adservices.common.AdData
 import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
 import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
@@ -29,13 +27,17 @@
 import androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest
 import androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest
 import androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData
+import androidx.privacysandbox.ads.adservices.java.VersionCompatUtil
 import androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures.Companion.from
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.google.common.truth.Truth
 import java.time.Instant
+import org.junit.After
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Test
@@ -48,6 +50,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.invocation.InvocationOnMock
+import org.mockito.quality.Strictness
 
 @SmallTest
 @SuppressWarnings("NewApi")
@@ -55,27 +58,47 @@
 @SdkSuppress(minSdkVersion = 30)
 class CustomAudienceManagerFuturesTest {
 
+    private var mSession: StaticMockitoSession? = null
+    private val mValidAdExtServicesSdkExtVersion = VersionCompatUtil.isSWithMinExtServicesVersion(9)
+
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+
+        if (mValidAdExtServicesSdkExtVersion) {
+            // setup a mockitoSession to return the mocked manager
+            // when the static method .get() is called
+            mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(android.adservices.customaudience.CustomAudienceManager::class.java)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mSession?.finishMocking()
     }
 
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testOlderVersions() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-
-        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Assume.assumeFalse("maxSdkVersion = API 33 ext 3 or API 31/32 ext 8",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion=*/ 4,
+                /* minExtServicesVersion=*/ 9))
         Truth.assertThat(from(mContext)).isEqualTo(null)
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testJoinCustomAudience() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 4,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val customAudienceManager = mockCustomAudienceManager(mContext)
+        val customAudienceManager =
+            mockCustomAudienceManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupResponse(customAudienceManager)
         val managerCompat = from(mContext)
 
@@ -100,12 +123,14 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testLeaveCustomAudience() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 4,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val customAudienceManager = mockCustomAudienceManager(mContext)
+        val customAudienceManager =
+            mockCustomAudienceManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupResponse(customAudienceManager)
         val managerCompat = from(mContext)
 
@@ -124,7 +149,6 @@
     }
 
     @SdkSuppress(minSdkVersion = 30)
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     companion object {
         private lateinit var mContext: Context
         private val uri: Uri = Uri.parse("abc.com")
@@ -138,10 +162,18 @@
         private const val metadata = "metadata"
         private val ads: List<AdData> = listOf(AdData(uri, metadata))
 
-        private fun mockCustomAudienceManager(spyContext: Context): CustomAudienceManager {
+        private fun mockCustomAudienceManager(
+            spyContext: Context,
+            isExtServices: Boolean
+        ): CustomAudienceManager {
             val customAudienceManager = mock(CustomAudienceManager::class.java)
-            `when`(spyContext.getSystemService(CustomAudienceManager::class.java))
-                .thenReturn(customAudienceManager)
+            // mock the .get() method if using extServices version, otherwise mock getSystemService
+            if (isExtServices) {
+                `when`(CustomAudienceManager.get(any())).thenReturn(customAudienceManager)
+            } else {
+                `when`(spyContext.getSystemService(CustomAudienceManager::class.java))
+                    .thenReturn(customAudienceManager)
+            }
             return customAudienceManager
         }
 
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/FledgeCtsDebuggableTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/FledgeCtsDebuggableTest.java
index 7925188..fdd7e82 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/FledgeCtsDebuggableTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/FledgeCtsDebuggableTest.java
@@ -26,6 +26,7 @@
 import android.util.Log;
 
 import androidx.annotation.RequiresApi;
+import androidx.javascriptengine.JavaScriptSandbox;
 import androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig;
 import androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager;
 import androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome;
@@ -36,7 +37,7 @@
 import androidx.privacysandbox.ads.adservices.customaudience.CustomAudience;
 import androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest;
 import androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData;
-import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.VersionCompatUtil;
 import androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures;
 import androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures;
 import androidx.test.core.app.ApplicationProvider;
@@ -175,6 +176,10 @@
         testUtil.disableFledgeEnrollmentCheck(true);
         testUtil.enableAdServiceSystemService(true);
         testUtil.enforceFledgeJsIsolateMaxHeapSize(false);
+
+        if (VersionCompatUtil.INSTANCE.isSWithMinExtServicesVersion(9)) {
+            testUtil.enableBackCompat();
+        }
     }
 
     @AfterClass
@@ -196,10 +201,15 @@
         testUtil.disableFledgeEnrollmentCheck(false);
         testUtil.enableAdServiceSystemService(false);
         testUtil.enforceFledgeJsIsolateMaxHeapSize(true);
+
+        if (VersionCompatUtil.INSTANCE.isSWithMinExtServicesVersion(9)) {
+            testUtil.disableBackCompat();
+        }
     }
 
     @Before
     public void setup() throws Exception {
+        Assume.assumeTrue(JavaScriptSandbox.isSupported());
         mAdSelectionClient =
                 new AdSelectionClient(sContext);
         mCustomAudienceClient =
@@ -211,8 +221,11 @@
 
     @Test
     public void testFledgeAuctionSelectionFlow_overall_Success() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -255,8 +268,11 @@
 
     @Test
     public void testAdSelection_etldViolation_failure() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -310,8 +326,11 @@
 
     @Test
     public void testReportImpression_etldViolation_failure() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -379,8 +398,11 @@
 
     @Test
     public void testAdSelection_skipAdsMalformedBiddingLogic_success() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -432,8 +454,11 @@
 
     @Test
     public void testAdSelection_malformedScoringLogic_failure() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -485,8 +510,11 @@
 
     @Test
     public void testAdSelection_skipAdsFailedGettingBiddingLogic_success() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -536,8 +564,11 @@
 
     @Test
     public void testAdSelection_errorGettingScoringLogic_failure() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -594,8 +625,11 @@
 
     @Test
     public void testAdSelectionFlow_skipNonActivatedCA_Success() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -648,8 +682,11 @@
 
     @Test
     public void testAdSelectionFlow_skipExpiredCA_Success() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -708,8 +745,11 @@
 
     @Test
     public void testAdSelectionFlow_skipCAsThatTimeoutDuringBidding_Success() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -759,8 +799,11 @@
 
     @Test
     public void testAdSelection_overallTimeout_Failure() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
index 57e0e98..33988c1 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
@@ -92,6 +92,7 @@
     public void overrideAllowlists(boolean override) {
         String overrideStr = override ? "*" : "null";
         runShellCommand("device_config put adservices ppapi_app_allow_list " + overrideStr);
+        runShellCommand("device_config put adservices msmt_api_app_allow_list " + overrideStr);
         runShellCommand("device_config put adservices ppapi_app_signature_allow_list "
                 + overrideStr);
         runShellCommand(
@@ -114,6 +115,18 @@
         }
     }
 
+    public void enableBackCompat() {
+        runShellCommand("device_config put adservices enable_back_compat true");
+        runShellCommand("device_config put adservices consent_source_of_truth 3");
+        runShellCommand("device_config put adservices blocked_topics_source_of_truth 3");
+    }
+
+    public void disableBackCompat() {
+        runShellCommand("device_config put adservices enable_back_compat false");
+        runShellCommand("device_config put adservices consent_source_of_truth null");
+        runShellCommand("device_config put adservices blocked_topics_source_of_truth null");
+    }
+
     // Override measurement related kill switch to ignore the effect of actual PH values.
     // If isOverride = true, override measurement related kill switch to OFF to allow adservices
     // If isOverride = false, override measurement related kill switch to meaningless value so that
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
index c35cb46..c7be1d1 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
@@ -19,7 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import androidx.privacysandbox.ads.adservices.adid.AdId;
-import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.VersionCompatUtil;
 import androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures;
 import androidx.privacysandbox.ads.adservices.java.endtoend.TestUtil;
 import androidx.test.core.app.ApplicationProvider;
@@ -63,8 +63,11 @@
 
     @Test
     public void testAdId() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         AdIdManagerFutures adIdManager =
                 AdIdManagerFutures.from(ApplicationProvider.getApplicationContext());
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/appsetid/AppSetIdManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/appsetid/AppSetIdManagerTest.java
index ecebc1e..fb2fa2f 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/appsetid/AppSetIdManagerTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/appsetid/AppSetIdManagerTest.java
@@ -19,7 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import androidx.privacysandbox.ads.adservices.appsetid.AppSetId;
-import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.VersionCompatUtil;
 import androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures;
 import androidx.privacysandbox.ads.adservices.java.endtoend.TestUtil;
 import androidx.test.core.app.ApplicationProvider;
@@ -60,8 +60,11 @@
 
     @Test
     public void testAppSetId() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         AppSetIdManagerFutures appSetIdManager =
                 AppSetIdManagerFutures.from(ApplicationProvider.getApplicationContext());
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
index 7bc4ea9..c43168e 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
@@ -22,7 +22,7 @@
 
 import android.net.Uri;
 
-import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.VersionCompatUtil;
 import androidx.privacysandbox.ads.adservices.java.endtoend.TestUtil;
 import androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures;
 import androidx.privacysandbox.ads.adservices.measurement.DeletionRequest;
@@ -47,6 +47,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 
+@SuppressWarnings("NewApi")
 @RunWith(JUnit4.class)
 @SdkSuppress(minSdkVersion = 28) // API 28 required for device_config used by this test
 // TODO: Consider refactoring so that we're not duplicating code.
@@ -78,6 +79,9 @@
         mTestUtil.overrideDisableMeasurementEnrollmentCheck("1");
         mMeasurementManager =
                 MeasurementManagerFutures.from(ApplicationProvider.getApplicationContext());
+        if (VersionCompatUtil.INSTANCE.isSWithMinExtServicesVersion(9)) {
+            mTestUtil.enableBackCompat();
+        }
 
         // Put in a short sleep to make sure the updated config propagates
         // before starting the tests
@@ -92,14 +96,21 @@
         mTestUtil.overrideMeasurementKillSwitches(false);
         mTestUtil.overrideAdIdKillSwitch(false);
         mTestUtil.overrideDisableMeasurementEnrollmentCheck("0");
+        if (VersionCompatUtil.INSTANCE.isSWithMinExtServicesVersion(9)) {
+            mTestUtil.disableBackCompat();
+        }
+
         // Cool-off rate limiter
         TimeUnit.SECONDS.sleep(1);
     }
 
     @Test
     public void testRegisterSource_NoServerSetup_NoErrors() throws Exception {
-        // Skip the test if SDK extension 5 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 5);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 5,
+                        /* minExtServicesVersion=*/ 9));
 
         assertThat(mMeasurementManager.registerSourceAsync(
                 SOURCE_REGISTRATION_URI,
@@ -109,8 +120,12 @@
 
     @Test
     public void testRegisterAppSources_NoServerSetup_NoErrors() throws Exception {
+        // Skip the test if the right SDK extension is not present
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 5,
+                        /* minExtServicesVersion=*/ 9));
         // Skip the test if SDK extension 5 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 5);
 
         SourceRegistrationRequest request =
                 new SourceRegistrationRequest.Builder(
@@ -122,18 +137,23 @@
 
     @Test
     public void testRegisterTrigger_NoServerSetup_NoErrors() throws Exception {
-        // Skip the test if SDK extension 5 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 5);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 5,
+                        /* minExtServicesVersion=*/ 9));
 
         assertThat(mMeasurementManager.registerTriggerAsync(TRIGGER_REGISTRATION_URI).get())
                 .isNotNull();
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 33)
     public void registerWebSource_NoErrors() throws Exception {
-        // Skip the test if SDK extension 5 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 5);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 5,
+                        /* minExtServicesVersion=*/ 9));
 
         WebSourceParams webSourceParams =
                 new WebSourceParams(SOURCE_REGISTRATION_URI, false);
@@ -152,10 +172,12 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 33)
     public void registerWebTrigger_NoErrors() throws Exception {
-        // Skip the test if SDK extension 5 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 5);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 5,
+                        /* minExtServicesVersion=*/ 9));
 
         WebTriggerParams webTriggerParams =
                 new WebTriggerParams(TRIGGER_REGISTRATION_URI, false);
@@ -169,11 +191,12 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 33)
     public void testDeleteRegistrations_withRequest_withNoRange_withCallback_NoErrors()
             throws Exception {
         // Skip the test if SDK extension 5 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 5);
+        // This test should not run for back compat because it depends on adServices running in
+        // system server
+        Assume.assumeTrue(VersionCompatUtil.INSTANCE.isTPlusWithMinAdServicesVersion(5));
 
         DeletionRequest deletionRequest =
                 new DeletionRequest.Builder(
@@ -187,11 +210,13 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 33)
     public void testDeleteRegistrations_withRequest_withEmptyLists_withRange_withCallback_NoErrors()
             throws Exception {
-        // Skip the test if SDK extension 5 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 5);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 5,
+                        /* minExtServicesVersion=*/ 9));
 
         DeletionRequest deletionRequest =
                 new DeletionRequest.Builder(
@@ -207,11 +232,13 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 33)
     public void testDeleteRegistrations_withRequest_withInvalidArguments_withCallback_hasError()
             throws Exception {
-        // Skip the test if SDK extension 5 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 5);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 5,
+                        /* minExtServicesVersion=*/ 9));
 
         DeletionRequest deletionRequest =
                 new DeletionRequest.Builder(
@@ -230,10 +257,12 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 33)
     public void testMeasurementApiStatus_returnResultStatus() throws Exception {
-        // Skip the test if SDK extension 5 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 5);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 5,
+                        /* minExtServicesVersion=*/ 9));
 
         int result = mMeasurementManager.getMeasurementApiStatusAsync().get();
         assertThat(result).isEqualTo(1);
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
index 266394a..9b46fff 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
@@ -18,7 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.VersionCompatUtil;
 import androidx.privacysandbox.ads.adservices.java.endtoend.TestUtil;
 import androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures;
 import androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest;
@@ -73,6 +73,10 @@
         mTestUtil.shouldForceUseBundledFiles(true);
         // Enable verbose logging.
         mTestUtil.enableVerboseLogging();
+
+        if (VersionCompatUtil.INSTANCE.isSWithMinExtServicesVersion(9)) {
+            mTestUtil.enableBackCompat();
+        }
     }
 
     @After
@@ -84,13 +88,19 @@
         mTestUtil.overrideAllowlists(false);
         mTestUtil.enableEnrollmentCheck(false);
         mTestUtil.shouldForceUseBundledFiles(false);
+        if (VersionCompatUtil.INSTANCE.isSWithMinExtServicesVersion(9)) {
+            mTestUtil.disableBackCompat();
+        }
     }
 
     @Ignore // b/278931615
     @Test
     public void testTopicsManager_runClassifier() throws Exception {
-        // Skip the test if SDK extension 4 is not present.
-        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+        // Skip the test if the right SDK extension is not present.
+        Assume.assumeTrue(
+                VersionCompatUtil.INSTANCE.isTestableVersion(
+                        /* minAdServicesVersion=*/ 4,
+                        /* minExtServicesVersion=*/ 9));
 
         TopicsManagerFutures topicsManager =
                 TopicsManagerFutures.from(ApplicationProvider.getApplicationContext());
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt
index 0106e76..cb58c43 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt
@@ -21,10 +21,9 @@
 import android.net.Uri
 import android.os.Looper
 import android.os.OutcomeReceiver
-import android.os.ext.SdkExtensions
 import android.view.InputEvent
-import androidx.annotation.RequiresExtension
 import androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures
+import androidx.privacysandbox.ads.adservices.java.VersionCompatUtil
 import androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures.Companion.from
 import androidx.privacysandbox.ads.adservices.measurement.DeletionRequest
 import androidx.privacysandbox.ads.adservices.measurement.SourceRegistrationRequest
@@ -36,6 +35,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.google.common.truth.Truth.assertThat
 import java.time.Instant
 import java.util.concurrent.ExecutionException
@@ -46,6 +47,7 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
+import org.junit.After
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Test
@@ -61,6 +63,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.invocation.InvocationOnMock
+import org.mockito.quality.Strictness
 
 @SmallTest
 @SuppressWarnings("NewApi")
@@ -68,27 +71,46 @@
 @SdkSuppress(minSdkVersion = 30)
 class MeasurementManagerFuturesTest {
 
+    private var mSession: StaticMockitoSession? = null
+    private val mValidAdExtServicesSdkExtVersion = VersionCompatUtil.isSWithMinExtServicesVersion(9)
+
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+
+        if (mValidAdExtServicesSdkExtVersion) {
+            // setup a mockitoSession to return the mocked manager
+            // when the static method .get() is called
+            mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(android.adservices.measurement.MeasurementManager::class.java)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mSession?.finishMocking()
     }
 
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testMeasurementOlderVersions() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-
-        Assume.assumeTrue("maxSdkVersion = API 33 ext 4", sdkExtVersion < 5)
+        Assume.assumeFalse("maxSdkVersion = API 33 ext 4 or API 31/32 ext 8",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion=*/ 5,
+                /* minExtServicesVersion=*/ 9))
         assertThat(from(mContext)).isEqualTo(null)
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testDeleteRegistrationsAsync() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 5,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
+        val mMeasurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = from(mContext)
 
         // Set up the request.
@@ -98,7 +120,7 @@
             assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
             null
         }
-        doAnswer(answer).`when`(measurementManager).deleteRegistrations(any(), any(), any())
+        doAnswer(answer).`when`(mMeasurementManager).deleteRegistrations(any(), any(), any())
 
         // Actually invoke the compat code.
         val request = DeletionRequest(
@@ -115,28 +137,30 @@
         val captor = ArgumentCaptor.forClass(
             android.adservices.measurement.DeletionRequest::class.java
         )
-        verify(measurementManager).deleteRegistrations(captor.capture(), any(), any())
+        verify(mMeasurementManager).deleteRegistrations(captor.capture(), any(), any())
 
         // Verify that the request that the compat code makes to the platform is correct.
         verifyDeletionRequest(captor.value)
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testRegisterSourceAsync() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 5,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
+        val mMeasurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val inputEvent = mock(InputEvent::class.java)
-        val measurementManager = mockMeasurementManager(mContext)
         val managerCompat = from(mContext)
+
         val answer = { args: InvocationOnMock ->
             assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
             val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(3)
             receiver.onResult(Object())
             null
         }
-        doAnswer(answer).`when`(measurementManager).registerSource(any(), any(), any(), any())
+        doAnswer(answer).`when`(mMeasurementManager).registerSource(any(), any(), any(), any())
 
         // Actually invoke the compat code.
         managerCompat!!.registerSourceAsync(uri1, inputEvent).get()
@@ -144,7 +168,7 @@
         // Verify that the compat code was invoked correctly.
         val captor1 = ArgumentCaptor.forClass(Uri::class.java)
         val captor2 = ArgumentCaptor.forClass(InputEvent::class.java)
-        verify(measurementManager).registerSource(
+        verify(mMeasurementManager).registerSource(
             captor1.capture(),
             captor2.capture(),
             any(),
@@ -156,27 +180,29 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testRegisterTriggerAsync() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 5,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
+        val mMeasurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = from(mContext)
+
         val answer = { args: InvocationOnMock ->
             assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
             val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
             receiver.onResult(Object())
             null
         }
-        doAnswer(answer).`when`(measurementManager).registerTrigger(any(), any(), any())
+        doAnswer(answer).`when`(mMeasurementManager).registerTrigger(any(), any(), any())
 
         // Actually invoke the compat code.
         managerCompat!!.registerTriggerAsync(uri1).get()
 
         // Verify that the compat code was invoked correctly.
         val captor1 = ArgumentCaptor.forClass(Uri::class.java)
-        verify(measurementManager).registerTrigger(
+        verify(mMeasurementManager).registerTrigger(
             captor1.capture(),
             any(),
             any())
@@ -186,20 +212,22 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testRegisterWebSourceAsync() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 5,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
+        val mMeasurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = from(mContext)
+
         val answer = { args: InvocationOnMock ->
             assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
             val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
             receiver.onResult(Object())
             null
         }
-        doAnswer(answer).`when`(measurementManager).registerWebSource(any(), any(), any())
+        doAnswer(answer).`when`(mMeasurementManager).registerWebSource(any(), any(), any())
 
         val request = WebSourceRegistrationRequest.Builder(
             listOf(WebSourceParams(uri2, false)), uri1)
@@ -212,7 +240,7 @@
         // Verify that the compat code was invoked correctly.
         val captor1 = ArgumentCaptor.forClass(
             android.adservices.measurement.WebSourceRegistrationRequest::class.java)
-        verify(measurementManager).registerWebSource(
+        verify(mMeasurementManager).registerWebSource(
             captor1.capture(),
             any(),
             any())
@@ -226,20 +254,22 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testRegisterWebTriggerAsync() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 5,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
+        val mMeasurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = from(mContext)
+
         val answer = { args: InvocationOnMock ->
             assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
             val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
             receiver.onResult(Object())
             null
         }
-        doAnswer(answer).`when`(measurementManager).registerWebTrigger(any(), any(), any())
+        doAnswer(answer).`when`(mMeasurementManager).registerWebTrigger(any(), any(), any())
 
         val request = WebTriggerRegistrationRequest(listOf(WebTriggerParams(uri1, false)), uri2)
 
@@ -249,7 +279,7 @@
         // Verify that the compat code was invoked correctly.
         val captor1 = ArgumentCaptor.forClass(
             android.adservices.measurement.WebTriggerRegistrationRequest::class.java)
-        verify(measurementManager).registerWebTrigger(
+        verify(mMeasurementManager).registerWebTrigger(
             captor1.capture(),
             any(),
             any())
@@ -263,13 +293,15 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testMeasurementApiStatusAsync() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 5,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
+        val mMeasurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = from(mContext)
+
         val state = MeasurementManager.MEASUREMENT_API_STATE_DISABLED
         val answer = { args: InvocationOnMock ->
             assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
@@ -277,14 +309,14 @@
             receiver.onResult(state)
             null
         }
-        doAnswer(answer).`when`(measurementManager).getMeasurementApiStatus(any(), any())
+        doAnswer(answer).`when`(mMeasurementManager).getMeasurementApiStatus(any(), any())
 
         // Actually invoke the compat code.
         val result = managerCompat!!.getMeasurementApiStatusAsync()
         result.get()
 
         // Verify that the compat code was invoked correctly.
-        verify(measurementManager).getMeasurementApiStatus(any(), any())
+        verify(mMeasurementManager).getMeasurementApiStatus(any(), any())
 
         // Verify that the result.
         assertThat(result.get() == state)
@@ -292,21 +324,23 @@
 
     @ExperimentalFeatures.RegisterSourceOptIn
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testRegisterSourceAsync_allSuccess() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 5,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
+        val mMeasurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val inputEvent = mock(InputEvent::class.java)
-        val measurementManager = mockMeasurementManager(mContext)
         val managerCompat = from(mContext)
+
         val successCallback = { args: InvocationOnMock ->
             assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
             val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(3)
             receiver.onResult(Object())
             null
         }
-        doAnswer(successCallback).`when`(measurementManager)
+        doAnswer(successCallback).`when`(mMeasurementManager)
             .registerSource(any(), any(), any(), any())
 
         // Actually invoke the compat code.
@@ -316,12 +350,12 @@
         managerCompat!!.registerSourceAsync(request).get()
 
         // Verify that the compat code was invoked correctly.
-        verify(measurementManager).registerSource(
+        verify(mMeasurementManager).registerSource(
             eq(uri1),
             eq(inputEvent),
             any(Executor::class.java),
             any())
-        verify(measurementManager).registerSource(
+        verify(mMeasurementManager).registerSource(
             eq(uri2),
             eq(inputEvent),
             any(Executor::class.java),
@@ -329,15 +363,17 @@
     }
 
     @ExperimentalFeatures.RegisterSourceOptIn
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     @Test
     fun testRegisterSource_15thOf20Fails_atLeast15thExecutes() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 5,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
+        val mMeasurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val mockInputEvent = mock(InputEvent::class.java)
         val managerCompat = from(mContext)
+
         val successCallback = { args: InvocationOnMock ->
             val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(3)
             receiver.onResult(Object())
@@ -354,10 +390,10 @@
         val uris = (1..20).map { i ->
             val uri = Uri.parse("www.uri$i.com")
             if (i == 15) {
-                doAnswer(errorCallback).`when`(measurementManager)
+                doAnswer(errorCallback).`when`(mMeasurementManager)
                     .registerSource(eq(uri), any(), any(), any())
             } else {
-                doAnswer(successCallback).`when`(measurementManager)
+                doAnswer(successCallback).`when`(mMeasurementManager)
                     .registerSource(eq(uri), any(), any(), any())
             }
             uri
@@ -382,13 +418,13 @@
         // registerSource gets called 1-20 times. We cannot predict the exact number because
         // uri15 would crash asynchronously. Other uris may succeed and those threads on default
         // dispatcher won't crash.
-        verify(measurementManager, atLeastOnce()).registerSource(
+        verify(mMeasurementManager, atLeastOnce()).registerSource(
             any(),
             eq(mockInputEvent),
             any(),
             any()
         )
-        verify(measurementManager, atMost(20)).registerSource(
+        verify(mMeasurementManager, atMost(20)).registerSource(
             any(),
             eq(mockInputEvent),
             any(),
@@ -397,17 +433,25 @@
     }
 
     @SdkSuppress(minSdkVersion = 30)
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     companion object {
 
         private val uri1: Uri = Uri.parse("www.abc.com")
         private val uri2: Uri = Uri.parse("http://www.xyz.com")
         private lateinit var mContext: Context
 
-        private fun mockMeasurementManager(spyContext: Context): MeasurementManager {
+        private fun mockMeasurementManager(
+            spyContext: Context,
+            isExtServices: Boolean
+        ): MeasurementManager {
             val measurementManager = mock(MeasurementManager::class.java)
-            `when`(spyContext.getSystemService(MeasurementManager::class.java))
-                .thenReturn(measurementManager)
+            // mock the .get() method if using extServices version, otherwise mock getSystemService
+            if (isExtServices) {
+                `when`(android.adservices.measurement.MeasurementManager.get(any()))
+                    .thenReturn(measurementManager)
+            } else {
+                `when`(spyContext.getSystemService(MeasurementManager::class.java))
+                    .thenReturn(measurementManager)
+            }
             return measurementManager
         }
 
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt
index c44c32f..6d84365 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt
@@ -20,8 +20,7 @@
 import android.adservices.topics.TopicsManager
 import android.content.Context
 import android.os.OutcomeReceiver
-import android.os.ext.SdkExtensions
-import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.java.VersionCompatUtil
 import androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures.Companion.from
 import androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest
 import androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse
@@ -29,8 +28,11 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.ListenableFuture
+import org.junit.After
 import org.junit.Assert
 import org.junit.Assume
 import org.junit.Before
@@ -44,6 +46,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.invocation.InvocationOnMock
+import org.mockito.quality.Strictness
 
 @SmallTest
 @SuppressWarnings("NewApi")
@@ -51,43 +54,65 @@
 @SdkSuppress(minSdkVersion = 30)
 class TopicsManagerFuturesTest {
 
+    private var mSession: StaticMockitoSession? = null
+    private val mValidAdExtServicesSdkExtVersion = VersionCompatUtil.isSWithMinExtServicesVersion(9)
+
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+
+        if (mValidAdExtServicesSdkExtVersion) {
+            // setup a mockitoSession to return the mocked manager
+            // when the static method .get() is called
+            mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(android.adservices.topics.TopicsManager::class.java)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mSession?.finishMocking()
     }
 
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testTopicsOlderVersions() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-
-        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Assume.assumeFalse("maxSdkVersion = API 33 ext 3 or API 31/32 ext 8",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion=*/ 4,
+                /* minExtServicesVersion=*/ 9))
         assertThat(from(mContext)).isEqualTo(null)
     }
 
     @Test
     @SuppressWarnings("NewApi")
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testTopicsAsync() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 4,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val topicsManager = mockTopicsManager(mContext)
+        val topicsManager = mockTopicsManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupTopicsResponse(topicsManager)
         val managerCompat = from(mContext)
 
         // Actually invoke the compat code.
-        val request =
-            GetTopicsRequest.Builder().setAdsSdkName(mSdkName).setShouldRecordObservation(true)
-                .build()
+        val request = GetTopicsRequest.Builder()
+            .setAdsSdkName(mSdkName)
+            .setShouldRecordObservation(true)
+            .build()
 
-        val result: ListenableFuture<GetTopicsResponse> = managerCompat!!.getTopicsAsync(request)
+        val result: ListenableFuture<GetTopicsResponse> =
+            managerCompat!!.getTopicsAsync(request)
 
         // Verify that the result of the compat call is correct.
         verifyResponse(result.get())
 
         // Verify that the compat code was invoked correctly.
-        val captor = ArgumentCaptor.forClass(android.adservices.topics.GetTopicsRequest::class.java)
+        val captor = ArgumentCaptor
+            .forClass(android.adservices.topics.GetTopicsRequest::class.java)
         verify(topicsManager).getTopics(captor.capture(), any(), any())
 
         // Verify that the request that the compat code makes to the platform is correct.
@@ -96,12 +121,13 @@
 
     @Test
     @SuppressWarnings("NewApi")
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testTopicsAsyncPreviewSupported() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            VersionCompatUtil.isTestableVersion(
+                /* minAdServicesVersion= */ 5,
+                /* minExtServicesVersion=*/ 9))
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val topicsManager = mockTopicsManager(mContext)
+        val topicsManager = mockTopicsManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupTopicsResponse(topicsManager)
         val managerCompat = from(mContext)
 
@@ -110,13 +136,15 @@
             GetTopicsRequest.Builder().setAdsSdkName(mSdkName).setShouldRecordObservation(false)
                 .build()
 
-        val result: ListenableFuture<GetTopicsResponse> = managerCompat!!.getTopicsAsync(request)
+        val result: ListenableFuture<GetTopicsResponse> =
+            managerCompat!!.getTopicsAsync(request)
 
         // Verify that the result of the compat call is correct.
         verifyResponse(result.get())
 
         // Verify that the compat code was invoked correctly.
-        val captor = ArgumentCaptor.forClass(android.adservices.topics.GetTopicsRequest::class.java)
+        val captor =
+            ArgumentCaptor.forClass(android.adservices.topics.GetTopicsRequest::class.java)
         verify(topicsManager).getTopics(captor.capture(), any(), any())
 
         // Verify that the request that the compat code makes to the platform is correct.
@@ -127,14 +155,18 @@
         private lateinit var mContext: Context
         private val mSdkName: String = "sdk1"
 
-        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
-        private fun mockTopicsManager(spyContext: Context): TopicsManager {
+        private fun mockTopicsManager(spyContext: Context, isExtServices: Boolean): TopicsManager {
             val topicsManager = mock(TopicsManager::class.java)
-            `when`(spyContext.getSystemService(TopicsManager::class.java)).thenReturn(topicsManager)
+            // mock the .get() method if using extServices version, otherwise mock getSystemService
+            if (isExtServices) {
+                `when`(TopicsManager.get(any())).thenReturn(topicsManager)
+            } else {
+                `when`(spyContext.getSystemService(TopicsManager::class.java))
+                    .thenReturn(topicsManager)
+            }
             return topicsManager
         }
 
-        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
         private fun setupTopicsResponse(topicsManager: TopicsManager) {
             // Set up the response that TopicsManager will return when the compat code calls it.
             val topic1 = Topic(1, 1, 1)
@@ -152,7 +184,6 @@
             )
         }
 
-        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
         private fun verifyRequest(topicsRequest: android.adservices.topics.GetTopicsRequest) {
             // Set up the request that we expect the compat code to invoke.
             val expectedRequest =
@@ -161,7 +192,6 @@
             Assert.assertEquals(expectedRequest.adsSdkName, topicsRequest.adsSdkName)
         }
 
-        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
         private fun verifyRequestPreviewApi(
             topicsRequest: android.adservices.topics.GetTopicsRequest
         ) {
@@ -176,7 +206,6 @@
             )
         }
 
-        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
         private fun verifyResponse(getTopicsResponse: GetTopicsResponse) {
             Assert.assertEquals(2, getTopicsResponse.topics.size)
             val topic1 = getTopicsResponse.topics[0]
diff --git a/privacysandbox/ads/ads-adservices/build.gradle b/privacysandbox/ads/ads-adservices/build.gradle
index 866cc02..1c3fa23 100644
--- a/privacysandbox/ads/ads-adservices/build.gradle
+++ b/privacysandbox/ads/ads-adservices/build.gradle
@@ -41,6 +41,7 @@
 
     androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy) // DexMaker has it"s own MockMaker
     androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(libs.dexmakerMockitoInlineExtended)
 }
 
 android {
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
index 197f46b..ce7e661 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
@@ -20,14 +20,18 @@
 import android.os.OutcomeReceiver
 import android.os.ext.SdkExtensions
 import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
+import org.junit.After
 import org.junit.Assert
-import org.junit.Assume.assumeTrue
+import org.junit.Assume
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -44,27 +48,42 @@
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = 30)
 class AdIdManagerTest {
+    private var mSession: StaticMockitoSession? = null
+    private val mValidAdServicesSdkExtVersion = AdServicesInfo.adServicesVersion() >= 4
+    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersion() >= 9
 
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+
+        if (mValidAdExtServicesSdkExtVersion) {
+            // setup a mockitoSession to return the mocked manager
+            // when the static method .get() is called
+            mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(android.adservices.adid.AdIdManager::class.java)
+                .startMocking();
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mSession?.finishMocking()
     }
 
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testAdIdOlderVersions() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-        assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", !mValidAdServicesSdkExtVersion)
+        Assume.assumeTrue("maxSdkVersion = API 31/32 ext 8", !mValidAdExtServicesSdkExtVersion)
         assertThat(AdIdManager.obtain(mContext)).isEqualTo(null)
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testAdIdAsync() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val adIdManager = mockAdIdManager(mContext)
+        val adIdManager = mockAdIdManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupResponse(adIdManager)
         val managerCompat = AdIdManager.obtain(mContext)
 
@@ -85,10 +104,19 @@
     companion object {
         private lateinit var mContext: Context
 
-        private fun mockAdIdManager(spyContext: Context): android.adservices.adid.AdIdManager {
+        private fun mockAdIdManager(
+            spyContext: Context,
+            isExtServices: Boolean
+        ): android.adservices.adid.AdIdManager {
             val adIdManager = mock(android.adservices.adid.AdIdManager::class.java)
-            `when`(spyContext.getSystemService(android.adservices.adid.AdIdManager::class.java))
-                .thenReturn(adIdManager)
+            // only mock the .get() method if using extServices version
+            if (isExtServices) {
+                `when`(android.adservices.adid.AdIdManager.get(any()))
+                    .thenReturn(adIdManager)
+            } else {
+                `when`(spyContext.getSystemService(android.adservices.adid.AdIdManager::class.java))
+                    .thenReturn(adIdManager)
+            }
             return adIdManager
         }
 
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt
index 8d71ea2..8f4b4a24 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt
@@ -20,17 +20,19 @@
 import android.content.Context
 import android.net.Uri
 import android.os.OutcomeReceiver
-import android.os.ext.SdkExtensions
-import androidx.annotation.RequiresExtension
 import androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager.Companion.obtain
 import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
 import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
+import org.junit.After
 import org.junit.Assert
 import org.junit.Assume
 import org.junit.Before
@@ -44,33 +46,50 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.invocation.InvocationOnMock
+import org.mockito.quality.Strictness
 
 @SmallTest
 @SuppressWarnings("NewApi")
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = 30)
 class AdSelectionManagerTest {
+    private var mSession: StaticMockitoSession? = null
+    private val mValidAdServicesSdkExtVersion = AdServicesInfo.adServicesVersion() >= 4
+    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersion() >= 9
+
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+
+        if (mValidAdExtServicesSdkExtVersion) {
+            // setup a mockitoSession to return the mocked manager
+            // when the static method .get() is called
+            mSession = mockitoSession()
+                .mockStatic(android.adservices.adselection.AdSelectionManager::class.java)
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mSession?.finishMocking()
     }
 
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testAdSelectionOlderVersions() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-
-        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", !mValidAdServicesSdkExtVersion)
+        Assume.assumeTrue("maxSdkVersion = API 31/32 ext 8", !mValidAdExtServicesSdkExtVersion)
         assertThat(obtain(mContext)).isEqualTo(null)
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testSelectAds() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val adSelectionManager = mockAdSelectionManager(mContext)
+        val adSelectionManager = mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupAdSelectionResponse(adSelectionManager)
         val managerCompat = obtain(mContext)
 
@@ -92,13 +111,13 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testReportImpression() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val adSelectionManager = mockAdSelectionManager(mContext)
+        val adSelectionManager = mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupAdSelectionResponse(adSelectionManager)
+
         val managerCompat = obtain(mContext)
         val reportImpressionRequest = ReportImpressionRequest(adSelectionId, adSelectionConfig)
 
@@ -117,7 +136,6 @@
     }
 
     @SdkSuppress(minSdkVersion = 30)
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     companion object {
         private lateinit var mContext: Context
         private const val adSelectionId = 1234L
@@ -146,13 +164,20 @@
         private val renderUri = Uri.parse("render-uri.com")
 
         private fun mockAdSelectionManager(
-            spyContext: Context
+            spyContext: Context,
+            isExtServices: Boolean
         ): android.adservices.adselection.AdSelectionManager {
             val adSelectionManager =
                 mock(android.adservices.adselection.AdSelectionManager::class.java)
             `when`(spyContext.getSystemService(
                 android.adservices.adselection.AdSelectionManager::class.java))
                 .thenReturn(adSelectionManager)
+            // only mock the .get() method if using extServices version
+            if (isExtServices) {
+                `when`(android.adservices.adselection.AdSelectionManager.get(any()))
+                    .thenReturn(adSelectionManager)
+            }
+
             return adSelectionManager
         }
 
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt
index f3f3e99..8167b8f 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt
@@ -20,12 +20,16 @@
 import android.os.OutcomeReceiver
 import android.os.ext.SdkExtensions
 import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
+import org.junit.After
 import org.junit.Assert
 import org.junit.Assume
 import org.junit.Before
@@ -44,28 +48,42 @@
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = 30)
 class AppSetIdManagerTest {
+    private var mSession: StaticMockitoSession? = null
+    private val mValidAdServicesSdkExtVersion = AdServicesInfo.adServicesVersion() >= 4
+    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersion() >= 9
+
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+
+        if (mValidAdExtServicesSdkExtVersion) {
+            // setup a mockitoSession to return the mocked manager
+            // when the static method .get() is called
+            mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(android.adservices.appsetid.AppSetIdManager::class.java)
+                .startMocking();
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mSession?.finishMocking()
     }
 
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testAppSetIdOlderVersions() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-
-        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", !mValidAdServicesSdkExtVersion)
+        Assume.assumeTrue("maxSdkVersion = API 31/32 ext 8", !mValidAdExtServicesSdkExtVersion)
         assertThat(AppSetIdManager.obtain(mContext)).isEqualTo(null)
     }
 
     @Test
-    @SuppressWarnings("NewApi")
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testAppSetIdAsync() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val appSetIdManager = mockAppSetIdManager(mContext)
+        val appSetIdManager = mockAppSetIdManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupResponse(appSetIdManager)
         val managerCompat = AppSetIdManager.obtain(mContext)
 
@@ -87,12 +105,19 @@
         private lateinit var mContext: Context
 
         private fun mockAppSetIdManager(
-            spyContext: Context
+            spyContext: Context,
+            isExtServices: Boolean
         ): android.adservices.appsetid.AppSetIdManager {
             val appSetIdManager = mock(android.adservices.appsetid.AppSetIdManager::class.java)
-            `when`(spyContext.getSystemService(
-                android.adservices.appsetid.AppSetIdManager::class.java))
-                .thenReturn(appSetIdManager)
+            // only mock the .get() method if using extServices version
+            if (isExtServices) {
+                `when`(android.adservices.appsetid.AppSetIdManager.get(any()))
+                    .thenReturn(appSetIdManager)
+            } else {
+                `when`(spyContext.getSystemService(
+                    android.adservices.appsetid.AppSetIdManager::class.java))
+                    .thenReturn(appSetIdManager)
+            }
             return appSetIdManager
         }
 
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt
index a107e7d..53d4f16 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt
@@ -20,19 +20,21 @@
 import android.content.Context
 import android.net.Uri
 import android.os.OutcomeReceiver
-import android.os.ext.SdkExtensions
-import androidx.annotation.RequiresExtension
 import androidx.privacysandbox.ads.adservices.common.AdData
 import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
 import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
 import androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager.Companion.obtain
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.google.common.truth.Truth
 import java.time.Instant
 import kotlinx.coroutines.runBlocking
+import org.junit.After
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Test
@@ -45,6 +47,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.invocation.InvocationOnMock
+import org.mockito.quality.Strictness
 
 @SmallTest
 @SuppressWarnings("NewApi")
@@ -52,27 +55,44 @@
 @SdkSuppress(minSdkVersion = 30)
 class CustomAudienceManagerTest {
 
+    private var mSession: StaticMockitoSession? = null
+    private val mValidAdServicesSdkExtVersion = AdServicesInfo.adServicesVersion() >= 4
+    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersion() >= 9
+
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+
+        if (mValidAdExtServicesSdkExtVersion) {
+            // setup a mockitoSession to return the mocked manager
+            // when the static method .get() is called
+            mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(android.adservices.customaudience.CustomAudienceManager::class.java)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mSession?.finishMocking()
     }
 
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testOlderVersions() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-
-        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", !mValidAdServicesSdkExtVersion)
+        Assume.assumeTrue("maxSdkVersion = API 31/32 ext 8", !mValidAdExtServicesSdkExtVersion)
         Truth.assertThat(obtain(mContext)).isEqualTo(null)
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testJoinCustomAudience() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val customAudienceManager = mockCustomAudienceManager(mContext)
+        val customAudienceManager =
+            mockCustomAudienceManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupResponse(customAudienceManager)
         val managerCompat = obtain(mContext)
 
@@ -99,12 +119,12 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testLeaveCustomAudience() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val customAudienceManager = mockCustomAudienceManager(mContext)
+        val customAudienceManager =
+            mockCustomAudienceManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupResponse(customAudienceManager)
         val managerCompat = obtain(mContext)
 
@@ -125,7 +145,6 @@
     }
 
     @SdkSuppress(minSdkVersion = 30)
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     companion object {
         private lateinit var mContext: Context
         private val uri: Uri = Uri.parse("abc.com")
@@ -139,10 +158,17 @@
         private const val metadata = "metadata"
         private val ads: List<AdData> = listOf(AdData(uri, metadata))
 
-        private fun mockCustomAudienceManager(spyContext: Context): CustomAudienceManager {
+        private fun mockCustomAudienceManager(
+            spyContext: Context,
+            isExtServices: Boolean
+        ): CustomAudienceManager {
             val customAudienceManager = mock(CustomAudienceManager::class.java)
             `when`(spyContext.getSystemService(CustomAudienceManager::class.java))
                 .thenReturn(customAudienceManager)
+            // only mock the .get() method if using extServices version
+            if (isExtServices) {
+                `when`(CustomAudienceManager.get(any())).thenReturn(customAudienceManager)
+            }
             return customAudienceManager
         }
 
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
index 1e0a826..1b9f302 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
@@ -20,20 +20,21 @@
 import android.content.Context
 import android.net.Uri
 import android.os.OutcomeReceiver
-import android.os.ext.SdkExtensions
 import android.view.InputEvent
-import androidx.annotation.RequiresExtension
 import androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
 import androidx.privacysandbox.ads.adservices.measurement.MeasurementManager.Companion.obtain
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import androidx.testutils.fail
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.google.common.truth.Truth.assertThat
 import java.time.Instant
-import kotlin.IllegalArgumentException
 import kotlinx.coroutines.runBlocking
+import org.junit.After
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Test
@@ -49,6 +50,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.invocation.InvocationOnMock
+import org.mockito.quality.Strictness
 
 @SmallTest
 @SuppressWarnings("NewApi")
@@ -56,27 +58,43 @@
 @SdkSuppress(minSdkVersion = 30)
 class MeasurementManagerTest {
 
+    private var mSession: StaticMockitoSession? = null
+    private val mValidAdServicesSdkExtVersion = AdServicesInfo.adServicesVersion() >= 5
+    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersion() >= 9
+
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+
+        if (mValidAdExtServicesSdkExtVersion) {
+            // setup a mockitoSession to return the mocked manager
+            // when the static method .get() is called
+            mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(android.adservices.measurement.MeasurementManager::class.java)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mSession?.finishMocking()
     }
 
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testMeasurementOlderVersions() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-
-        Assume.assumeTrue("maxSdkVersion = API 33 ext 4", sdkExtVersion < 5)
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 4", !mValidAdServicesSdkExtVersion)
+        Assume.assumeTrue("maxSdkVersion = API 31/32 ext 8", !mValidAdExtServicesSdkExtVersion)
         assertThat(obtain(mContext)).isEqualTo(null)
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testDeleteRegistrations() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
+        val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
 
         // Set up the request.
@@ -111,14 +129,14 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testRegisterSource() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
+        val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val inputEvent = mock(InputEvent::class.java)
-        val measurementManager = mockMeasurementManager(mContext)
         val managerCompat = obtain(mContext)
+
         val answer = { args: InvocationOnMock ->
             val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(3)
             receiver.onResult(Object())
@@ -146,12 +164,11 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testRegisterTrigger() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
+        val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
         val answer = { args: InvocationOnMock ->
             val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
@@ -177,12 +194,11 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testRegisterWebSource() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
+        val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
         val answer = { args: InvocationOnMock ->
             val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
@@ -218,15 +234,15 @@
     }
 
     @ExperimentalFeatures.RegisterSourceOptIn
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     @Test
     fun testRegisterSource_allSuccess() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
+        val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val mockInputEvent = mock(InputEvent::class.java)
         val managerCompat = obtain(mContext)
+
         val successCallback = { args: InvocationOnMock ->
             val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(3)
             receiver.onResult(Object())
@@ -252,15 +268,15 @@
     }
 
     @ExperimentalFeatures.RegisterSourceOptIn
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     @Test
     fun testRegisterSource_15thOf20Fails_remaining5DoNotExecute() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
+        val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val mockInputEvent = mock(InputEvent::class.java)
         val managerCompat = obtain(mContext)
+
         val successCallback = { args: InvocationOnMock ->
             val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(3)
             receiver.onResult(Object())
@@ -317,12 +333,11 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testRegisterWebTrigger() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
+        val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
         val answer = { args: InvocationOnMock ->
             val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
@@ -356,63 +371,33 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testMeasurementApiStatus() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
-        val managerCompat = obtain(mContext)
-        val state = MeasurementManager.MEASUREMENT_API_STATE_ENABLED
-        val answer = { args: InvocationOnMock ->
-            val receiver = args.getArgument<OutcomeReceiver<Int, Exception>>(1)
-            receiver.onResult(state)
-            null
-        }
-        doAnswer(answer).`when`(measurementManager).getMeasurementApiStatus(any(), any())
-
-        // Actually invoke the compat code.
-        val actualResult = runBlocking {
-            managerCompat!!.getMeasurementApiStatus()
-        }
-
-        // Verify that the compat code was invoked correctly.
-        verify(measurementManager).getMeasurementApiStatus(any(), any())
-
-        // Verify that the request that the compat code makes to the platform is correct.
-        assertThat(actualResult == state)
+        val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
+        callAndVerifyGetMeasurementApiStatus(
+            measurementManager,
+            /* state= */ MeasurementManager.MEASUREMENT_API_STATE_ENABLED,
+            /* expectedResult= */ MeasurementManager.MEASUREMENT_API_STATE_ENABLED)
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testMeasurementApiStatusUnknown() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val measurementManager = mockMeasurementManager(mContext)
-        val managerCompat = obtain(mContext)
-        val answer = { args: InvocationOnMock ->
-            val receiver = args.getArgument<OutcomeReceiver<Int, Exception>>(1)
-            receiver.onResult(6 /* Greater than values returned in SdkExtensions.AD_SERVICES = 5 */)
-            null
-        }
-        doAnswer(answer).`when`(measurementManager).getMeasurementApiStatus(any(), any())
+        val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersion)
 
-        // Actually invoke the compat code.
-        val actualResult = runBlocking {
-            managerCompat!!.getMeasurementApiStatus()
-        }
-
-        // Verify that the compat code was invoked correctly.
-        verify(measurementManager).getMeasurementApiStatus(any(), any())
-
-        // Verify that the request that the compat code makes to the platform is correct.
+        // Call with a value greater than values returned in SdkExtensions.AD_SERVICES = 5
         // Since the compat code does not know the returned state, it sets it to UNKNOWN.
-        assertThat(actualResult == 5)
+        callAndVerifyGetMeasurementApiStatus(
+            measurementManager,
+            /* state= */ 6,
+            /* expectedResult= */ 5)
     }
 
     @SdkSuppress(minSdkVersion = 30)
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     companion object {
 
         private val uri1: Uri = Uri.parse("www.abc.com")
@@ -420,13 +405,46 @@
 
         private lateinit var mContext: Context
 
-        private fun mockMeasurementManager(spyContext: Context): MeasurementManager {
+        private fun mockMeasurementManager(
+            spyContext: Context,
+            isExtServices: Boolean
+        ): MeasurementManager {
             val measurementManager = mock(MeasurementManager::class.java)
             `when`(spyContext.getSystemService(MeasurementManager::class.java))
                 .thenReturn(measurementManager)
+            // only mock the .get() method if using the extServices version
+            if (isExtServices) {
+                `when`(MeasurementManager.get(any()))
+                    .thenReturn(measurementManager)
+            }
             return measurementManager
         }
 
+        private fun callAndVerifyGetMeasurementApiStatus(
+            measurementManager: android.adservices.measurement.MeasurementManager,
+            state: Int,
+            expectedResult: Int
+        ) {
+            val managerCompat = obtain(mContext)
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<Int, Exception>>(1)
+                receiver.onResult(state)
+                null
+            }
+            doAnswer(answer).`when`(measurementManager).getMeasurementApiStatus(any(), any())
+
+            // Actually invoke the compat code.
+            val actualResult = runBlocking {
+                managerCompat!!.getMeasurementApiStatus()
+            }
+
+            // Verify that the compat code was invoked correctly.
+            verify(measurementManager).getMeasurementApiStatus(any(), any())
+
+            // Verify that the request that the compat code makes to the platform is correct.
+            assertThat(actualResult == expectedResult)
+        }
+
         private fun verifyDeletionRequest(request: android.adservices.measurement.DeletionRequest) {
             // Set up the request that we expect the compat code to invoke.
             val expectedRequest = android.adservices.measurement.DeletionRequest.Builder()
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
index d79e94e..82a492b 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
@@ -20,15 +20,17 @@
 import android.adservices.topics.TopicsManager
 import android.content.Context
 import android.os.OutcomeReceiver
-import android.os.ext.SdkExtensions
-import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
 import androidx.privacysandbox.ads.adservices.topics.TopicsManager.Companion.obtain
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
+import org.junit.After
 import org.junit.Assert
 import org.junit.Assume
 import org.junit.Before
@@ -42,6 +44,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.invocation.InvocationOnMock
+import org.mockito.quality.Strictness
 
 @SmallTest
 @SuppressWarnings("NewApi")
@@ -49,42 +52,60 @@
 @SdkSuppress(minSdkVersion = 30)
 class TopicsManagerTest {
 
+    private var mSession: StaticMockitoSession? = null
+    private val mValidAdServicesSdkExt4Version = AdServicesInfo.adServicesVersion() >= 4
+    private val mValidAdServicesSdkExt5Version = AdServicesInfo.adServicesVersion() >= 5
+    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersion() >= 9
+
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+
+        if (mValidAdExtServicesSdkExtVersion) {
+            // setup a mockitoSession to return the mocked manager
+            // when the static method .get() is called
+            mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(android.adservices.topics.TopicsManager::class.java)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mSession?.finishMocking()
     }
 
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testTopicsOlderVersions() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
-
-        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", !mValidAdServicesSdkExt4Version)
+        Assume.assumeTrue("maxSdkVersion = API 31/32 ext 8", !mValidAdExtServicesSdkExtVersion)
         assertThat(obtain(mContext)).isEqualTo(null)
     }
 
     @Test
-    @SuppressWarnings("NewApi")
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     fun testTopicsAsync() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+            mValidAdServicesSdkExt4Version || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
-        val topicsManager = mockTopicsManager(mContext)
+        val topicsManager = mockTopicsManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupTopicsResponse(topicsManager)
         val managerCompat = obtain(mContext)
 
         // Actually invoke the compat code.
         val result = runBlocking {
-            val request =
-                GetTopicsRequest.Builder().setAdsSdkName(mSdkName).setShouldRecordObservation(true)
-                    .build()
+            val request = GetTopicsRequest.Builder()
+                .setAdsSdkName(mSdkName)
+                .setShouldRecordObservation(true)
+                .build()
 
             managerCompat!!.getTopics(request)
         }
 
         // Verify that the compat code was invoked correctly.
-        val captor = ArgumentCaptor.forClass(android.adservices.topics.GetTopicsRequest::class.java)
+        val captor = ArgumentCaptor
+            .forClass(android.adservices.topics.GetTopicsRequest::class.java)
         verify(topicsManager).getTopics(captor.capture(), any(), any())
 
         // Verify that the request that the compat code makes to the platform is correct.
@@ -95,26 +116,28 @@
     }
 
     @Test
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
     fun testTopicsAsyncPreviewSupported() {
-        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 5 or API 31/32 ext 9",
+            mValidAdServicesSdkExt5Version || mValidAdExtServicesSdkExtVersion)
 
-        Assume.assumeTrue("minSdkVersion = API 33 ext 5", sdkExtVersion >= 5)
-        val topicsManager = mockTopicsManager(mContext)
+        val topicsManager = mockTopicsManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupTopicsResponse(topicsManager)
         val managerCompat = obtain(mContext)
 
         // Actually invoke the compat Preview API code.
         val result = runBlocking {
             val request =
-                GetTopicsRequest.Builder().setAdsSdkName(mSdkName).setShouldRecordObservation(false)
+                GetTopicsRequest.Builder()
+                    .setAdsSdkName(mSdkName)
+                    .setShouldRecordObservation(false)
                     .build()
 
             managerCompat!!.getTopics(request)
         }
 
         // Verify that the compat code was invoked correctly.
-        val captor = ArgumentCaptor.forClass(android.adservices.topics.GetTopicsRequest::class.java)
+        val captor = ArgumentCaptor
+            .forClass(android.adservices.topics.GetTopicsRequest::class.java)
         verify(topicsManager).getTopics(captor.capture(), any(), any())
 
         // Verify that the request that the compat code makes to the platform is correct.
@@ -125,14 +148,17 @@
     }
 
     @SdkSuppress(minSdkVersion = 30)
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
     companion object {
         private lateinit var mContext: Context
         private val mSdkName: String = "sdk1"
 
-        private fun mockTopicsManager(spyContext: Context): TopicsManager {
+        private fun mockTopicsManager(spyContext: Context, isExtServices: Boolean): TopicsManager {
             val topicsManager = mock(TopicsManager::class.java)
             `when`(spyContext.getSystemService(TopicsManager::class.java)).thenReturn(topicsManager)
+            // only mock the .get() method if using extServices version
+            if (isExtServices) {
+                `when`(TopicsManager.get(any())).thenReturn(topicsManager)
+            }
             return topicsManager
         }
 
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
index 17431a8..632da1b 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
@@ -20,13 +20,8 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.os.LimitExceededException
-import android.os.ext.SdkExtensions
-import androidx.annotation.DoNotInline
-import androidx.annotation.RequiresExtension
 import androidx.annotation.RequiresPermission
-import androidx.core.os.asOutcomeReceiver
 import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
-import kotlinx.coroutines.suspendCancellableCoroutine
 
 /**
  * AdId Manager provides APIs for app and ad-SDKs to access advertising ID. The advertising ID is a
@@ -45,38 +40,6 @@
     @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
     abstract suspend fun getAdId(): AdId
 
-    @SuppressLint("ClassVerificationFailure", "NewApi")
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
-    private class Api33Ext4Impl(
-        private val mAdIdManager: android.adservices.adid.AdIdManager
-    ) : AdIdManager() {
-        constructor(context: Context) : this(
-            context.getSystemService<android.adservices.adid.AdIdManager>(
-                android.adservices.adid.AdIdManager::class.java
-            )
-        )
-
-        @DoNotInline
-        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
-        override suspend fun getAdId(): AdId {
-            return convertResponse(getAdIdAsyncInternal())
-        }
-
-        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
-        private suspend fun
-            getAdIdAsyncInternal(): android.adservices.adid.AdId = suspendCancellableCoroutine {
-                continuation ->
-            mAdIdManager.getAdId(
-                Runnable::run,
-                continuation.asOutcomeReceiver()
-            )
-        }
-
-        private fun convertResponse(response: android.adservices.adid.AdId): AdId {
-            return AdId(response.adId, response.isLimitAdTrackingEnabled)
-        }
-    }
-
     companion object {
         /**
          *  Creates [AdIdManager].
@@ -87,10 +50,11 @@
         @JvmStatic
         @SuppressLint("NewApi", "ClassVerificationFailure")
         fun obtain(context: Context): AdIdManager? {
-            return if (AdServicesInfo.version() >= 4) {
-                Api33Ext4Impl(context)
+            return if (AdServicesInfo.adServicesVersion() >= 4) {
+                AdIdManagerApi33Ext4Impl(context)
+            } else if (AdServicesInfo.extServicesVersion() >= 9) {
+                AdIdManagerApi31Ext9Impl(context)
             } else {
-                // TODO(b/261770989): Extend this to older versions.
                 null
             }
         }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi31Ext9Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi31Ext9Impl.kt
new file mode 100644
index 0000000..28d3d76
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi31Ext9Impl.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.privacysandbox.ads.adservices.adid
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
+class AdIdManagerApi31Ext9Impl(context: Context) : AdIdManagerImplCommon(
+    android.adservices.adid.AdIdManager.get(context))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi33Ext4Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi33Ext4Impl.kt
new file mode 100644
index 0000000..a043fba
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi33Ext4Impl.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.privacysandbox.ads.adservices.adid
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+class AdIdManagerApi33Ext4Impl(context: Context) : AdIdManagerImplCommon(
+    context.getSystemService(
+        android.adservices.adid.AdIdManager::class.java))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerImplCommon.kt
new file mode 100644
index 0000000..4ba59b2
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerImplCommon.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.privacysandbox.ads.adservices.adid
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.os.Build
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.annotation.RestrictTo
+import androidx.core.os.asOutcomeReceiver
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("ClassVerificationFailure", "NewApi")
+@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
+open class AdIdManagerImplCommon(
+    private val mAdIdManager: android.adservices.adid.AdIdManager
+) : AdIdManager() {
+
+    @DoNotInline
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+    override suspend fun getAdId(): AdId {
+        return convertResponse(getAdIdAsyncInternal())
+    }
+
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+    private suspend fun
+        getAdIdAsyncInternal(): android.adservices.adid.AdId = suspendCancellableCoroutine {
+            continuation ->
+        mAdIdManager.getAdId(
+            Runnable::run,
+            continuation.asOutcomeReceiver()
+        )
+    }
+
+    private fun convertResponse(response: android.adservices.adid.AdId): AdId {
+        return AdId(response.adId, response.isLimitAdTrackingEnabled)
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
index 7e280c4..311bc63 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
@@ -21,16 +21,9 @@
 import android.content.Context
 import android.os.LimitExceededException
 import android.os.TransactionTooLargeException
-import android.os.ext.SdkExtensions
-import androidx.annotation.DoNotInline
-import androidx.annotation.RequiresExtension
 import androidx.annotation.RequiresPermission
-import androidx.core.os.asOutcomeReceiver
-import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
-import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
 import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
 import java.util.concurrent.TimeoutException
-import kotlinx.coroutines.suspendCancellableCoroutine
 
 /**
  * AdSelection Manager provides APIs for app and ad-SDKs to run ad selection processes as well
@@ -75,109 +68,6 @@
     @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
     abstract suspend fun reportImpression(reportImpressionRequest: ReportImpressionRequest)
 
-    @SuppressLint("NewApi", "ClassVerificationFailure")
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
-    private class Api33Ext4Impl(
-        private val mAdSelectionManager: android.adservices.adselection.AdSelectionManager
-    ) : AdSelectionManager() {
-        constructor(context: Context) : this(
-            context.getSystemService<android.adservices.adselection.AdSelectionManager>(
-                android.adservices.adselection.AdSelectionManager::class.java
-            )
-        )
-
-        @DoNotInline
-        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
-        override suspend fun selectAds(adSelectionConfig: AdSelectionConfig): AdSelectionOutcome {
-            return convertResponse(selectAdsInternal(convertAdSelectionConfig(adSelectionConfig)))
-        }
-
-        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
-        private suspend fun selectAdsInternal(
-            adSelectionConfig: android.adservices.adselection.AdSelectionConfig
-        ): android.adservices.adselection.AdSelectionOutcome = suspendCancellableCoroutine { cont
-            ->
-            mAdSelectionManager.selectAds(
-                adSelectionConfig,
-                Runnable::run,
-                cont.asOutcomeReceiver()
-            )
-        }
-
-        private fun convertAdSelectionConfig(
-            request: AdSelectionConfig
-        ): android.adservices.adselection.AdSelectionConfig {
-            return android.adservices.adselection.AdSelectionConfig.Builder()
-                .setAdSelectionSignals(convertAdSelectionSignals(request.adSelectionSignals))
-                .setCustomAudienceBuyers(convertBuyers(request.customAudienceBuyers))
-                .setDecisionLogicUri(request.decisionLogicUri)
-                .setSeller(android.adservices.common.AdTechIdentifier.fromString(
-                    request.seller.identifier))
-                .setPerBuyerSignals(convertPerBuyerSignals(request.perBuyerSignals))
-                .setSellerSignals(convertAdSelectionSignals(request.sellerSignals))
-                .setTrustedScoringSignalsUri(request.trustedScoringSignalsUri)
-                .build()
-        }
-
-        private fun convertAdSelectionSignals(
-            request: AdSelectionSignals
-        ): android.adservices.common.AdSelectionSignals {
-            return android.adservices.common.AdSelectionSignals.fromString(request.signals)
-        }
-
-        private fun convertBuyers(
-            buyers: List<AdTechIdentifier>
-        ): MutableList<android.adservices.common.AdTechIdentifier> {
-            var ids = mutableListOf<android.adservices.common.AdTechIdentifier>()
-            for (buyer in buyers) {
-                ids.add(android.adservices.common.AdTechIdentifier.fromString(buyer.identifier))
-            }
-            return ids
-        }
-
-        private fun convertPerBuyerSignals(
-            request: Map<AdTechIdentifier, AdSelectionSignals>
-        ): Map<android.adservices.common.AdTechIdentifier,
-            android.adservices.common.AdSelectionSignals?> {
-            var map = HashMap<android.adservices.common.AdTechIdentifier,
-                android.adservices.common.AdSelectionSignals?>()
-            for (key in request.keys) {
-                val id = android.adservices.common.AdTechIdentifier.fromString(key.identifier)
-                val value = if (request[key] != null) convertAdSelectionSignals(request[key]!!)
-                    else null
-                map[id] = value
-            }
-            return map
-        }
-
-        private fun convertResponse(
-            response: android.adservices.adselection.AdSelectionOutcome
-        ): AdSelectionOutcome {
-            return AdSelectionOutcome(response.adSelectionId, response.renderUri)
-        }
-
-        @DoNotInline
-        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
-        override suspend fun reportImpression(reportImpressionRequest: ReportImpressionRequest) {
-            suspendCancellableCoroutine<Any> { continuation ->
-                mAdSelectionManager.reportImpression(
-                    convertReportImpressionRequest(reportImpressionRequest),
-                    Runnable::run,
-                    continuation.asOutcomeReceiver()
-                )
-            }
-        }
-
-        private fun convertReportImpressionRequest(
-            request: ReportImpressionRequest
-        ): android.adservices.adselection.ReportImpressionRequest {
-            return android.adservices.adselection.ReportImpressionRequest(
-                request.adSelectionId,
-                convertAdSelectionConfig(request.adSelectionConfig)
-            )
-        }
-    }
-
     companion object {
         /**
          *  Creates [AdSelectionManager].
@@ -188,8 +78,10 @@
         @JvmStatic
         @SuppressLint("NewApi", "ClassVerificationFailure")
         fun obtain(context: Context): AdSelectionManager? {
-            return if (AdServicesInfo.version() >= 4) {
-                Api33Ext4Impl(context)
+            return if (AdServicesInfo.adServicesVersion() >= 4) {
+                AdSelectionManagerApi33Ext4Impl(context)
+            } else if (AdServicesInfo.extServicesVersion() >= 9) {
+                AdSelectionManagerApi31Ext9Impl(context)
             } else {
                 null
             }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi31Ext9Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi31Ext9Impl.kt
new file mode 100644
index 0000000..d968dd2
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi31Ext9Impl.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
+class AdSelectionManagerApi31Ext9Impl(context: Context) : AdSelectionManagerImplCommon(
+    android.adservices.adselection.AdSelectionManager.get(context))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi33Ext4Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi33Ext4Impl.kt
new file mode 100644
index 0000000..b3cdd9c
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi33Ext4Impl.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+class AdSelectionManagerApi33Ext4Impl(context: Context) : AdSelectionManagerImplCommon(
+    context.getSystemService(
+        android.adservices.adselection.AdSelectionManager::class.java))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerImplCommon.kt
new file mode 100644
index 0000000..e780975
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerImplCommon.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.os.Build
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.annotation.RestrictTo
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
+open class AdSelectionManagerImplCommon(
+    protected val mAdSelectionManager: android.adservices.adselection.AdSelectionManager
+    ) : AdSelectionManager() {
+
+    @DoNotInline
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    override suspend fun selectAds(adSelectionConfig: AdSelectionConfig): AdSelectionOutcome {
+        return convertResponse(selectAdsInternal(convertAdSelectionConfig(adSelectionConfig)))
+    }
+
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    private suspend fun selectAdsInternal(
+        adSelectionConfig: android.adservices.adselection.AdSelectionConfig
+    ): android.adservices.adselection.AdSelectionOutcome = suspendCancellableCoroutine { cont
+        ->
+        mAdSelectionManager.selectAds(
+            adSelectionConfig,
+            Runnable::run,
+            cont.asOutcomeReceiver()
+        )
+    }
+
+    private fun convertAdSelectionConfig(
+        request: AdSelectionConfig
+    ): android.adservices.adselection.AdSelectionConfig {
+        return android.adservices.adselection.AdSelectionConfig.Builder()
+            .setAdSelectionSignals(convertAdSelectionSignals(request.adSelectionSignals))
+            .setCustomAudienceBuyers(convertBuyers(request.customAudienceBuyers))
+            .setDecisionLogicUri(request.decisionLogicUri)
+            .setSeller(android.adservices.common.AdTechIdentifier.fromString(
+                request.seller.identifier))
+            .setPerBuyerSignals(convertPerBuyerSignals(request.perBuyerSignals))
+            .setSellerSignals(convertAdSelectionSignals(request.sellerSignals))
+            .setTrustedScoringSignalsUri(request.trustedScoringSignalsUri)
+            .build()
+    }
+
+    private fun convertAdSelectionSignals(
+        request: AdSelectionSignals
+    ): android.adservices.common.AdSelectionSignals {
+        return android.adservices.common.AdSelectionSignals.fromString(request.signals)
+    }
+
+    private fun convertBuyers(
+        buyers: List<AdTechIdentifier>
+    ): MutableList<android.adservices.common.AdTechIdentifier> {
+        val ids = mutableListOf<android.adservices.common.AdTechIdentifier>()
+        for (buyer in buyers) {
+            ids.add(android.adservices.common.AdTechIdentifier.fromString(buyer.identifier))
+        }
+        return ids
+    }
+
+    private fun convertPerBuyerSignals(
+        request: Map<AdTechIdentifier, AdSelectionSignals>
+    ): Map<android.adservices.common.AdTechIdentifier,
+        android.adservices.common.AdSelectionSignals?> {
+        val map = HashMap<android.adservices.common.AdTechIdentifier,
+            android.adservices.common.AdSelectionSignals?>()
+        for (key in request.keys) {
+            val id = android.adservices.common.AdTechIdentifier.fromString(key.identifier)
+            val value = if (request[key] != null) convertAdSelectionSignals(request[key]!!)
+            else null
+            map[id] = value
+        }
+        return map
+    }
+
+    private fun convertResponse(
+        response: android.adservices.adselection.AdSelectionOutcome
+    ): AdSelectionOutcome {
+        return AdSelectionOutcome(response.adSelectionId, response.renderUri)
+    }
+
+    @DoNotInline
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    override suspend fun reportImpression(reportImpressionRequest: ReportImpressionRequest) {
+        suspendCancellableCoroutine<Any> { continuation ->
+            mAdSelectionManager.reportImpression(
+                convertReportImpressionRequest(reportImpressionRequest),
+                Runnable::run,
+                continuation.asOutcomeReceiver()
+            )
+        }
+    }
+
+    private fun convertReportImpressionRequest(
+        request: ReportImpressionRequest
+    ): android.adservices.adselection.ReportImpressionRequest {
+        return android.adservices.adselection.ReportImpressionRequest(
+            request.adSelectionId,
+            convertAdSelectionConfig(request.adSelectionConfig)
+        )
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
index d780956..88d57f1 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
@@ -19,12 +19,7 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.os.LimitExceededException
-import android.os.ext.SdkExtensions
-import androidx.annotation.DoNotInline
-import androidx.annotation.RequiresExtension
-import androidx.core.os.asOutcomeReceiver
 import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
-import kotlinx.coroutines.suspendCancellableCoroutine
 
 /**
  * AppSetIdManager provides APIs for app and ad-SDKs to access appSetId for non-monetizing purpose.
@@ -39,39 +34,6 @@
      */
     abstract suspend fun getAppSetId(): AppSetId
 
-    @SuppressLint("ClassVerificationFailure", "NewApi")
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
-    private class Api33Ext4Impl(
-        private val mAppSetIdManager: android.adservices.appsetid.AppSetIdManager
-    ) : AppSetIdManager() {
-        constructor(context: Context) : this(
-            context.getSystemService<android.adservices.appsetid.AppSetIdManager>(
-                android.adservices.appsetid.AppSetIdManager::class.java
-            )
-        )
-
-        @DoNotInline
-        override suspend fun getAppSetId(): AppSetId {
-            return convertResponse(getAppSetIdAsyncInternal())
-        }
-
-        private suspend fun getAppSetIdAsyncInternal(): android.adservices.appsetid.AppSetId =
-            suspendCancellableCoroutine {
-                    continuation ->
-                mAppSetIdManager.getAppSetId(
-                    Runnable::run,
-                    continuation.asOutcomeReceiver()
-                )
-            }
-
-        private fun convertResponse(response: android.adservices.appsetid.AppSetId): AppSetId {
-            if (response.scope == android.adservices.appsetid.AppSetId.SCOPE_APP) {
-                return AppSetId(response.id, AppSetId.SCOPE_APP)
-            }
-            return AppSetId(response.id, AppSetId.SCOPE_DEVELOPER)
-        }
-    }
-
     companion object {
 
         /**
@@ -83,10 +45,11 @@
         @JvmStatic
         @SuppressLint("NewApi", "ClassVerificationFailure")
         fun obtain(context: Context): AppSetIdManager? {
-            return if (AdServicesInfo.version() >= 4) {
-                Api33Ext4Impl(context)
+            return if (AdServicesInfo.adServicesVersion() >= 4) {
+                AppSetIdManagerApi33Ext4Impl(context)
+            } else if (AdServicesInfo.extServicesVersion() >= 9) {
+                AppSetIdManagerApi31Ext9Impl(context)
             } else {
-                // TODO(b/261770989): Extend this to older versions.
                 null
             }
         }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi31Ext9Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi31Ext9Impl.kt
new file mode 100644
index 0000000..390e785
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi31Ext9Impl.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.privacysandbox.ads.adservices.appsetid
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
+class AppSetIdManagerApi31Ext9Impl(context: Context) : AppSetIdManagerImplCommon(
+    android.adservices.appsetid.AppSetIdManager.get(context))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi33Ext4Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi33Ext4Impl.kt
new file mode 100644
index 0000000..84af87a
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi33Ext4Impl.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.privacysandbox.ads.adservices.appsetid
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+class AppSetIdManagerApi33Ext4Impl(context: Context) : AppSetIdManagerImplCommon(
+    context.getSystemService(
+        android.adservices.appsetid.AppSetIdManager::class.java))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerImplCommon.kt
new file mode 100644
index 0000000..720370d
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerImplCommon.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.privacysandbox.ads.adservices.appsetid
+
+import android.annotation.SuppressLint
+import android.os.Build
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+import androidx.core.os.asOutcomeReceiver
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("ClassVerificationFailure", "NewApi")
+@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
+open class AppSetIdManagerImplCommon(
+    private val mAppSetIdManager: android.adservices.appsetid.AppSetIdManager
+) : AppSetIdManager() {
+
+    @DoNotInline
+    override suspend fun getAppSetId(): AppSetId {
+        return convertResponse(getAppSetIdAsyncInternal())
+    }
+
+    private suspend fun getAppSetIdAsyncInternal(): android.adservices.appsetid.AppSetId =
+        suspendCancellableCoroutine {
+                continuation ->
+            mAppSetIdManager.getAppSetId(
+                Runnable::run,
+                continuation.asOutcomeReceiver()
+            )
+        }
+
+    private fun convertResponse(response: android.adservices.appsetid.AppSetId): AppSetId {
+        if (response.scope == android.adservices.appsetid.AppSetId.SCOPE_APP) {
+            return AppSetId(response.id, AppSetId.SCOPE_APP)
+        }
+        return AppSetId(response.id, AppSetId.SCOPE_DEVELOPER)
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
index 981aeaa..a9b91d7 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
@@ -20,16 +20,8 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.os.LimitExceededException
-import android.os.ext.SdkExtensions
-import androidx.annotation.DoNotInline
-import androidx.annotation.RequiresExtension
 import androidx.annotation.RequiresPermission
-import androidx.core.os.asOutcomeReceiver
-import androidx.privacysandbox.ads.adservices.common.AdData
-import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
-import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
 import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
-import kotlinx.coroutines.suspendCancellableCoroutine
 
 /**
  * This class provides APIs for app and ad-SDKs to join / leave custom audiences.
@@ -94,111 +86,6 @@
     @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
     abstract suspend fun leaveCustomAudience(request: LeaveCustomAudienceRequest)
 
-    @SuppressLint("ClassVerificationFailure", "NewApi")
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
-    private class Api33Ext4Impl(
-        private val customAudienceManager: android.adservices.customaudience.CustomAudienceManager
-        ) : CustomAudienceManager() {
-        constructor(context: Context) : this(
-            context.getSystemService<android.adservices.customaudience.CustomAudienceManager>(
-                android.adservices.customaudience.CustomAudienceManager::class.java
-            )
-        )
-
-        @DoNotInline
-        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
-        override suspend fun joinCustomAudience(request: JoinCustomAudienceRequest) {
-            suspendCancellableCoroutine { continuation ->
-                customAudienceManager.joinCustomAudience(
-                    convertJoinRequest(request),
-                    Runnable::run,
-                    continuation.asOutcomeReceiver()
-                )
-            }
-        }
-
-        @DoNotInline
-        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
-        override suspend fun leaveCustomAudience(request: LeaveCustomAudienceRequest) {
-            suspendCancellableCoroutine { continuation ->
-                customAudienceManager.leaveCustomAudience(
-                    convertLeaveRequest(request),
-                    Runnable::run,
-                    continuation.asOutcomeReceiver()
-                )
-            }
-        }
-
-        private fun convertJoinRequest(
-            request: JoinCustomAudienceRequest
-        ): android.adservices.customaudience.JoinCustomAudienceRequest {
-            return android.adservices.customaudience.JoinCustomAudienceRequest.Builder()
-                .setCustomAudience(convertCustomAudience(request.customAudience))
-                .build()
-        }
-
-        private fun convertLeaveRequest(
-            request: LeaveCustomAudienceRequest
-        ): android.adservices.customaudience.LeaveCustomAudienceRequest {
-            return android.adservices.customaudience.LeaveCustomAudienceRequest.Builder()
-                .setBuyer(convertAdTechIdentifier(request.buyer))
-                .setName(request.name)
-                .build()
-        }
-
-        private fun convertCustomAudience(
-            request: CustomAudience
-        ): android.adservices.customaudience.CustomAudience {
-            return android.adservices.customaudience.CustomAudience.Builder()
-                .setActivationTime(request.activationTime)
-                .setAds(convertAdData(request.ads))
-                .setBiddingLogicUri(request.biddingLogicUri)
-                .setBuyer(convertAdTechIdentifier(request.buyer))
-                .setDailyUpdateUri(request.dailyUpdateUri)
-                .setExpirationTime(request.expirationTime)
-                .setName(request.name)
-                .setTrustedBiddingData(convertTrustedSignals(request.trustedBiddingSignals))
-                .setUserBiddingSignals(convertBiddingSignals(request.userBiddingSignals))
-                .build()
-        }
-
-        private fun convertAdData(
-            input: List<AdData>
-        ): List<android.adservices.common.AdData> {
-            val result = mutableListOf<android.adservices.common.AdData>()
-            for (ad in input) {
-                result.add(android.adservices.common.AdData.Builder()
-                    .setMetadata(ad.metadata)
-                    .setRenderUri(ad.renderUri)
-                    .build())
-            }
-            return result
-        }
-
-        private fun convertAdTechIdentifier(
-            input: AdTechIdentifier
-        ): android.adservices.common.AdTechIdentifier {
-            return android.adservices.common.AdTechIdentifier.fromString(input.identifier)
-        }
-
-        private fun convertTrustedSignals(
-            input: TrustedBiddingData?
-        ): android.adservices.customaudience.TrustedBiddingData? {
-            if (input == null) return null
-            return android.adservices.customaudience.TrustedBiddingData.Builder()
-                .setTrustedBiddingKeys(input.trustedBiddingKeys)
-                .setTrustedBiddingUri(input.trustedBiddingUri)
-                .build()
-        }
-
-        private fun convertBiddingSignals(
-            input: AdSelectionSignals?
-        ): android.adservices.common.AdSelectionSignals? {
-            if (input == null) return null
-            return android.adservices.common.AdSelectionSignals.fromString(input.signals)
-        }
-    }
-
     companion object {
         /**
          *  Creates [CustomAudienceManager].
@@ -209,8 +96,10 @@
         @JvmStatic
         @SuppressLint("NewApi", "ClassVerificationFailure")
         fun obtain(context: Context): CustomAudienceManager? {
-            return if (AdServicesInfo.version() >= 4) {
-                Api33Ext4Impl(context)
+            return if (AdServicesInfo.adServicesVersion() >= 4) {
+                CustomAudienceManagerApi33Ext4Impl(context)
+            } else if (AdServicesInfo.extServicesVersion() >= 9) {
+                CustomAudienceManagerApi31Ext9Impl(context)
             } else {
                 null
             }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi31Ext9Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi31Ext9Impl.kt
new file mode 100644
index 0000000..1e294ce
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi31Ext9Impl.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
+class CustomAudienceManagerApi31Ext9Impl(context: Context) : CustomAudienceManagerImplCommon(
+    android.adservices.customaudience.CustomAudienceManager.get(context))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi33Ext4Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi33Ext4Impl.kt
new file mode 100644
index 0000000..fd658d7
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi33Ext4Impl.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+class CustomAudienceManagerApi33Ext4Impl(context: Context) : CustomAudienceManagerImplCommon(
+    context.getSystemService(android.adservices.customaudience.CustomAudienceManager::class.java))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerImplCommon.kt
new file mode 100644
index 0000000..44f09a8
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerImplCommon.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.os.Build
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.annotation.RestrictTo
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
+open class CustomAudienceManagerImplCommon(
+    protected val customAudienceManager: android.adservices.customaudience.CustomAudienceManager
+    ) : CustomAudienceManager() {
+    @DoNotInline
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    override suspend fun joinCustomAudience(request: JoinCustomAudienceRequest) {
+        suspendCancellableCoroutine { continuation ->
+            customAudienceManager.joinCustomAudience(
+                convertJoinRequest(request),
+                Runnable::run,
+                continuation.asOutcomeReceiver()
+            )
+        }
+    }
+
+    @DoNotInline
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    override suspend fun leaveCustomAudience(request: LeaveCustomAudienceRequest) {
+        suspendCancellableCoroutine { continuation ->
+            customAudienceManager.leaveCustomAudience(
+                convertLeaveRequest(request),
+                Runnable::run,
+                continuation.asOutcomeReceiver()
+            )
+        }
+    }
+
+    private fun convertJoinRequest(
+        request: JoinCustomAudienceRequest
+    ): android.adservices.customaudience.JoinCustomAudienceRequest {
+        return android.adservices.customaudience.JoinCustomAudienceRequest.Builder()
+            .setCustomAudience(convertCustomAudience(request.customAudience))
+            .build()
+    }
+
+    private fun convertLeaveRequest(
+        request: LeaveCustomAudienceRequest
+    ): android.adservices.customaudience.LeaveCustomAudienceRequest {
+        return android.adservices.customaudience.LeaveCustomAudienceRequest.Builder()
+            .setBuyer(convertAdTechIdentifier(request.buyer))
+            .setName(request.name)
+            .build()
+    }
+
+    private fun convertCustomAudience(
+        request: CustomAudience
+    ): android.adservices.customaudience.CustomAudience {
+        return android.adservices.customaudience.CustomAudience.Builder()
+            .setActivationTime(request.activationTime)
+            .setAds(convertAdData(request.ads))
+            .setBiddingLogicUri(request.biddingLogicUri)
+            .setBuyer(convertAdTechIdentifier(request.buyer))
+            .setDailyUpdateUri(request.dailyUpdateUri)
+            .setExpirationTime(request.expirationTime)
+            .setName(request.name)
+            .setTrustedBiddingData(convertTrustedSignals(request.trustedBiddingSignals))
+            .setUserBiddingSignals(convertBiddingSignals(request.userBiddingSignals))
+            .build()
+    }
+
+    private fun convertAdData(
+        input: List<AdData>
+    ): List<android.adservices.common.AdData> {
+        val result = mutableListOf<android.adservices.common.AdData>()
+        for (ad in input) {
+            result.add(android.adservices.common.AdData.Builder()
+                .setMetadata(ad.metadata)
+                .setRenderUri(ad.renderUri)
+                .build())
+        }
+        return result
+    }
+
+    private fun convertAdTechIdentifier(
+        input: AdTechIdentifier
+    ): android.adservices.common.AdTechIdentifier {
+        return android.adservices.common.AdTechIdentifier.fromString(input.identifier)
+    }
+
+    private fun convertTrustedSignals(
+        input: TrustedBiddingData?
+    ): android.adservices.customaudience.TrustedBiddingData? {
+        if (input == null) return null
+        return android.adservices.customaudience.TrustedBiddingData.Builder()
+            .setTrustedBiddingKeys(input.trustedBiddingKeys)
+            .setTrustedBiddingUri(input.trustedBiddingUri)
+            .build()
+    }
+
+    private fun convertBiddingSignals(
+        input: AdSelectionSignals?
+    ): android.adservices.common.AdSelectionSignals? {
+        if (input == null) return null
+        return android.adservices.common.AdSelectionSignals.fromString(input.signals)
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
index 9b36692..6f8b056 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
@@ -21,24 +21,38 @@
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
 
-/**
- * Temporary replacement for BuildCompat.AD_SERVICES_EXTENSION_INT.
- * TODO(b/261755947) Replace with AD_SERVICES_EXTENSION_INT after new core library release
- */
 internal object AdServicesInfo {
 
-    fun version(): Int {
-        return if (Build.VERSION.SDK_INT >= 30) {
+    fun adServicesVersion(): Int {
+        return if (Build.VERSION.SDK_INT >= 33) {
             Extensions30Impl.getAdServicesVersion()
         } else {
             0
         }
     }
 
+    fun extServicesVersion(): Int {
+        return if (Build.VERSION.SDK_INT == 31 || Build.VERSION.SDK_INT == 32) {
+            Extensions30ExtImpl.getAdExtServicesVersion()
+        } else {
+            0
+        }
+    }
+
     @RequiresApi(30)
     private object Extensions30Impl {
         @DoNotInline
         fun getAdServicesVersion() =
             SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
     }
+
+    @RequiresApi(30)
+    private object Extensions30ExtImpl {
+        // For ExtServices, there is no AD_SERVICES extension version, so we need to check
+        // for the build version. Use S for now, but this can be changed to R when we add
+        // support for R later.
+        @DoNotInline
+        fun getAdExtServicesVersion() =
+            SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S)
+    }
 }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
index de62bd2..4641d2b 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
@@ -20,18 +20,11 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.net.Uri
-import android.os.ext.SdkExtensions
 import android.util.Log
 import android.view.InputEvent
-import androidx.annotation.DoNotInline
-import androidx.annotation.RequiresExtension
 import androidx.annotation.RequiresPermission
-import androidx.core.os.asOutcomeReceiver
 import androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures
 import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
 
 /**
  * This class provides APIs to manage ads attribution using Privacy Sandbox.
@@ -103,167 +96,6 @@
     @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
     abstract suspend fun getMeasurementApiStatus(): Int
 
-    @SuppressLint("NewApi", "ClassVerificationFailure")
-    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
-    private class Api33Ext5Impl(
-        private val mMeasurementManager: android.adservices.measurement.MeasurementManager
-    ) : MeasurementManager() {
-        constructor(context: Context) : this(
-            context.getSystemService<android.adservices.measurement.MeasurementManager>(
-                android.adservices.measurement.MeasurementManager::class.java
-            )
-        )
-
-        @DoNotInline
-        override suspend fun deleteRegistrations(deletionRequest: DeletionRequest) {
-            suspendCancellableCoroutine<Any> { continuation ->
-                mMeasurementManager.deleteRegistrations(
-                    convertDeletionRequest(deletionRequest),
-                    Runnable::run,
-                    continuation.asOutcomeReceiver()
-                )
-            }
-        }
-
-        private fun convertDeletionRequest(
-            request: DeletionRequest
-        ): android.adservices.measurement.DeletionRequest {
-            return android.adservices.measurement.DeletionRequest.Builder()
-                .setDeletionMode(request.deletionMode)
-                .setMatchBehavior(request.matchBehavior)
-                .setStart(request.start)
-                .setEnd(request.end)
-                .setDomainUris(request.domainUris)
-                .setOriginUris(request.originUris)
-                .build()
-        }
-
-        @DoNotInline
-        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
-        override suspend fun registerSource(attributionSource: Uri, inputEvent: InputEvent?) {
-            suspendCancellableCoroutine<Any> { continuation ->
-                mMeasurementManager.registerSource(
-                    attributionSource,
-                    inputEvent,
-                    Runnable::run,
-                    continuation.asOutcomeReceiver()
-                )
-            }
-        }
-
-        @DoNotInline
-        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
-        override suspend fun registerTrigger(trigger: Uri) {
-            suspendCancellableCoroutine<Any> { continuation ->
-                mMeasurementManager.registerTrigger(
-                    trigger,
-                    Runnable::run,
-                    continuation.asOutcomeReceiver())
-            }
-        }
-
-        @DoNotInline
-        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
-        override suspend fun registerWebSource(request: WebSourceRegistrationRequest) {
-            suspendCancellableCoroutine<Any> { continuation ->
-                mMeasurementManager.registerWebSource(
-                    convertWebSourceRequest(request),
-                    Runnable::run,
-                    continuation.asOutcomeReceiver())
-            }
-        }
-
-        @DoNotInline
-        @ExperimentalFeatures.RegisterSourceOptIn
-        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
-        override suspend fun registerSource(
-            request: SourceRegistrationRequest
-        ): Unit = coroutineScope {
-            request.registrationUris.forEach { uri ->
-                launch {
-                    suspendCancellableCoroutine<Any> { continuation ->
-                        mMeasurementManager.registerSource(
-                            uri,
-                            request.inputEvent,
-                            Runnable::run,
-                            continuation.asOutcomeReceiver()
-                        )
-                    }
-                }
-            }
-        }
-
-        private fun convertWebSourceRequest(
-            request: WebSourceRegistrationRequest
-        ): android.adservices.measurement.WebSourceRegistrationRequest {
-            return android.adservices.measurement.WebSourceRegistrationRequest
-                .Builder(
-                    convertWebSourceParams(request.webSourceParams),
-                    request.topOriginUri)
-                .setWebDestination(request.webDestination)
-                .setAppDestination(request.appDestination)
-                .setInputEvent(request.inputEvent)
-                .setVerifiedDestination(request.verifiedDestination)
-                .build()
-        }
-
-        private fun convertWebSourceParams(
-            request: List<WebSourceParams>
-        ): List<android.adservices.measurement.WebSourceParams> {
-            var result = mutableListOf<android.adservices.measurement.WebSourceParams>()
-            for (param in request) {
-                result.add(android.adservices.measurement.WebSourceParams
-                    .Builder(param.registrationUri)
-                    .setDebugKeyAllowed(param.debugKeyAllowed)
-                    .build())
-            }
-            return result
-        }
-
-        @DoNotInline
-        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
-        override suspend fun registerWebTrigger(request: WebTriggerRegistrationRequest) {
-            suspendCancellableCoroutine<Any> { continuation ->
-                mMeasurementManager.registerWebTrigger(
-                    convertWebTriggerRequest(request),
-                    Runnable::run,
-                    continuation.asOutcomeReceiver())
-            }
-        }
-
-        private fun convertWebTriggerRequest(
-            request: WebTriggerRegistrationRequest
-        ): android.adservices.measurement.WebTriggerRegistrationRequest {
-            return android.adservices.measurement.WebTriggerRegistrationRequest
-                .Builder(
-                    convertWebTriggerParams(request.webTriggerParams),
-                    request.destination)
-                .build()
-        }
-
-        private fun convertWebTriggerParams(
-            request: List<WebTriggerParams>
-        ): List<android.adservices.measurement.WebTriggerParams> {
-            var result = mutableListOf<android.adservices.measurement.WebTriggerParams>()
-            for (param in request) {
-                result.add(android.adservices.measurement.WebTriggerParams
-                    .Builder(param.registrationUri)
-                    .setDebugKeyAllowed(param.debugKeyAllowed)
-                    .build())
-            }
-            return result
-        }
-
-        @DoNotInline
-        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
-        override suspend fun getMeasurementApiStatus(): Int = suspendCancellableCoroutine {
-                continuation ->
-            mMeasurementManager.getMeasurementApiStatus(
-                Runnable::run,
-                continuation.asOutcomeReceiver())
-        }
-    }
-
     companion object {
         /**
          * This state indicates that Measurement APIs are unavailable. Invoking them will result
@@ -284,9 +116,12 @@
         @JvmStatic
         @SuppressLint("NewApi", "ClassVerificationFailure")
         fun obtain(context: Context): MeasurementManager? {
-            Log.d("MeasurementManager", "AdServicesInfo.version=${AdServicesInfo.version()}")
-            return if (AdServicesInfo.version() >= 5) {
-                Api33Ext5Impl(context)
+            Log.d("MeasurementManager",
+                "AdServicesInfo.version=${AdServicesInfo.adServicesVersion()}")
+            return if (AdServicesInfo.adServicesVersion() >= 5) {
+                MeasurementManagerApi33Ext5Impl(context)
+            } else if (AdServicesInfo.extServicesVersion() >= 9) {
+                MeasurementManagerApi31Ext9Impl(context)
             } else {
                 null
             }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi31Ext9Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi31Ext9Impl.kt
new file mode 100644
index 0000000..4d55606
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi31Ext9Impl.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
+class MeasurementManagerApi31Ext9Impl(context: Context) : MeasurementManagerImplCommon(
+    android.adservices.measurement.MeasurementManager.get(context))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi33Ext5Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi33Ext5Impl.kt
new file mode 100644
index 0000000..cf0de76
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi33Ext5Impl.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
+class MeasurementManagerApi33Ext5Impl(context: Context) : MeasurementManagerImplCommon(
+    context.getSystemService(android.adservices.measurement.MeasurementManager::class.java))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerImplCommon.kt
new file mode 100644
index 0000000..3618240
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerImplCommon.kt
@@ -0,0 +1,190 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.net.Uri
+import android.os.Build
+import android.os.ext.SdkExtensions
+import android.view.InputEvent
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.annotation.RestrictTo
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
+@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
+open class MeasurementManagerImplCommon(
+    protected val mMeasurementManager: android.adservices.measurement.MeasurementManager
+    ) : MeasurementManager() {
+    @DoNotInline
+    override suspend fun deleteRegistrations(deletionRequest: DeletionRequest) {
+        suspendCancellableCoroutine<Any> { continuation ->
+            mMeasurementManager.deleteRegistrations(
+                convertDeletionRequest(deletionRequest),
+                Runnable::run,
+                continuation.asOutcomeReceiver()
+            )
+        }
+    }
+
+    private fun convertDeletionRequest(
+        request: DeletionRequest
+    ): android.adservices.measurement.DeletionRequest {
+        return android.adservices.measurement.DeletionRequest.Builder()
+            .setDeletionMode(request.deletionMode)
+            .setMatchBehavior(request.matchBehavior)
+            .setStart(request.start)
+            .setEnd(request.end)
+            .setDomainUris(request.domainUris)
+            .setOriginUris(request.originUris)
+            .build()
+    }
+
+    @DoNotInline
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    override suspend fun registerSource(attributionSource: Uri, inputEvent: InputEvent?) {
+        suspendCancellableCoroutine<Any> { continuation ->
+            mMeasurementManager.registerSource(
+                attributionSource,
+                inputEvent,
+                Runnable::run,
+                continuation.asOutcomeReceiver()
+            )
+        }
+    }
+
+    @DoNotInline
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    override suspend fun registerTrigger(trigger: Uri) {
+        suspendCancellableCoroutine<Any> { continuation ->
+            mMeasurementManager.registerTrigger(
+                trigger,
+                Runnable::run,
+                continuation.asOutcomeReceiver())
+        }
+    }
+
+    @DoNotInline
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    override suspend fun registerWebSource(request: WebSourceRegistrationRequest) {
+        suspendCancellableCoroutine<Any> { continuation ->
+            mMeasurementManager.registerWebSource(
+                convertWebSourceRequest(request),
+                Runnable::run,
+                continuation.asOutcomeReceiver())
+        }
+    }
+
+    @DoNotInline
+    @ExperimentalFeatures.RegisterSourceOptIn
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    override suspend fun registerSource(
+        request: SourceRegistrationRequest
+    ): Unit = coroutineScope {
+        request.registrationUris.forEach { uri ->
+            launch {
+                suspendCancellableCoroutine<Any> { continuation ->
+                    mMeasurementManager.registerSource(
+                        uri,
+                        request.inputEvent,
+                        Runnable::run,
+                        continuation.asOutcomeReceiver()
+                    )
+                }
+            }
+        }
+    }
+
+    private fun convertWebSourceRequest(
+        request: WebSourceRegistrationRequest
+    ): android.adservices.measurement.WebSourceRegistrationRequest {
+        return android.adservices.measurement.WebSourceRegistrationRequest
+            .Builder(
+                convertWebSourceParams(request.webSourceParams),
+                request.topOriginUri)
+            .setWebDestination(request.webDestination)
+            .setAppDestination(request.appDestination)
+            .setInputEvent(request.inputEvent)
+            .setVerifiedDestination(request.verifiedDestination)
+            .build()
+    }
+
+    private fun convertWebSourceParams(
+        request: List<WebSourceParams>
+    ): List<android.adservices.measurement.WebSourceParams> {
+        var result = mutableListOf<android.adservices.measurement.WebSourceParams>()
+        for (param in request) {
+            result.add(android.adservices.measurement.WebSourceParams
+                .Builder(param.registrationUri)
+                .setDebugKeyAllowed(param.debugKeyAllowed)
+                .build())
+        }
+        return result
+    }
+
+    @DoNotInline
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    override suspend fun registerWebTrigger(request: WebTriggerRegistrationRequest) {
+        suspendCancellableCoroutine<Any> { continuation ->
+            mMeasurementManager.registerWebTrigger(
+                convertWebTriggerRequest(request),
+                Runnable::run,
+                continuation.asOutcomeReceiver())
+        }
+    }
+
+    private fun convertWebTriggerRequest(
+        request: WebTriggerRegistrationRequest
+    ): android.adservices.measurement.WebTriggerRegistrationRequest {
+        return android.adservices.measurement.WebTriggerRegistrationRequest
+            .Builder(
+                convertWebTriggerParams(request.webTriggerParams),
+                request.destination)
+            .build()
+    }
+
+    private fun convertWebTriggerParams(
+        request: List<WebTriggerParams>
+    ): List<android.adservices.measurement.WebTriggerParams> {
+        var result = mutableListOf<android.adservices.measurement.WebTriggerParams>()
+        for (param in request) {
+            result.add(android.adservices.measurement.WebTriggerParams
+                .Builder(param.registrationUri)
+                .setDebugKeyAllowed(param.debugKeyAllowed)
+                .build())
+        }
+        return result
+    }
+
+    @DoNotInline
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    override suspend fun getMeasurementApiStatus(): Int = suspendCancellableCoroutine {
+            continuation ->
+        mMeasurementManager.getMeasurementApiStatus(
+            Runnable::run,
+            continuation.asOutcomeReceiver())
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
index 828fdf9..ab533f0 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
@@ -50,10 +50,12 @@
         @JvmStatic
         @SuppressLint("NewApi", "ClassVerificationFailure")
         fun obtain(context: Context): TopicsManager? {
-            return if (AdServicesInfo.version() >= 5) {
+            return if (AdServicesInfo.adServicesVersion() >= 5) {
                 TopicsManagerApi33Ext5Impl(context)
-            } else if (AdServicesInfo.version() == 4) {
+            } else if (AdServicesInfo.adServicesVersion() == 4) {
                 TopicsManagerApi33Ext4Impl(context)
+            } else if (AdServicesInfo.extServicesVersion() >= 9) {
+                TopicsManagerApi31Ext9Impl(context)
             } else {
                 null
             }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerApi31Ext9Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerApi31Ext9Impl.kt
new file mode 100644
index 0000000..c9aedf9
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerApi31Ext9Impl.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("NewApi", "ClassVerificationFailure")
+@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
+class TopicsManagerApi31Ext9Impl(context: Context) : TopicsManagerImplCommon(
+    android.adservices.topics.TopicsManager.get(context)) {
+
+    override fun convertRequest(
+        request: GetTopicsRequest
+    ): android.adservices.topics.GetTopicsRequest {
+        return android.adservices.topics.GetTopicsRequest.Builder()
+            .setAdsSdkName(request.adsSdkName)
+            .setShouldRecordObservation(request.shouldRecordObservation)
+            .build()
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerImplCommon.kt
index 7dc5752..2b09952 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerImplCommon.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerImplCommon.kt
@@ -1,7 +1,24 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
 
 import android.adservices.common.AdServicesPermissions
 import android.annotation.SuppressLint
+import android.os.Build
 import android.os.ext.SdkExtensions
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresExtension
@@ -13,6 +30,7 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @SuppressLint("NewApi")
 @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
 open class TopicsManagerImplCommon(
     private val mTopicsManager: android.adservices.topics.TopicsManager
 ) : TopicsManager() {
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
index 5de128a..e8985b4 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
@@ -92,7 +92,6 @@
 dependencies {
     api(libs.kotlinStdlib)
     api(libs.kotlinCoroutinesCore)
-    implementation(libs.multidex)
     implementation("androidx.core:core-ktx:1.12.0-alpha05")
 
     api project(path: ':privacysandbox:sdkruntime:sdkruntime-core')
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
index abc5297..e45d496 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
@@ -81,9 +81,10 @@
 
         mSandboxedSdkView2 = SandboxedSdkView(this@MainActivity)
         mSandboxedSdkView2.addStateChangedListener(StateChangeListener(mSandboxedSdkView2))
-        mSandboxedSdkView2.layoutParams = ViewGroup.LayoutParams(400, 400)
+        mSandboxedSdkView2.layoutParams = findViewById<LinearLayout>(
+            R.id.bottom_banner_container).layoutParams
         runOnUiThread {
-            findViewById<LinearLayout>(R.id.ad_layout).addView(mSandboxedSdkView2)
+            findViewById<LinearLayout>(R.id.bottom_banner_container).addView(mSandboxedSdkView2)
         }
         mSandboxedSdkView2.setAdapter(SandboxedUiAdapterFactory.createFromCoreLibInfo(
             sdkApi.loadAd(/*isWebView=*/ false, "Hey!")
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml
index dda74f2..1051e95 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -15,17 +15,20 @@
   limitations under the License.
   -->
 
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:weightSum="5"
+    android:orientation="vertical"
     tools:context=".MainActivity">
 
     <ScrollView
         android:id="@+id/scroll_view"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_weight="4"
+        android:layout_height="0dp"
         android:orientation="vertical">
         <LinearLayout
             android:id="@+id/ad_layout"
@@ -80,4 +83,10 @@
                 android:text="@string/long_text" />
         </LinearLayout>
     </ScrollView>
-</androidx.constraintlayout.widget.ConstraintLayout>
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:id="@+id/bottom_banner_container"
+        android:orientation="vertical" />
+</androidx.appcompat.widget.LinearLayoutCompat>
diff --git a/privacysandbox/ui/ui-client/api/current.txt b/privacysandbox/ui/ui-client/api/current.txt
index bc9a40d..47ff65b 100644
--- a/privacysandbox/ui/ui-client/api/current.txt
+++ b/privacysandbox/ui/ui-client/api/current.txt
@@ -48,9 +48,9 @@
     ctor public SandboxedSdkView(android.content.Context context);
     ctor public SandboxedSdkView(android.content.Context context, optional android.util.AttributeSet? attrs);
     method public void addStateChangedListener(androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionStateChangedListener stateChangedListener);
+    method public void orderProviderUiAboveClientUi(boolean providerUiOnTop);
     method public void removeStateChangedListener(androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionStateChangedListener stateChangedListener);
     method public void setAdapter(androidx.privacysandbox.ui.core.SandboxedUiAdapter sandboxedUiAdapter);
-    method public void setZOrderOnTopAndEnableUserInteraction(boolean setOnTop);
   }
 
 }
diff --git a/privacysandbox/ui/ui-client/api/restricted_current.txt b/privacysandbox/ui/ui-client/api/restricted_current.txt
index bc9a40d..47ff65b 100644
--- a/privacysandbox/ui/ui-client/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-client/api/restricted_current.txt
@@ -48,9 +48,9 @@
     ctor public SandboxedSdkView(android.content.Context context);
     ctor public SandboxedSdkView(android.content.Context context, optional android.util.AttributeSet? attrs);
     method public void addStateChangedListener(androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionStateChangedListener stateChangedListener);
+    method public void orderProviderUiAboveClientUi(boolean providerUiOnTop);
     method public void removeStateChangedListener(androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionStateChangedListener stateChangedListener);
     method public void setAdapter(androidx.privacysandbox.ui.core.SandboxedUiAdapter sandboxedUiAdapter);
-    method public void setZOrderOnTopAndEnableUserInteraction(boolean setOnTop);
   }
 
 }
diff --git a/privacysandbox/ui/ui-client/build.gradle b/privacysandbox/ui/ui-client/build.gradle
index 2ef9295..fad9e90 100644
--- a/privacysandbox/ui/ui-client/build.gradle
+++ b/privacysandbox/ui/ui-client/build.gradle
@@ -31,7 +31,7 @@
     implementation("androidx.core:core:1.12.0-alpha05")
 
     implementation("androidx.lifecycle:lifecycle-common:2.2.0")
-    implementation project(path: ':privacysandbox:sdkruntime:sdkruntime-client')
+    implementation("androidx.privacysandbox.sdkruntime:sdkruntime-client:1.0.0-alpha08")
     implementation project(path: ':privacysandbox:ui:ui-core')
 
     androidTestImplementation(project(":internal-testutils-runtime"))
@@ -46,6 +46,7 @@
     androidTestImplementation(libs.espressoCore)
     androidTestImplementation(libs.mockitoCore)
     androidTestImplementation(libs.multidex)
+    androidTestImplementation(libs.testUiautomator)
     androidTestImplementation project(path: ':appcompat:appcompat')
 }
 
diff --git a/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml b/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml
index b2720e1..15f02d9 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml
+++ b/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml
@@ -14,7 +14,10 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+    <!-- This override is okay because the associated tests only run on T+ -->
+    <uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator, androidx.test.uiautomator" />
     <application android:supportsRtl="true"
         android:name="androidx.multidex.MultiDexApplication"
         android:theme="@style/Theme.AppCompat">
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
index fd13072..8028c83 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import android.content.pm.ActivityInfo
 import android.content.res.Configuration
+import android.graphics.Rect
 import android.os.Build
 import android.os.IBinder
 import android.view.SurfaceView
@@ -27,15 +28,22 @@
 import android.view.ViewGroup
 import android.view.ViewTreeObserver
 import android.widget.LinearLayout
+import android.widget.ScrollView
 import androidx.annotation.RequiresApi
 import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionState
 import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionStateChangedListener
 import androidx.privacysandbox.ui.client.view.SandboxedSdkView
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
 import androidx.testutils.withActivity
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.CountDownLatch
@@ -45,7 +53,6 @@
 import org.junit.Assert.assertTrue
 import org.junit.Assume
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -58,6 +65,8 @@
 
     companion object {
         const val TIMEOUT = 1000.toLong()
+        // Longer timeout used for expensive operations like device rotation.
+        const val UI_INTENSIVE_TIMEOUT = 2000.toLong()
     }
 
     private lateinit var context: Context
@@ -183,7 +192,7 @@
         Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
         context = InstrumentationRegistry.getInstrumentation().targetContext
         activity = activityScenarioRule.withActivity { this }
-        view = SandboxedSdkView(context)
+        view = SandboxedSdkView(activity)
         stateChangedListener = StateChangedListener()
         view.addStateChangedListener(stateChangedListener)
 
@@ -279,13 +288,13 @@
         assertThat(adapter.isZOrderOnTop).isTrue()
 
         // When state changes to false, the provider should be notified.
-        view.setZOrderOnTopAndEnableUserInteraction(false)
+        view.orderProviderUiAboveClientUi(false)
         assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
         assertThat(adapter.isZOrderOnTop).isFalse()
 
         // When state changes back to true, the provider should be notified.
         session.zOrderChangedLatch = CountDownLatch(1)
-        view.setZOrderOnTopAndEnableUserInteraction(true)
+        view.orderProviderUiAboveClientUi(true)
         assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
         assertThat(adapter.isZOrderOnTop).isTrue()
     }
@@ -302,14 +311,14 @@
 
         // When Z-order state is unchanged, the provider should not be notified.
         session.zOrderChangedLatch = CountDownLatch(1)
-        view.setZOrderOnTopAndEnableUserInteraction(true)
+        view.orderProviderUiAboveClientUi(true)
         assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
         assertThat(adapter.isZOrderOnTop).isTrue()
     }
 
     @Test
     fun setZOrderNotOnTopBeforeOpeningSession() {
-        view.setZOrderOnTopAndEnableUserInteraction(false)
+        view.orderProviderUiAboveClientUi(false)
         addViewToLayout()
         assertThat(openSessionLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
         val session = testSandboxedUiAdapter.testSession!!
@@ -324,7 +333,7 @@
         testSandboxedUiAdapter.delayOpenSessionCallback = true
         addViewToLayout()
         assertThat(openSessionLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
-        view.setZOrderOnTopAndEnableUserInteraction(false)
+        view.orderProviderUiAboveClientUi(false)
         val session = testSandboxedUiAdapter.testSession!!
         assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
         activity.runOnUiThread {
@@ -338,17 +347,19 @@
     }
 
     @Test
-    @Ignore("b/272324246")
     fun onConfigurationChangedTest() {
         addViewToLayout()
-
-        openSessionLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)
-        assertTrue(openSessionLatch.count == 0.toLong())
-        activity.runOnUiThread {
-            activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
-        }
-        configChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)
-        assertTrue(configChangedLatch.count == 0.toLong())
+        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+        assertThat(openSessionLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        // newWindow() will be triggered by a window state change, even if the activity handles
+        // orientation changes without recreating the activity.
+        device.performActionAndWait({
+            device.setOrientationLeft()
+        }, Until.newWindow(), UI_INTENSIVE_TIMEOUT)
+        assertThat(configChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        device.performActionAndWait({
+            device.setOrientationNatural()
+        }, Until.newWindow(), UI_INTENSIVE_TIMEOUT)
     }
 
     @Test
@@ -358,7 +369,7 @@
         activity.runOnUiThread {
             activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
         }
-        assertThat(configChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+        assertThat(configChangedLatch.await(UI_INTENSIVE_TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
     }
 
     @Test
@@ -462,6 +473,33 @@
         assertThat(testSandboxedUiAdapter.inputToken).isEqualTo(token)
     }
 
+    @Test
+    fun getBoundingParent_withoutScrollParent() {
+        addViewToLayout()
+        onView(withId(R.id.mainlayout)).check(matches(isDisplayed()))
+        val boundingRect = Rect()
+        assertThat(view.getBoundingParent(boundingRect)).isTrue()
+        val rootView: ViewGroup = activity.findViewById(android.R.id.content)
+        val rootRect = Rect()
+        rootView.getGlobalVisibleRect(rootRect)
+        assertThat(boundingRect).isEqualTo(rootRect)
+    }
+
+    @Test
+    fun getBoundingParent_withScrollParent() {
+        val scrollViewRect = Rect()
+        val scrollView = activity.findViewById<ScrollView>(R.id.scroll_view)
+        activity.runOnUiThread {
+            scrollView.visibility = View.VISIBLE
+            scrollView.addView(view)
+        }
+        onView(withId(R.id.scroll_view)).check(matches(isDisplayed()))
+        assertThat(scrollView.getGlobalVisibleRect(scrollViewRect)).isTrue()
+        val boundingRect = Rect()
+        assertThat(view.getBoundingParent(boundingRect)).isTrue()
+        assertThat(scrollViewRect).isEqualTo(boundingRect)
+    }
+
     /**
      * Ensures that ACTIVE will only be sent to registered state change listeners after the next
      * frame commit.
diff --git a/privacysandbox/ui/ui-client/src/androidTest/res/layout/activity_main.xml b/privacysandbox/ui/ui-client/src/androidTest/res/layout/activity_main.xml
index 9922ac7..71da52f 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/res/layout/activity_main.xml
+++ b/privacysandbox/ui/ui-client/src/androidTest/res/layout/activity_main.xml
@@ -19,4 +19,9 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:id="@+id/mainlayout">
+    <ScrollView
+        android:layout_width="500px"
+        android:layout_height="500px"
+        android:id="@+id/scroll_view"
+        android:visibility="gone" />
 </LinearLayout>
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
index de5b048..70ceae1 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
@@ -133,6 +133,7 @@
 
             override fun notifyZOrderChanged(isZOrderOnTop: Boolean) {
                 surfaceView.setZOrderOnTop(isZOrderOnTop)
+                remoteSessionController.notifyZOrderChanged(isZOrderOnTop)
             }
 
             override fun close() {
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
index bbf3694..49c553f 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
@@ -18,13 +18,22 @@
 
 import android.content.Context
 import android.content.res.Configuration
+import android.graphics.Rect
 import android.os.Build
 import android.os.IBinder
 import android.util.AttributeSet
+import android.view.SurfaceControl
+import android.view.SurfaceHolder
 import android.view.SurfaceView
 import android.view.View
 import android.view.ViewGroup
+import android.view.ViewParent
+import android.view.ViewTreeObserver
 import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionState.Active
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionState.Idle
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionState.Loading
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
 import java.util.concurrent.CopyOnWriteArrayList
 import kotlin.math.min
@@ -95,6 +104,24 @@
         visibility = GONE
     }
 
+    // This will only be invoked when the content view has been set and the window is attached.
+    private val surfaceChangedCallback = object : SurfaceHolder.Callback {
+        override fun surfaceCreated(p0: SurfaceHolder) {
+            setClippingBounds(true)
+            viewTreeObserver.addOnGlobalLayoutListener(globalLayoutChangeListener)
+        }
+
+        override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {
+        }
+
+        override fun surfaceDestroyed(p0: SurfaceHolder) {
+        }
+    }
+
+    // This will only be invoked when the content view has been set and the window is attached.
+    private val globalLayoutChangeListener =
+        ViewTreeObserver.OnGlobalLayoutListener { setClippingBounds() }
+
     private var adapter: SandboxedUiAdapter? = null
     private var client: Client? = null
     private var isZOrderOnTop = true
@@ -103,6 +130,8 @@
     private var requestedHeight = -1
     private var isTransitionGroupSet = false
     private var windowInputToken: IBinder? = null
+    private var currentClippingBounds = Rect()
+    private var currentConfig = context.resources.configuration
     internal val stateListenerManager: StateListenerManager = StateListenerManager()
 
     /**
@@ -133,17 +162,72 @@
     /**
      * Sets the Z-ordering of the [SandboxedSdkView]'s surface, relative to its window.
      *
-     * When [setOnTop] is true, every [android.view.MotionEvent] on the [SandboxedSdkView] will be
-     * sent to the UI provider. When [setOnTop] is false, every [android.view.MotionEvent] will be
-     * sent to the client. By default, motion events are sent to the UI provider.
+     * When [providerUiOnTop] is true, every [android.view.MotionEvent] on the [SandboxedSdkView]
+     * will be sent to the UI provider. When [providerUiOnTop] is false, every
+     * [android.view.MotionEvent] will be sent to the client. By default, motion events are sent to
+     * the UI provider.
      */
-    fun setZOrderOnTopAndEnableUserInteraction(setOnTop: Boolean) {
-        if (setOnTop == isZOrderOnTop) return
-        client?.notifyZOrderChanged(setOnTop)
-        isZOrderOnTop = setOnTop
+    fun orderProviderUiAboveClientUi(providerUiOnTop: Boolean) {
+        if (providerUiOnTop == isZOrderOnTop) return
+        client?.notifyZOrderChanged(providerUiOnTop)
+        isZOrderOnTop = providerUiOnTop
         checkClientOpenSession()
     }
 
+    internal fun setClippingBounds(forceUpdate: Boolean = false) {
+        checkNotNull(contentView)
+        check(isAttachedToWindow)
+
+        val updateRequired = getBoundingParent(currentClippingBounds) || forceUpdate
+        if (!updateRequired) {
+            return
+        }
+
+        val sv: SurfaceView = contentView as SurfaceView
+        val attachedSurfaceControl = checkNotNull(sv.rootSurfaceControl) {
+            "attachedSurfaceControl should be non-null if the window is attached"
+        }
+        val name = "clippingBounds-${System.currentTimeMillis()}"
+        val clippingBoundsSurfaceControl =
+            SurfaceControl.Builder().setName(name)
+                .build()
+        val reparentSurfaceControlTransaction = SurfaceControl.Transaction()
+            .reparent(sv.surfaceControl, clippingBoundsSurfaceControl)
+
+        val reparentClippingBoundsTransaction =
+            checkNotNull(
+                attachedSurfaceControl.buildReparentTransaction(clippingBoundsSurfaceControl)) {
+                "Reparent transaction should be non-null if the window is attached"
+            }
+        reparentClippingBoundsTransaction.setCrop(
+            clippingBoundsSurfaceControl, currentClippingBounds)
+        reparentClippingBoundsTransaction.setVisibility(
+            clippingBoundsSurfaceControl, true)
+        reparentSurfaceControlTransaction.merge(reparentClippingBoundsTransaction)
+        attachedSurfaceControl.applyTransactionOnDraw(reparentSurfaceControlTransaction)
+    }
+
+    /**
+     * Computes the window space coordinates for the bounding parent of this view, and stores the
+     * result in [rect].
+     *
+     * Returns true if the coordinates have changed, false otherwise.
+     */
+    @VisibleForTesting
+    internal fun getBoundingParent(rect: Rect): Boolean {
+        val prevBounds = Rect(rect)
+        var viewParent: ViewParent? = parent
+        while (viewParent != null && viewParent is View) {
+            val v = viewParent as View
+            if (v.isScrollContainer || v.id == android.R.id.content) {
+                v.getGlobalVisibleRect(rect)
+                return prevBounds != rect
+            }
+            viewParent = viewParent.getParent()
+        }
+        return false
+    }
+
     private fun checkClientOpenSession() {
         val adapter = adapter
         if (client == null && adapter != null && windowInputToken != null &&
@@ -197,11 +281,17 @@
     }
 
     private fun removeContentView() {
+        removeCallbacks()
         if (childCount == 1) {
             super.removeViewAt(0)
         }
     }
 
+    private fun removeCallbacks() {
+        (contentView as? SurfaceView)?.holder?.removeCallback(surfaceChangedCallback)
+        viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutChangeListener)
+    }
+
     internal fun setContentView(contentView: View) {
         if (childCount > 1) {
             throw IllegalStateException("Number of children views must not exceed 1")
@@ -220,6 +310,10 @@
             stateListenerManager.currentUiSessionState =
                 SandboxedSdkUiSessionState.Active
         }
+
+        if (contentView is SurfaceView) {
+            contentView.holder.addCallback(surfaceChangedCallback)
+        }
     }
 
     internal fun onClientClosedSession(error: Throwable? = null) {
@@ -293,6 +387,7 @@
         client?.close()
         client = null
         windowInputToken = null
+        removeCallbacks()
         super.onDetachedFromWindow()
     }
 
@@ -309,9 +404,11 @@
 
     override fun onConfigurationChanged(config: Configuration?) {
         requireNotNull(config) { "Config cannot be null" }
-        if (context.resources.configuration == config)
+        if (config == currentConfig) {
             return
+        }
         super.onConfigurationChanged(config)
+        currentConfig = config
         client?.notifyConfigurationChanged(config)
         checkClientOpenSession()
     }
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
index 1e2ff92..c856d33 100644
--- a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
@@ -21,4 +21,5 @@
     void close();
     void notifyConfigurationChanged(in Configuration configuration);
     void notifyResized(int width, int height);
+    void notifyZOrderChanged(boolean isZOrderOnTop);
 }
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/test/BinderAdapterDelegateTest.kt b/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/test/BinderAdapterDelegateTest.kt
index f2bb448..5b8aaab 100644
--- a/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/test/BinderAdapterDelegateTest.kt
+++ b/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/test/BinderAdapterDelegateTest.kt
@@ -93,15 +93,15 @@
     }
 
     @Test
-    fun touchFocusTransferredForSwipeLeft() {
+    fun touchFocusNotTransferredForSwipeLeft() {
         onView(withId(R.id.surface_view)).perform(swipeLeft())
-        assertThat(transferTouchFocusLatch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(transferTouchFocusLatch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isFalse()
     }
 
     @Test
-    fun touchFocusTransferredForSlowSwipeLeft() {
+    fun touchFocusNotTransferredForSlowSwipeLeft() {
         onView(withId(R.id.surface_view)).perform(slowSwipeLeft())
-        assertThat(transferTouchFocusLatch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(transferTouchFocusLatch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isFalse()
     }
 
     @Test
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
index 1acb8cc..77138eb 100644
--- a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
@@ -26,6 +26,7 @@
 import android.os.Handler
 import android.os.IBinder
 import android.os.Looper
+import android.util.Log
 import android.view.SurfaceControlViewHost
 import androidx.annotation.RequiresApi
 import androidx.annotation.VisibleForTesting
@@ -56,6 +57,11 @@
     private val adapter: SandboxedUiAdapter
 ) : ISandboxedUiAdapter.Stub() {
 
+    companion object {
+        private const val TAG = "BinderAdapterDelegate"
+        private const val FRAME_TIMEOUT_MILLIS = 1000.toLong()
+    }
+
     fun openSession(
         context: Context,
         windowInputToken: IBinder,
@@ -91,7 +97,8 @@
                     mDisplayManager.getDisplay(displayId), windowInputToken
                 )
                 val sessionClient = SessionClientProxy(
-                    surfaceControlViewHost, initialWidth, initialHeight, remoteSessionClient
+                    surfaceControlViewHost, initialWidth, initialHeight, isZOrderOnTop,
+                    remoteSessionClient
                 )
                 openSession(
                     windowContext, windowInputToken, initialWidth, initialHeight, isZOrderOnTop,
@@ -107,6 +114,7 @@
         private val surfaceControlViewHost: SurfaceControlViewHost,
         private val initialWidth: Int,
         private val initialHeight: Int,
+        private val isZOrderOnTop: Boolean,
         private val remoteSessionClient: IRemoteSessionClient
     ) : SandboxedUiAdapter.SessionClient {
 
@@ -121,13 +129,25 @@
                 surfaceControlViewHost.setView(view, initialWidth, initialHeight)
             }
 
-            val surfacePackage = surfaceControlViewHost.surfacePackage
-            val remoteSessionController =
-                RemoteSessionController(surfaceControlViewHost, session)
-            remoteSessionClient.onRemoteSessionOpened(
-                surfacePackage, remoteSessionController,
-                /* isZOrderOnTop= */ true
-            )
+            // This var is not locked as it will be set to false by the first event that can trigger
+            // sending the remote session opened callback.
+            var alreadyOpenedSession = false
+            view.viewTreeObserver.registerFrameCommitCallback {
+                if (!alreadyOpenedSession) {
+                    alreadyOpenedSession = true
+                    sendRemoteSessionOpened(session)
+                }
+            }
+
+            // If a frame commit callback is not triggered within the timeout (such as when the
+            // screen is off), open the session anyway.
+            Handler(Looper.getMainLooper()).postDelayed({
+                if (!alreadyOpenedSession) {
+                    Log.w(TAG, "Frame not committed within $FRAME_TIMEOUT_MILLIS ms.")
+                    alreadyOpenedSession = true
+                    sendRemoteSessionOpened(session)
+                }
+            }, FRAME_TIMEOUT_MILLIS)
         }
 
         override fun onSessionError(throwable: Throwable) {
@@ -138,6 +158,16 @@
             remoteSessionClient.onResizeRequested(width, height)
         }
 
+        private fun sendRemoteSessionOpened(session: SandboxedUiAdapter.Session) {
+            val surfacePackage = surfaceControlViewHost.surfacePackage
+            val remoteSessionController =
+                RemoteSessionController(surfaceControlViewHost, session)
+            remoteSessionClient.onRemoteSessionOpened(
+                surfacePackage, remoteSessionController,
+                isZOrderOnTop
+            )
+        }
+
         @VisibleForTesting
         private inner class RemoteSessionController(
             val surfaceControlViewHost: SurfaceControlViewHost,
@@ -157,6 +187,10 @@
                 }
             }
 
+            override fun notifyZOrderChanged(isZOrderOnTop: Boolean) {
+                session.notifyZOrderChanged(isZOrderOnTop)
+            }
+
             override fun close() {
                 val mHandler = Handler(Looper.getMainLooper())
                 mHandler.post {
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/TouchFocusTransferringView.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/TouchFocusTransferringView.kt
index 1ef330a..132f6e3 100644
--- a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/TouchFocusTransferringView.kt
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/TouchFocusTransferringView.kt
@@ -25,11 +25,12 @@
 import android.widget.FrameLayout
 import androidx.annotation.RequiresApi
 import androidx.core.view.GestureDetectorCompat
+import kotlin.math.abs
 
 /**
  * A container [ViewGroup] that delegates touch events to the host or the UI provider.
  *
- * Touch events will first be passed to a scroll detector. If a scroll or fling
+ * Touch events will first be passed to a scroll detector. If a vertical scroll or fling
  * is detected, the gesture will be transferred to the host. Otherwise, the touch event will pass
  * through and be handled by the provider of UI.
  *
@@ -61,7 +62,7 @@
     /**
      * Handles intercepted touch events before they reach the UI provider.
      *
-     * If a scroll or fling event is caught, this is indicated by the [isScrolling] var.
+     * If a vertical scroll or fling event is caught, this is indicated by the [isScrolling] var.
      */
     private class ScrollDetector(context: Context) : GestureDetector.SimpleOnGestureListener() {
 
@@ -71,8 +72,12 @@
         private val gestureDetector: GestureDetectorCompat = GestureDetectorCompat(context, this)
 
         override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean {
-            isScrolling = true
-            return false
+            // A scroll is vertical if its y displacement is greater than its x displacement.
+            if (abs(dY) > abs(dX)) {
+                isScrolling = true
+                return false
+            }
+            return true
         }
 
         override fun onFling(
@@ -81,8 +86,12 @@
             velocityX: Float,
             velocityY: Float
         ): Boolean {
-            isScrolling = true
-            return false
+            // A fling is vertical if its y velocity is greater than its x velocity.
+            if (abs(velocityY) > abs(velocityX)) {
+                isScrolling = true
+                return false
+            }
+            return true
         }
 
         fun onTouchEvent(ev: MotionEvent) {
diff --git a/privacysandbox/ui/ui-tests/build.gradle b/privacysandbox/ui/ui-tests/build.gradle
index cf66642..77e6e30 100644
--- a/privacysandbox/ui/ui-tests/build.gradle
+++ b/privacysandbox/ui/ui-tests/build.gradle
@@ -44,6 +44,9 @@
 
 android {
     namespace "androidx.privacysandbox.ui.tests"
+    defaultConfig {
+        multiDexEnabled true
+    }
 }
 
 androidx {
diff --git a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
index ae63942..1528e16 100644
--- a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
+++ b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
@@ -22,6 +22,8 @@
 import android.os.Binder
 import android.os.Build
 import android.os.IBinder
+import android.os.SystemClock
+import android.view.MotionEvent
 import android.view.View
 import android.view.View.OnLayoutChangeListener
 import android.view.ViewGroup
@@ -39,6 +41,7 @@
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit
@@ -77,7 +80,7 @@
         errorLatch = CountDownLatch(1)
         stateChangeListener = TestStateChangeListener(errorLatch)
         view.addStateChangedListener(stateChangeListener)
-        activity.runOnUiThread(Runnable {
+        activity.runOnUiThread {
             val linearLayout = LinearLayout(context)
             linearLayout.layoutParams = LinearLayout.LayoutParams(
                 LinearLayout.LayoutParams.MATCH_PARENT,
@@ -86,7 +89,7 @@
             activity.setContentView(linearLayout)
             view.layoutParams = LinearLayout.LayoutParams(100, 100)
             linearLayout.addView(view)
-        })
+        }
     }
 
     @Ignore // b/271299184
@@ -166,7 +169,7 @@
         val adapter = TestSandboxedUiAdapter(openSessionLatch, null, false)
         val coreLibInfo = adapter.toCoreLibInfo(context)
         val adapterFromCoreLibInfo = SandboxedUiAdapterFactory.createFromCoreLibInfo(coreLibInfo)
-        var testSessionClient = TestSandboxedUiAdapter.TestSessionClient()
+        val testSessionClient = TestSandboxedUiAdapter.TestSessionClient()
 
         adapterFromCoreLibInfo.openSession(
             context,
@@ -178,10 +181,9 @@
             testSessionClient
         )
 
-        openSessionLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)
-        assertTrue(openSessionLatch.count == 0.toLong())
-        assertTrue(adapter.isOpenSessionCalled)
-        assertTrue(testSessionClient.isSessionOpened)
+        assertThat(openSessionLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(adapter.isOpenSessionCalled).isTrue()
+        assertThat(testSessionClient.isSessionOpened).isTrue()
     }
 
     @Test
@@ -203,6 +205,60 @@
         assertTrue(configChangedLatch.count == 0.toLong())
     }
 
+    /**
+     * Tests that the provider receives Z-order change updates.
+     */
+    @Test
+    fun testZOrderChanged() {
+        val openSessionLatch = CountDownLatch(1)
+        val adapter = TestSandboxedUiAdapter(
+            openSessionLatch,
+            null,
+            /* hasFailingTestSession=*/false
+        )
+        val coreLibInfo = adapter.toCoreLibInfo(context)
+        val adapterFromCoreLibInfo = SandboxedUiAdapterFactory.createFromCoreLibInfo(coreLibInfo)
+        view.setAdapter(adapterFromCoreLibInfo)
+        assertThat(openSessionLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        view.orderProviderUiAboveClientUi(!adapter.initialZOrderOnTop)
+        assertThat(adapter.zOrderLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+    }
+
+    /**
+     * Tests that the provider does not receive Z-order updates if the Z-order is unchanged.
+     */
+    @Test
+    fun testZOrderUnchanged() {
+        val openSessionLatch = CountDownLatch(1)
+        val adapter = TestSandboxedUiAdapter(
+            openSessionLatch,
+            null,
+            /* hasFailingTestSession=*/false
+        )
+        val coreLibInfo = adapter.toCoreLibInfo(context)
+        val adapterFromCoreLibInfo = SandboxedUiAdapterFactory.createFromCoreLibInfo(coreLibInfo)
+        view.setAdapter(adapterFromCoreLibInfo)
+        assertThat(openSessionLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        view.orderProviderUiAboveClientUi(adapter.initialZOrderOnTop)
+        assertThat(adapter.zOrderLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+    }
+
+    @Test
+    fun testHostCanSetZOrderAboveBeforeOpeningSession() {
+        val adapter = openSessionAndWaitToBeActive(true)
+        injectInputEventOnView()
+        // the injected touch should be handled by the provider in Z-above mode
+        assertThat(adapter.touchedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+    }
+
+    @Test
+    fun testHostCanSetZOrderBelowBeforeOpeningSession() {
+        val adapter = openSessionAndWaitToBeActive(false)
+        injectInputEventOnView()
+        // the injected touch should not reach the provider in Z-below mode
+        assertThat(adapter.touchedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+    }
+
     @Test
     fun testSessionError() {
         val adapter = TestSandboxedUiAdapter(
@@ -219,6 +275,38 @@
         assertTrue(errorMessage == "Test Session Exception")
     }
 
+    private fun openSessionAndWaitToBeActive(initialZOrder: Boolean): TestSandboxedUiAdapter {
+        val adapter = TestSandboxedUiAdapter(
+            null,
+            null,
+            /* hasFailingTestSession=*/false
+        )
+        val coreLibInfo = adapter.toCoreLibInfo(context)
+        val adapterFromCoreLibInfo = SandboxedUiAdapterFactory.createFromCoreLibInfo(coreLibInfo)
+        view.orderProviderUiAboveClientUi(initialZOrder)
+        view.setAdapter(adapterFromCoreLibInfo)
+        val activeLatch = CountDownLatch(1)
+        view.addStateChangedListener { state ->
+            if (state is SandboxedSdkUiSessionState.Active) {
+                activeLatch.countDown()
+            }
+        }
+        assertThat(activeLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        return adapter
+    }
+
+    private fun injectInputEventOnView() {
+        activity.runOnUiThread {
+            val location = IntArray(2)
+            view.getLocationOnScreen(location)
+            InstrumentationRegistry.getInstrumentation().uiAutomation.injectInputEvent(
+                MotionEvent.obtain(
+                    SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN,
+                    (location[0] + 1).toFloat(),
+                    (location[1] + 1).toFloat(), 0), false)
+        }
+    }
+
     class TestStateChangeListener(private val errorLatch: CountDownLatch) :
         SandboxedSdkUiSessionStateChangedListener {
         var currentState: SandboxedSdkUiSessionState? = null
@@ -240,6 +328,9 @@
     ) : SandboxedUiAdapter {
 
         var isOpenSessionCalled = false
+        var initialZOrderOnTop = false
+        var zOrderLatch = CountDownLatch(1)
+        var touchedLatch = CountDownLatch(1)
         lateinit var session: SandboxedUiAdapter.Session
         lateinit var internalClient: SandboxedUiAdapter.SessionClient
 
@@ -254,6 +345,7 @@
         ) {
             internalClient = client
             isOpenSessionCalled = true
+            initialZOrderOnTop = isZOrderOnTop
             session = if (hasFailingTestSession) {
                 FailingTestSession(context)
             } else {
@@ -290,7 +382,12 @@
         ) : SandboxedUiAdapter.Session {
             override val view: View
                 get() {
-                    return View(context)
+                    return View(context).also {
+                        it.setOnTouchListener { _, _ ->
+                            touchedLatch.countDown()
+                            true
+                        }
+                    }
                 }
 
             init {
@@ -301,6 +398,7 @@
             }
 
             override fun notifyZOrderChanged(isZOrderOnTop: Boolean) {
+                zOrderLatch.countDown()
             }
 
             override fun notifyConfigurationChanged(configuration: Configuration) {
@@ -312,11 +410,12 @@
         }
 
         class TestSessionClient : SandboxedUiAdapter.SessionClient {
-
-            var isSessionOpened = false
+            private val latch = CountDownLatch(1)
+            val isSessionOpened: Boolean
+                get() = latch.await(TIMEOUT, TimeUnit.MILLISECONDS)
 
             override fun onSessionOpened(session: SandboxedUiAdapter.Session) {
-                isSessionOpened = true
+                latch.countDown()
             }
 
             override fun onSessionError(throwable: Throwable) {
diff --git a/room/integration-tests/autovaluetestapp/build.gradle b/room/integration-tests/autovaluetestapp/build.gradle
index e44eda7..bec90d2 100644
--- a/room/integration-tests/autovaluetestapp/build.gradle
+++ b/room/integration-tests/autovaluetestapp/build.gradle
@@ -14,13 +14,6 @@
  * limitations under the License.
  */
 
-buildscript {
-    // TODO: Remove this when this test app no longer depends on 1.0.0 of vectordrawable-animated.
-    // vectordrawable and vectordrawable-animated were accidentally using the same package name
-    // which is no longer valid in namespaced resource world.
-    project.ext["android.uniquePackageNames"] = false
-}
-
 plugins {
     id("AndroidXPlugin")
     id("com.android.application")
diff --git a/room/room-common/build.gradle b/room/room-common/build.gradle
index 4f3fd52..49e5177 100644
--- a/room/room-common/build.gradle
+++ b/room/room-common/build.gradle
@@ -14,27 +14,52 @@
  * limitations under the License.
  */
 
+
+import androidx.build.LibraryType
+import androidx.build.PlatformIdentifier
 import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
-    id("kotlin")
 }
 
-dependencies {
-    api("androidx.annotation:annotation:1.3.0")
-    api(libs.kotlinStdlibJdk8)
-    testImplementation(libs.junit)
-    testImplementation(libs.mockitoCore4)
-    testImplementation(libs.guava)
-    testImplementation(project(":kruth:kruth"))
+androidXMultiplatform {
+    jvm() {
+        withJava()
+    }
+    mac()
+    linux()
+    ios()
+
+    defaultPlatform(PlatformIdentifier.JVM)
+
+    sourceSets {
+        all {
+            languageSettings.optIn("kotlin.RequiresOptIn")
+        }
+
+        commonMain {
+            dependencies {
+                api(libs.kotlinStdlib)
+                api("androidx.annotation:annotation:1.3.0")
+            }
+        }
+        commonTest {
+            dependencies {
+                implementation(project(":kruth:kruth"))
+                implementation(libs.junit)
+                implementation(libs.guava)
+            }
+        }
+    }
 }
 
 androidx {
     name = "Room-Common"
+    type = LibraryType.PUBLISHED_LIBRARY
     publish = Publish.SNAPSHOT_AND_RELEASE
     inceptionYear = "2017"
     description = "Android Room-Common"
+    legacyDisableKotlinStrictApiMode = true
     metalavaK2UastEnabled = true
 }
diff --git a/room/room-common/src/main/java/androidx/room/AmbiguousColumnResolver.kt b/room/room-common/src/commonMain/kotlin/androidx/room/AmbiguousColumnResolver.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/AmbiguousColumnResolver.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/AmbiguousColumnResolver.kt
diff --git a/room/room-common/src/main/java/androidx/room/AutoMigration.kt b/room/room-common/src/commonMain/kotlin/androidx/room/AutoMigration.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/AutoMigration.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/AutoMigration.kt
diff --git a/room/room-common/src/main/java/androidx/room/BuiltInTypeConverters.kt b/room/room-common/src/commonMain/kotlin/androidx/room/BuiltInTypeConverters.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/BuiltInTypeConverters.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/BuiltInTypeConverters.kt
diff --git a/room/room-common/src/main/java/androidx/room/ColumnInfo.kt b/room/room-common/src/commonMain/kotlin/androidx/room/ColumnInfo.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/ColumnInfo.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/ColumnInfo.kt
diff --git a/room/room-common/src/main/java/androidx/room/Dao.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Dao.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Dao.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Dao.kt
diff --git a/room/room-common/src/main/java/androidx/room/Database.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Database.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Database.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Database.kt
diff --git a/room/room-common/src/main/java/androidx/room/DatabaseView.kt b/room/room-common/src/commonMain/kotlin/androidx/room/DatabaseView.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/DatabaseView.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/DatabaseView.kt
diff --git a/room/room-common/src/main/java/androidx/room/Delete.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Delete.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Delete.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Delete.kt
diff --git a/room/room-common/src/main/java/androidx/room/DeleteColumn.kt b/room/room-common/src/commonMain/kotlin/androidx/room/DeleteColumn.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/DeleteColumn.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/DeleteColumn.kt
diff --git a/room/room-common/src/main/java/androidx/room/DeleteTable.kt b/room/room-common/src/commonMain/kotlin/androidx/room/DeleteTable.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/DeleteTable.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/DeleteTable.kt
diff --git a/room/room-common/src/main/java/androidx/room/Embedded.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Embedded.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Embedded.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Embedded.kt
diff --git a/room/room-common/src/main/java/androidx/room/Entity.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Entity.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Entity.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Entity.kt
diff --git a/room/room-common/src/main/java/androidx/room/ForeignKey.kt b/room/room-common/src/commonMain/kotlin/androidx/room/ForeignKey.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/ForeignKey.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/ForeignKey.kt
diff --git a/room/room-common/src/main/java/androidx/room/Fts3.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Fts3.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Fts3.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Fts3.kt
diff --git a/room/room-common/src/commonMain/kotlin/androidx/room/Fts4.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Fts4.kt
new file mode 100644
index 0000000..fcd8895
--- /dev/null
+++ b/room/room-common/src/commonMain/kotlin/androidx/room/Fts4.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2018 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.room
+
+import androidx.annotation.RequiresApi
+import androidx.room.FtsOptions.TOKENIZER_SIMPLE
+import kotlin.reflect.KClass
+
+/**
+ * Marks an [Entity] annotated class as a FTS4 entity. This class will have a mapping SQLite
+ * FTS4 table in the database.
+ *
+ * [FTS3 and FTS4] (https://www.sqlite.org/fts3.html) are SQLite virtual table modules
+ * that allows full-text searches to be performed on a set of documents.
+ *
+ * An FTS entity table always has a column named `rowid` that is the equivalent of an
+ * `INTEGER PRIMARY KEY` index. Therefore, an FTS entity can only have a single field
+ * annotated with [PrimaryKey], it must be named `rowid` and must be of
+ * `INTEGER` affinity. The field can be optionally omitted in the class but can still be
+ * used in queries.
+ *
+ * All fields in an FTS entity are of `TEXT` affinity, except the for the 'rowid' and
+ * 'languageid' fields.
+ *
+ * Example:
+ *
+ * ```
+ * @Entity
+ * @Fts4
+ * data class Mail (
+ *   @PrimaryKey
+ *   @ColumnInfo(name = "rowid")
+ *   val rowId: Int,
+ *   val subject: String,
+ *   val body: String
+ * )
+ * ```
+ *
+ * @see [Entity]
+ * @see [Dao]
+ * @see [Database]
+ * @see [PrimaryKey]
+ * @see [ColumnInfo]
+ */
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.BINARY)
+@RequiresApi(16)
+public annotation class Fts4(
+
+    /**
+     * The tokenizer to be used in the FTS table.
+     *
+     * The default value is [FtsOptions.TOKENIZER_SIMPLE]. Tokenizer arguments can be defined
+     * with [tokenizerArgs].
+     *
+     * If a custom tokenizer is used, the tokenizer and its arguments are not verified at compile
+     * time.
+     *
+     * For details, see [SQLite tokernizers documentation](https://www.sqlite.org/fts3.html#tokenizer)
+     *
+     * @return The tokenizer to use on the FTS table. Built-in available tokenizers are
+     * [FtsOptions.TOKENIZER_SIMPLE], [FtsOptions.TOKENIZER_PORTER] and
+     * [FtsOptions.TOKENIZER_UNICODE61].
+     * @see [tokenizerArgs]
+     */
+    val tokenizer: String = TOKENIZER_SIMPLE,
+
+    /**
+     * Optional arguments to configure the defined tokenizer.
+     *
+     * Tokenizer arguments consist of an argument name, followed by an "=" character, followed by
+     * the option value. For example, `separators=.` defines the dot character as an
+     * additional separator when using the [FtsOptions.TOKENIZER_UNICODE61] tokenizer.
+     *
+     * The available arguments that can be defined depend on the tokenizer defined, see the
+     * [SQLite tokenizers documentation](https://www.sqlite.org/fts3.html#tokenizer) for details.
+     *
+     * @return A list of tokenizer arguments strings.
+     */
+    val tokenizerArgs: Array<String> = [],
+
+    /**
+     * The external content entity who's mapping table will be used as content for the FTS table.
+     *
+     * Declaring this value makes the mapping FTS table of this entity operate in "external content"
+     * mode. In such mode the FTS table does not store its own content but instead uses the data in
+     * the entity mapped table defined in this value. This option allows FTS4 to forego storing the
+     * text being indexed which can be used to achieve significant space savings.
+     *
+     * In "external mode" the content table and the FTS table need to be synced. Room will create
+     * the necessary triggers to keep the tables in sync. Therefore, all write operations should
+     * be performed against the content entity table and not the FTS table.
+     *
+     * The content sync triggers created by Room will be removed before migrations are executed and
+     * are re-created once migrations are complete. This prevents the triggers from interfering with
+     * migrations but means that if data needs to be migrated then write operations might need to be
+     * done in both the FTS and content tables.
+     *
+     * See the [External Content FTS4 Tables](https://www.sqlite.org/fts3.html#_external_content_fts4_tables_)
+     * documentation for details.
+     *
+     * @return The external content entity.
+     */
+    val contentEntity: KClass<*> = Any::class,
+
+    /**
+     * The column name to be used as 'languageid'.
+     *
+     * Allows the FTS4 extension to use the defined column name to specify the language stored in
+     * each row. When this is defined a field of type `INTEGER` with the same name must
+     * exist in the class.
+     *
+     * FTS queries are affected by defining this option, see
+     * [the languageid= option documentation](https://www.sqlite.org/fts3.html#the_languageid_option)
+     * for details.
+     *
+     * @return The column name to be used as 'languageid'.
+     */
+    val languageId: String = "",
+
+    /**
+     * The FTS version used to store text matching information.
+     *
+     * The default value is [MatchInfo.FTS4]. Disk space consumption can be reduced by
+     * setting this option to FTS3, see
+     * [the matchinfo= option documentation](https://www.sqlite.org/fts3.html#the_matchinfo_option)
+     * for details.
+     *
+     * @return The match info version, either [MatchInfo.FTS4] or [MatchInfo.FTS3].
+     */
+    val matchInfo: FtsOptions.MatchInfo = FtsOptions.MatchInfo.FTS4,
+
+    /**
+     * The list of column names on the FTS table that won't be indexed.
+     *
+     * For details, see the
+     * [notindexed= option documentation](https://www.sqlite.org/fts3.html#the_notindexed_option).
+     *
+     * @return A list of column names that will not be indexed by the FTS extension.
+     */
+    val notIndexed: Array<String> = [],
+
+    /**
+     * The list of prefix sizes to index.
+     *
+     * For details,
+     * [the prefix= option documentation](https://www.sqlite.org/fts3.html#the_prefix_option).
+     *
+     * @return A list of non-zero positive prefix sizes to index.
+     */
+    val prefix: IntArray = [],
+
+    /**
+     * The preferred 'rowid' order of the FTS table.
+     *
+     * The default value is [Order.ASC]. If many queries are run against the FTS table use
+     * `ORDER BY row DESC` then it may improve performance to set this option to
+     * [Order.DESC], enabling the FTS module to store its data in a way that optimizes
+     * returning results in descending order by `rowid`.
+     *
+     * @return The preferred order, either [Order.ASC] or [Order.DESC].
+     */
+    val order: FtsOptions.Order = FtsOptions.Order.ASC
+)
diff --git a/room/room-common/src/main/java/androidx/room/FtsOptions.kt b/room/room-common/src/commonMain/kotlin/androidx/room/FtsOptions.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/FtsOptions.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/FtsOptions.kt
diff --git a/room/room-common/src/main/java/androidx/room/Ignore.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Ignore.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Ignore.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Ignore.kt
diff --git a/room/room-common/src/main/java/androidx/room/Index.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Index.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Index.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Index.kt
diff --git a/room/room-common/src/main/java/androidx/room/Insert.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Insert.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Insert.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Insert.kt
diff --git a/room/room-common/src/main/java/androidx/room/Junction.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Junction.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Junction.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Junction.kt
diff --git a/room/room-common/src/main/java/androidx/room/MapColumn.kt b/room/room-common/src/commonMain/kotlin/androidx/room/MapColumn.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/MapColumn.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/MapColumn.kt
diff --git a/room/room-common/src/main/java/androidx/room/MapInfo.kt b/room/room-common/src/commonMain/kotlin/androidx/room/MapInfo.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/MapInfo.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/MapInfo.kt
diff --git a/room/room-common/src/main/java/androidx/room/OnConflictStrategy.kt b/room/room-common/src/commonMain/kotlin/androidx/room/OnConflictStrategy.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/OnConflictStrategy.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/OnConflictStrategy.kt
diff --git a/room/room-common/src/main/java/androidx/room/PrimaryKey.kt b/room/room-common/src/commonMain/kotlin/androidx/room/PrimaryKey.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/PrimaryKey.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/PrimaryKey.kt
diff --git a/room/room-common/src/main/java/androidx/room/ProvidedAutoMigrationSpec.kt b/room/room-common/src/commonMain/kotlin/androidx/room/ProvidedAutoMigrationSpec.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/ProvidedAutoMigrationSpec.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/ProvidedAutoMigrationSpec.kt
diff --git a/room/room-common/src/main/java/androidx/room/ProvidedTypeConverter.kt b/room/room-common/src/commonMain/kotlin/androidx/room/ProvidedTypeConverter.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/ProvidedTypeConverter.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/ProvidedTypeConverter.kt
diff --git a/room/room-common/src/main/java/androidx/room/Query.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Query.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Query.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Query.kt
diff --git a/room/room-common/src/main/java/androidx/room/RawQuery.kt b/room/room-common/src/commonMain/kotlin/androidx/room/RawQuery.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/RawQuery.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/RawQuery.kt
diff --git a/room/room-common/src/main/java/androidx/room/Relation.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Relation.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Relation.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Relation.kt
diff --git a/room/room-common/src/main/java/androidx/room/RenameColumn.kt b/room/room-common/src/commonMain/kotlin/androidx/room/RenameColumn.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/RenameColumn.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/RenameColumn.kt
diff --git a/room/room-common/src/main/java/androidx/room/RenameTable.kt b/room/room-common/src/commonMain/kotlin/androidx/room/RenameTable.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/RenameTable.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/RenameTable.kt
diff --git a/room/room-common/src/main/java/androidx/room/RewriteQueriesToDropUnusedColumns.kt b/room/room-common/src/commonMain/kotlin/androidx/room/RewriteQueriesToDropUnusedColumns.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/RewriteQueriesToDropUnusedColumns.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/RewriteQueriesToDropUnusedColumns.kt
diff --git a/room/room-common/src/main/java/androidx/room/RoomMasterTable.kt b/room/room-common/src/commonMain/kotlin/androidx/room/RoomMasterTable.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/RoomMasterTable.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/RoomMasterTable.kt
diff --git a/room/room-common/src/main/java/androidx/room/RoomWarnings.kt b/room/room-common/src/commonMain/kotlin/androidx/room/RoomWarnings.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/RoomWarnings.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/RoomWarnings.kt
diff --git a/room/room-common/src/main/java/androidx/room/SkipQueryVerification.kt b/room/room-common/src/commonMain/kotlin/androidx/room/SkipQueryVerification.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/SkipQueryVerification.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/SkipQueryVerification.kt
diff --git a/room/room-common/src/main/java/androidx/room/Transaction.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Transaction.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Transaction.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Transaction.kt
diff --git a/room/room-common/src/main/java/androidx/room/TypeConverter.kt b/room/room-common/src/commonMain/kotlin/androidx/room/TypeConverter.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/TypeConverter.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/TypeConverter.kt
diff --git a/room/room-common/src/main/java/androidx/room/TypeConverters.kt b/room/room-common/src/commonMain/kotlin/androidx/room/TypeConverters.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/TypeConverters.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/TypeConverters.kt
diff --git a/room/room-common/src/main/java/androidx/room/Update.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Update.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Update.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Update.kt
diff --git a/room/room-common/src/main/java/androidx/room/Upsert.kt b/room/room-common/src/commonMain/kotlin/androidx/room/Upsert.kt
similarity index 100%
rename from room/room-common/src/main/java/androidx/room/Upsert.kt
rename to room/room-common/src/commonMain/kotlin/androidx/room/Upsert.kt
diff --git a/room/room-common/src/test/java/androidx/room/AmbiguousColumnResolverTest.kt b/room/room-common/src/commonTest/kotlin/androidx/room/AmbiguousColumnResolverTest.kt
similarity index 100%
rename from room/room-common/src/test/java/androidx/room/AmbiguousColumnResolverTest.kt
rename to room/room-common/src/commonTest/kotlin/androidx/room/AmbiguousColumnResolverTest.kt
diff --git a/room/room-common/src/test/java/androidx/room/AnnotationRetentionPolicyTest.kt b/room/room-common/src/commonTest/kotlin/androidx/room/AnnotationRetentionPolicyTest.kt
similarity index 100%
rename from room/room-common/src/test/java/androidx/room/AnnotationRetentionPolicyTest.kt
rename to room/room-common/src/commonTest/kotlin/androidx/room/AnnotationRetentionPolicyTest.kt
diff --git a/room/room-common/src/main/java/androidx/room/Fts4.kt b/room/room-common/src/main/java/androidx/room/Fts4.kt
deleted file mode 100644
index f9ed6e1..0000000
--- a/room/room-common/src/main/java/androidx/room/Fts4.kt
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright 2018 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.room
-
-import androidx.annotation.RequiresApi
-import androidx.room.FtsOptions.MatchInfo
-import androidx.room.FtsOptions.Order
-import androidx.room.FtsOptions.TOKENIZER_SIMPLE
-import kotlin.reflect.KClass
-
-/**
- * Marks an [Entity] annotated class as a FTS4 entity. This class will have a mapping SQLite
- * FTS4 table in the database.
- *
- * [FTS3 and FTS4] (https://www.sqlite.org/fts3.html) are SQLite virtual table modules
- * that allows full-text searches to be performed on a set of documents.
- *
- * An FTS entity table always has a column named `rowid` that is the equivalent of an
- * `INTEGER PRIMARY KEY` index. Therefore, an FTS entity can only have a single field
- * annotated with [PrimaryKey], it must be named `rowid` and must be of
- * `INTEGER` affinity. The field can be optionally omitted in the class but can still be
- * used in queries.
- *
- * All fields in an FTS entity are of `TEXT` affinity, except the for the 'rowid' and
- * 'languageid' fields.
- *
- * Example:
- *
- * ```
- * @Entity
- * @Fts4
- * data class Mail (
- *   @PrimaryKey
- *   @ColumnInfo(name = "rowid")
- *   val rowId: Int,
- *   val subject: String,
- *   val body: String
- * )
- * ```
- *
- * @see [Entity]
- * @see [Dao]
- * @see [Database]
- * @see [PrimaryKey]
- * @see [ColumnInfo]
- */
-@Target(AnnotationTarget.CLASS)
-@Retention(AnnotationRetention.BINARY)
-@RequiresApi(16)
-public annotation class Fts4(
-
-    /**
-     * The tokenizer to be used in the FTS table.
-     *
-     * The default value is [FtsOptions.TOKENIZER_SIMPLE]. Tokenizer arguments can be defined
-     * with [tokenizerArgs].
-     *
-     * If a custom tokenizer is used, the tokenizer and its arguments are not verified at compile
-     * time.
-     *
-     * For details, see [SQLite tokernizers documentation](https://www.sqlite.org/fts3.html#tokenizer)
-     *
-     * @return The tokenizer to use on the FTS table. Built-in available tokenizers are
-     * [FtsOptions.TOKENIZER_SIMPLE], [FtsOptions.TOKENIZER_PORTER] and
-     * [FtsOptions.TOKENIZER_UNICODE61].
-     * @see [tokenizerArgs]
-     */
-    val tokenizer: String = TOKENIZER_SIMPLE,
-
-    /**
-     * Optional arguments to configure the defined tokenizer.
-     *
-     * Tokenizer arguments consist of an argument name, followed by an "=" character, followed by
-     * the option value. For example, `separators=.` defines the dot character as an
-     * additional separator when using the [FtsOptions.TOKENIZER_UNICODE61] tokenizer.
-     *
-     * The available arguments that can be defined depend on the tokenizer defined, see the
-     * [SQLite tokenizers documentation](https://www.sqlite.org/fts3.html#tokenizer) for details.
-     *
-     * @return A list of tokenizer arguments strings.
-     */
-    val tokenizerArgs: Array<String> = [],
-
-    /**
-     * The external content entity who's mapping table will be used as content for the FTS table.
-     *
-     * Declaring this value makes the mapping FTS table of this entity operate in "external content"
-     * mode. In such mode the FTS table does not store its own content but instead uses the data in
-     * the entity mapped table defined in this value. This option allows FTS4 to forego storing the
-     * text being indexed which can be used to achieve significant space savings.
-     *
-     * In "external mode" the content table and the FTS table need to be synced. Room will create
-     * the necessary triggers to keep the tables in sync. Therefore, all write operations should
-     * be performed against the content entity table and not the FTS table.
-     *
-     * The content sync triggers created by Room will be removed before migrations are executed and
-     * are re-created once migrations are complete. This prevents the triggers from interfering with
-     * migrations but means that if data needs to be migrated then write operations might need to be
-     * done in both the FTS and content tables.
-     *
-     * See the [External Content FTS4 Tables](https://www.sqlite.org/fts3.html#_external_content_fts4_tables_)
-     * documentation for details.
-     *
-     * @return The external content entity.
-     */
-    val contentEntity: KClass<*> = Any::class,
-
-    /**
-     * The column name to be used as 'languageid'.
-     *
-     * Allows the FTS4 extension to use the defined column name to specify the language stored in
-     * each row. When this is defined a field of type `INTEGER` with the same name must
-     * exist in the class.
-     *
-     * FTS queries are affected by defining this option, see
-     * [the languageid= option documentation](https://www.sqlite.org/fts3.html#the_languageid_option)
-     * for details.
-     *
-     * @return The column name to be used as 'languageid'.
-     */
-    val languageId: String = "",
-
-    /**
-     * The FTS version used to store text matching information.
-     *
-     * The default value is [MatchInfo.FTS4]. Disk space consumption can be reduced by
-     * setting this option to FTS3, see
-     * [the matchinfo= option documentation](https://www.sqlite.org/fts3.html#the_matchinfo_option)
-     * for details.
-     *
-     * @return The match info version, either [MatchInfo.FTS4] or [MatchInfo.FTS3].
-     */
-    val matchInfo: MatchInfo = MatchInfo.FTS4,
-
-    /**
-     * The list of column names on the FTS table that won't be indexed.
-     *
-     * For details, see the
-     * [notindexed= option documentation](https://www.sqlite.org/fts3.html#the_notindexed_option).
-     *
-     * @return A list of column names that will not be indexed by the FTS extension.
-     */
-    val notIndexed: Array<String> = [],
-
-    /**
-     * The list of prefix sizes to index.
-     *
-     * For details,
-     * [the prefix= option documentation](https://www.sqlite.org/fts3.html#the_prefix_option).
-     *
-     * @return A list of non-zero positive prefix sizes to index.
-     */
-    val prefix: IntArray = [],
-
-    /**
-     * The preferred 'rowid' order of the FTS table.
-     *
-     * The default value is [Order.ASC]. If many queries are run against the FTS table use
-     * `ORDER BY row DESC` then it may improve performance to set this option to
-     * [Order.DESC], enabling the FTS module to store its data in a way that optimizes
-     * returning results in descending order by `rowid`.
-     *
-     * @return The preferred order, either [Order.ASC] or [Order.DESC].
-     */
-    val order: Order = Order.ASC
-)
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/SourceSet.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/SourceSet.kt
index 6f0f335..f212956 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/SourceSet.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/SourceSet.kt
@@ -18,9 +18,11 @@
 
 import androidx.room.compiler.processing.util.Source
 import java.io.File
+import java.util.regex.Pattern
 
 private val BY_ROUNDS_PATH_PATTERN =
-    "(byRounds${File.separator}[0-9]+${File.separator})?(.*)".toPattern()
+    ("(byRounds${Pattern.quote(File.separator)}[0-9]+" +
+        "${Pattern.quote(File.separator)})?(.*)").toPattern()
 
 /**
  * Represents sources that are positioned in the [root] folder.
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XElement.kt
index 9dee083..c2c933e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XElement.kt
@@ -61,10 +61,10 @@
 
     /**
      * Returns the immediate enclosing element. This uses Element.getEnclosingElement() on the
-     * Java side, and KSNode.parent on the KSP side. For non-nested classes we return null as we
-     * don't model packages yet. For fields declared in primary constructors in Kotlin we return
+     * Java side, and KSNode.parent on the KSP side. For non-nested classes we return null.
+     * For fields declared in primary constructors in Kotlin we return
      * the enclosing type, not the constructor. For top-level properties or functions in Kotlin
-     * we return JavacTypeElement on the Java side and KspFileMemberContainer or
+     * we return JavacTypeElement on the Javac/KAPT side and KspFileMemberContainer or
      * KspSyntheticFileMemberContainer on the KSP side.
      */
     val enclosingElement: XElement?
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XPackageElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XPackageElement.kt
new file mode 100644
index 0000000..dcf7701
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XPackageElement.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.room.compiler.processing
+
+interface XPackageElement : XElement, XAnnotated {
+    val qualifiedName: String
+}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeElement.kt
index d0d6a55..98883ca 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeElement.kt
@@ -31,6 +31,11 @@
     val packageName: String
 
     /**
+     * The package that contains this element.
+     */
+    val packageElement: XPackageElement
+
+    /**
      * The type represented by this [XTypeElement].
      */
     override val type: XType
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/compat/XConverters.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/compat/XConverters.kt
index 03b17bf..52180c4 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/compat/XConverters.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/compat/XConverters.kt
@@ -52,10 +52,12 @@
 import androidx.room.compiler.processing.ksp.KspExecutableParameterElement
 import androidx.room.compiler.processing.ksp.KspExecutableType
 import androidx.room.compiler.processing.ksp.KspFieldElement
+import androidx.room.compiler.processing.ksp.KspFileMemberContainer
 import androidx.room.compiler.processing.ksp.KspProcessingEnv
 import androidx.room.compiler.processing.ksp.KspType
 import androidx.room.compiler.processing.ksp.KspTypeElement
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticContinuationParameterElement
+import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticFileMemberContainer
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticReceiverParameterElement
 import com.google.devtools.ksp.processing.Resolver
@@ -273,6 +275,8 @@
         }
     }
 
+    // Todo(kuanyingchou): consider adding `env` to XElement as the when expression may break
+    //  when we add new XElement subclasses.
     @Deprecated("This will be removed in a future version of XProcessing.")
     @JvmStatic
     fun XElement.getProcessingEnv(): XProcessingEnv {
@@ -282,6 +286,10 @@
             is KspSyntheticContinuationParameterElement -> this.env
             is KspSyntheticPropertyMethodElement -> this.env
             is KspSyntheticReceiverParameterElement -> this.env
+            is KspSyntheticPropertyMethodElement.Setter.SyntheticExecutableParameterElement ->
+                this.env
+            is KspFileMemberContainer -> this.env
+            is KspSyntheticFileMemberContainer -> this.env
             else -> error("Unexpected element: $this")
         }
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacPackageElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacPackageElement.kt
new file mode 100644
index 0000000..4519d9a
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacPackageElement.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.room.compiler.processing.javac
+
+import androidx.room.compiler.processing.XElement
+import androidx.room.compiler.processing.XMemberContainer
+import androidx.room.compiler.processing.XPackageElement
+import androidx.room.compiler.processing.javac.kotlin.KmFlags
+import java.lang.UnsupportedOperationException
+import javax.lang.model.element.PackageElement
+
+internal class JavacPackageElement(
+    env: JavacProcessingEnv,
+    private val packageElement: PackageElement
+) : JavacElement(env, packageElement), XPackageElement {
+    override val qualifiedName: String by lazy {
+        packageElement.qualifiedName.toString()
+    }
+    override val kotlinMetadata: KmFlags?
+        get() = null
+    override val name: String by lazy {
+        packageElement.simpleName.toString()
+    }
+    override val fallbackLocationText: String
+        get() = qualifiedName
+    override val enclosingElement: XElement?
+        get() = null
+    override val closestMemberContainer: XMemberContainer
+        get() = throw UnsupportedOperationException("Packages don't have a closestMemberContainer" +
+            " as we don't consider packages a member container for now and it" +
+            " has no enclosingElement.")
+}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
index 89bfdb6..1f614c7 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
@@ -312,10 +312,7 @@
                 wrapExecutableElement(element)
             }
             is PackageElement -> {
-                error(
-                    "Cannot get elements with annotation $annotationName. Package " +
-                        "elements are not supported by XProcessing."
-                )
+                JavacPackageElement(this, element)
             }
             else -> error("Unsupported element $element with annotation $annotationName")
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
index 3e30e02..506fcb2 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
@@ -49,9 +49,14 @@
     override val name: String
         get() = element.simpleName.toString()
 
+    override val packageName: String by lazy {
+        packageElement.qualifiedName
+    }
+
     @Suppress("UnstableApiUsage")
-    override val packageName: String
-        get() = MoreElements.getPackage(element).qualifiedName.toString()
+    override val packageElement: JavacPackageElement by lazy {
+        JavacPackageElement(env, MoreElements.getPackage(element))
+    }
 
     override val kotlinMetadata by lazy {
         KmClassContainer.createFor(env, element)
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
index 27d1c91..a8f4ef6 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
@@ -70,7 +70,7 @@
     }
     // When a top level function/property is compiled, its containing class does not exist in KSP,
     // neither the file. So instead, we synthesize one
-    return KspSyntheticFileMemberContainer(ownerJvmClassName)
+    return KspSyntheticFileMemberContainer(env, ownerJvmClassName)
 }
 
 private fun KSDeclaration.findEnclosingAncestorClassDeclaration(): KSClassDeclaration? {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
index 9cce0e0..5d6a56b 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
@@ -113,12 +113,14 @@
  * Root package comes as <root> instead of "" so we work around it here.
  */
 internal fun KSDeclaration.getNormalizedPackageName(): String {
-    return packageName.asString().let {
-        if (it == "<root>") {
-            ""
-        } else {
-            it
-        }
+    return packageName.asString().getNormalizedPackageName()
+}
+
+internal fun String.getNormalizedPackageName(): String {
+    return if (this == "<root>") {
+        ""
+    } else {
+        this
     }
 }
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeJavaPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeJavaPoetExt.kt
index 442930d..44457bd 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeJavaPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeJavaPoetExt.kt
@@ -179,7 +179,10 @@
 ): JTypeName {
     return if (declaration is KSTypeAlias) {
         replaceTypeAliases(resolver).asJTypeName(resolver, typeResolutionContext)
-    } else if (this.arguments.isNotEmpty() && !resolver.isJavaRawType(this)) {
+    } else if (this.arguments.isNotEmpty() && !resolver.isJavaRawType(this) &&
+            // Excluding generic value classes otherwise we may generate something
+            // like `Object<String>`.
+            !declaration.isValueClass()) {
         val args: Array<JTypeName> = this.arguments
             .map { typeArg -> typeArg.asJTypeName(resolver, typeResolutionContext) }
             .map { it.tryBox() }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFileMemberContainer.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFileMemberContainer.kt
index 58171da..7a61c7b 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFileMemberContainer.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFileMemberContainer.kt
@@ -32,7 +32,7 @@
  * [XMemberContainer] implementation for KSFiles.
  */
 internal class KspFileMemberContainer(
-    private val env: KspProcessingEnv,
+    internal val env: KspProcessingEnv,
     private val ksFile: KSFile
 ) : KspMemberContainer,
     XAnnotated by KspAnnotated.create(
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPackageElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPackageElement.kt
new file mode 100644
index 0000000..4b34bd7
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPackageElement.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.room.compiler.processing.ksp
+
+import androidx.room.compiler.processing.XElement
+import androidx.room.compiler.processing.XMemberContainer
+import androidx.room.compiler.processing.XPackageElement
+import com.google.devtools.ksp.KspExperimental
+import com.google.devtools.ksp.symbol.KSAnnotation
+import java.lang.UnsupportedOperationException
+
+// This is not a KspElement as we don't have a backing model in KSP.
+internal class KspPackageElement(
+    env: KspProcessingEnv,
+    private val packageName: String
+) : KspAnnotated(env), XPackageElement {
+
+    override val qualifiedName: String by lazy {
+        packageName.getNormalizedPackageName()
+    }
+
+    override val name: String by lazy {
+        qualifiedName.substringAfterLast(".")
+    }
+
+    override fun kindName(): String = "package"
+
+    override val fallbackLocationText: String
+        get() = qualifiedName
+
+    override val docComment: String? = null
+
+    override fun validate(): Boolean = true
+
+    override val enclosingElement: XElement? = null
+    override val closestMemberContainer: XMemberContainer
+        get() = throw UnsupportedOperationException(
+            "Packages don't have a closestMemberContainer as we don't consider packages " +
+                "a member container for now and it has no enclosingElement.")
+
+    @OptIn(KspExperimental::class)
+    override fun annotations(): Sequence<KSAnnotation> {
+        return env.resolver.getPackageAnnotations(qualifiedName)
+    }
+}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
index 094650c..8386afc 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
@@ -19,6 +19,7 @@
 import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XRoundEnv
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
+import com.google.devtools.ksp.KspExperimental
 import com.google.devtools.ksp.symbol.ClassKind
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
@@ -40,6 +41,7 @@
         )
     }
 
+    @OptIn(KspExperimental::class)
     override fun getElementsAnnotatedWith(annotationQualifiedName: String): Set<XElement> {
         if (annotationQualifiedName == "*") {
             return emptySet()
@@ -51,6 +53,7 @@
                         is KSPropertyDeclaration -> {
                            add(KspFieldElement.create(env, symbol))
                         }
+
                         is KSClassDeclaration -> {
                             when (symbol.classKind) {
                                 ClassKind.ENUM_ENTRY ->
@@ -58,9 +61,11 @@
                                 else -> add(KspTypeElement.create(env, symbol))
                             }
                         }
+
                         is KSFunctionDeclaration -> {
                             add(KspExecutableElement.create(env, symbol))
                         }
+
                         is KSPropertyAccessor -> {
                             if (symbol.receiver.isStatic() &&
                                 symbol.receiver.parentDeclaration is KSClassDeclaration &&
@@ -89,20 +94,27 @@
                                 )
                             }
                         }
+
                         is KSValueParameter -> {
                             add(KspExecutableParameterElement.create(env, symbol))
                         }
+
                         else ->
                             error("Unsupported $symbol with annotation $annotationQualifiedName")
                     }
                 }
-            }
-            .filter {
-                // Due to the bug in https://github.com/google/ksp/issues/1198, KSP may incorrectly
-                // copy annotations from a constructor KSValueParameter to its KSPropertyDeclaration
-                // which we remove manually, so check here to make sure this is in sync with the
-                // actual annotations on the element.
-                it.getAllAnnotations().any { it.qualifiedName == annotationQualifiedName }
-            }.toSet()
+
+            env.resolver.getPackagesWithAnnotation(annotationQualifiedName)
+                .forEach { packageName ->
+                    add(KspPackageElement(env, packageName))
+                }
+        }
+        .filter {
+            // Due to the bug in https://github.com/google/ksp/issues/1198, KSP may incorrectly
+            // copy annotations from a constructor KSValueParameter to its KSPropertyDeclaration
+            // which we remove manually, so check here to make sure this is in sync with the
+            // actual annotations on the element.
+            it.getAllAnnotations().any { it.qualifiedName == annotationQualifiedName }
+        }.toSet()
     }
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
index 7333f7d..8c0aeb1 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
@@ -124,7 +124,11 @@
             // This matches javac's Types#directSupertypes().
             listOf(env.requireType(TypeName.OBJECT)) + superInterfaces
         } else {
-            check(superClasses.size == 1)
+            check(superClasses.size == 1) {
+                "Class ${this.typeName} should have only one super class. Found" +
+                    " ${superClasses.size}" +
+                    " (${superClasses.joinToString { it.typeName.toString() }})."
+            }
             superClasses + superInterfaces
         }
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index 44bb15d..51f1463 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -26,6 +26,7 @@
 import androidx.room.compiler.processing.XMemberContainer
 import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XPackageElement
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.XTypeParameterElement
@@ -68,7 +69,11 @@
     }
 
     override val packageName: String by lazy {
-        declaration.getNormalizedPackageName()
+        packageElement.qualifiedName
+    }
+
+    override val packageElement: XPackageElement by lazy {
+        KspPackageElement(env, declaration.packageName.asString())
     }
 
     override val enclosingTypeElement: XTypeElement? by lazy {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainer.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainer.kt
index 2f47522..cb2432f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainer.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainer.kt
@@ -23,6 +23,7 @@
 import androidx.room.compiler.processing.XEquality
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.ksp.KspMemberContainer
+import androidx.room.compiler.processing.ksp.KspProcessingEnv
 import androidx.room.compiler.processing.ksp.KspType
 import com.google.devtools.ksp.symbol.KSDeclaration
 import com.squareup.javapoet.ClassName
@@ -37,6 +38,7 @@
  * https://docs.oracle.com/javase/specs/jls/se7/html/jls-13.html#jls-13.1
  */
 internal class KspSyntheticFileMemberContainer(
+    internal val env: KspProcessingEnv,
     private val binaryName: String
 ) : KspMemberContainer, XEquality {
     override val equalityItems: Array<out Any?> by lazy {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
index 1247381..3f57122 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
@@ -255,8 +255,8 @@
             return "synthetic property getter"
         }
 
-        private class SyntheticExecutableParameterElement(
-            private val env: KspProcessingEnv,
+        internal class SyntheticExecutableParameterElement(
+            internal val env: KspProcessingEnv,
             override val enclosingElement: Setter
         ) : XExecutableParameterElement,
             XAnnotated by KspAnnotated.create(
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XRoundEnvTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XRoundEnvTest.kt
index 78a6f4d..6a33c86 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XRoundEnvTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XRoundEnvTest.kt
@@ -123,6 +123,60 @@
     }
 
     @Test
+    fun getAnnotatedPackageElements() {
+        val source = Source.java(
+            // Packages can be annotated in `package-info.java` files.
+            "foo.bar.foobar.package-info",
+            """
+            @OtherAnnotation(value = "xx")
+            package foo.bar.foobar;
+            import androidx.room.compiler.processing.testcode.OtherAnnotation;
+            """.trimIndent()
+        )
+
+        runProcessorTest(listOf(source)) { testInvocation ->
+            (testInvocation.roundEnv.getElementsAnnotatedWith(
+                OtherAnnotation::class
+            ).single() as XPackageElement).apply {
+                assertThat(name).isEqualTo("foobar")
+                assertThat(qualifiedName).isEqualTo("foo.bar.foobar")
+                assertThat(kindName()).isEqualTo("package")
+                assertThat(validate()).isTrue()
+            }.getAllAnnotations().single().apply {
+                assertThat(qualifiedName)
+                    .isEqualTo("androidx.room.compiler.processing.testcode.OtherAnnotation")
+            }.annotationValues.single().apply {
+                assertThat(name).isEqualTo("value")
+                assertThat(value).isEqualTo("xx")
+            }
+        }
+    }
+
+    @Test
+    fun defaultPackage() {
+        val javaSource = Source.java(
+            "FooBar",
+            """
+            class FooBar {}
+            """.trimIndent()
+        )
+        val kotlinSource = Source.kotlin(
+            "FooBarKt.kt",
+            """
+            class FooBarKt
+            """.trimIndent()
+        )
+        runProcessorTest(listOf(javaSource, kotlinSource)) { testInvocation ->
+            testInvocation.processingEnv.requireTypeElement("FooBar").apply {
+                assertThat(packageName).isEqualTo("")
+            }
+            testInvocation.processingEnv.requireTypeElement("FooBarKt").apply {
+                assertThat(packageName).isEqualTo("")
+            }
+        }
+    }
+
+    @Test
     fun misalignedAnnotationTargetFailsCompilation() {
         val source = Source.kotlin(
             "Baz.kt",
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 1a68ad3..af27478 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
@@ -39,6 +39,8 @@
 import androidx.room.compiler.processing.util.runKspTest
 import androidx.room.compiler.processing.util.runProcessorTest
 import com.google.devtools.ksp.getClassDeclarationByName
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.ParameterizedTypeName
 import com.squareup.javapoet.TypeVariableName
@@ -56,9 +58,8 @@
 import com.squareup.kotlinpoet.javapoet.KTypeVariableName
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
-@RunWith(JUnit4::class)
+@RunWith(TestParameterInjector::class)
 class XTypeTest {
     @Test
     fun typeArguments() {
@@ -1842,22 +1843,57 @@
     }
 
     @Test
-    fun valueTypes() {
+    fun valueTypes(@TestParameter isPrecompiled: Boolean,) {
         val kotlinSrc = Source.kotlin(
             "KotlinClass.kt",
             """
             @JvmInline value class PackageName(val value: String)
+            @JvmInline value class MyResult<T>(val value: T)
 
             class KotlinClass {
+                // @JvmName disables name mangling for functions that use inline classes and
+                // make them visible to Java:
+                // https://kotlinlang.org/docs/inline-classes.html#calling-from-java-code
+                @JvmName("getResult")
+                fun getResult(): MyResult<String> = TODO()
+                @JvmName("setResult")
+                fun setResult(result: MyResult<String>) { }
                 fun getPackageNames(): Set<PackageName> = emptySet()
                 fun setPackageNames(pkgNames: Set<PackageName>) { }
             }
             """.trimIndent()
         )
         runProcessorTest(
-            sources = listOf(kotlinSrc)
+            sources = if (isPrecompiled) { emptyList() } else { listOf(kotlinSrc) },
+            classpath = if (isPrecompiled) { compileFiles(listOf(kotlinSrc)) } else { emptyList() }
         ) { invocation ->
             val kotlinElm = invocation.processingEnv.requireTypeElement("KotlinClass")
+
+            kotlinElm.getDeclaredMethodByJvmName("getResult").apply {
+                assertThat(returnType.asTypeName().java.toString())
+                    .isEqualTo("java.lang.Object")
+                if (invocation.isKsp) {
+                    assertThat(returnType.asTypeName().kotlin.toString())
+                        .isEqualTo("MyResult<kotlin.String>")
+                } else {
+                    // Can't generate Kotlin code with KAPT
+                    assertThat(returnType.asTypeName().kotlin.toString())
+                        .isEqualTo("androidx.room.compiler.codegen.Unavailable")
+                }
+            }
+            kotlinElm.getDeclaredMethodByJvmName("setResult").apply {
+                assertThat(parameters.single().type.asTypeName().java.toString())
+                    .isEqualTo("java.lang.Object")
+                if (invocation.isKsp) {
+                    assertThat(parameters.single().type.asTypeName().kotlin.toString())
+                        .isEqualTo("MyResult<kotlin.String>")
+                } else {
+                    // Can't generate Kotlin code with KAPT
+                    assertThat(parameters.single().type.asTypeName().kotlin.toString())
+                        .isEqualTo("androidx.room.compiler.codegen.Unavailable")
+                }
+            }
+
             kotlinElm.getMethodByJvmName("getPackageNames").apply {
                 assertThat(returnType.typeName.toString())
                     .isEqualTo("java.util.Set<PackageName>")
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt
index 1808bb1..0bb58a2 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt
@@ -17,6 +17,7 @@
 package androidx.room.compiler.processing.ksp
 
 import androidx.kruth.assertThat
+import androidx.kruth.assertThrows
 import androidx.room.compiler.codegen.XClassName
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.asClassName
@@ -620,4 +621,31 @@
             assertParamType(asMember.parameterTypes.first())
         }
     }
+
+    @Test
+    fun oneSuperClass() {
+        val src = Source.java(
+            "foo.bar.Baz",
+            """
+            package foo.bar;
+            class A {}
+            interface B {}
+            class Baz extends A implements B, C {}
+            """.trimIndent()
+        )
+        runKspTest(
+            listOf(src)
+        ) { invocation ->
+            val typeElement = invocation.processingEnv.requireTypeElement("foo.bar.Baz")
+            val exception = assertThrows(IllegalStateException::class) {
+                typeElement.type.superTypes
+            }
+            exception.hasMessageThat().isEqualTo(
+                "Class foo.bar.Baz should have only one super class." +
+                    " Found 2 (foo.bar.A, error.NonExistentClass).")
+            invocation.assertCompilationResult {
+                compilationDidFail()
+            }
+        }
+    }
 }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainerTest.kt
index 134aed5..aeb12a4 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainerTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainerTest.kt
@@ -19,6 +19,7 @@
 import androidx.kruth.assertThat
 import androidx.kruth.assertWithMessage
 import androidx.room.compiler.processing.ksp.KspFieldElement
+import androidx.room.compiler.processing.ksp.KspProcessingEnv
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.compileFiles
 import androidx.room.compiler.processing.util.getField
@@ -56,7 +57,8 @@
             val className = elements.map {
                 val owner = invocation.kspResolver.getOwnerJvmClassName(it as KSPropertyDeclaration)
                 assertWithMessage(it.toString()).that(owner).isNotNull()
-                KspSyntheticFileMemberContainer(owner!!).asClassName()
+                KspSyntheticFileMemberContainer(
+                    invocation.processingEnv as KspProcessingEnv, owner!!).asClassName()
             }.first()
             assertThat(className.packageName).isEmpty()
             assertThat(className.simpleNames).containsExactly("AppKt")
@@ -135,7 +137,8 @@
                     val field = target.getField("member") as KspFieldElement
                     val owner = invocation.kspResolver.getOwnerJvmClassName(field.declaration)
                     assertWithMessage(qName).that(owner).isNotNull()
-                    val synthetic = KspSyntheticFileMemberContainer(owner!!)
+                    val synthetic = KspSyntheticFileMemberContainer(
+                        invocation.processingEnv as KspProcessingEnv, owner!!)
                     assertWithMessage(qName).that(target.asClassName())
                         .isEqualTo(synthetic.asClassName())
                 }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
index ca22005..8a9f49f 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
@@ -22,12 +22,17 @@
 import com.google.testing.junit.testparameterinjector.TestParameter
 import com.google.testing.junit.testparameterinjector.TestParameterInjector
 import org.jetbrains.kotlin.config.JvmDefaultMode
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TestName
 import org.junit.runner.RunWith
 
 @RunWith(TestParameterInjector::class)
 class DaoKotlinCodeGenTest : BaseDaoKotlinCodeGenTest() {
 
+    @get:Rule
+    val testName = TestName()
+
     val databaseSrc = Source.kotlin(
         "MyDatabase.kt",
         """
@@ -42,7 +47,6 @@
 
     @Test
     fun pojoRowAdapter_variableProperty() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -70,13 +74,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_variableProperty_java() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -119,14 +122,13 @@
         )
         runTest(
             sources = listOf(src, javaEntity, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     // b/274760383
     @Test
     fun pojoRowAdapter_otherModule() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val lib = compileFiles(
             sources = listOf(
                 Source.kotlin(
@@ -171,14 +173,13 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName),
+            expectedFilePath = getTestGoldenPath(testName.methodName),
             compiledFiles = lib
         )
     }
 
     @Test
     fun pojoRowAdapter_internalVisibility() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -207,13 +208,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_primitives() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -243,13 +243,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_primitives_nullable() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -279,13 +278,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_boolean() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -311,13 +309,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_string() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -342,13 +339,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_byteArray() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -374,13 +370,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_enum() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -411,13 +406,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_uuid() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -449,13 +443,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_embedded() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -488,13 +481,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_customTypeConverter() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -529,13 +521,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_customTypeConverter_provided() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -571,13 +562,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_customTypeConverter_composite() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -618,13 +608,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_customTypeConverter_nullAware() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -666,13 +655,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_customTypeConverter_internalVisibility() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -707,13 +695,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun coroutineResultBinder() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -735,13 +722,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc, COMMON.COROUTINES_ROOM),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun multiTypedPagingSourceResultBinder() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -784,13 +770,12 @@
                 COMMON.LIMIT_OFFSET_LISTENABLE_FUTURE_PAGING_SOURCE,
                 COMMON.LISTENABLE_FUTURE_PAGING_SOURCE
             ),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun basicParameterAdapter_string() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -814,13 +799,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun collectionParameterAdapter_string() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -850,13 +834,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun arrayParameterAdapter() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -895,13 +878,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun preparedQueryAdapter() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -938,13 +920,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun rawQuery() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -966,7 +947,7 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
@@ -975,6 +956,7 @@
         @TestParameter("DISABLE", "ALL_COMPATIBILITY", "ALL_INCOMPATIBLE")
         jvmDefaultMode: JvmDefaultMode
     ) {
+        // For parametrized tests, use method name from reflection
         val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
@@ -1008,7 +990,6 @@
 
     @Test
     fun delegatingFunctions_boxedPrimitiveBridge() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1039,7 +1020,7 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName),
+            expectedFilePath = getTestGoldenPath(testName.methodName),
         )
     }
 
@@ -1048,6 +1029,7 @@
         @TestParameter("DISABLE", "ALL_COMPATIBILITY", "ALL_INCOMPATIBLE")
         jvmDefaultMode: JvmDefaultMode
     ) {
+        // For parametrized tests, use method name from reflection
         val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
@@ -1116,7 +1098,6 @@
 
     @Test
     fun transactionMethodAdapter_abstractClass() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1165,13 +1146,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc, COMMON.COROUTINES_ROOM, COMMON.ROOM_DATABASE_KTX),
-            expectedFilePath = getTestGoldenPath(testName),
+            expectedFilePath = getTestGoldenPath(testName.methodName),
         )
     }
 
     @Test
     fun deleteOrUpdateMethodAdapter() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1202,13 +1182,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun insertOrUpsertMethodAdapter() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1251,13 +1230,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun queryResultAdapter_list() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val dbSource = Source.kotlin(
             "MyDatabase.kt",
             """
@@ -1297,13 +1275,12 @@
         )
         runTest(
             sources = listOf(src, dbSource),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun queryResultAdapter_array() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val dbSource = Source.kotlin(
             "MyDatabase.kt",
             """
@@ -1346,13 +1323,12 @@
         )
         runTest(
             sources = listOf(src, dbSource),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun abstractClassWithParam() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1373,13 +1349,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun queryResultAdapter_optional() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1401,13 +1376,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun queryResultAdapter_guavaOptional() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1429,13 +1403,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun queryResultAdapter_immutable_list() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1458,13 +1431,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun queryResultAdapter_map() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1515,13 +1487,12 @@
         )
         runTest(
             sources = listOf(src),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun queryResultAdapter_nestedMap() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1588,13 +1559,12 @@
         )
         runTest(
             sources = listOf(src),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun queryResultAdapter_guavaImmutableMultimap() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1630,13 +1600,12 @@
         )
         runTest(
             sources = listOf(src),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun queryResultAdapter_guavaImmutableMap() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1669,13 +1638,12 @@
         )
         runTest(
             sources = listOf(src),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun queryResultAdapter_map_ambiguousIndexAdapter() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1718,13 +1686,12 @@
         )
         runTest(
             sources = listOf(src),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun queryResultAdapter_nestedMap_ambiguousIndexAdapter() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1772,13 +1739,12 @@
         )
         runTest(
             sources = listOf(src),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun entityRowAdapter() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1813,13 +1779,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun paging_dataSource() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1847,13 +1812,12 @@
                 COMMON.DATA_SOURCE_FACTORY,
                 COMMON.POSITIONAL_DATA_SOURCE
             ),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun callableQuery_rx2() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1907,13 +1871,12 @@
                 COMMON.PUBLISHER,
                 COMMON.RX2_EMPTY_RESULT_SET_EXCEPTION
             ),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun callableQuery_rx3() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -1967,13 +1930,12 @@
                 COMMON.PUBLISHER,
                 COMMON.RX3_EMPTY_RESULT_SET_EXCEPTION
             ),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun preparedCallableQuery_rx2() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -2013,13 +1975,12 @@
                 COMMON.PUBLISHER,
                 COMMON.RX2_EMPTY_RESULT_SET_EXCEPTION
             ),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun preparedCallableQuery_rx3() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -2059,13 +2020,12 @@
                 COMMON.PUBLISHER,
                 COMMON.RX3_EMPTY_RESULT_SET_EXCEPTION
             ),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun coroutines() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -2099,13 +2059,12 @@
                 COMMON.FLOW,
                 COMMON.COROUTINES_ROOM
             ),
-            expectedFilePath = getTestGoldenPath(testName),
+            expectedFilePath = getTestGoldenPath(testName.methodName),
         )
     }
 
     @Test
     fun shortcutMethods_rx2() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -2155,13 +2114,12 @@
                 COMMON.RX2_COMPLETABLE,
                 COMMON.RX2_EMPTY_RESULT_SET_EXCEPTION,
             ),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun shortcutMethods_rx3() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -2211,13 +2169,12 @@
                 COMMON.RX3_COMPLETABLE,
                 COMMON.RX3_EMPTY_RESULT_SET_EXCEPTION
             ),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun guavaCallable() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -2260,13 +2217,12 @@
                 COMMON.LISTENABLE_FUTURE,
                 COMMON.GUAVA_ROOM,
             ),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun liveDataCallable() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -2296,14 +2252,13 @@
                 databaseSrc,
                 COMMON.COROUTINES_ROOM
             ),
-            expectedFilePath = getTestGoldenPath(testName),
+            expectedFilePath = getTestGoldenPath(testName.methodName),
             compiledFiles = compileFiles(listOf(COMMON.LIVE_DATA))
         )
     }
 
     @Test
     fun shortcutMethods_suspend() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -2338,13 +2293,12 @@
                 databaseSrc,
                 COMMON.COROUTINES_ROOM
             ),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun pojoRowAdapter_valueClassConverter() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -2386,7 +2340,7 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt
index be78225..0a769ce 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt
@@ -19,10 +19,15 @@
 import COMMON
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.compileFiles
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TestName
 
 class DaoRelationshipKotlinCodeGenTest : BaseDaoKotlinCodeGenTest() {
 
+    @get:Rule
+    val testName = TestName()
+
     val databaseSrc = Source.kotlin(
         "MyDatabase.kt",
         """
@@ -46,7 +51,6 @@
 
     @Test
     fun relations() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -128,13 +132,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun relations_nullable() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -216,13 +219,12 @@
         )
         runTest(
             sources = listOf(src, databaseSrc),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun relations_longSparseArray() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -305,13 +307,12 @@
         runTest(
             sources = listOf(src, databaseSrc),
             compiledFiles = compileFiles(listOf(COMMON.LONG_SPARSE_ARRAY)),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun relations_arrayMap() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -394,13 +395,12 @@
         runTest(
             sources = listOf(src, databaseSrc),
             compiledFiles = compileFiles(listOf(COMMON.ARRAY_MAP)),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun relations_byteBufferKey() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDao.kt",
             """
@@ -454,7 +454,7 @@
         )
         runTest(
             sources = listOf(src),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseKotlinCodeGenTest.kt
index a9a2180..e72ed1d 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseKotlinCodeGenTest.kt
@@ -22,13 +22,17 @@
 import androidx.room.compiler.processing.util.runKspTest
 import androidx.room.processor.Context
 import loadTestSource
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TestName
 
 class DatabaseKotlinCodeGenTest {
 
+    @get:Rule
+    val testName = TestName()
+
     @Test
     fun database_simple() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDatabase.kt",
             """
@@ -54,13 +58,12 @@
         )
         runTest(
             sources = listOf(src),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun database_withFtsAndView() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDatabase.kt",
             """
@@ -121,13 +124,12 @@
         )
         runTest(
             sources = listOf(src),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
     @Test
     fun database_internalVisibility() {
-        val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
             "MyDatabase.kt",
             """
@@ -153,7 +155,7 @@
         )
         runTest(
             sources = listOf(src),
-            expectedFilePath = getTestGoldenPath(testName)
+            expectedFilePath = getTestGoldenPath(testName.methodName)
         )
     }
 
diff --git a/room/room-runtime/lint-baseline.xml b/room/room-runtime/lint-baseline.xml
index c295a53..5b31a9b 100644
--- a/room/room-runtime/lint-baseline.xml
+++ b/room/room-runtime/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="NewApi"
@@ -11,6 +11,96 @@
     </issue>
 
     <issue
+        id="WrongConstant"
+        message="Must be one of: RoomSQLiteQuery.NULL, RoomSQLiteQuery.LONG, RoomSQLiteQuery.DOUBLE, RoomSQLiteQuery.STRING, RoomSQLiteQuery.BLOB"
+        errorLine1="        bindingTypes = IntArray(limit)"
+        errorLine2="        ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/room/RoomSQLiteQuery.kt"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: RoomSQLiteQuery.NULL, RoomSQLiteQuery.LONG, RoomSQLiteQuery.DOUBLE, RoomSQLiteQuery.STRING, RoomSQLiteQuery.BLOB"
+        errorLine1="            when (bindingTypes[index]) {"
+        errorLine2="                  ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/room/RoomSQLiteQuery.kt"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: RoomSQLiteQuery.NULL, RoomSQLiteQuery.LONG, RoomSQLiteQuery.DOUBLE, RoomSQLiteQuery.STRING, RoomSQLiteQuery.BLOB"
+        errorLine1="        bindingTypes[index] = NULL"
+        errorLine2="        ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/room/RoomSQLiteQuery.kt"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: RoomSQLiteQuery.NULL, RoomSQLiteQuery.LONG, RoomSQLiteQuery.DOUBLE, RoomSQLiteQuery.STRING, RoomSQLiteQuery.BLOB"
+        errorLine1="        bindingTypes[index] = LONG"
+        errorLine2="        ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/room/RoomSQLiteQuery.kt"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: RoomSQLiteQuery.NULL, RoomSQLiteQuery.LONG, RoomSQLiteQuery.DOUBLE, RoomSQLiteQuery.STRING, RoomSQLiteQuery.BLOB"
+        errorLine1="        bindingTypes[index] = DOUBLE"
+        errorLine2="        ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/room/RoomSQLiteQuery.kt"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: RoomSQLiteQuery.NULL, RoomSQLiteQuery.LONG, RoomSQLiteQuery.DOUBLE, RoomSQLiteQuery.STRING, RoomSQLiteQuery.BLOB"
+        errorLine1="        bindingTypes[index] = STRING"
+        errorLine2="        ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/room/RoomSQLiteQuery.kt"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: RoomSQLiteQuery.NULL, RoomSQLiteQuery.LONG, RoomSQLiteQuery.DOUBLE, RoomSQLiteQuery.STRING, RoomSQLiteQuery.BLOB"
+        errorLine1="        bindingTypes[index] = BLOB"
+        errorLine2="        ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/room/RoomSQLiteQuery.kt"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: RoomSQLiteQuery.NULL, RoomSQLiteQuery.LONG, RoomSQLiteQuery.DOUBLE, RoomSQLiteQuery.STRING, RoomSQLiteQuery.BLOB"
+        errorLine1="        System.arraycopy(other.bindingTypes, 0, bindingTypes, 0, argCount)"
+        errorLine2="                               ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/room/RoomSQLiteQuery.kt"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: RoomSQLiteQuery.NULL, RoomSQLiteQuery.LONG, RoomSQLiteQuery.DOUBLE, RoomSQLiteQuery.STRING, RoomSQLiteQuery.BLOB"
+        errorLine1="        System.arraycopy(other.bindingTypes, 0, bindingTypes, 0, argCount)"
+        errorLine2="                                                ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/room/RoomSQLiteQuery.kt"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: RoomSQLiteQuery.NULL, RoomSQLiteQuery.LONG, RoomSQLiteQuery.DOUBLE, RoomSQLiteQuery.STRING, RoomSQLiteQuery.BLOB"
+        errorLine1="        Arrays.fill(bindingTypes, NULL)"
+        errorLine2="                    ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/room/RoomSQLiteQuery.kt"/>
+    </issue>
+
+    <issue
         id="BanThreadSleep"
         message="Uses Thread.sleep()"
         errorLine1="        Thread.sleep(5)"
diff --git a/samples/MediaRoutingDemo/src/main/AndroidManifest.xml b/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
index 8afa6de..c671c5a 100644
--- a/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
+++ b/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
@@ -87,6 +87,9 @@
         <receiver android:name="androidx.mediarouter.media.MediaTransferReceiver"
             android:exported="true" />
 
+        <receiver android:name="androidx.mediarouter.media.SystemRoutingUsingMediaRouter2Receiver"
+            android:exported="true" />
+
         <service
             android:name=".services.SampleMediaRouteProviderService"
             android:exported="true"
diff --git a/samples/SupportLeanbackDemos/lint-baseline.xml b/samples/SupportLeanbackDemos/lint-baseline.xml
index 213e53b..0b9624b 100644
--- a/samples/SupportLeanbackDemos/lint-baseline.xml
+++ b/samples/SupportLeanbackDemos/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="MissingSuperCall"
@@ -1354,15 +1354,6 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setItem(PhotoItem photoItem) {"
-        errorLine2="                        ~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/DetailsFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    protected void onCreate(Bundle savedInstanceState) {"
         errorLine2="                            ~~~~~~">
         <location
@@ -1444,8 +1435,8 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setItem(PhotoItem photoItem) {"
-        errorLine2="                        ~~~~~~~~~">
+        errorLine1="    public void onSaveInstanceState(Bundle outState) {"
+        errorLine2="                                    ~~~~~~">
         <location
             file="src/main/java/com/example/android/leanback/DetailsSupportFragment.java"/>
     </issue>
@@ -1474,393 +1465,6 @@
         errorLine1="    protected void onCreate(Bundle savedInstanceState) {"
         errorLine2="                            ~~~~~~">
         <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void onConfigurationChanged(Configuration newConfig) {"
-        errorLine2="                                       ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected void onSaveInstanceState(Bundle outState) {"
-        errorLine2="                                       ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected void onRestoreInstanceState(Bundle savedInstanceState) {"
-        errorLine2="                                          ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="               ~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                    ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreate(Bundle savedInstance) {"
-        errorLine2="                             ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="               ~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                    ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                          ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                      ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public long onGuidedActionEditedAndProceed(GuidedAction action) {"
-        errorLine2="                                                   ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public GuidedActionsStylist onCreateActionsStylist() {"
-        errorLine2="               ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="               ~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                    ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                          ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                      ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public long onGuidedActionEditedAndProceed(GuidedAction action) {"
-        errorLine2="                                                   ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public boolean onSubGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                                ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="               ~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="                                                          ~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="               ~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public GuidanceStylist onCreateGuidanceStylist() {"
-        errorLine2="               ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                    ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                          ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                      ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="               ~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                    ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected void onCreate(Bundle savedInstanceState) {"
-        errorLine2="                            ~~~~~~">
-        <location
             file="src/main/java/com/example/android/leanback/GuidedStepHalfScreenActivity.java"/>
     </issue>
 
@@ -1885,33 +1489,6 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                    ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepHalfScreenActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepHalfScreenActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepHalfScreenActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
         errorLine2="               ~~~~~~~~">
         <location
@@ -1930,357 +1507,6 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                    ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepHalfScreenActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepHalfScreenActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                          ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepHalfScreenActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                      ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepHalfScreenActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepHalfScreenActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected void onCreate(Bundle savedInstanceState) {"
-        errorLine2="                            ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void onConfigurationChanged(Configuration newConfig) {"
-        errorLine2="                                       ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected void onSaveInstanceState(Bundle outState) {"
-        errorLine2="                                       ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected void onRestoreInstanceState(Bundle savedInstanceState) {"
-        errorLine2="                                          ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(@NonNull List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreate(Bundle savedInstance) {"
-        errorLine2="                             ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(@NonNull List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                          ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                      ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public long onGuidedActionEditedAndProceed(GuidedAction action) {"
-        errorLine2="                                                   ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public GuidedActionsStylist onCreateActionsStylist() {"
-        errorLine2="               ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(@NonNull List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                          ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                      ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public long onGuidedActionEditedAndProceed(GuidedAction action) {"
-        errorLine2="                                                   ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public boolean onSubGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                                ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="               ~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="                                 ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public View onCreateView(LayoutInflater inflater, ViewGroup container,"
-        errorLine2="                                                          ~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="                Bundle savedInstanceState) {"
-        errorLine2="                ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public GuidanceStylist onCreateGuidanceStylist() {"
-        errorLine2="               ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(@NonNull List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                          ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                      ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(@NonNull List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    protected void onCreate(Bundle savedInstanceState) {"
         errorLine2="                            ~~~~~~">
         <location
@@ -2291,16 +1517,7 @@
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
-        errorLine2="                                         ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportHalfScreenActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
+        errorLine2="               ~~~~~~~~">
         <location
             file="src/main/java/com/example/android/leanback/GuidedStepSupportHalfScreenActivity.java"/>
     </issue>
@@ -2317,8 +1534,8 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateActions(@NonNull List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                         ~~~~~~">
+        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
+        errorLine2="               ~~~~~~~~">
         <location
             file="src/main/java/com/example/android/leanback/GuidedStepSupportHalfScreenActivity.java"/>
     </issue>
@@ -2326,26 +1543,8 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                          ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportHalfScreenActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onCreateButtonActions(List&lt;GuidedAction> actions, Bundle savedInstanceState) {"
-        errorLine2="                                                                      ~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/GuidedStepSupportHalfScreenActivity.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void onGuidedActionClicked(GuidedAction action) {"
-        errorLine2="                                          ~~~~~~~~~~~~">
+        errorLine1="        public Guidance onCreateGuidance(Bundle savedInstanceState) {"
+        errorLine2="                                         ~~~~~~">
         <location
             file="src/main/java/com/example/android/leanback/GuidedStepSupportHalfScreenActivity.java"/>
     </issue>
@@ -2731,60 +1930,6 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected View onCreateContentView(LayoutInflater layoutInflater, ViewGroup viewGroup) {"
-        errorLine2="              ~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/OnboardingDemoFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected View onCreateContentView(LayoutInflater layoutInflater, ViewGroup viewGroup) {"
-        errorLine2="                                       ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/OnboardingDemoFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected View onCreateContentView(LayoutInflater layoutInflater, ViewGroup viewGroup) {"
-        errorLine2="                                                                      ~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/OnboardingDemoFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected View onCreateForegroundView(LayoutInflater layoutInflater, ViewGroup viewGroup) {"
-        errorLine2="              ~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/OnboardingDemoFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected View onCreateForegroundView(LayoutInflater layoutInflater, ViewGroup viewGroup) {"
-        errorLine2="                                          ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/OnboardingDemoFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected View onCreateForegroundView(LayoutInflater layoutInflater, ViewGroup viewGroup) {"
-        errorLine2="                                                                         ~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/android/leanback/OnboardingDemoFragment.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    protected Animator onCreateEnterAnimation() {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -2821,7 +1966,7 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected View onCreateBackgroundView("
+        errorLine1="    protected View onCreateBackgroundView(LayoutInflater layoutInflater, ViewGroup viewGroup) {"
         errorLine2="              ~~~~">
         <location
             file="src/main/java/com/example/android/leanback/OnboardingDemoSupportFragment.java"/>
@@ -2830,8 +1975,8 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected View onCreateContentView("
-        errorLine2="              ~~~~">
+        errorLine1="    protected View onCreateBackgroundView(LayoutInflater layoutInflater, ViewGroup viewGroup) {"
+        errorLine2="                                          ~~~~~~~~~~~~~~">
         <location
             file="src/main/java/com/example/android/leanback/OnboardingDemoSupportFragment.java"/>
     </issue>
@@ -2839,8 +1984,8 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected View onCreateForegroundView("
-        errorLine2="              ~~~~">
+        errorLine1="    protected View onCreateBackgroundView(LayoutInflater layoutInflater, ViewGroup viewGroup) {"
+        errorLine2="                                                                         ~~~~~~~~~">
         <location
             file="src/main/java/com/example/android/leanback/OnboardingDemoSupportFragment.java"/>
     </issue>
diff --git a/settings.gradle b/settings.gradle
index 75096da..571ab12 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -707,6 +707,7 @@
 includeProject(":core:uwb:uwb-rxjava3", [BuildType.MAIN])
 includeProject(":credentials:credentials", [BuildType.MAIN])
 includeProject(":credentials:credentials-samples", "credentials/credentials/samples", [BuildType.MAIN])
+includeProject(":credentials:credentials-fido:credentials-fido", [BuildType.MAIN])
 includeProject(":credentials:credentials-play-services-auth", [BuildType.MAIN])
 includeProject(":credentials:credentials-provider", [BuildType.MAIN])
 includeProject(":cursoradapter:cursoradapter", [BuildType.MAIN])
@@ -896,7 +897,7 @@
 includeProject(":paging:paging-rxjava2-ktx", [BuildType.MAIN])
 includeProject(":paging:paging-rxjava3", [BuildType.MAIN])
 includeProject(":paging:paging-samples", "paging/samples", [BuildType.MAIN, BuildType.COMPOSE])
-includeProject(":paging:paging-testing", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":paging:paging-testing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.INFRAROGUE, BuildType.KMP])
 includeProject(":palette:palette", [BuildType.MAIN])
 includeProject(":palette:palette-ktx", [BuildType.MAIN])
 includeProject(":percentlayout:percentlayout", [BuildType.MAIN])
@@ -947,7 +948,7 @@
 includeProject(":room:integration-tests:room-testapp-kotlin", "room/integration-tests/kotlintestapp", [BuildType.MAIN])
 includeProject(":room:integration-tests:room-testapp-noappcompat", "room/integration-tests/noappcompattestapp", [BuildType.MAIN])
 includeProject(":room:room-benchmark", "room/benchmark", [BuildType.MAIN])
-includeProject(":room:room-common", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":room:room-common", [BuildType.MAIN, BuildType.COMPOSE, BuildType.KMP])
 includeProject(":room:room-compiler", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":room:room-compiler-processing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
 includeProject(":room:room-compiler-processing-testing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
@@ -1015,8 +1016,6 @@
 includeProject(":tv:tv-material", [BuildType.COMPOSE])
 includeProject(":tv:integration-tests:playground", [BuildType.COMPOSE])
 includeProject(":tv:integration-tests:presentation", [BuildType.COMPOSE])
-includeProject(":tv:integration-tests:macrobenchmark", [BuildType.COMPOSE])
-includeProject(":tv:integration-tests:macrobenchmark-target", [BuildType.COMPOSE])
 includeProject(":tv:tv-samples", "tv/samples", [BuildType.COMPOSE])
 includeProject(":tvprovider:tvprovider", [BuildType.MAIN])
 includeProject(":vectordrawable:integration-tests:testapp", [BuildType.MAIN])
@@ -1247,6 +1246,11 @@
     if (projectPath in included) return
     included.add(projectPath)
     for (reference in projectReferences[projectPath]) {
+        if (!allProjects.containsKey(reference) || allProjects[reference] == "") {
+            throw new GradleException("Project $reference does not exist.\n" +
+                    "Please check the build.gradle file for your $projectPath project " +
+                    "and update the project dependencies.")
+        }
         addReferences(reference, included)
     }
 }
diff --git a/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceSerializeMetrics.java b/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceSerializeMetrics.java
index 7158a10..cc3df0a 100644
--- a/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceSerializeMetrics.java
+++ b/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceSerializeMetrics.java
@@ -59,6 +59,7 @@
 @RunWith(AndroidJUnit4.class)
 @LargeTest
 @SdkSuppress(minSdkVersion = 19)
+@SuppressWarnings("deprecation")
 public class SliceSerializeMetrics {
 
     private static final boolean WRITE_SAMPLE_FILE = false;
diff --git a/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceViewMetrics.java b/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceViewMetrics.java
index 05482e8..b892033 100644
--- a/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceViewMetrics.java
+++ b/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceViewMetrics.java
@@ -39,6 +39,7 @@
 @RunWith(Parameterized.class)
 @SmallTest
 @SdkSuppress(minSdkVersion = 19)
+@SuppressWarnings("deprecation")
 public class SliceViewMetrics {
 
     private final int mMode;
diff --git a/slice/slice-builders-ktx/api/current.txt b/slice/slice-builders-ktx/api/current.txt
index 05623ec..6d9933f 100644
--- a/slice/slice-builders-ktx/api/current.txt
+++ b/slice/slice-builders-ktx/api/current.txt
@@ -1,49 +1,49 @@
 // Signature format: 4.0
 package androidx.slice.builders {
 
-  public final class CellBuilderDsl extends androidx.slice.builders.GridRowBuilder.CellBuilder {
-    ctor public CellBuilderDsl();
+  @Deprecated public final class CellBuilderDsl extends androidx.slice.builders.GridRowBuilder.CellBuilder {
+    ctor @Deprecated public CellBuilderDsl();
   }
 
-  public final class GridRowBuilderDsl extends androidx.slice.builders.GridRowBuilder {
-    ctor public GridRowBuilderDsl();
+  @Deprecated public final class GridRowBuilderDsl extends androidx.slice.builders.GridRowBuilder {
+    ctor @Deprecated public GridRowBuilderDsl();
   }
 
   public final class GridRowBuilderKt {
-    method public static inline androidx.slice.builders.GridRowBuilder cell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
-    method public static inline androidx.slice.builders.GridRowBuilder seeMoreCell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
+    method @Deprecated public static inline androidx.slice.builders.GridRowBuilder cell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
+    method @Deprecated public static inline androidx.slice.builders.GridRowBuilder seeMoreCell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
   }
 
-  public final class HeaderBuilderDsl extends androidx.slice.builders.ListBuilder.HeaderBuilder {
-    ctor public HeaderBuilderDsl();
+  @Deprecated public final class HeaderBuilderDsl extends androidx.slice.builders.ListBuilder.HeaderBuilder {
+    ctor @Deprecated public HeaderBuilderDsl();
   }
 
-  public final class InputRangeBuilderDsl extends androidx.slice.builders.ListBuilder.InputRangeBuilder {
-    ctor public InputRangeBuilderDsl();
+  @Deprecated public final class InputRangeBuilderDsl extends androidx.slice.builders.ListBuilder.InputRangeBuilder {
+    ctor @Deprecated public InputRangeBuilderDsl();
   }
 
-  public final class ListBuilderDsl extends androidx.slice.builders.ListBuilder {
-    ctor public ListBuilderDsl(android.content.Context context, android.net.Uri uri, long ttl);
+  @Deprecated public final class ListBuilderDsl extends androidx.slice.builders.ListBuilder {
+    ctor @Deprecated public ListBuilderDsl(android.content.Context context, android.net.Uri uri, long ttl);
   }
 
   public final class ListBuilderKt {
-    method public static inline androidx.slice.builders.ListBuilder gridRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.GridRowBuilderDsl,kotlin.Unit> buildGrid);
-    method public static inline androidx.slice.builders.ListBuilder header(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.HeaderBuilderDsl,kotlin.Unit> buildHeader);
-    method public static inline androidx.slice.builders.ListBuilder inputRange(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.InputRangeBuilderDsl,kotlin.Unit> buildInputRange);
-    method public static inline androidx.slice.Slice list(android.content.Context context, android.net.Uri uri, long ttl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.ListBuilderDsl,kotlin.Unit> addRows);
-    method public static inline androidx.slice.builders.ListBuilder range(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RangeBuilderDsl,kotlin.Unit> buildRange);
-    method public static inline androidx.slice.builders.ListBuilder row(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
-    method public static inline androidx.slice.builders.ListBuilder seeMoreRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
-    method public static androidx.slice.builders.SliceAction tapSliceAction(android.app.PendingIntent pendingIntent, androidx.core.graphics.drawable.IconCompat icon, optional int imageMode, CharSequence title);
-    method public static androidx.slice.builders.SliceAction toggleSliceAction(android.app.PendingIntent pendingIntent, optional androidx.core.graphics.drawable.IconCompat? icon, CharSequence title, boolean isChecked);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder gridRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.GridRowBuilderDsl,kotlin.Unit> buildGrid);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder header(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.HeaderBuilderDsl,kotlin.Unit> buildHeader);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder inputRange(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.InputRangeBuilderDsl,kotlin.Unit> buildInputRange);
+    method @Deprecated public static inline androidx.slice.Slice list(android.content.Context context, android.net.Uri uri, long ttl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.ListBuilderDsl,kotlin.Unit> addRows);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder range(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RangeBuilderDsl,kotlin.Unit> buildRange);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder row(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder seeMoreRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
+    method @Deprecated public static androidx.slice.builders.SliceAction tapSliceAction(android.app.PendingIntent pendingIntent, androidx.core.graphics.drawable.IconCompat icon, optional int imageMode, CharSequence title);
+    method @Deprecated public static androidx.slice.builders.SliceAction toggleSliceAction(android.app.PendingIntent pendingIntent, optional androidx.core.graphics.drawable.IconCompat? icon, CharSequence title, boolean isChecked);
   }
 
-  public final class RangeBuilderDsl extends androidx.slice.builders.ListBuilder.RangeBuilder {
-    ctor public RangeBuilderDsl();
+  @Deprecated public final class RangeBuilderDsl extends androidx.slice.builders.ListBuilder.RangeBuilder {
+    ctor @Deprecated public RangeBuilderDsl();
   }
 
-  public final class RowBuilderDsl extends androidx.slice.builders.ListBuilder.RowBuilder {
-    ctor public RowBuilderDsl();
+  @Deprecated public final class RowBuilderDsl extends androidx.slice.builders.ListBuilder.RowBuilder {
+    ctor @Deprecated public RowBuilderDsl();
   }
 
 }
diff --git a/slice/slice-builders-ktx/api/restricted_current.txt b/slice/slice-builders-ktx/api/restricted_current.txt
index 05623ec..6d9933f 100644
--- a/slice/slice-builders-ktx/api/restricted_current.txt
+++ b/slice/slice-builders-ktx/api/restricted_current.txt
@@ -1,49 +1,49 @@
 // Signature format: 4.0
 package androidx.slice.builders {
 
-  public final class CellBuilderDsl extends androidx.slice.builders.GridRowBuilder.CellBuilder {
-    ctor public CellBuilderDsl();
+  @Deprecated public final class CellBuilderDsl extends androidx.slice.builders.GridRowBuilder.CellBuilder {
+    ctor @Deprecated public CellBuilderDsl();
   }
 
-  public final class GridRowBuilderDsl extends androidx.slice.builders.GridRowBuilder {
-    ctor public GridRowBuilderDsl();
+  @Deprecated public final class GridRowBuilderDsl extends androidx.slice.builders.GridRowBuilder {
+    ctor @Deprecated public GridRowBuilderDsl();
   }
 
   public final class GridRowBuilderKt {
-    method public static inline androidx.slice.builders.GridRowBuilder cell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
-    method public static inline androidx.slice.builders.GridRowBuilder seeMoreCell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
+    method @Deprecated public static inline androidx.slice.builders.GridRowBuilder cell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
+    method @Deprecated public static inline androidx.slice.builders.GridRowBuilder seeMoreCell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
   }
 
-  public final class HeaderBuilderDsl extends androidx.slice.builders.ListBuilder.HeaderBuilder {
-    ctor public HeaderBuilderDsl();
+  @Deprecated public final class HeaderBuilderDsl extends androidx.slice.builders.ListBuilder.HeaderBuilder {
+    ctor @Deprecated public HeaderBuilderDsl();
   }
 
-  public final class InputRangeBuilderDsl extends androidx.slice.builders.ListBuilder.InputRangeBuilder {
-    ctor public InputRangeBuilderDsl();
+  @Deprecated public final class InputRangeBuilderDsl extends androidx.slice.builders.ListBuilder.InputRangeBuilder {
+    ctor @Deprecated public InputRangeBuilderDsl();
   }
 
-  public final class ListBuilderDsl extends androidx.slice.builders.ListBuilder {
-    ctor public ListBuilderDsl(android.content.Context context, android.net.Uri uri, long ttl);
+  @Deprecated public final class ListBuilderDsl extends androidx.slice.builders.ListBuilder {
+    ctor @Deprecated public ListBuilderDsl(android.content.Context context, android.net.Uri uri, long ttl);
   }
 
   public final class ListBuilderKt {
-    method public static inline androidx.slice.builders.ListBuilder gridRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.GridRowBuilderDsl,kotlin.Unit> buildGrid);
-    method public static inline androidx.slice.builders.ListBuilder header(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.HeaderBuilderDsl,kotlin.Unit> buildHeader);
-    method public static inline androidx.slice.builders.ListBuilder inputRange(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.InputRangeBuilderDsl,kotlin.Unit> buildInputRange);
-    method public static inline androidx.slice.Slice list(android.content.Context context, android.net.Uri uri, long ttl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.ListBuilderDsl,kotlin.Unit> addRows);
-    method public static inline androidx.slice.builders.ListBuilder range(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RangeBuilderDsl,kotlin.Unit> buildRange);
-    method public static inline androidx.slice.builders.ListBuilder row(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
-    method public static inline androidx.slice.builders.ListBuilder seeMoreRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
-    method public static androidx.slice.builders.SliceAction tapSliceAction(android.app.PendingIntent pendingIntent, androidx.core.graphics.drawable.IconCompat icon, optional int imageMode, CharSequence title);
-    method public static androidx.slice.builders.SliceAction toggleSliceAction(android.app.PendingIntent pendingIntent, optional androidx.core.graphics.drawable.IconCompat? icon, CharSequence title, boolean isChecked);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder gridRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.GridRowBuilderDsl,kotlin.Unit> buildGrid);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder header(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.HeaderBuilderDsl,kotlin.Unit> buildHeader);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder inputRange(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.InputRangeBuilderDsl,kotlin.Unit> buildInputRange);
+    method @Deprecated public static inline androidx.slice.Slice list(android.content.Context context, android.net.Uri uri, long ttl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.ListBuilderDsl,kotlin.Unit> addRows);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder range(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RangeBuilderDsl,kotlin.Unit> buildRange);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder row(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder seeMoreRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
+    method @Deprecated public static androidx.slice.builders.SliceAction tapSliceAction(android.app.PendingIntent pendingIntent, androidx.core.graphics.drawable.IconCompat icon, optional int imageMode, CharSequence title);
+    method @Deprecated public static androidx.slice.builders.SliceAction toggleSliceAction(android.app.PendingIntent pendingIntent, optional androidx.core.graphics.drawable.IconCompat? icon, CharSequence title, boolean isChecked);
   }
 
-  public final class RangeBuilderDsl extends androidx.slice.builders.ListBuilder.RangeBuilder {
-    ctor public RangeBuilderDsl();
+  @Deprecated public final class RangeBuilderDsl extends androidx.slice.builders.ListBuilder.RangeBuilder {
+    ctor @Deprecated public RangeBuilderDsl();
   }
 
-  public final class RowBuilderDsl extends androidx.slice.builders.ListBuilder.RowBuilder {
-    ctor public RowBuilderDsl();
+  @Deprecated public final class RowBuilderDsl extends androidx.slice.builders.ListBuilder.RowBuilder {
+    ctor @Deprecated public RowBuilderDsl();
   }
 
 }
diff --git a/slice/slice-builders-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest.kt b/slice/slice-builders-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest.kt
index a05802a..6faebd4 100644
--- a/slice/slice-builders-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest.kt
+++ b/slice/slice-builders-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest.kt
@@ -33,6 +33,7 @@
 
 @SdkSuppress(minSdkVersion = 19)
 @MediumTest
+@Suppress("DEPRECATION")
 class SliceBuildersKtxTest {
     private val testUri = Uri.parse("content://com.example.android.sliceuri")
     private val context = ApplicationProvider.getApplicationContext() as android.content.Context
diff --git a/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/GridRowBuilder.kt b/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/GridRowBuilder.kt
index 504b13b..9188617 100644
--- a/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/GridRowBuilder.kt
+++ b/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/GridRowBuilder.kt
@@ -20,6 +20,14 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class GridRowBuilderDsl : GridRowBuilder()
 
@@ -28,17 +36,41 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class CellBuilderDsl : GridRowBuilder.CellBuilder()
 
 /**
  * @see GridRowBuilder.addCell
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun GridRowBuilderDsl.cell(buildCell: CellBuilderDsl.() -> Unit) =
     addCell(CellBuilderDsl().apply { buildCell() })
 
 /**
  * @see GridRowBuilder.setSeeMoreCell
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun GridRowBuilderDsl.seeMoreCell(buildCell: CellBuilderDsl.() -> Unit) =
     setSeeMoreCell(CellBuilderDsl().apply { buildCell() })
diff --git a/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/ListBuilder.kt b/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/ListBuilder.kt
index 101ed21..a782bc4 100644
--- a/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/ListBuilder.kt
+++ b/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/ListBuilder.kt
@@ -32,6 +32,14 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class ListBuilderDsl(context: Context, uri: Uri, ttl: Long) : ListBuilder(context, uri, ttl)
 
@@ -40,6 +48,14 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class RowBuilderDsl : ListBuilder.RowBuilder()
 
@@ -48,6 +64,14 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class InputRangeBuilderDsl : ListBuilder.InputRangeBuilder()
 
@@ -56,6 +80,14 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class RangeBuilderDsl : ListBuilder.RangeBuilder()
 
@@ -64,6 +96,14 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class HeaderBuilderDsl : ListBuilder.HeaderBuilder()
 
@@ -94,6 +134,14 @@
  * </pre>
  * @see ListBuilder.build
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun list(
     context: Context,
     uri: Uri,
@@ -104,42 +152,98 @@
 /**
  * @see ListBuilder.setHeader
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun ListBuilderDsl.header(buildHeader: HeaderBuilderDsl.() -> Unit) =
     setHeader(HeaderBuilderDsl().apply { buildHeader() })
 
 /**
  * @see ListBuilder.addGridRow
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun ListBuilderDsl.gridRow(buildGrid: GridRowBuilderDsl.() -> Unit) =
     addGridRow(GridRowBuilderDsl().apply { buildGrid() })
 
 /**
  * @see ListBuilder.addRow
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun ListBuilderDsl.row(buildRow: RowBuilderDsl.() -> Unit) =
     addRow(RowBuilderDsl().apply { buildRow() })
 
 /**
  * @see ListBuilder.setSeeMoreRow
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun ListBuilderDsl.seeMoreRow(buildRow: RowBuilderDsl.() -> Unit) =
     setSeeMoreRow(RowBuilderDsl().apply { buildRow() })
 
 /**
  * @see ListBuilder.addInputRange
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun ListBuilderDsl.inputRange(buildInputRange: InputRangeBuilderDsl.() -> Unit) =
     addInputRange(InputRangeBuilderDsl().apply { buildInputRange() })
 
 /**
  * @see ListBuilder.addRange
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun ListBuilderDsl.range(buildRange: RangeBuilderDsl.() -> Unit) =
     addRange(RangeBuilderDsl().apply { buildRange() })
 
 /**
  * Factory method to build a tappable [SliceAction].
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 fun tapSliceAction(
     pendingIntent: PendingIntent,
     icon: IconCompat,
@@ -150,6 +254,14 @@
 /**
  * Factory method to build a toggleable [SliceAction].
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 fun toggleSliceAction(
     pendingIntent: PendingIntent,
     icon: IconCompat? = null,
diff --git a/slice/slice-builders/api/current.txt b/slice/slice-builders/api/current.txt
index 1351608..6801791 100644
--- a/slice/slice-builders/api/current.txt
+++ b/slice/slice-builders/api/current.txt
@@ -1,190 +1,190 @@
 // Signature format: 4.0
 package androidx.slice.builders {
 
-  @RequiresApi(19) public class GridRowBuilder {
-    ctor public GridRowBuilder();
-    method public androidx.slice.builders.GridRowBuilder addCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
-    method public androidx.slice.builders.GridRowBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.GridRowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.GridRowBuilder setSeeMoreAction(android.app.PendingIntent);
-    method public androidx.slice.builders.GridRowBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.GridRowBuilder setSeeMoreCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
+  @Deprecated @RequiresApi(19) public class GridRowBuilder {
+    ctor @Deprecated public GridRowBuilder();
+    method @Deprecated public androidx.slice.builders.GridRowBuilder addCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setSeeMoreAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setSeeMoreCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
   }
 
-  public static class GridRowBuilder.CellBuilder {
-    ctor public GridRowBuilder.CellBuilder();
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat?, int, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence?, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence?, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence?, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(android.app.PendingIntent);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setSliceAction(androidx.slice.builders.SliceAction);
+  @Deprecated public static class GridRowBuilder.CellBuilder {
+    ctor @Deprecated public GridRowBuilder.CellBuilder();
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat?, int, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setSliceAction(androidx.slice.builders.SliceAction);
   }
 
-  @RequiresApi(19) public class ListBuilder extends androidx.slice.builders.TemplateSliceBuilder {
-    ctor @RequiresApi(26) public ListBuilder(android.content.Context, android.net.Uri, java.time.Duration?);
-    ctor public ListBuilder(android.content.Context, android.net.Uri, long);
-    method public androidx.slice.builders.ListBuilder addAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder addGridRow(androidx.slice.builders.GridRowBuilder);
-    method public androidx.slice.builders.ListBuilder addInputRange(androidx.slice.builders.ListBuilder.InputRangeBuilder);
-    method public androidx.slice.builders.ListBuilder addRange(androidx.slice.builders.ListBuilder.RangeBuilder);
-    method public androidx.slice.builders.ListBuilder addRating(androidx.slice.builders.ListBuilder.RatingBuilder);
-    method public androidx.slice.builders.ListBuilder addRow(androidx.slice.builders.ListBuilder.RowBuilder);
-    method public androidx.slice.builders.ListBuilder addSelection(androidx.slice.builders.SelectionBuilder);
-    method public androidx.slice.builders.ListBuilder setAccentColor(@ColorInt int);
-    method public androidx.slice.builders.ListBuilder setHeader(androidx.slice.builders.ListBuilder.HeaderBuilder);
-    method @RequiresApi(21) public androidx.slice.builders.ListBuilder setHostExtras(android.os.PersistableBundle);
-    method public androidx.slice.builders.ListBuilder setIsError(boolean);
-    method public androidx.slice.builders.ListBuilder setKeywords(java.util.Set<java.lang.String!>);
-    method public androidx.slice.builders.ListBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder setSeeMoreAction(android.app.PendingIntent);
-    method public androidx.slice.builders.ListBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.ListBuilder setSeeMoreRow(androidx.slice.builders.ListBuilder.RowBuilder);
-    field public static final int ACTION_WITH_LABEL = 6; // 0x6
-    field public static final int ICON_IMAGE = 0; // 0x0
-    field public static final long INFINITY = -1L; // 0xffffffffffffffffL
-    field public static final int LARGE_IMAGE = 2; // 0x2
-    field public static final int RANGE_MODE_DETERMINATE = 0; // 0x0
-    field public static final int RANGE_MODE_INDETERMINATE = 1; // 0x1
-    field public static final int RANGE_MODE_STAR_RATING = 2; // 0x2
-    field public static final int RAW_IMAGE_LARGE = 4; // 0x4
-    field public static final int RAW_IMAGE_SMALL = 3; // 0x3
-    field public static final int SMALL_IMAGE = 1; // 0x1
-    field public static final int UNKNOWN_IMAGE = 5; // 0x5
+  @Deprecated @RequiresApi(19) public class ListBuilder extends androidx.slice.builders.TemplateSliceBuilder {
+    ctor @Deprecated @RequiresApi(26) public ListBuilder(android.content.Context, android.net.Uri, java.time.Duration?);
+    ctor @Deprecated public ListBuilder(android.content.Context, android.net.Uri, long);
+    method @Deprecated public androidx.slice.builders.ListBuilder addAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder addGridRow(androidx.slice.builders.GridRowBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addInputRange(androidx.slice.builders.ListBuilder.InputRangeBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addRange(androidx.slice.builders.ListBuilder.RangeBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addRating(androidx.slice.builders.ListBuilder.RatingBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addRow(androidx.slice.builders.ListBuilder.RowBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addSelection(androidx.slice.builders.SelectionBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder setAccentColor(@ColorInt int);
+    method @Deprecated public androidx.slice.builders.ListBuilder setHeader(androidx.slice.builders.ListBuilder.HeaderBuilder);
+    method @Deprecated @RequiresApi(21) public androidx.slice.builders.ListBuilder setHostExtras(android.os.PersistableBundle);
+    method @Deprecated public androidx.slice.builders.ListBuilder setIsError(boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder setKeywords(java.util.Set<java.lang.String!>);
+    method @Deprecated public androidx.slice.builders.ListBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder setSeeMoreAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.ListBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.ListBuilder setSeeMoreRow(androidx.slice.builders.ListBuilder.RowBuilder);
+    field @Deprecated public static final int ACTION_WITH_LABEL = 6; // 0x6
+    field @Deprecated public static final int ICON_IMAGE = 0; // 0x0
+    field @Deprecated public static final long INFINITY = -1L; // 0xffffffffffffffffL
+    field @Deprecated public static final int LARGE_IMAGE = 2; // 0x2
+    field @Deprecated public static final int RANGE_MODE_DETERMINATE = 0; // 0x0
+    field @Deprecated public static final int RANGE_MODE_INDETERMINATE = 1; // 0x1
+    field @Deprecated public static final int RANGE_MODE_STAR_RATING = 2; // 0x2
+    field @Deprecated public static final int RAW_IMAGE_LARGE = 4; // 0x4
+    field @Deprecated public static final int RAW_IMAGE_SMALL = 3; // 0x3
+    field @Deprecated public static final int SMALL_IMAGE = 1; // 0x1
+    field @Deprecated public static final int UNKNOWN_IMAGE = 5; // 0x5
   }
 
-  public static class ListBuilder.HeaderBuilder {
-    ctor public ListBuilder.HeaderBuilder();
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence, boolean);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence, boolean);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence, boolean);
+  @Deprecated public static class ListBuilder.HeaderBuilder {
+    ctor @Deprecated public ListBuilder.HeaderBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence, boolean);
   }
 
-  public static class ListBuilder.InputRangeBuilder {
-    ctor public ListBuilder.InputRangeBuilder();
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(android.app.PendingIntent);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setMax(int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setMin(int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setThumb(androidx.core.graphics.drawable.IconCompat);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setValue(int);
+  @Deprecated public static class ListBuilder.InputRangeBuilder {
+    ctor @Deprecated public ListBuilder.InputRangeBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setMax(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setMin(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setThumb(androidx.core.graphics.drawable.IconCompat);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setValue(int);
   }
 
-  public static class ListBuilder.RangeBuilder {
-    ctor public ListBuilder.RangeBuilder();
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setMax(int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setMode(int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setValue(int);
+  @Deprecated public static class ListBuilder.RangeBuilder {
+    ctor @Deprecated public ListBuilder.RangeBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setMax(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setMode(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setValue(int);
   }
 
-  public static final class ListBuilder.RatingBuilder {
-    ctor public ListBuilder.RatingBuilder();
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(android.app.PendingIntent);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setMax(int);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setMin(int);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setValue(float);
+  @Deprecated public static final class ListBuilder.RatingBuilder {
+    ctor @Deprecated public ListBuilder.RatingBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setMax(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setMin(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setValue(float);
   }
 
-  public static class ListBuilder.RowBuilder {
-    ctor public ListBuilder.RowBuilder();
-    ctor public ListBuilder.RowBuilder(android.net.Uri);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(long);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setEndOfSection(boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence?, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence?, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(long);
+  @Deprecated public static class ListBuilder.RowBuilder {
+    ctor @Deprecated public ListBuilder.RowBuilder();
+    ctor @Deprecated public ListBuilder.RowBuilder(android.net.Uri);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(long);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setEndOfSection(boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(long);
   }
 
-  @RequiresApi(19) public class SelectionBuilder {
-    ctor public SelectionBuilder();
-    method public androidx.slice.builders.SelectionBuilder! addOption(String!, CharSequence!);
-    method public androidx.slice.builders.SelectionBuilder! setContentDescription(CharSequence?);
-    method public androidx.slice.builders.SelectionBuilder! setInputAction(android.app.PendingIntent);
-    method public androidx.slice.builders.SelectionBuilder! setInputAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.SelectionBuilder! setLayoutDirection(int);
-    method public androidx.slice.builders.SelectionBuilder! setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.SelectionBuilder! setSelectedOption(String!);
-    method public androidx.slice.builders.SelectionBuilder! setSubtitle(CharSequence?);
-    method public androidx.slice.builders.SelectionBuilder! setTitle(CharSequence?);
+  @Deprecated @RequiresApi(19) public class SelectionBuilder {
+    ctor @Deprecated public SelectionBuilder();
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! addOption(String!, CharSequence!);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setContentDescription(CharSequence?);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setInputAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setInputAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setSelectedOption(String!);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setSubtitle(CharSequence?);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setTitle(CharSequence?);
   }
 
-  @RequiresApi(19) public class SliceAction implements androidx.slice.core.SliceAction {
-    method public static androidx.slice.builders.SliceAction! create(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! create(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! createDeeplink(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! createDeeplink(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
-    method public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, CharSequence, boolean);
-    method public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
-    method public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, CharSequence, boolean);
-    method public android.app.PendingIntent getAction();
-    method public CharSequence? getContentDescription();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method public int getImageMode();
-    method public String? getKey();
-    method public int getPriority();
-    method public CharSequence getTitle();
-    method public boolean isActivity();
-    method public boolean isChecked();
-    method public boolean isDefaultToggle();
-    method public boolean isToggle();
-    method public androidx.slice.builders.SliceAction setChecked(boolean);
-    method public androidx.slice.core.SliceAction setContentDescription(CharSequence);
-    method public androidx.slice.builders.SliceAction setKey(String);
-    method public androidx.slice.builders.SliceAction setPriority(@IntRange(from=0) int);
+  @Deprecated @RequiresApi(19) public class SliceAction implements androidx.slice.core.SliceAction {
+    method @Deprecated public static androidx.slice.builders.SliceAction! create(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! create(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createDeeplink(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createDeeplink(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, CharSequence, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, CharSequence, boolean);
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public CharSequence? getContentDescription();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated public int getImageMode();
+    method @Deprecated public String? getKey();
+    method @Deprecated public int getPriority();
+    method @Deprecated public CharSequence getTitle();
+    method @Deprecated public boolean isActivity();
+    method @Deprecated public boolean isChecked();
+    method @Deprecated public boolean isDefaultToggle();
+    method @Deprecated public boolean isToggle();
+    method @Deprecated public androidx.slice.builders.SliceAction setChecked(boolean);
+    method @Deprecated public androidx.slice.core.SliceAction setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.SliceAction setKey(String);
+    method @Deprecated public androidx.slice.builders.SliceAction setPriority(@IntRange(from=0) int);
   }
 
-  @RequiresApi(19) public abstract class TemplateSliceBuilder {
-    method public androidx.slice.Slice build();
+  @Deprecated @RequiresApi(19) public abstract class TemplateSliceBuilder {
+    method @Deprecated public androidx.slice.Slice build();
   }
 
 }
diff --git a/slice/slice-builders/api/restricted_current.txt b/slice/slice-builders/api/restricted_current.txt
index 84c8575..fb90065 100644
--- a/slice/slice-builders/api/restricted_current.txt
+++ b/slice/slice-builders/api/restricted_current.txt
@@ -1,210 +1,210 @@
 // Signature format: 4.0
 package androidx.slice.builders {
 
-  @RequiresApi(19) public class GridRowBuilder {
-    ctor public GridRowBuilder();
-    method public androidx.slice.builders.GridRowBuilder addCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
-    method public androidx.slice.builders.GridRowBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.GridRowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.GridRowBuilder setSeeMoreAction(android.app.PendingIntent);
-    method public androidx.slice.builders.GridRowBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.GridRowBuilder setSeeMoreCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
+  @Deprecated @RequiresApi(19) public class GridRowBuilder {
+    ctor @Deprecated public GridRowBuilder();
+    method @Deprecated public androidx.slice.builders.GridRowBuilder addCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setSeeMoreAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setSeeMoreCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
   }
 
-  public static class GridRowBuilder.CellBuilder {
-    ctor public GridRowBuilder.CellBuilder();
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat?, int, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence?, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence?, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence?, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(android.app.PendingIntent);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setSliceAction(androidx.slice.builders.SliceAction);
+  @Deprecated public static class GridRowBuilder.CellBuilder {
+    ctor @Deprecated public GridRowBuilder.CellBuilder();
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat?, int, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setSliceAction(androidx.slice.builders.SliceAction);
   }
 
-  @RequiresApi(19) public class ListBuilder extends androidx.slice.builders.TemplateSliceBuilder {
-    ctor @RequiresApi(26) public ListBuilder(android.content.Context, android.net.Uri, java.time.Duration?);
-    ctor public ListBuilder(android.content.Context, android.net.Uri, long);
-    method public androidx.slice.builders.ListBuilder addAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder addGridRow(androidx.slice.builders.GridRowBuilder);
-    method public androidx.slice.builders.ListBuilder addInputRange(androidx.slice.builders.ListBuilder.InputRangeBuilder);
-    method public androidx.slice.builders.ListBuilder addRange(androidx.slice.builders.ListBuilder.RangeBuilder);
-    method public androidx.slice.builders.ListBuilder addRating(androidx.slice.builders.ListBuilder.RatingBuilder);
-    method public androidx.slice.builders.ListBuilder addRow(androidx.slice.builders.ListBuilder.RowBuilder);
-    method public androidx.slice.builders.ListBuilder addSelection(androidx.slice.builders.SelectionBuilder);
-    method public androidx.slice.builders.ListBuilder setAccentColor(@ColorInt int);
-    method public androidx.slice.builders.ListBuilder setHeader(androidx.slice.builders.ListBuilder.HeaderBuilder);
-    method @RequiresApi(21) public androidx.slice.builders.ListBuilder setHostExtras(android.os.PersistableBundle);
-    method public androidx.slice.builders.ListBuilder setIsError(boolean);
-    method public androidx.slice.builders.ListBuilder setKeywords(java.util.Set<java.lang.String!>);
-    method public androidx.slice.builders.ListBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder setSeeMoreAction(android.app.PendingIntent);
-    method public androidx.slice.builders.ListBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.ListBuilder setSeeMoreRow(androidx.slice.builders.ListBuilder.RowBuilder);
-    field public static final int ACTION_WITH_LABEL = 6; // 0x6
-    field public static final int ICON_IMAGE = 0; // 0x0
-    field public static final long INFINITY = -1L; // 0xffffffffffffffffL
-    field public static final int LARGE_IMAGE = 2; // 0x2
-    field public static final int RANGE_MODE_DETERMINATE = 0; // 0x0
-    field public static final int RANGE_MODE_INDETERMINATE = 1; // 0x1
-    field public static final int RANGE_MODE_STAR_RATING = 2; // 0x2
-    field public static final int RAW_IMAGE_LARGE = 4; // 0x4
-    field public static final int RAW_IMAGE_SMALL = 3; // 0x3
-    field public static final int SMALL_IMAGE = 1; // 0x1
-    field public static final int UNKNOWN_IMAGE = 5; // 0x5
+  @Deprecated @RequiresApi(19) public class ListBuilder extends androidx.slice.builders.TemplateSliceBuilder {
+    ctor @Deprecated @RequiresApi(26) public ListBuilder(android.content.Context, android.net.Uri, java.time.Duration?);
+    ctor @Deprecated public ListBuilder(android.content.Context, android.net.Uri, long);
+    method @Deprecated public androidx.slice.builders.ListBuilder addAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder addGridRow(androidx.slice.builders.GridRowBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addInputRange(androidx.slice.builders.ListBuilder.InputRangeBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addRange(androidx.slice.builders.ListBuilder.RangeBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addRating(androidx.slice.builders.ListBuilder.RatingBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addRow(androidx.slice.builders.ListBuilder.RowBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addSelection(androidx.slice.builders.SelectionBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder setAccentColor(@ColorInt int);
+    method @Deprecated public androidx.slice.builders.ListBuilder setHeader(androidx.slice.builders.ListBuilder.HeaderBuilder);
+    method @Deprecated @RequiresApi(21) public androidx.slice.builders.ListBuilder setHostExtras(android.os.PersistableBundle);
+    method @Deprecated public androidx.slice.builders.ListBuilder setIsError(boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder setKeywords(java.util.Set<java.lang.String!>);
+    method @Deprecated public androidx.slice.builders.ListBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder setSeeMoreAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.ListBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.ListBuilder setSeeMoreRow(androidx.slice.builders.ListBuilder.RowBuilder);
+    field @Deprecated public static final int ACTION_WITH_LABEL = 6; // 0x6
+    field @Deprecated public static final int ICON_IMAGE = 0; // 0x0
+    field @Deprecated public static final long INFINITY = -1L; // 0xffffffffffffffffL
+    field @Deprecated public static final int LARGE_IMAGE = 2; // 0x2
+    field @Deprecated public static final int RANGE_MODE_DETERMINATE = 0; // 0x0
+    field @Deprecated public static final int RANGE_MODE_INDETERMINATE = 1; // 0x1
+    field @Deprecated public static final int RANGE_MODE_STAR_RATING = 2; // 0x2
+    field @Deprecated public static final int RAW_IMAGE_LARGE = 4; // 0x4
+    field @Deprecated public static final int RAW_IMAGE_SMALL = 3; // 0x3
+    field @Deprecated public static final int SMALL_IMAGE = 1; // 0x1
+    field @Deprecated public static final int UNKNOWN_IMAGE = 5; // 0x5
   }
 
-  public static class ListBuilder.HeaderBuilder {
-    ctor public ListBuilder.HeaderBuilder();
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public ListBuilder.HeaderBuilder(android.net.Uri);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence, boolean);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence, boolean);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence, boolean);
+  @Deprecated public static class ListBuilder.HeaderBuilder {
+    ctor @Deprecated public ListBuilder.HeaderBuilder();
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public ListBuilder.HeaderBuilder(android.net.Uri);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence, boolean);
   }
 
-  public static class ListBuilder.InputRangeBuilder {
-    ctor public ListBuilder.InputRangeBuilder();
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(android.app.PendingIntent);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setMax(int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setMin(int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setThumb(androidx.core.graphics.drawable.IconCompat);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setValue(int);
+  @Deprecated public static class ListBuilder.InputRangeBuilder {
+    ctor @Deprecated public ListBuilder.InputRangeBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setMax(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setMin(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setThumb(androidx.core.graphics.drawable.IconCompat);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setValue(int);
   }
 
-  public static class ListBuilder.RangeBuilder {
-    ctor public ListBuilder.RangeBuilder();
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setMax(int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setMode(int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setValue(int);
+  @Deprecated public static class ListBuilder.RangeBuilder {
+    ctor @Deprecated public ListBuilder.RangeBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setMax(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setMode(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setValue(int);
   }
 
-  public static final class ListBuilder.RatingBuilder {
-    ctor public ListBuilder.RatingBuilder();
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(android.app.PendingIntent);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setMax(int);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setMin(int);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setValue(float);
+  @Deprecated public static final class ListBuilder.RatingBuilder {
+    ctor @Deprecated public ListBuilder.RatingBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setMax(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setMin(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setValue(float);
   }
 
-  public static class ListBuilder.RowBuilder {
-    ctor public ListBuilder.RowBuilder();
-    ctor public ListBuilder.RowBuilder(android.net.Uri);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(long);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setEndOfSection(boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence?, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence?, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(long);
+  @Deprecated public static class ListBuilder.RowBuilder {
+    ctor @Deprecated public ListBuilder.RowBuilder();
+    ctor @Deprecated public ListBuilder.RowBuilder(android.net.Uri);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(long);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setEndOfSection(boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(long);
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class MessagingSliceBuilder extends androidx.slice.builders.TemplateSliceBuilder {
-    ctor public MessagingSliceBuilder(android.content.Context, android.net.Uri);
-    method public androidx.slice.builders.MessagingSliceBuilder! add(androidx.core.util.Consumer<androidx.slice.builders.MessagingSliceBuilder.MessageBuilder!>!);
-    method public androidx.slice.builders.MessagingSliceBuilder! add(androidx.slice.builders.MessagingSliceBuilder.MessageBuilder!);
-    field public static final int MAXIMUM_RETAINED_MESSAGES = 50; // 0x32
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class MessagingSliceBuilder extends androidx.slice.builders.TemplateSliceBuilder {
+    ctor @Deprecated public MessagingSliceBuilder(android.content.Context, android.net.Uri);
+    method @Deprecated public androidx.slice.builders.MessagingSliceBuilder! add(androidx.core.util.Consumer<androidx.slice.builders.MessagingSliceBuilder.MessageBuilder!>!);
+    method @Deprecated public androidx.slice.builders.MessagingSliceBuilder! add(androidx.slice.builders.MessagingSliceBuilder.MessageBuilder!);
+    field @Deprecated public static final int MAXIMUM_RETAINED_MESSAGES = 50; // 0x32
   }
 
-  public static final class MessagingSliceBuilder.MessageBuilder extends androidx.slice.builders.TemplateSliceBuilder {
-    ctor public MessagingSliceBuilder.MessageBuilder(androidx.slice.builders.MessagingSliceBuilder!);
-    method @RequiresApi(23) public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addSource(android.graphics.drawable.Icon!);
-    method public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addSource(androidx.core.graphics.drawable.IconCompat!);
-    method public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addText(CharSequence!);
-    method public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addTimestamp(long);
+  @Deprecated public static final class MessagingSliceBuilder.MessageBuilder extends androidx.slice.builders.TemplateSliceBuilder {
+    ctor @Deprecated public MessagingSliceBuilder.MessageBuilder(androidx.slice.builders.MessagingSliceBuilder!);
+    method @Deprecated @RequiresApi(23) public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addSource(android.graphics.drawable.Icon!);
+    method @Deprecated public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addSource(androidx.core.graphics.drawable.IconCompat!);
+    method @Deprecated public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addText(CharSequence!);
+    method @Deprecated public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addTimestamp(long);
   }
 
-  @RequiresApi(19) public class SelectionBuilder {
-    ctor public SelectionBuilder();
-    method public androidx.slice.builders.SelectionBuilder! addOption(String!, CharSequence!);
-    method public androidx.slice.builders.SelectionBuilder! setContentDescription(CharSequence?);
-    method public androidx.slice.builders.SelectionBuilder! setInputAction(android.app.PendingIntent);
-    method public androidx.slice.builders.SelectionBuilder! setInputAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.SelectionBuilder! setLayoutDirection(int);
-    method public androidx.slice.builders.SelectionBuilder! setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.SelectionBuilder! setSelectedOption(String!);
-    method public androidx.slice.builders.SelectionBuilder! setSubtitle(CharSequence?);
-    method public androidx.slice.builders.SelectionBuilder! setTitle(CharSequence?);
+  @Deprecated @RequiresApi(19) public class SelectionBuilder {
+    ctor @Deprecated public SelectionBuilder();
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! addOption(String!, CharSequence!);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setContentDescription(CharSequence?);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setInputAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setInputAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setSelectedOption(String!);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setSubtitle(CharSequence?);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setTitle(CharSequence?);
   }
 
-  @RequiresApi(19) public class SliceAction implements androidx.slice.core.SliceAction {
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, CharSequence, boolean);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, CharSequence, long, boolean);
-    method public static androidx.slice.builders.SliceAction! create(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! create(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! createDeeplink(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! createDeeplink(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
-    method public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, CharSequence, boolean);
-    method public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
-    method public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, CharSequence, boolean);
-    method public android.app.PendingIntent getAction();
-    method public CharSequence? getContentDescription();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method public int getImageMode();
-    method public String? getKey();
-    method public int getPriority();
-    method public CharSequence getTitle();
-    method public boolean isActivity();
-    method public boolean isChecked();
-    method public boolean isDefaultToggle();
-    method public boolean isToggle();
-    method public androidx.slice.builders.SliceAction setChecked(boolean);
-    method public androidx.slice.core.SliceAction setContentDescription(CharSequence);
-    method public androidx.slice.builders.SliceAction setKey(String);
-    method public androidx.slice.builders.SliceAction setPriority(@IntRange(from=0) int);
+  @Deprecated @RequiresApi(19) public class SliceAction implements androidx.slice.core.SliceAction {
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, CharSequence, boolean);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, CharSequence, long, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! create(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! create(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createDeeplink(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createDeeplink(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, CharSequence, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, CharSequence, boolean);
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public CharSequence? getContentDescription();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated public int getImageMode();
+    method @Deprecated public String? getKey();
+    method @Deprecated public int getPriority();
+    method @Deprecated public CharSequence getTitle();
+    method @Deprecated public boolean isActivity();
+    method @Deprecated public boolean isChecked();
+    method @Deprecated public boolean isDefaultToggle();
+    method @Deprecated public boolean isToggle();
+    method @Deprecated public androidx.slice.builders.SliceAction setChecked(boolean);
+    method @Deprecated public androidx.slice.core.SliceAction setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.SliceAction setKey(String);
+    method @Deprecated public androidx.slice.builders.SliceAction setPriority(@IntRange(from=0) int);
   }
 
-  @RequiresApi(19) public abstract class TemplateSliceBuilder {
-    method public androidx.slice.Slice build();
+  @Deprecated @RequiresApi(19) public abstract class TemplateSliceBuilder {
+    method @Deprecated public androidx.slice.Slice build();
   }
 
 }
diff --git a/slice/slice-builders/lint-baseline.xml b/slice/slice-builders/lint-baseline.xml
index 125de78..3679718 100644
--- a/slice/slice-builders/lint-baseline.xml
+++ b/slice/slice-builders/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-beta02" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta02)" variant="all" version="8.1.0-beta02">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="WrongConstant"
@@ -28,976 +28,4 @@
             file="src/main/java/androidx/slice/builders/impl/ListBuilderImpl.java"/>
     </issue>
 
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceAction getPrimaryAction() {"
-        errorLine2="           ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/GridRowBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public List&lt;CellBuilder> getCells() {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/GridRowBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public CellBuilder getSeeMoreCell() {"
-        errorLine2="           ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/GridRowBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public PendingIntent getSeeMoreIntent() {"
-        errorLine2="           ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/GridRowBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public CharSequence getDescription() {"
-        errorLine2="           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/GridRowBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public List&lt;Object> getObjects() {"
-        errorLine2="               ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/GridRowBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public List&lt;Integer> getTypes() {"
-        errorLine2="               ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/GridRowBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public List&lt;Boolean> getLoadings() {"
-        errorLine2="               ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/GridRowBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public CharSequence getCellDescription() {"
-        errorLine2="               ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/GridRowBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public PendingIntent getContentIntent() {"
-        errorLine2="               ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/GridRowBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public GridRowBuilderListV1Impl(@NonNull ListBuilderImpl parent, GridRowBuilder builder) {"
-        errorLine2="                                                                     ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void addCell(CellBuilder builder) {"
-        errorLine2="                        ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSeeMoreAction(PendingIntent intent) {"
-        errorLine2="                                 ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setPrimaryAction(SliceAction action) {"
-        errorLine2="                                 ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setContentDescription(CharSequence description) {"
-        errorLine2="                                      ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void fillFrom(CellBuilder builder) {"
-        errorLine2="                             ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public ListBuilderBasicImpl(Slice.Builder b, SliceSpec spec) {"
-        errorLine2="                                ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public ListBuilderBasicImpl(Slice.Builder b, SliceSpec spec) {"
-        errorLine2="                                                 ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public MessagingBasicImpl(Slice.Builder builder, SliceSpec spec) {"
-        errorLine2="                              ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public MessagingBasicImpl(Slice.Builder builder, SliceSpec spec) {"
-        errorLine2="                                                     ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void add(TemplateBuilderImpl builder) {"
-        errorLine2="                    ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public TemplateBuilderImpl createMessageBuilder() {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public MessageBuilder(MessagingBasicImpl parent) {"
-        errorLine2="                              ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void addSource(Icon source) {"
-        errorLine2="                              ~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void addText(CharSequence text) {"
-        errorLine2="                            ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    void add(TemplateBuilderImpl builder);"
-        errorLine2="             ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    TemplateBuilderImpl createMessageBuilder();"
-        errorLine2="    ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        void addSource(Icon source);"
-        errorLine2="                       ~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        void addText(CharSequence text);"
-        errorLine2="                     ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public MessagingListV1Impl(Slice.Builder b, SliceSpec spec) {"
-        errorLine2="                               ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public MessagingListV1Impl(Slice.Builder b, SliceSpec spec) {"
-        errorLine2="                                                ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void add(TemplateBuilderImpl builder) {"
-        errorLine2="                    ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public TemplateBuilderImpl createMessageBuilder() {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public MessageBuilder(MessagingListV1Impl parent) {"
-        errorLine2="                              ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void addSource(Icon source) {"
-        errorLine2="                              ~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void addText(CharSequence text) {"
-        errorLine2="                            ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public MessagingSliceBuilder add(MessageBuilder builder) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public MessagingSliceBuilder add(MessageBuilder builder) {"
-        errorLine2="                                     ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public MessagingSliceBuilder add(Consumer&lt;MessageBuilder> c) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public MessagingSliceBuilder add(Consumer&lt;MessageBuilder> c) {"
-        errorLine2="                                     ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected TemplateBuilderImpl selectImpl() {"
-        errorLine2="              ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public MessageBuilder(MessagingSliceBuilder parent) {"
-        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public MessageBuilder addSource(Icon source) {"
-        errorLine2="               ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public MessageBuilder addSource(Icon source) {"
-        errorLine2="                                        ~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public MessageBuilder addSource(IconCompat source) {"
-        errorLine2="               ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public MessageBuilder addSource(IconCompat source) {"
-        errorLine2="                                        ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public MessageBuilder addText(CharSequence text) {"
-        errorLine2="               ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public MessageBuilder addText(CharSequence text) {"
-        errorLine2="                                      ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public MessageBuilder addTimestamp(long timestamp) {"
-        errorLine2="               ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/MessagingSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public MessagingV1Impl(Slice.Builder b, SliceSpec spec) {"
-        errorLine2="                           ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public MessagingV1Impl(Slice.Builder b, SliceSpec spec) {"
-        errorLine2="                                            ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void add(TemplateBuilderImpl builder) {"
-        errorLine2="                    ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public TemplateBuilderImpl createMessageBuilder() {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public MessageBuilder(MessagingV1Impl parent) {"
-        errorLine2="                              ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void addSource(Icon source) {"
-        errorLine2="                              ~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void addText(CharSequence text) {"
-        errorLine2="                            ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilder addOption(String optionKey, CharSequence optionText) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilder addOption(String optionKey, CharSequence optionText) {"
-        errorLine2="                                      ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilder addOption(String optionKey, CharSequence optionText) {"
-        errorLine2="                                                        ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilder setPrimaryAction(@NonNull SliceAction primaryAction) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilder setInputAction(@NonNull PendingIntent inputAction) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilder setInputAction(@NonNull RemoteCallback inputAction) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilder setSelectedOption(String selectedOption) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilder setSelectedOption(String selectedOption) {"
-        errorLine2="                                              ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilder setTitle(@Nullable CharSequence title) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilder setSubtitle(@Nullable CharSequence subtitle) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilder setContentDescription(@Nullable CharSequence contentDescription) {"
-        errorLine2="           ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilder setLayoutDirection("
-        errorLine2="           ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public List&lt;Pair&lt;String, CharSequence>> getOptions() {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceAction getPrimaryAction() {"
-        errorLine2="           ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public PendingIntent getInputAction() {"
-        errorLine2="           ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public String getSelectedOption() {"
-        errorLine2="           ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public CharSequence getTitle() {"
-        errorLine2="           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public CharSequence getSubtitle() {"
-        errorLine2="           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public CharSequence getContentDescription() {"
-        errorLine2="           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SelectionBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilderBasicImpl(Slice.Builder sliceBuilder,"
-        errorLine2="                                     ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/SelectionBuilderBasicImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="                                     SelectionBuilder selectionBuilder) {"
-        errorLine2="                                     ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/SelectionBuilderBasicImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilderImpl(Slice.Builder sliceBuilder,"
-        errorLine2="                                ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/SelectionBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="                                SelectionBuilder selectionBuilder) {"
-        errorLine2="                                ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/SelectionBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected SelectionBuilder getSelectionBuilder() {"
-        errorLine2="              ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/SelectionBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SelectionBuilderListV2Impl(Slice.Builder parentSliceBuilder,"
-        errorLine2="                                      ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/SelectionBuilderListV2Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="                                  SelectionBuilder selectionBuilder) {"
-        errorLine2="                                  ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/SelectionBuilderListV2Impl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static SliceAction create(@NonNull PendingIntent action,"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SliceAction.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static SliceAction create(@NonNull RemoteCallback action,"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SliceAction.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static SliceAction createDeeplink(@NonNull PendingIntent action,"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SliceAction.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static SliceAction createDeeplink(@NonNull RemoteCallback action,"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SliceAction.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static SliceAction createToggle(@NonNull PendingIntent action,"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SliceAction.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static SliceAction createToggle(@NonNull RemoteCallback action,"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SliceAction.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static SliceAction createToggle(@NonNull PendingIntent action,"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SliceAction.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static SliceAction createToggle(@NonNull RemoteCallback action,"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/SliceAction.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected TemplateBuilderImpl(Slice.Builder b, SliceSpec spec) {"
-        errorLine2="                                  ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected TemplateBuilderImpl(Slice.Builder b, SliceSpec spec) {"
-        errorLine2="                                                   ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected TemplateBuilderImpl(Slice.Builder b, SliceSpec spec, Clock clock) {"
-        errorLine2="                                  ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected TemplateBuilderImpl(Slice.Builder b, SliceSpec spec, Clock clock) {"
-        errorLine2="                                                   ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected TemplateBuilderImpl(Slice.Builder b, SliceSpec spec, Clock clock) {"
-        errorLine2="                                                                   ~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected void setBuilder(Slice.Builder builder) {"
-        errorLine2="                              ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public Slice build() {"
-        errorLine2="           ~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public Slice.Builder getBuilder() {"
-        errorLine2="           ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public Slice.Builder createChildBuilder() {"
-        errorLine2="           ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public Clock getClock() {"
-        errorLine2="           ~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceSpec getSpec() {"
-        errorLine2="           ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected TemplateSliceBuilder(TemplateBuilderImpl impl) {"
-        errorLine2="                                   ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/TemplateSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public TemplateSliceBuilder(Context context, Uri uri) {"
-        errorLine2="                                ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/TemplateSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public TemplateSliceBuilder(Context context, Uri uri) {"
-        errorLine2="                                                 ~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/TemplateSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected Slice.Builder getBuilder() {"
-        errorLine2="              ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/TemplateSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected TemplateBuilderImpl selectImpl() {"
-        errorLine2="              ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/TemplateSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected boolean checkCompatible(SliceSpec candidate) {"
-        errorLine2="                                      ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/TemplateSliceBuilder.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected Clock getClock() {"
-        errorLine2="              ~~~~~">
-        <location
-            file="src/main/java/androidx/slice/builders/TemplateSliceBuilder.java"/>
-    </issue>
-
 </issues>
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/GridRowBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/GridRowBuilder.java
index 4523685..dddc854 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/GridRowBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/GridRowBuilder.java
@@ -53,8 +53,13 @@
  * rest of the content, this will take up space as a cell item in a row if added.
  *
  * @see ListBuilder#addGridRow(GridRowBuilder)
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class GridRowBuilder {
 
     private final List<CellBuilder> mCells = new ArrayList<>();
@@ -256,7 +261,12 @@
      * @see ListBuilder#ICON_IMAGE
      * @see ListBuilder#SMALL_IMAGE
      * @see ListBuilder#ICON_IMAGE
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public static class CellBuilder {
         /**
          */
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/ListBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/ListBuilder.java
index 77d5f5b..1b27322 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/ListBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/ListBuilder.java
@@ -140,8 +140,13 @@
  * @see androidx.slice.SliceProvider
  * @see androidx.slice.SliceProvider#onBindSlice(Uri)
  * @see androidx.slice.widget.SliceView
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class ListBuilder extends TemplateSliceBuilder {
 
     private boolean mHasSeeMore;
@@ -565,7 +570,12 @@
      * A range row supports displaying a horizontal progress indicator.
      *
      * @see ListBuilder#addRange(RangeBuilder)
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public static class RangeBuilder {
 
         private int mValue;
@@ -817,8 +827,13 @@
      * An star rating row supports displaying a horizontal tappable stars allowing rating input.
      *
      * @see ListBuilder#addRating(RatingBuilder)
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
     @SuppressLint("MissingBuildMethod")
+    @Deprecated
     public static final class RatingBuilder {
         /**
          */
@@ -1085,7 +1100,12 @@
      * An input range row supports displaying a horizontal slider allowing slider input.
      *
      * @see ListBuilder#addInputRange(InputRangeBuilder)
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public static class InputRangeBuilder {
 
         private int mMin = 0;
@@ -1481,7 +1501,12 @@
      * </ul>
      *
      * @see ListBuilder#addRow(RowBuilder)
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public static class RowBuilder {
 
         private final Uri mUri;
@@ -2007,7 +2032,12 @@
      * @see ListBuilder#setHeader(HeaderBuilder)
      * @see ListBuilder#addAction(SliceAction)
      * @see SliceAction
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public static class HeaderBuilder {
         private final Uri mUri;
         private CharSequence mTitle;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/MessagingSliceBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/MessagingSliceBuilder.java
index 4041d50..b995566 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/MessagingSliceBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/MessagingSliceBuilder.java
@@ -41,6 +41,7 @@
  */
 @RestrictTo(LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class MessagingSliceBuilder extends TemplateSliceBuilder {
 
     /**
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/SelectionBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/SelectionBuilder.java
index 4e5550a..1e72a08 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/SelectionBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/SelectionBuilder.java
@@ -38,8 +38,13 @@
  *
  * A selection presents a list of options to the user and allows the user to select exactly one
  * option.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SelectionBuilder {
     private final List<Pair<String, CharSequence>> mOptions;
     private final Set<String> mOptionKeys;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/SliceAction.java b/slice/slice-builders/src/main/java/androidx/slice/builders/SliceAction.java
index 64263dd..bc06970 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/SliceAction.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/SliceAction.java
@@ -36,8 +36,13 @@
 /**
  * Class representing an action, supports tappable icons, custom toggle icons, and default
  * toggles, as well as date and time pickers.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SliceAction implements androidx.slice.core.SliceAction {
 
     private final SliceActionImpl mSliceAction;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/TemplateSliceBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/TemplateSliceBuilder.java
index 07accb6..f52b6d1 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/TemplateSliceBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/TemplateSliceBuilder.java
@@ -39,8 +39,13 @@
 
 /**
  * Base class of builders of various template types.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public abstract class TemplateSliceBuilder {
 
     private static final String TAG = "TemplateSliceBuilder";
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
index ec399bc..f7baa96 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
@@ -45,6 +45,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class GridRowBuilderListV1Impl extends TemplateBuilderImpl {
 
     private SliceAction mPrimaryAction;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java
index 8718ac9..d9f47d6 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java
@@ -42,6 +42,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public interface ListBuilder {
 
     /**
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java
index faf5588..5848e3b 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java
@@ -60,6 +60,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class ListBuilderBasicImpl extends TemplateBuilderImpl implements ListBuilder {
     boolean mIsError;
     private Set<String> mKeywords;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderImpl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderImpl.java
index 111bd0b..684f094 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderImpl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderImpl.java
@@ -85,6 +85,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class ListBuilderImpl extends TemplateBuilderImpl implements ListBuilder {
     private List<Slice> mSliceActions;
     private Set<String> mKeywords;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java
index 239e6fb..834b44a 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java
@@ -37,6 +37,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class MessagingBasicImpl extends TemplateBuilderImpl implements
         MessagingBuilder {
     private MessageBuilder mLastMessage;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBuilder.java
index 03f48ce..3741273 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBuilder.java
@@ -27,6 +27,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public interface MessagingBuilder {
     /**
      * Add a subslice to this builder.
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java
index 8dacf8f..90a82f7 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java
@@ -33,6 +33,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class MessagingListV1Impl extends TemplateBuilderImpl implements MessagingBuilder{
 
     private final ListBuilderImpl mListBuilder;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java
index d913d79..0adf579 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java
@@ -31,6 +31,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class MessagingV1Impl extends TemplateBuilderImpl implements MessagingBuilder {
 
     /**
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderBasicImpl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderBasicImpl.java
index 1c341f1..9dc3fca 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderBasicImpl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderBasicImpl.java
@@ -33,6 +33,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SelectionBuilderBasicImpl extends SelectionBuilderImpl {
     public SelectionBuilderBasicImpl(Slice.Builder sliceBuilder,
                                      SelectionBuilder selectionBuilder) {
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderImpl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderImpl.java
index f25d0a6..0647caf 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderImpl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderImpl.java
@@ -28,6 +28,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public abstract class SelectionBuilderImpl extends TemplateBuilderImpl {
     private final SelectionBuilder mSelectionBuilder;
 
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderListV2Impl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderListV2Impl.java
index 0c25ad0..1d2d673 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderListV2Impl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderListV2Impl.java
@@ -41,6 +41,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SelectionBuilderListV2Impl extends SelectionBuilderImpl {
     public SelectionBuilderListV2Impl(Slice.Builder parentSliceBuilder,
                                   SelectionBuilder selectionBuilder) {
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java
index 8cb26f66..a1be533 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java
@@ -43,6 +43,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public abstract class TemplateBuilderImpl {
 
     private Slice.Builder mSliceBuilder;
diff --git a/slice/slice-core/api/current.txt b/slice/slice-core/api/current.txt
index 6a6b471..bc4b051 100644
--- a/slice/slice-core/api/current.txt
+++ b/slice/slice-core/api/current.txt
@@ -1,87 +1,87 @@
 // Signature format: 4.0
 package androidx.slice {
 
-  @RequiresApi(19) public final class Slice implements androidx.versionedparcelable.VersionedParcelable {
-    method public java.util.List<java.lang.String!> getHints();
-    method public java.util.List<androidx.slice.SliceItem!> getItems();
-    method public android.net.Uri getUri();
-    field public static final String EXTRA_SELECTION = "android.app.slice.extra.SELECTION";
+  @Deprecated @RequiresApi(19) public final class Slice implements androidx.versionedparcelable.VersionedParcelable {
+    method @Deprecated public java.util.List<java.lang.String!> getHints();
+    method @Deprecated public java.util.List<androidx.slice.SliceItem!> getItems();
+    method @Deprecated public android.net.Uri getUri();
+    field @Deprecated public static final String EXTRA_SELECTION = "android.app.slice.extra.SELECTION";
   }
 
-  @RequiresApi(28) public class SliceConvert {
-    method public static android.app.slice.Slice? unwrap(androidx.slice.Slice?);
-    method public static androidx.slice.Slice? wrap(android.app.slice.Slice?, android.content.Context);
+  @Deprecated @RequiresApi(28) public class SliceConvert {
+    method @Deprecated public static android.app.slice.Slice? unwrap(androidx.slice.Slice?);
+    method @Deprecated public static androidx.slice.Slice? wrap(android.app.slice.Slice?, android.content.Context);
   }
 
-  @RequiresApi(19) public final class SliceItem implements androidx.versionedparcelable.VersionedParcelable {
-    method public static android.text.ParcelableSpan createSensitiveSpan();
-    method public void fireAction(android.content.Context?, android.content.Intent?) throws android.app.PendingIntent.CanceledException;
-    method public android.app.PendingIntent? getAction();
-    method public String getFormat();
-    method public java.util.List<java.lang.String!> getHints();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method public int getInt();
-    method public long getLong();
-    method public CharSequence? getRedactedText();
-    method public androidx.slice.Slice? getSlice();
-    method public String? getSubType();
-    method public CharSequence? getText();
-    method public boolean hasHint(String);
-    method public void onPostParceling();
-    method public void onPreParceling(boolean);
+  @Deprecated @RequiresApi(19) public final class SliceItem implements androidx.versionedparcelable.VersionedParcelable {
+    method @Deprecated public static android.text.ParcelableSpan createSensitiveSpan();
+    method @Deprecated public void fireAction(android.content.Context?, android.content.Intent?) throws android.app.PendingIntent.CanceledException;
+    method @Deprecated public android.app.PendingIntent? getAction();
+    method @Deprecated public String getFormat();
+    method @Deprecated public java.util.List<java.lang.String!> getHints();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated public int getInt();
+    method @Deprecated public long getLong();
+    method @Deprecated public CharSequence? getRedactedText();
+    method @Deprecated public androidx.slice.Slice? getSlice();
+    method @Deprecated public String? getSubType();
+    method @Deprecated public CharSequence? getText();
+    method @Deprecated public boolean hasHint(String);
+    method @Deprecated public void onPostParceling();
+    method @Deprecated public void onPreParceling(boolean);
   }
 
-  @RequiresApi(19) public abstract class SliceManager {
-    method public abstract int checkSlicePermission(android.net.Uri, int, int);
-    method public static androidx.slice.SliceManager getInstance(android.content.Context);
-    method public abstract java.util.List<android.net.Uri!> getPinnedSlices();
-    method public abstract void grantSlicePermission(String, android.net.Uri);
-    method public abstract void revokeSlicePermission(String, android.net.Uri);
+  @Deprecated @RequiresApi(19) public abstract class SliceManager {
+    method @Deprecated public abstract int checkSlicePermission(android.net.Uri, int, int);
+    method @Deprecated public static androidx.slice.SliceManager getInstance(android.content.Context);
+    method @Deprecated public abstract java.util.List<android.net.Uri!> getPinnedSlices();
+    method @Deprecated public abstract void grantSlicePermission(String, android.net.Uri);
+    method @Deprecated public abstract void revokeSlicePermission(String, android.net.Uri);
   }
 
-  public abstract class SliceProvider extends android.content.ContentProvider {
-    ctor public SliceProvider();
-    ctor public SliceProvider(java.lang.String!...);
-    method public final int bulkInsert(android.net.Uri, android.content.ContentValues![]);
-    method @RequiresApi(19) public final android.net.Uri? canonicalize(android.net.Uri);
-    method public final int delete(android.net.Uri, String?, String![]?);
-    method @RequiresApi(19) public java.util.List<android.net.Uri!> getPinnedSlices();
-    method public final String? getType(android.net.Uri);
-    method public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
-    method @RequiresApi(19) public abstract androidx.slice.Slice? onBindSlice(android.net.Uri);
-    method public final boolean onCreate();
-    method public android.app.PendingIntent? onCreatePermissionRequest(android.net.Uri, String);
-    method @RequiresApi(19) public abstract boolean onCreateSliceProvider();
-    method @RequiresApi(19) public java.util.Collection<android.net.Uri!> onGetSliceDescendants(android.net.Uri);
-    method @RequiresApi(19) public android.net.Uri onMapIntentToUri(android.content.Intent);
-    method @RequiresApi(19) public void onSlicePinned(android.net.Uri);
-    method @RequiresApi(19) public void onSliceUnpinned(android.net.Uri);
-    method @RequiresApi(28) public final android.database.Cursor? query(android.net.Uri, String![]?, android.os.Bundle?, android.os.CancellationSignal?);
-    method public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
-    method @RequiresApi(16) public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?, android.os.CancellationSignal?);
-    method public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+  @Deprecated public abstract class SliceProvider extends android.content.ContentProvider {
+    ctor @Deprecated public SliceProvider();
+    ctor @Deprecated public SliceProvider(java.lang.String!...);
+    method @Deprecated public final int bulkInsert(android.net.Uri, android.content.ContentValues![]);
+    method @Deprecated @RequiresApi(19) public final android.net.Uri? canonicalize(android.net.Uri);
+    method @Deprecated public final int delete(android.net.Uri, String?, String![]?);
+    method @Deprecated @RequiresApi(19) public java.util.List<android.net.Uri!> getPinnedSlices();
+    method @Deprecated public final String? getType(android.net.Uri);
+    method @Deprecated public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method @Deprecated @RequiresApi(19) public abstract androidx.slice.Slice? onBindSlice(android.net.Uri);
+    method @Deprecated public final boolean onCreate();
+    method @Deprecated public android.app.PendingIntent? onCreatePermissionRequest(android.net.Uri, String);
+    method @Deprecated @RequiresApi(19) public abstract boolean onCreateSliceProvider();
+    method @Deprecated @RequiresApi(19) public java.util.Collection<android.net.Uri!> onGetSliceDescendants(android.net.Uri);
+    method @Deprecated @RequiresApi(19) public android.net.Uri onMapIntentToUri(android.content.Intent);
+    method @Deprecated @RequiresApi(19) public void onSlicePinned(android.net.Uri);
+    method @Deprecated @RequiresApi(19) public void onSliceUnpinned(android.net.Uri);
+    method @Deprecated @RequiresApi(28) public final android.database.Cursor? query(android.net.Uri, String![]?, android.os.Bundle?, android.os.CancellationSignal?);
+    method @Deprecated public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method @Deprecated @RequiresApi(16) public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?, android.os.CancellationSignal?);
+    method @Deprecated public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
   }
 
 }
 
 package androidx.slice.core {
 
-  @RequiresApi(19) public interface SliceAction {
-    method public android.app.PendingIntent getAction();
-    method public CharSequence? getContentDescription();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method public int getImageMode();
-    method public String? getKey();
-    method public int getPriority();
-    method public CharSequence getTitle();
-    method public boolean isActivity();
-    method public boolean isChecked();
-    method public boolean isDefaultToggle();
-    method public boolean isToggle();
-    method public androidx.slice.core.SliceAction setChecked(boolean);
-    method public androidx.slice.core.SliceAction setContentDescription(CharSequence);
-    method public androidx.slice.core.SliceAction setKey(String);
-    method public androidx.slice.core.SliceAction setPriority(@IntRange(from=0) int);
+  @Deprecated @RequiresApi(19) public interface SliceAction {
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public CharSequence? getContentDescription();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated public int getImageMode();
+    method @Deprecated public String? getKey();
+    method @Deprecated public int getPriority();
+    method @Deprecated public CharSequence getTitle();
+    method @Deprecated public boolean isActivity();
+    method @Deprecated public boolean isChecked();
+    method @Deprecated public boolean isDefaultToggle();
+    method @Deprecated public boolean isToggle();
+    method @Deprecated public androidx.slice.core.SliceAction setChecked(boolean);
+    method @Deprecated public androidx.slice.core.SliceAction setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.core.SliceAction setKey(String);
+    method @Deprecated public androidx.slice.core.SliceAction setPriority(@IntRange(from=0) int);
   }
 
 }
diff --git a/slice/slice-core/api/restricted_current.txt b/slice/slice-core/api/restricted_current.txt
index 332afef..b000782 100644
--- a/slice/slice-core/api/restricted_current.txt
+++ b/slice/slice-core/api/restricted_current.txt
@@ -1,307 +1,307 @@
 // Signature format: 4.0
 package androidx.slice {
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Clock {
-    method public long currentTimeMillis();
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Clock {
+    method @Deprecated public long currentTimeMillis();
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class CornerDrawable extends android.graphics.drawable.InsetDrawable {
-    ctor public CornerDrawable(android.graphics.drawable.Drawable?, float);
+  @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class CornerDrawable extends android.graphics.drawable.InsetDrawable {
+    ctor @Deprecated public CornerDrawable(android.graphics.drawable.Drawable?, float);
   }
 
-  @RequiresApi(19) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true, isCustom=true) public final class Slice extends androidx.versionedparcelable.CustomVersionedParcelable implements androidx.versionedparcelable.VersionedParcelable {
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.slice.Slice? bindSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>?);
-    method public java.util.List<java.lang.String!> getHints();
-    method public java.util.List<androidx.slice.SliceItem!> getItems();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceSpec? getSpec();
-    method public android.net.Uri getUri();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean hasHint(String);
-    field public static final String EXTRA_SELECTION = "android.app.slice.extra.SELECTION";
-    field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final String SUBTYPE_RANGE_MODE = "range_mode";
+  @Deprecated @RequiresApi(19) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true, isCustom=true) public final class Slice extends androidx.versionedparcelable.CustomVersionedParcelable implements androidx.versionedparcelable.VersionedParcelable {
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.slice.Slice? bindSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>?);
+    method @Deprecated public java.util.List<java.lang.String!> getHints();
+    method @Deprecated public java.util.List<androidx.slice.SliceItem!> getItems();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceSpec? getSpec();
+    method @Deprecated public android.net.Uri getUri();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean hasHint(String);
+    field @Deprecated public static final String EXTRA_SELECTION = "android.app.slice.extra.SELECTION";
+    field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final String SUBTYPE_RANGE_MODE = "range_mode";
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static class Slice.Builder {
-    ctor public Slice.Builder(android.net.Uri);
-    ctor public Slice.Builder(androidx.slice.Slice.Builder);
-    method public androidx.slice.Slice.Builder addAction(android.app.PendingIntent, androidx.slice.Slice, String?);
-    method public androidx.slice.Slice.Builder addAction(androidx.slice.Slice, String?, androidx.slice.SliceItem.ActionHandler);
-    method public androidx.slice.Slice.Builder addHints(java.lang.String!...);
-    method public androidx.slice.Slice.Builder addHints(java.util.List<java.lang.String!>);
-    method public androidx.slice.Slice.Builder addIcon(androidx.core.graphics.drawable.IconCompat, String?, java.lang.String!...);
-    method public androidx.slice.Slice.Builder addIcon(androidx.core.graphics.drawable.IconCompat, String?, java.util.List<java.lang.String!>);
-    method public androidx.slice.Slice.Builder addInt(int, String?, java.lang.String!...);
-    method public androidx.slice.Slice.Builder addInt(int, String?, java.util.List<java.lang.String!>);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public androidx.slice.Slice.Builder addItem(androidx.slice.SliceItem);
-    method public androidx.slice.Slice.Builder addLong(long, String?, java.lang.String!...);
-    method public androidx.slice.Slice.Builder addLong(long, String?, java.util.List<java.lang.String!>);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.Slice.Builder addRemoteInput(android.app.RemoteInput, String?, java.lang.String!...);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.Slice.Builder addRemoteInput(android.app.RemoteInput, String?, java.util.List<java.lang.String!>);
-    method public androidx.slice.Slice.Builder addSubSlice(androidx.slice.Slice);
-    method public androidx.slice.Slice.Builder addSubSlice(androidx.slice.Slice, String?);
-    method public androidx.slice.Slice.Builder addText(CharSequence?, String?, java.lang.String!...);
-    method public androidx.slice.Slice.Builder addText(CharSequence?, String?, java.util.List<java.lang.String!>);
+  @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static class Slice.Builder {
+    ctor @Deprecated public Slice.Builder(android.net.Uri);
+    ctor @Deprecated public Slice.Builder(androidx.slice.Slice.Builder);
+    method @Deprecated public androidx.slice.Slice.Builder addAction(android.app.PendingIntent, androidx.slice.Slice, String?);
+    method @Deprecated public androidx.slice.Slice.Builder addAction(androidx.slice.Slice, String?, androidx.slice.SliceItem.ActionHandler);
+    method @Deprecated public androidx.slice.Slice.Builder addHints(java.lang.String!...);
+    method @Deprecated public androidx.slice.Slice.Builder addHints(java.util.List<java.lang.String!>);
+    method @Deprecated public androidx.slice.Slice.Builder addIcon(androidx.core.graphics.drawable.IconCompat, String?, java.lang.String!...);
+    method @Deprecated public androidx.slice.Slice.Builder addIcon(androidx.core.graphics.drawable.IconCompat, String?, java.util.List<java.lang.String!>);
+    method @Deprecated public androidx.slice.Slice.Builder addInt(int, String?, java.lang.String!...);
+    method @Deprecated public androidx.slice.Slice.Builder addInt(int, String?, java.util.List<java.lang.String!>);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public androidx.slice.Slice.Builder addItem(androidx.slice.SliceItem);
+    method @Deprecated public androidx.slice.Slice.Builder addLong(long, String?, java.lang.String!...);
+    method @Deprecated public androidx.slice.Slice.Builder addLong(long, String?, java.util.List<java.lang.String!>);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.Slice.Builder addRemoteInput(android.app.RemoteInput, String?, java.lang.String!...);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.Slice.Builder addRemoteInput(android.app.RemoteInput, String?, java.util.List<java.lang.String!>);
+    method @Deprecated public androidx.slice.Slice.Builder addSubSlice(androidx.slice.Slice);
+    method @Deprecated public androidx.slice.Slice.Builder addSubSlice(androidx.slice.Slice, String?);
+    method @Deprecated public androidx.slice.Slice.Builder addText(CharSequence?, String?, java.lang.String!...);
+    method @Deprecated public androidx.slice.Slice.Builder addText(CharSequence?, String?, java.util.List<java.lang.String!>);
     method @Deprecated public androidx.slice.Slice.Builder! addTimestamp(long, String?, java.lang.String!...);
-    method public androidx.slice.Slice.Builder addTimestamp(long, String?, java.util.List<java.lang.String!>);
-    method public androidx.slice.Slice build();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.Slice.Builder setSpec(androidx.slice.SliceSpec?);
+    method @Deprecated public androidx.slice.Slice.Builder addTimestamp(long, String?, java.util.List<java.lang.String!>);
+    method @Deprecated public androidx.slice.Slice build();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.Slice.Builder setSpec(androidx.slice.SliceSpec?);
   }
 
-  @RequiresApi(28) public class SliceConvert {
-    method public static android.app.slice.Slice? unwrap(androidx.slice.Slice?);
-    method public static androidx.slice.Slice? wrap(android.app.slice.Slice?, android.content.Context);
+  @Deprecated @RequiresApi(28) public class SliceConvert {
+    method @Deprecated public static android.app.slice.Slice? unwrap(androidx.slice.Slice?);
+    method @Deprecated public static androidx.slice.Slice? wrap(android.app.slice.Slice?, android.content.Context);
   }
 
-  @RequiresApi(19) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true, ignoreParcelables=true, isCustom=true) public final class SliceItem extends androidx.versionedparcelable.CustomVersionedParcelable {
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem();
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(android.app.PendingIntent, androidx.slice.Slice?, String, String?, String![]);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(androidx.slice.SliceItem.ActionHandler, androidx.slice.Slice?, String, String?, String![]);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(Object!, String, String?, String![]);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(Object!, String, String?, java.util.List<java.lang.String!>);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void addHint(String);
-    method public static android.text.ParcelableSpan createSensitiveSpan();
-    method public void fireAction(android.content.Context?, android.content.Intent?) throws android.app.PendingIntent.CanceledException;
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean fireActionInternal(android.content.Context?, android.content.Intent?) throws android.app.PendingIntent.CanceledException;
-    method public android.app.PendingIntent? getAction();
-    method public String getFormat();
-    method public java.util.List<java.lang.String!> getHints();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method public int getInt();
-    method public long getLong();
-    method public CharSequence? getRedactedText();
-    method @RequiresApi(20) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.app.RemoteInput? getRemoteInput();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public CharSequence? getSanitizedText();
-    method public androidx.slice.Slice? getSlice();
-    method public String? getSubType();
-    method public CharSequence? getText();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public boolean hasAnyHints(java.lang.String!...);
-    method public boolean hasHint(String);
+  @Deprecated @RequiresApi(19) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true, ignoreParcelables=true, isCustom=true) public final class SliceItem extends androidx.versionedparcelable.CustomVersionedParcelable {
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem();
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(android.app.PendingIntent, androidx.slice.Slice?, String, String?, String![]);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(androidx.slice.SliceItem.ActionHandler, androidx.slice.Slice?, String, String?, String![]);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(Object!, String, String?, String![]);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(Object!, String, String?, java.util.List<java.lang.String!>);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void addHint(String);
+    method @Deprecated public static android.text.ParcelableSpan createSensitiveSpan();
+    method @Deprecated public void fireAction(android.content.Context?, android.content.Intent?) throws android.app.PendingIntent.CanceledException;
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean fireActionInternal(android.content.Context?, android.content.Intent?) throws android.app.PendingIntent.CanceledException;
+    method @Deprecated public android.app.PendingIntent? getAction();
+    method @Deprecated public String getFormat();
+    method @Deprecated public java.util.List<java.lang.String!> getHints();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated public int getInt();
+    method @Deprecated public long getLong();
+    method @Deprecated public CharSequence? getRedactedText();
+    method @Deprecated @RequiresApi(20) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.app.RemoteInput? getRemoteInput();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public CharSequence? getSanitizedText();
+    method @Deprecated public androidx.slice.Slice? getSlice();
+    method @Deprecated public String? getSubType();
+    method @Deprecated public CharSequence? getText();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public boolean hasAnyHints(java.lang.String!...);
+    method @Deprecated public boolean hasHint(String);
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static interface SliceItem.ActionHandler {
-    method public void onAction(androidx.slice.SliceItem, android.content.Context?, android.content.Intent?);
+  @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static interface SliceItem.ActionHandler {
+    method @Deprecated public void onAction(androidx.slice.SliceItem, android.content.Context?, android.content.Intent?);
   }
 
-  @RequiresApi(19) public abstract class SliceManager {
-    method @androidx.core.content.PermissionChecker.PermissionResult public abstract int checkSlicePermission(android.net.Uri, int, int);
-    method public static androidx.slice.SliceManager getInstance(android.content.Context);
-    method public abstract java.util.List<android.net.Uri!> getPinnedSlices();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public abstract java.util.Set<androidx.slice.SliceSpec!> getPinnedSpecs(android.net.Uri);
-    method public abstract void grantSlicePermission(String, android.net.Uri);
-    method public abstract void revokeSlicePermission(String, android.net.Uri);
+  @Deprecated @RequiresApi(19) public abstract class SliceManager {
+    method @Deprecated @androidx.core.content.PermissionChecker.PermissionResult public abstract int checkSlicePermission(android.net.Uri, int, int);
+    method @Deprecated public static androidx.slice.SliceManager getInstance(android.content.Context);
+    method @Deprecated public abstract java.util.List<android.net.Uri!> getPinnedSlices();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public abstract java.util.Set<androidx.slice.SliceSpec!> getPinnedSpecs(android.net.Uri);
+    method @Deprecated public abstract void grantSlicePermission(String, android.net.Uri);
+    method @Deprecated public abstract void revokeSlicePermission(String, android.net.Uri);
   }
 
-  public abstract class SliceProvider extends android.content.ContentProvider implements androidx.core.app.CoreComponentFactory.CompatWrapped {
-    ctor public SliceProvider();
-    ctor public SliceProvider(java.lang.String!...);
-    method public final int bulkInsert(android.net.Uri, android.content.ContentValues![]);
-    method @RequiresApi(19) public final android.net.Uri? canonicalize(android.net.Uri);
-    method public final int delete(android.net.Uri, String?, String![]?);
-    method @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.slice.Clock? getClock();
-    method @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static java.util.Set<androidx.slice.SliceSpec!>? getCurrentSpecs();
-    method @RequiresApi(19) public java.util.List<android.net.Uri!> getPinnedSlices();
-    method public final String? getType(android.net.Uri);
+  @Deprecated public abstract class SliceProvider extends android.content.ContentProvider implements androidx.core.app.CoreComponentFactory.CompatWrapped {
+    ctor @Deprecated public SliceProvider();
+    ctor @Deprecated public SliceProvider(java.lang.String!...);
+    method @Deprecated public final int bulkInsert(android.net.Uri, android.content.ContentValues![]);
+    method @Deprecated @RequiresApi(19) public final android.net.Uri? canonicalize(android.net.Uri);
+    method @Deprecated public final int delete(android.net.Uri, String?, String![]?);
+    method @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.slice.Clock? getClock();
+    method @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static java.util.Set<androidx.slice.SliceSpec!>? getCurrentSpecs();
+    method @Deprecated @RequiresApi(19) public java.util.List<android.net.Uri!> getPinnedSlices();
+    method @Deprecated public final String? getType(android.net.Uri);
     method @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public Object? getWrapper();
-    method public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
-    method @RequiresApi(19) public abstract androidx.slice.Slice? onBindSlice(android.net.Uri);
-    method public final boolean onCreate();
-    method public android.app.PendingIntent? onCreatePermissionRequest(android.net.Uri, String);
-    method @RequiresApi(19) public abstract boolean onCreateSliceProvider();
-    method @RequiresApi(19) public java.util.Collection<android.net.Uri!> onGetSliceDescendants(android.net.Uri);
-    method @RequiresApi(19) public android.net.Uri onMapIntentToUri(android.content.Intent);
-    method @RequiresApi(19) public void onSlicePinned(android.net.Uri);
-    method @RequiresApi(19) public void onSliceUnpinned(android.net.Uri);
-    method @RequiresApi(28) public final android.database.Cursor? query(android.net.Uri, String![]?, android.os.Bundle?, android.os.CancellationSignal?);
-    method public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
-    method @RequiresApi(16) public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?, android.os.CancellationSignal?);
-    method public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+    method @Deprecated public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method @Deprecated @RequiresApi(19) public abstract androidx.slice.Slice? onBindSlice(android.net.Uri);
+    method @Deprecated public final boolean onCreate();
+    method @Deprecated public android.app.PendingIntent? onCreatePermissionRequest(android.net.Uri, String);
+    method @Deprecated @RequiresApi(19) public abstract boolean onCreateSliceProvider();
+    method @Deprecated @RequiresApi(19) public java.util.Collection<android.net.Uri!> onGetSliceDescendants(android.net.Uri);
+    method @Deprecated @RequiresApi(19) public android.net.Uri onMapIntentToUri(android.content.Intent);
+    method @Deprecated @RequiresApi(19) public void onSlicePinned(android.net.Uri);
+    method @Deprecated @RequiresApi(19) public void onSliceUnpinned(android.net.Uri);
+    method @Deprecated @RequiresApi(28) public final android.database.Cursor? query(android.net.Uri, String![]?, android.os.Bundle?, android.os.CancellationSignal?);
+    method @Deprecated public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method @Deprecated @RequiresApi(16) public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?, android.os.CancellationSignal?);
+    method @Deprecated public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true) public final class SliceSpec implements androidx.versionedparcelable.VersionedParcelable {
-    ctor public SliceSpec(String, int);
-    method public boolean canRender(androidx.slice.SliceSpec);
-    method public int getRevision();
-    method public String getType();
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true) public final class SliceSpec implements androidx.versionedparcelable.VersionedParcelable {
+    ctor @Deprecated public SliceSpec(String, int);
+    method @Deprecated public boolean canRender(androidx.slice.SliceSpec);
+    method @Deprecated public int getRevision();
+    method @Deprecated public String getType();
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceSpecs {
-    field public static final androidx.slice.SliceSpec! BASIC;
-    field public static final androidx.slice.SliceSpec! LIST;
-    field public static final androidx.slice.SliceSpec! LIST_V2;
-    field public static final androidx.slice.SliceSpec! MESSAGING;
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceSpecs {
+    field @Deprecated public static final androidx.slice.SliceSpec! BASIC;
+    field @Deprecated public static final androidx.slice.SliceSpec! LIST;
+    field @Deprecated public static final androidx.slice.SliceSpec! LIST_V2;
+    field @Deprecated public static final androidx.slice.SliceSpec! MESSAGING;
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SystemClock implements androidx.slice.Clock {
-    ctor public SystemClock();
-    method public long currentTimeMillis();
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SystemClock implements androidx.slice.Clock {
+    ctor @Deprecated public SystemClock();
+    method @Deprecated public long currentTimeMillis();
   }
 
 }
 
 package androidx.slice.compat {
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class CompatPermissionManager {
-    ctor public CompatPermissionManager(android.content.Context, String, int, String![]);
-    method public int checkSlicePermission(android.net.Uri, int, int);
-    method public void grantSlicePermission(android.net.Uri, String);
-    method public void revokeSlicePermission(android.net.Uri, String);
-    field public static final String ALL_SUFFIX = "_all";
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class CompatPermissionManager {
+    ctor @Deprecated public CompatPermissionManager(android.content.Context, String, int, String![]);
+    method @Deprecated public int checkSlicePermission(android.net.Uri, int, int);
+    method @Deprecated public void grantSlicePermission(android.net.Uri, String);
+    method @Deprecated public void revokeSlicePermission(android.net.Uri, String);
+    field @Deprecated public static final String ALL_SUFFIX = "_all";
   }
 
-  public static class CompatPermissionManager.PermissionState {
-    method public String getKey();
-    method public boolean hasAccess(java.util.List<java.lang.String!>);
-    method public boolean hasAllPermissions();
-    method public java.util.Set<java.lang.String!> toPersistable();
+  @Deprecated public static class CompatPermissionManager.PermissionState {
+    method @Deprecated public String getKey();
+    method @Deprecated public boolean hasAccess(java.util.List<java.lang.String!>);
+    method @Deprecated public boolean hasAllPermissions();
+    method @Deprecated public java.util.Set<java.lang.String!> toPersistable();
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class SliceProviderCompat {
-    ctor public SliceProviderCompat(androidx.slice.SliceProvider, androidx.slice.compat.CompatPermissionManager, android.content.Context);
-    method public static void addSpecs(android.os.Bundle, java.util.Set<androidx.slice.SliceSpec!>);
-    method public static androidx.slice.Slice? bindSlice(android.content.Context, android.content.Intent, java.util.Set<androidx.slice.SliceSpec!>);
-    method public static androidx.slice.Slice? bindSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>);
-    method public android.os.Bundle? call(String, String?, android.os.Bundle);
-    method public static int checkSlicePermission(android.content.Context, String?, android.net.Uri, int, int);
-    method public String? getCallingPackage();
-    method public static java.util.List<android.net.Uri!> getPinnedSlices(android.content.Context);
-    method public static java.util.Set<androidx.slice.SliceSpec!>? getPinnedSpecs(android.content.Context, android.net.Uri);
-    method public static java.util.Collection<android.net.Uri!> getSliceDescendants(android.content.Context, android.net.Uri);
-    method public static java.util.Set<androidx.slice.SliceSpec!> getSpecs(android.os.Bundle);
-    method public static void grantSlicePermission(android.content.Context, String?, String?, android.net.Uri);
-    method public static android.net.Uri? mapIntentToUri(android.content.Context, android.content.Intent);
-    method public static void pinSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>);
-    method public static void revokeSlicePermission(android.content.Context, String?, String?, android.net.Uri);
-    method public static void unpinSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>);
-    field public static final String ARG_SUPPORTS_VERSIONED_PARCELABLE = "supports_versioned_parcelable";
-    field public static final String EXTRA_BIND_URI = "slice_uri";
-    field public static final String EXTRA_INTENT = "slice_intent";
-    field public static final String EXTRA_PID = "pid";
-    field public static final String EXTRA_PKG = "pkg";
-    field public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
-    field public static final String EXTRA_RESULT = "result";
-    field public static final String EXTRA_SLICE = "slice";
-    field public static final String EXTRA_SLICE_DESCENDANTS = "slice_descendants";
-    field public static final String EXTRA_SUPPORTED_SPECS = "specs";
-    field public static final String EXTRA_SUPPORTED_SPECS_REVS = "revs";
-    field public static final String EXTRA_UID = "uid";
-    field public static final String METHOD_CHECK_PERMISSION = "check_perms";
-    field public static final String METHOD_GET_DESCENDANTS = "get_descendants";
-    field public static final String METHOD_GET_PINNED_SPECS = "get_specs";
-    field public static final String METHOD_GRANT_PERMISSION = "grant_perms";
-    field public static final String METHOD_MAP_INTENT = "map_slice";
-    field public static final String METHOD_MAP_ONLY_INTENT = "map_only";
-    field public static final String METHOD_PIN = "pin_slice";
-    field public static final String METHOD_REVOKE_PERMISSION = "revoke_perms";
-    field public static final String METHOD_SLICE = "bind_slice";
-    field public static final String METHOD_UNPIN = "unpin_slice";
-    field public static final String PERMS_PREFIX = "slice_perms_";
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class SliceProviderCompat {
+    ctor @Deprecated public SliceProviderCompat(androidx.slice.SliceProvider, androidx.slice.compat.CompatPermissionManager, android.content.Context);
+    method @Deprecated public static void addSpecs(android.os.Bundle, java.util.Set<androidx.slice.SliceSpec!>);
+    method @Deprecated public static androidx.slice.Slice? bindSlice(android.content.Context, android.content.Intent, java.util.Set<androidx.slice.SliceSpec!>);
+    method @Deprecated public static androidx.slice.Slice? bindSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>);
+    method @Deprecated public android.os.Bundle? call(String, String?, android.os.Bundle);
+    method @Deprecated public static int checkSlicePermission(android.content.Context, String?, android.net.Uri, int, int);
+    method @Deprecated public String? getCallingPackage();
+    method @Deprecated public static java.util.List<android.net.Uri!> getPinnedSlices(android.content.Context);
+    method @Deprecated public static java.util.Set<androidx.slice.SliceSpec!>? getPinnedSpecs(android.content.Context, android.net.Uri);
+    method @Deprecated public static java.util.Collection<android.net.Uri!> getSliceDescendants(android.content.Context, android.net.Uri);
+    method @Deprecated public static java.util.Set<androidx.slice.SliceSpec!> getSpecs(android.os.Bundle);
+    method @Deprecated public static void grantSlicePermission(android.content.Context, String?, String?, android.net.Uri);
+    method @Deprecated public static android.net.Uri? mapIntentToUri(android.content.Context, android.content.Intent);
+    method @Deprecated public static void pinSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>);
+    method @Deprecated public static void revokeSlicePermission(android.content.Context, String?, String?, android.net.Uri);
+    method @Deprecated public static void unpinSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>);
+    field @Deprecated public static final String ARG_SUPPORTS_VERSIONED_PARCELABLE = "supports_versioned_parcelable";
+    field @Deprecated public static final String EXTRA_BIND_URI = "slice_uri";
+    field @Deprecated public static final String EXTRA_INTENT = "slice_intent";
+    field @Deprecated public static final String EXTRA_PID = "pid";
+    field @Deprecated public static final String EXTRA_PKG = "pkg";
+    field @Deprecated public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
+    field @Deprecated public static final String EXTRA_RESULT = "result";
+    field @Deprecated public static final String EXTRA_SLICE = "slice";
+    field @Deprecated public static final String EXTRA_SLICE_DESCENDANTS = "slice_descendants";
+    field @Deprecated public static final String EXTRA_SUPPORTED_SPECS = "specs";
+    field @Deprecated public static final String EXTRA_SUPPORTED_SPECS_REVS = "revs";
+    field @Deprecated public static final String EXTRA_UID = "uid";
+    field @Deprecated public static final String METHOD_CHECK_PERMISSION = "check_perms";
+    field @Deprecated public static final String METHOD_GET_DESCENDANTS = "get_descendants";
+    field @Deprecated public static final String METHOD_GET_PINNED_SPECS = "get_specs";
+    field @Deprecated public static final String METHOD_GRANT_PERMISSION = "grant_perms";
+    field @Deprecated public static final String METHOD_MAP_INTENT = "map_slice";
+    field @Deprecated public static final String METHOD_MAP_ONLY_INTENT = "map_only";
+    field @Deprecated public static final String METHOD_PIN = "pin_slice";
+    field @Deprecated public static final String METHOD_REVOKE_PERMISSION = "revoke_perms";
+    field @Deprecated public static final String METHOD_SLICE = "bind_slice";
+    field @Deprecated public static final String METHOD_UNPIN = "unpin_slice";
+    field @Deprecated public static final String PERMS_PREFIX = "slice_perms_";
   }
 
 }
 
 package androidx.slice.core {
 
-  @RequiresApi(19) public interface SliceAction {
-    method public android.app.PendingIntent getAction();
-    method public CharSequence? getContentDescription();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method @androidx.slice.core.SliceHints.ImageMode public int getImageMode();
-    method public String? getKey();
-    method public int getPriority();
-    method public CharSequence getTitle();
-    method public boolean isActivity();
-    method public boolean isChecked();
-    method public boolean isDefaultToggle();
-    method public boolean isToggle();
-    method public androidx.slice.core.SliceAction setChecked(boolean);
-    method public androidx.slice.core.SliceAction setContentDescription(CharSequence);
-    method public androidx.slice.core.SliceAction setKey(String);
-    method public androidx.slice.core.SliceAction setPriority(@IntRange(from=0) int);
+  @Deprecated @RequiresApi(19) public interface SliceAction {
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public CharSequence? getContentDescription();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated @androidx.slice.core.SliceHints.ImageMode public int getImageMode();
+    method @Deprecated public String? getKey();
+    method @Deprecated public int getPriority();
+    method @Deprecated public CharSequence getTitle();
+    method @Deprecated public boolean isActivity();
+    method @Deprecated public boolean isChecked();
+    method @Deprecated public boolean isDefaultToggle();
+    method @Deprecated public boolean isToggle();
+    method @Deprecated public androidx.slice.core.SliceAction setChecked(boolean);
+    method @Deprecated public androidx.slice.core.SliceAction setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.core.SliceAction setKey(String);
+    method @Deprecated public androidx.slice.core.SliceAction setPriority(@IntRange(from=0) int);
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceActionImpl implements androidx.slice.core.SliceAction {
-    ctor public SliceActionImpl(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, @androidx.slice.core.SliceHints.ImageMode int, CharSequence);
-    ctor public SliceActionImpl(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence);
-    ctor public SliceActionImpl(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
-    ctor public SliceActionImpl(android.app.PendingIntent, CharSequence, boolean);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceActionImpl(androidx.slice.SliceItem);
-    method public androidx.slice.Slice buildPrimaryActionSlice(androidx.slice.Slice.Builder);
-    method public androidx.slice.Slice buildSlice(androidx.slice.Slice.Builder);
-    method public android.app.PendingIntent getAction();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceItem? getActionItem();
-    method public CharSequence? getContentDescription();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method @androidx.slice.core.SliceHints.ImageMode public int getImageMode();
-    method public String? getKey();
-    method public int getPriority();
-    method public androidx.slice.SliceItem? getSliceItem();
-    method public String? getSubtype();
-    method public CharSequence getTitle();
-    method public boolean isActivity();
-    method public boolean isChecked();
-    method public boolean isDefaultToggle();
-    method public boolean isToggle();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static int parseImageMode(androidx.slice.SliceItem);
-    method public void setActivity(boolean);
-    method public androidx.slice.core.SliceActionImpl setChecked(boolean);
-    method public androidx.slice.core.SliceAction? setContentDescription(CharSequence);
-    method public androidx.slice.core.SliceActionImpl setKey(String);
-    method public androidx.slice.core.SliceActionImpl setPriority(@IntRange(from=0) int);
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceActionImpl implements androidx.slice.core.SliceAction {
+    ctor @Deprecated public SliceActionImpl(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, @androidx.slice.core.SliceHints.ImageMode int, CharSequence);
+    ctor @Deprecated public SliceActionImpl(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence);
+    ctor @Deprecated public SliceActionImpl(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
+    ctor @Deprecated public SliceActionImpl(android.app.PendingIntent, CharSequence, boolean);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceActionImpl(androidx.slice.SliceItem);
+    method @Deprecated public androidx.slice.Slice buildPrimaryActionSlice(androidx.slice.Slice.Builder);
+    method @Deprecated public androidx.slice.Slice buildSlice(androidx.slice.Slice.Builder);
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceItem? getActionItem();
+    method @Deprecated public CharSequence? getContentDescription();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated @androidx.slice.core.SliceHints.ImageMode public int getImageMode();
+    method @Deprecated public String? getKey();
+    method @Deprecated public int getPriority();
+    method @Deprecated public androidx.slice.SliceItem? getSliceItem();
+    method @Deprecated public String? getSubtype();
+    method @Deprecated public CharSequence getTitle();
+    method @Deprecated public boolean isActivity();
+    method @Deprecated public boolean isChecked();
+    method @Deprecated public boolean isDefaultToggle();
+    method @Deprecated public boolean isToggle();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static int parseImageMode(androidx.slice.SliceItem);
+    method @Deprecated public void setActivity(boolean);
+    method @Deprecated public androidx.slice.core.SliceActionImpl setChecked(boolean);
+    method @Deprecated public androidx.slice.core.SliceAction? setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.core.SliceActionImpl setKey(String);
+    method @Deprecated public androidx.slice.core.SliceActionImpl setPriority(@IntRange(from=0) int);
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceHints {
-    field public static final int ACTION_WITH_LABEL = 6; // 0x6
-    field public static final int DETERMINATE_RANGE = 0; // 0x0
-    field public static final String HINT_ACTIVITY = "activity";
-    field public static final String HINT_CACHED = "cached";
-    field public static final String HINT_END_OF_SECTION = "end_of_section";
-    field public static final String HINT_OVERLAY = "overlay";
-    field public static final String HINT_RAW = "raw";
-    field public static final String HINT_SELECTION_OPTION = "selection_option";
-    field public static final String HINT_SHOW_LABEL = "show_label";
-    field public static final int ICON_IMAGE = 0; // 0x0
-    field public static final int INDETERMINATE_RANGE = 1; // 0x1
-    field public static final long INFINITY = -1L; // 0xffffffffffffffffL
-    field public static final int LARGE_IMAGE = 2; // 0x2
-    field public static final int RAW_IMAGE_LARGE = 4; // 0x4
-    field public static final int RAW_IMAGE_SMALL = 3; // 0x3
-    field public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
-    field public static final int SMALL_IMAGE = 1; // 0x1
-    field public static final int STAR_RATING = 2; // 0x2
-    field public static final String SUBTYPE_ACTION_KEY = "action_key";
-    field public static final String SUBTYPE_DATE_PICKER = "date_picker";
-    field public static final String SUBTYPE_HOST_EXTRAS = "host_extras";
-    field public static final String SUBTYPE_MILLIS = "millis";
-    field public static final String SUBTYPE_MIN = "min";
-    field public static final String SUBTYPE_SELECTION = "selection";
-    field public static final String SUBTYPE_SELECTION_OPTION_KEY = "selection_option_key";
-    field public static final String SUBTYPE_SELECTION_OPTION_VALUE = "selection_option_value";
-    field public static final String SUBTYPE_TIME_PICKER = "time_picker";
-    field public static final int UNKNOWN_IMAGE = 5; // 0x5
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceHints {
+    field @Deprecated public static final int ACTION_WITH_LABEL = 6; // 0x6
+    field @Deprecated public static final int DETERMINATE_RANGE = 0; // 0x0
+    field @Deprecated public static final String HINT_ACTIVITY = "activity";
+    field @Deprecated public static final String HINT_CACHED = "cached";
+    field @Deprecated public static final String HINT_END_OF_SECTION = "end_of_section";
+    field @Deprecated public static final String HINT_OVERLAY = "overlay";
+    field @Deprecated public static final String HINT_RAW = "raw";
+    field @Deprecated public static final String HINT_SELECTION_OPTION = "selection_option";
+    field @Deprecated public static final String HINT_SHOW_LABEL = "show_label";
+    field @Deprecated public static final int ICON_IMAGE = 0; // 0x0
+    field @Deprecated public static final int INDETERMINATE_RANGE = 1; // 0x1
+    field @Deprecated public static final long INFINITY = -1L; // 0xffffffffffffffffL
+    field @Deprecated public static final int LARGE_IMAGE = 2; // 0x2
+    field @Deprecated public static final int RAW_IMAGE_LARGE = 4; // 0x4
+    field @Deprecated public static final int RAW_IMAGE_SMALL = 3; // 0x3
+    field @Deprecated public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
+    field @Deprecated public static final int SMALL_IMAGE = 1; // 0x1
+    field @Deprecated public static final int STAR_RATING = 2; // 0x2
+    field @Deprecated public static final String SUBTYPE_ACTION_KEY = "action_key";
+    field @Deprecated public static final String SUBTYPE_DATE_PICKER = "date_picker";
+    field @Deprecated public static final String SUBTYPE_HOST_EXTRAS = "host_extras";
+    field @Deprecated public static final String SUBTYPE_MILLIS = "millis";
+    field @Deprecated public static final String SUBTYPE_MIN = "min";
+    field @Deprecated public static final String SUBTYPE_SELECTION = "selection";
+    field @Deprecated public static final String SUBTYPE_SELECTION_OPTION_KEY = "selection_option_key";
+    field @Deprecated public static final String SUBTYPE_SELECTION_OPTION_VALUE = "selection_option_value";
+    field @Deprecated public static final String SUBTYPE_TIME_PICKER = "time_picker";
+    field @Deprecated public static final int UNKNOWN_IMAGE = 5; // 0x5
   }
 
-  @IntDef({androidx.slice.core.SliceHints.LARGE_IMAGE, androidx.slice.core.SliceHints.SMALL_IMAGE, androidx.slice.core.SliceHints.ICON_IMAGE, androidx.slice.core.SliceHints.RAW_IMAGE_SMALL, androidx.slice.core.SliceHints.RAW_IMAGE_LARGE, androidx.slice.core.SliceHints.UNKNOWN_IMAGE, androidx.slice.core.SliceHints.ACTION_WITH_LABEL}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceHints.ImageMode {
+  @Deprecated @IntDef({androidx.slice.core.SliceHints.LARGE_IMAGE, androidx.slice.core.SliceHints.SMALL_IMAGE, androidx.slice.core.SliceHints.ICON_IMAGE, androidx.slice.core.SliceHints.RAW_IMAGE_SMALL, androidx.slice.core.SliceHints.RAW_IMAGE_LARGE, androidx.slice.core.SliceHints.UNKNOWN_IMAGE, androidx.slice.core.SliceHints.ACTION_WITH_LABEL}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceHints.ImageMode {
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceQuery {
-    method public static androidx.slice.SliceItem? find(androidx.slice.Slice?, String?);
-    method public static androidx.slice.SliceItem? find(androidx.slice.Slice?, String?, String?, String?);
-    method public static androidx.slice.SliceItem? find(androidx.slice.Slice?, String?, String![]?, String![]?);
-    method public static androidx.slice.SliceItem? find(androidx.slice.SliceItem?, String?);
-    method public static androidx.slice.SliceItem? find(androidx.slice.SliceItem?, String?, String?, String?);
-    method public static androidx.slice.SliceItem? find(androidx.slice.SliceItem?, String?, String![]?, String![]?);
-    method public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.Slice, String?, String?, String?);
-    method public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.Slice, String?, String![]?, String![]?);
-    method public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.SliceItem, String?);
-    method public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.SliceItem, String?, String?, String?);
-    method public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.SliceItem, String?, String![]?, String![]?);
-    method public static androidx.slice.SliceItem? findItem(androidx.slice.Slice, android.net.Uri);
-    method public static androidx.slice.SliceItem? findNotContaining(androidx.slice.SliceItem?, java.util.List<androidx.slice.SliceItem!>);
-    method public static androidx.slice.SliceItem? findSubtype(androidx.slice.Slice?, String?, String?);
-    method public static androidx.slice.SliceItem? findSubtype(androidx.slice.SliceItem?, String?, String?);
-    method public static androidx.slice.SliceItem? findTopLevelItem(androidx.slice.Slice, String?, String?, String![]?, String![]?);
-    method public static boolean hasAnyHints(androidx.slice.SliceItem, java.lang.String!...);
-    method public static boolean hasHints(androidx.slice.Slice, java.lang.String!...);
-    method public static boolean hasHints(androidx.slice.SliceItem, java.lang.String!...);
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceQuery {
+    method @Deprecated public static androidx.slice.SliceItem? find(androidx.slice.Slice?, String?);
+    method @Deprecated public static androidx.slice.SliceItem? find(androidx.slice.Slice?, String?, String?, String?);
+    method @Deprecated public static androidx.slice.SliceItem? find(androidx.slice.Slice?, String?, String![]?, String![]?);
+    method @Deprecated public static androidx.slice.SliceItem? find(androidx.slice.SliceItem?, String?);
+    method @Deprecated public static androidx.slice.SliceItem? find(androidx.slice.SliceItem?, String?, String?, String?);
+    method @Deprecated public static androidx.slice.SliceItem? find(androidx.slice.SliceItem?, String?, String![]?, String![]?);
+    method @Deprecated public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.Slice, String?, String?, String?);
+    method @Deprecated public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.Slice, String?, String![]?, String![]?);
+    method @Deprecated public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.SliceItem, String?);
+    method @Deprecated public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.SliceItem, String?, String?, String?);
+    method @Deprecated public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.SliceItem, String?, String![]?, String![]?);
+    method @Deprecated public static androidx.slice.SliceItem? findItem(androidx.slice.Slice, android.net.Uri);
+    method @Deprecated public static androidx.slice.SliceItem? findNotContaining(androidx.slice.SliceItem?, java.util.List<androidx.slice.SliceItem!>);
+    method @Deprecated public static androidx.slice.SliceItem? findSubtype(androidx.slice.Slice?, String?, String?);
+    method @Deprecated public static androidx.slice.SliceItem? findSubtype(androidx.slice.SliceItem?, String?, String?);
+    method @Deprecated public static androidx.slice.SliceItem? findTopLevelItem(androidx.slice.Slice, String?, String?, String![]?, String![]?);
+    method @Deprecated public static boolean hasAnyHints(androidx.slice.SliceItem, java.lang.String!...);
+    method @Deprecated public static boolean hasHints(androidx.slice.Slice, java.lang.String!...);
+    method @Deprecated public static boolean hasHints(androidx.slice.SliceItem, java.lang.String!...);
   }
 
 }
diff --git a/slice/slice-core/lint-baseline.xml b/slice/slice-core/lint-baseline.xml
index f6b771e..4b5f8f1 100644
--- a/slice/slice-core/lint-baseline.xml
+++ b/slice/slice-core/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-beta02" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta02)" variant="all" version="8.1.0-beta02">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="BanUncheckedReflection"
@@ -10,85 +10,4 @@
             file="src/main/java/androidx/slice/SliceManagerWrapper.java"/>
     </issue>
 
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public SliceProviderWrapper(androidx.slice.SliceProvider provider,"
-        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="                String[] autoGrantPermissions) {"
-        errorLine2="                ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public PendingIntent onCreatePermissionRequest(Uri sliceUri) {"
-        errorLine2="               ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Bundle call(String method, String arg, Bundle extras) {"
-        errorLine2="               ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Bundle call(String method, String arg, Bundle extras) {"
-        errorLine2="                           ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Bundle call(String method, String arg, Bundle extras) {"
-        errorLine2="                                          ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Bundle call(String method, String arg, Bundle extras) {"
-        errorLine2="                                                      ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Collection&lt;Uri> onGetSliceDescendants(Uri uri) {"
-        errorLine2="               ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Collection&lt;Uri> onGetSliceDescendants(Uri uri) {"
-        errorLine2="                                                     ~~~">
-        <location
-            file="src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java"/>
-    </issue>
-
 </issues>
diff --git a/slice/slice-core/src/main/java/androidx/slice/ArrayUtils.java b/slice/slice-core/src/main/java/androidx/slice/ArrayUtils.java
index e5dd970..82d9b0a 100644
--- a/slice/slice-core/src/main/java/androidx/slice/ArrayUtils.java
+++ b/slice/slice-core/src/main/java/androidx/slice/ArrayUtils.java
@@ -27,6 +27,7 @@
  */
 @RestrictTo(Scope.LIBRARY_GROUP)
 @RequiresApi(19)
+@Deprecated
 class ArrayUtils {
 
     public static <T> boolean contains(T[] array, T item) {
diff --git a/slice/slice-core/src/main/java/androidx/slice/Clock.java b/slice/slice-core/src/main/java/androidx/slice/Clock.java
index f68560c..fc5c059 100644
--- a/slice/slice-core/src/main/java/androidx/slice/Clock.java
+++ b/slice/slice-core/src/main/java/androidx/slice/Clock.java
@@ -25,6 +25,7 @@
  */
 @RestrictTo(LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public interface Clock {
     long currentTimeMillis();
 }
diff --git a/slice/slice-core/src/main/java/androidx/slice/CornerDrawable.java b/slice/slice-core/src/main/java/androidx/slice/CornerDrawable.java
index 9ccb915..740cb1c 100644
--- a/slice/slice-core/src/main/java/androidx/slice/CornerDrawable.java
+++ b/slice/slice-core/src/main/java/androidx/slice/CornerDrawable.java
@@ -32,6 +32,7 @@
  *
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Deprecated
 public class CornerDrawable extends InsetDrawable {
     private float mCornerRadius;
     private final Path mPath = new Path();
diff --git a/slice/slice-core/src/main/java/androidx/slice/Slice.java b/slice/slice-core/src/main/java/androidx/slice/Slice.java
index 8044c1e..b9ce51d 100644
--- a/slice/slice-core/src/main/java/androidx/slice/Slice.java
+++ b/slice/slice-core/src/main/java/androidx/slice/Slice.java
@@ -90,9 +90,14 @@
  * <p>Slices are constructed using {@link androidx.slice.builders.TemplateSliceBuilder}s
  * in a tree structure that provides the OS some information about how the content should be
  * displayed.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @VersionedParcelize(allowSerialization = true, isCustom = true)
 @RequiresApi(19)
+@Deprecated
 public final class Slice extends CustomVersionedParcelable implements VersionedParcelable {
 
     /**
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceConvert.java b/slice/slice-core/src/main/java/androidx/slice/SliceConvert.java
index 3711e65..abae809e 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceConvert.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceConvert.java
@@ -43,8 +43,13 @@
 /**
  * Convert between {@link androidx.slice.Slice androidx.slice.Slice} and
  * {@link android.app.slice.Slice android.app.slice.Slice}
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(28)
+@Deprecated
 public class SliceConvert {
 
     private static final String TAG = "SliceConvert";
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceItem.java b/slice/slice-core/src/main/java/androidx/slice/SliceItem.java
index c35fdef..47d353c 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceItem.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceItem.java
@@ -86,9 +86,14 @@
  * The hints that a {@link SliceItem} are a set of strings which annotate
  * the content. The hints that are guaranteed to be understood by the system
  * are defined on {@link Slice}.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @VersionedParcelize(allowSerialization = true, ignoreParcelables = true, isCustom = true)
 @RequiresApi(19)
+@Deprecated
 public final class SliceItem extends CustomVersionedParcelable {
 
     private static final String HINTS = "hints";
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceItemHolder.java b/slice/slice-core/src/main/java/androidx/slice/SliceItemHolder.java
index ab2ebba..4e822a3 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceItemHolder.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceItemHolder.java
@@ -50,6 +50,7 @@
 @VersionedParcelize(allowSerialization = true, ignoreParcelables = true,
         factory = SliceItemHolder.SliceItemPool.class)
 @RequiresApi(19)
+@Deprecated
 public class SliceItemHolder implements VersionedParcelable {
 
     public static final Object sSerializeLock = new Object();
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceManager.java b/slice/slice-core/src/main/java/androidx/slice/SliceManager.java
index 390f09a..eff8508 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceManager.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceManager.java
@@ -33,8 +33,13 @@
  * Class to handle interactions with {@link Slice}s.
  * <p>
  * The SliceViewManager manages permissions and pinned state for slices.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public abstract class SliceManager {
 
     /**
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceManagerCompat.java b/slice/slice-core/src/main/java/androidx/slice/SliceManagerCompat.java
index 549d939..89b8ba8 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceManagerCompat.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceManagerCompat.java
@@ -32,6 +32,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 class SliceManagerCompat extends SliceManager {
 
     private final Context mContext;
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceManagerWrapper.java b/slice/slice-core/src/main/java/androidx/slice/SliceManagerWrapper.java
index d1bd08e..7d86844 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceManagerWrapper.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceManagerWrapper.java
@@ -36,6 +36,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(api = 28)
+@Deprecated
 class SliceManagerWrapper extends SliceManager {
 
     private final android.app.slice.SliceManager mManager;
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java b/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java
index 9cd8ddc..399ba9f 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java
@@ -130,7 +130,12 @@
  * </pre>
  *
  * @see Slice
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public abstract class SliceProvider extends ContentProvider implements
         CoreComponentFactory.CompatWrapped {
 
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceSpec.java b/slice/slice-core/src/main/java/androidx/slice/SliceSpec.java
index 13bacd9..5c7b61f 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceSpec.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceSpec.java
@@ -45,6 +45,7 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @VersionedParcelize(allowSerialization = true)
 @RequiresApi(19)
+@Deprecated
 public final class SliceSpec implements VersionedParcelable {
 
     @ParcelField(1)
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceSpecs.java b/slice/slice-core/src/main/java/androidx/slice/SliceSpecs.java
index 436d24b..e07a46f 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceSpecs.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceSpecs.java
@@ -24,6 +24,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class SliceSpecs {
 
     /**
diff --git a/slice/slice-core/src/main/java/androidx/slice/SystemClock.java b/slice/slice-core/src/main/java/androidx/slice/SystemClock.java
index 888d3e4..c3f5ac3 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SystemClock.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SystemClock.java
@@ -25,6 +25,7 @@
  */
 @RestrictTo(LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class SystemClock implements Clock {
     @Override
     public long currentTimeMillis() {
diff --git a/slice/slice-core/src/main/java/androidx/slice/compat/CompatPermissionManager.java b/slice/slice-core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
index dc700dd..b5ee3db 100644
--- a/slice/slice-core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
+++ b/slice/slice-core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
@@ -40,6 +40,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @RequiresApi(19)
+@Deprecated
 public class CompatPermissionManager {
     public static final String ALL_SUFFIX = "_all";
 
diff --git a/slice/slice-core/src/main/java/androidx/slice/compat/CompatPinnedList.java b/slice/slice-core/src/main/java/androidx/slice/compat/CompatPinnedList.java
index f9de140..67dea43 100644
--- a/slice/slice-core/src/main/java/androidx/slice/compat/CompatPinnedList.java
+++ b/slice/slice-core/src/main/java/androidx/slice/compat/CompatPinnedList.java
@@ -41,6 +41,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class CompatPinnedList {
 
     private static final String LAST_BOOT = "last_boot";
diff --git a/slice/slice-core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java b/slice/slice-core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
index 9d704cc..87711b7 100644
--- a/slice/slice-core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
+++ b/slice/slice-core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
@@ -43,6 +43,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SlicePermissionActivity extends AppCompatActivity implements OnClickListener,
         OnDismissListener {
 
diff --git a/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderCompat.java b/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
index a03e977..24a73f3 100644
--- a/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
+++ b/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
@@ -67,6 +67,7 @@
  */
 @RestrictTo(Scope.LIBRARY_GROUP)
 @RequiresApi(19)
+@Deprecated
 public class SliceProviderCompat {
     public static final String PERMS_PREFIX = "slice_perms_";
     private static final String TAG = "SliceProviderCompat";
diff --git a/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java b/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
index b24671f..69207a4 100644
--- a/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
+++ b/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
@@ -43,6 +43,7 @@
 /**
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
+@Deprecated
 public class SliceProviderWrapperContainer {
 
     /**
diff --git a/slice/slice-core/src/main/java/androidx/slice/core/SliceAction.java b/slice/slice-core/src/main/java/androidx/slice/core/SliceAction.java
index 6c47667..0df5949 100644
--- a/slice/slice-core/src/main/java/androidx/slice/core/SliceAction.java
+++ b/slice/slice-core/src/main/java/androidx/slice/core/SliceAction.java
@@ -26,8 +26,13 @@
 
 /**
  * Interface for a slice action, supports tappable icons, custom toggle icons, and default toggles.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public interface SliceAction {
 
     /**
diff --git a/slice/slice-core/src/main/java/androidx/slice/core/SliceActionImpl.java b/slice/slice-core/src/main/java/androidx/slice/core/SliceActionImpl.java
index 503cdbf..cd69d0e 100644
--- a/slice/slice-core/src/main/java/androidx/slice/core/SliceActionImpl.java
+++ b/slice/slice-core/src/main/java/androidx/slice/core/SliceActionImpl.java
@@ -64,6 +64,7 @@
  */
 @RestrictTo(LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class SliceActionImpl implements SliceAction {
 
     // Either mAction or mActionItem must be non-null.
diff --git a/slice/slice-core/src/main/java/androidx/slice/core/SliceHints.java b/slice/slice-core/src/main/java/androidx/slice/core/SliceHints.java
index e08441b..29e0353 100644
--- a/slice/slice-core/src/main/java/androidx/slice/core/SliceHints.java
+++ b/slice/slice-core/src/main/java/androidx/slice/core/SliceHints.java
@@ -31,6 +31,7 @@
  */
 @RestrictTo(LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class SliceHints {
 
     /**
diff --git a/slice/slice-core/src/main/java/androidx/slice/core/SliceQuery.java b/slice/slice-core/src/main/java/androidx/slice/core/SliceQuery.java
index 3111e1b..f4d3121 100644
--- a/slice/slice-core/src/main/java/androidx/slice/core/SliceQuery.java
+++ b/slice/slice-core/src/main/java/androidx/slice/core/SliceQuery.java
@@ -40,6 +40,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class SliceQuery {
 
     /**
diff --git a/slice/slice-view/api/current.txt b/slice/slice-view/api/current.txt
index 91918d9..0b172e8 100644
--- a/slice/slice-view/api/current.txt
+++ b/slice/slice-view/api/current.txt
@@ -1,235 +1,235 @@
 // Signature format: 4.0
 package androidx.slice {
 
-  @RequiresApi(19) public class SliceMetadata {
-    method public static androidx.slice.SliceMetadata from(android.content.Context?, androidx.slice.Slice);
-    method public long getExpiry();
-    method public int getHeaderType();
-    method public android.os.Bundle getHostExtras();
-    method public android.app.PendingIntent? getInputRangeAction();
-    method public long getLastUpdatedTime();
-    method public int getLoadingState();
-    method public androidx.slice.core.SliceAction? getPrimaryAction();
-    method public androidx.core.util.Pair<java.lang.Integer!,java.lang.Integer!>? getRange();
-    method public int getRangeValue();
-    method public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
-    method public java.util.List<java.lang.String!>? getSliceKeywords();
-    method public CharSequence? getSubtitle();
-    method public CharSequence? getSummary();
-    method public CharSequence? getTitle();
-    method public java.util.List<androidx.slice.core.SliceAction!>! getToggles();
-    method public boolean hasLargeMode();
-    method public boolean isCachedSlice();
-    method public boolean isErrorSlice();
-    method public boolean isPermissionSlice();
-    method public boolean isSelection();
-    method public boolean sendInputRangeAction(int) throws android.app.PendingIntent.CanceledException;
-    method public boolean sendToggleAction(androidx.slice.core.SliceAction!, boolean) throws android.app.PendingIntent.CanceledException;
-    field public static final int LOADED_ALL = 2; // 0x2
-    field public static final int LOADED_NONE = 0; // 0x0
-    field public static final int LOADED_PARTIAL = 1; // 0x1
+  @Deprecated @RequiresApi(19) public class SliceMetadata {
+    method @Deprecated public static androidx.slice.SliceMetadata from(android.content.Context?, androidx.slice.Slice);
+    method @Deprecated public long getExpiry();
+    method @Deprecated public int getHeaderType();
+    method @Deprecated public android.os.Bundle getHostExtras();
+    method @Deprecated public android.app.PendingIntent? getInputRangeAction();
+    method @Deprecated public long getLastUpdatedTime();
+    method @Deprecated public int getLoadingState();
+    method @Deprecated public androidx.slice.core.SliceAction? getPrimaryAction();
+    method @Deprecated public androidx.core.util.Pair<java.lang.Integer!,java.lang.Integer!>? getRange();
+    method @Deprecated public int getRangeValue();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
+    method @Deprecated public java.util.List<java.lang.String!>? getSliceKeywords();
+    method @Deprecated public CharSequence? getSubtitle();
+    method @Deprecated public CharSequence? getSummary();
+    method @Deprecated public CharSequence? getTitle();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>! getToggles();
+    method @Deprecated public boolean hasLargeMode();
+    method @Deprecated public boolean isCachedSlice();
+    method @Deprecated public boolean isErrorSlice();
+    method @Deprecated public boolean isPermissionSlice();
+    method @Deprecated public boolean isSelection();
+    method @Deprecated public boolean sendInputRangeAction(int) throws android.app.PendingIntent.CanceledException;
+    method @Deprecated public boolean sendToggleAction(androidx.slice.core.SliceAction!, boolean) throws android.app.PendingIntent.CanceledException;
+    field @Deprecated public static final int LOADED_ALL = 2; // 0x2
+    field @Deprecated public static final int LOADED_NONE = 0; // 0x0
+    field @Deprecated public static final int LOADED_PARTIAL = 1; // 0x1
   }
 
-  @RequiresApi(19) public class SliceStructure {
-    ctor public SliceStructure(androidx.slice.Slice!);
+  @Deprecated @RequiresApi(19) public class SliceStructure {
+    ctor @Deprecated public SliceStructure(androidx.slice.Slice!);
   }
 
-  @RequiresApi(19) public class SliceUtils {
-    method public static androidx.slice.Slice parseSlice(android.content.Context, java.io.InputStream, String, androidx.slice.SliceUtils.SliceActionListener) throws java.io.IOException, androidx.slice.SliceUtils.SliceParseException;
-    method public static void serializeSlice(androidx.slice.Slice, android.content.Context, java.io.OutputStream, androidx.slice.SliceUtils.SerializeOptions) throws java.lang.IllegalArgumentException;
-    method public static androidx.slice.Slice stripSlice(androidx.slice.Slice, int, boolean);
+  @Deprecated @RequiresApi(19) public class SliceUtils {
+    method @Deprecated public static androidx.slice.Slice parseSlice(android.content.Context, java.io.InputStream, String, androidx.slice.SliceUtils.SliceActionListener) throws java.io.IOException, androidx.slice.SliceUtils.SliceParseException;
+    method @Deprecated public static void serializeSlice(androidx.slice.Slice, android.content.Context, java.io.OutputStream, androidx.slice.SliceUtils.SerializeOptions) throws java.lang.IllegalArgumentException;
+    method @Deprecated public static androidx.slice.Slice stripSlice(androidx.slice.Slice, int, boolean);
   }
 
-  public static class SliceUtils.SerializeOptions {
-    ctor public SliceUtils.SerializeOptions();
-    method public androidx.slice.SliceUtils.SerializeOptions! setActionMode(int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setImageConversionFormat(android.graphics.Bitmap.CompressFormat!, int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setImageMode(int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setMaxImageHeight(int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setMaxImageWidth(int);
-    field public static final int MODE_CONVERT = 2; // 0x2
-    field public static final int MODE_REMOVE = 1; // 0x1
-    field public static final int MODE_THROW = 0; // 0x0
+  @Deprecated public static class SliceUtils.SerializeOptions {
+    ctor @Deprecated public SliceUtils.SerializeOptions();
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setActionMode(int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setImageConversionFormat(android.graphics.Bitmap.CompressFormat!, int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setImageMode(int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setMaxImageHeight(int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setMaxImageWidth(int);
+    field @Deprecated public static final int MODE_CONVERT = 2; // 0x2
+    field @Deprecated public static final int MODE_REMOVE = 1; // 0x1
+    field @Deprecated public static final int MODE_THROW = 0; // 0x0
   }
 
-  public static interface SliceUtils.SliceActionListener {
-    method public void onSliceAction(android.net.Uri!, android.content.Context!, android.content.Intent!);
+  @Deprecated public static interface SliceUtils.SliceActionListener {
+    method @Deprecated public void onSliceAction(android.net.Uri!, android.content.Context!, android.content.Intent!);
   }
 
-  public static class SliceUtils.SliceParseException extends java.lang.Exception {
+  @Deprecated public static class SliceUtils.SliceParseException extends java.lang.Exception {
   }
 
-  @RequiresApi(19) public abstract class SliceViewManager {
-    method public abstract androidx.slice.Slice? bindSlice(android.content.Intent);
-    method public abstract androidx.slice.Slice? bindSlice(android.net.Uri);
-    method public static androidx.slice.SliceViewManager getInstance(android.content.Context);
-    method @WorkerThread public abstract java.util.Collection<android.net.Uri!> getSliceDescendants(android.net.Uri);
-    method public abstract android.net.Uri? mapIntentToUri(android.content.Intent);
-    method public abstract void pinSlice(android.net.Uri);
-    method public abstract void registerSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
-    method public abstract void registerSliceCallback(android.net.Uri, java.util.concurrent.Executor, androidx.slice.SliceViewManager.SliceCallback);
-    method public abstract void unpinSlice(android.net.Uri);
-    method public abstract void unregisterSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
+  @Deprecated @RequiresApi(19) public abstract class SliceViewManager {
+    method @Deprecated public abstract androidx.slice.Slice? bindSlice(android.content.Intent);
+    method @Deprecated public abstract androidx.slice.Slice? bindSlice(android.net.Uri);
+    method @Deprecated public static androidx.slice.SliceViewManager getInstance(android.content.Context);
+    method @Deprecated @WorkerThread public abstract java.util.Collection<android.net.Uri!> getSliceDescendants(android.net.Uri);
+    method @Deprecated public abstract android.net.Uri? mapIntentToUri(android.content.Intent);
+    method @Deprecated public abstract void pinSlice(android.net.Uri);
+    method @Deprecated public abstract void registerSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
+    method @Deprecated public abstract void registerSliceCallback(android.net.Uri, java.util.concurrent.Executor, androidx.slice.SliceViewManager.SliceCallback);
+    method @Deprecated public abstract void unpinSlice(android.net.Uri);
+    method @Deprecated public abstract void unregisterSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
   }
 
-  public static interface SliceViewManager.SliceCallback {
-    method public void onSliceUpdated(androidx.slice.Slice?);
+  @Deprecated public static interface SliceViewManager.SliceCallback {
+    method @Deprecated public void onSliceUpdated(androidx.slice.Slice?);
   }
 
 }
 
 package androidx.slice.widget {
 
-  @RequiresApi(19) public class EventInfo {
-    ctor public EventInfo(int, int, int, int);
-    method public void setPosition(int, int, int);
-    field public static final int ACTION_TYPE_BUTTON = 1; // 0x1
-    field public static final int ACTION_TYPE_CONTENT = 3; // 0x3
-    field public static final int ACTION_TYPE_SEE_MORE = 4; // 0x4
-    field public static final int ACTION_TYPE_SELECTION = 5; // 0x5
-    field public static final int ACTION_TYPE_SLIDER = 2; // 0x2
-    field public static final int ACTION_TYPE_TOGGLE = 0; // 0x0
-    field public static final int POSITION_CELL = 2; // 0x2
-    field public static final int POSITION_END = 1; // 0x1
-    field public static final int POSITION_START = 0; // 0x0
-    field public static final int ROW_TYPE_GRID = 1; // 0x1
-    field public static final int ROW_TYPE_LIST = 0; // 0x0
-    field public static final int ROW_TYPE_MESSAGING = 2; // 0x2
-    field public static final int ROW_TYPE_PROGRESS = 5; // 0x5
-    field public static final int ROW_TYPE_SELECTION = 6; // 0x6
-    field public static final int ROW_TYPE_SHORTCUT = -1; // 0xffffffff
-    field public static final int ROW_TYPE_SLIDER = 4; // 0x4
-    field public static final int ROW_TYPE_TOGGLE = 3; // 0x3
-    field public static final int STATE_OFF = 0; // 0x0
-    field public static final int STATE_ON = 1; // 0x1
-    field public int actionCount;
-    field public int actionIndex;
-    field public int actionPosition;
-    field public int actionType;
-    field public int rowIndex;
-    field public int rowTemplateType;
-    field public int sliceMode;
-    field public int state;
+  @Deprecated @RequiresApi(19) public class EventInfo {
+    ctor @Deprecated public EventInfo(int, int, int, int);
+    method @Deprecated public void setPosition(int, int, int);
+    field @Deprecated public static final int ACTION_TYPE_BUTTON = 1; // 0x1
+    field @Deprecated public static final int ACTION_TYPE_CONTENT = 3; // 0x3
+    field @Deprecated public static final int ACTION_TYPE_SEE_MORE = 4; // 0x4
+    field @Deprecated public static final int ACTION_TYPE_SELECTION = 5; // 0x5
+    field @Deprecated public static final int ACTION_TYPE_SLIDER = 2; // 0x2
+    field @Deprecated public static final int ACTION_TYPE_TOGGLE = 0; // 0x0
+    field @Deprecated public static final int POSITION_CELL = 2; // 0x2
+    field @Deprecated public static final int POSITION_END = 1; // 0x1
+    field @Deprecated public static final int POSITION_START = 0; // 0x0
+    field @Deprecated public static final int ROW_TYPE_GRID = 1; // 0x1
+    field @Deprecated public static final int ROW_TYPE_LIST = 0; // 0x0
+    field @Deprecated public static final int ROW_TYPE_MESSAGING = 2; // 0x2
+    field @Deprecated public static final int ROW_TYPE_PROGRESS = 5; // 0x5
+    field @Deprecated public static final int ROW_TYPE_SELECTION = 6; // 0x6
+    field @Deprecated public static final int ROW_TYPE_SHORTCUT = -1; // 0xffffffff
+    field @Deprecated public static final int ROW_TYPE_SLIDER = 4; // 0x4
+    field @Deprecated public static final int ROW_TYPE_TOGGLE = 3; // 0x3
+    field @Deprecated public static final int STATE_OFF = 0; // 0x0
+    field @Deprecated public static final int STATE_ON = 1; // 0x1
+    field @Deprecated public int actionCount;
+    field @Deprecated public int actionIndex;
+    field @Deprecated public int actionPosition;
+    field @Deprecated public int actionType;
+    field @Deprecated public int rowIndex;
+    field @Deprecated public int rowTemplateType;
+    field @Deprecated public int sliceMode;
+    field @Deprecated public int state;
   }
 
-  @RequiresApi(19) public class GridContent extends androidx.slice.widget.SliceContent {
-    method public android.graphics.Point getFirstImageSize(android.content.Context);
-    method public boolean isValid();
+  @Deprecated @RequiresApi(19) public class GridContent extends androidx.slice.widget.SliceContent {
+    method @Deprecated public android.graphics.Point getFirstImageSize(android.content.Context);
+    method @Deprecated public boolean isValid();
   }
 
-  @RequiresApi(19) public class GridRowView extends androidx.slice.widget.SliceChildView implements android.view.View.OnClickListener android.view.View.OnTouchListener {
-    ctor public GridRowView(android.content.Context);
-    ctor public GridRowView(android.content.Context, android.util.AttributeSet?);
-    method protected boolean addImageItem(androidx.slice.SliceItem, androidx.slice.SliceItem?, int, android.view.ViewGroup, boolean);
-    method protected int getExtraBottomPadding();
-    method protected int getExtraTopPadding();
-    method protected int getMaxCells();
-    method protected int getTitleTextLayout();
+  @Deprecated @RequiresApi(19) public class GridRowView extends androidx.slice.widget.SliceChildView implements android.view.View.OnClickListener android.view.View.OnTouchListener {
+    ctor @Deprecated public GridRowView(android.content.Context);
+    ctor @Deprecated public GridRowView(android.content.Context, android.util.AttributeSet?);
+    method @Deprecated protected boolean addImageItem(androidx.slice.SliceItem, androidx.slice.SliceItem?, int, android.view.ViewGroup, boolean);
+    method @Deprecated protected int getExtraBottomPadding();
+    method @Deprecated protected int getExtraTopPadding();
+    method @Deprecated protected int getMaxCells();
+    method @Deprecated protected int getTitleTextLayout();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void onClick(android.view.View);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public boolean onTouch(android.view.View, android.view.MotionEvent);
-    method protected void populateViews();
-    method public void resetView();
-    method protected boolean scheduleMaxCellsUpdate();
+    method @Deprecated protected void populateViews();
+    method @Deprecated public void resetView();
+    method @Deprecated protected boolean scheduleMaxCellsUpdate();
   }
 
-  public interface RowStyleFactory {
-    method @StyleRes public int getRowStyleRes(androidx.slice.SliceItem);
+  @Deprecated public interface RowStyleFactory {
+    method @Deprecated @StyleRes public int getRowStyleRes(androidx.slice.SliceItem);
   }
 
-  @RequiresApi(19) public class RowView extends androidx.slice.widget.SliceChildView implements android.widget.AdapterView.OnItemSelectedListener android.view.View.OnClickListener {
-    ctor public RowView(android.content.Context);
-    method protected java.util.List<java.lang.String!> getEndItemKeys();
-    method protected androidx.slice.SliceItem? getPrimaryActionItem();
-    method protected String? getPrimaryActionKey();
-    method public void onClick(android.view.View);
-    method public void onItemSelected(android.widget.AdapterView<?>, android.view.View, int, long);
-    method public void onNothingSelected(android.widget.AdapterView<?>);
+  @Deprecated @RequiresApi(19) public class RowView extends androidx.slice.widget.SliceChildView implements android.widget.AdapterView.OnItemSelectedListener android.view.View.OnClickListener {
+    ctor @Deprecated public RowView(android.content.Context);
+    method @Deprecated protected java.util.List<java.lang.String!> getEndItemKeys();
+    method @Deprecated protected androidx.slice.SliceItem? getPrimaryActionItem();
+    method @Deprecated protected String? getPrimaryActionKey();
+    method @Deprecated public void onClick(android.view.View);
+    method @Deprecated public void onItemSelected(android.widget.AdapterView<?>, android.view.View, int, long);
+    method @Deprecated public void onNothingSelected(android.widget.AdapterView<?>);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void resetView();
   }
 
-  @RequiresApi(19) public class SliceAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.slice.widget.SliceAdapter.SliceViewHolder> {
-    ctor public SliceAdapter(android.content.Context);
-    method public androidx.slice.widget.GridRowView getGridRowView();
-    method public int getItemCount();
-    method public androidx.slice.widget.RowView getRowView();
+  @Deprecated @RequiresApi(19) public class SliceAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.slice.widget.SliceAdapter.SliceViewHolder> {
+    ctor @Deprecated public SliceAdapter(android.content.Context);
+    method @Deprecated public androidx.slice.widget.GridRowView getGridRowView();
+    method @Deprecated public int getItemCount();
+    method @Deprecated public androidx.slice.widget.RowView getRowView();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void onBindViewHolder(androidx.slice.widget.SliceAdapter.SliceViewHolder, int);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public androidx.slice.widget.SliceAdapter.SliceViewHolder onCreateViewHolder(android.view.ViewGroup, int);
   }
 
-  @RequiresApi(19) public abstract class SliceChildView extends android.widget.FrameLayout {
-    ctor public SliceChildView(android.content.Context);
-    ctor public SliceChildView(android.content.Context, android.util.AttributeSet?);
-    method public abstract void resetView();
-    method public void setSliceItem(androidx.slice.widget.SliceContent?, boolean, int, int, androidx.slice.widget.SliceView.OnSliceActionListener?);
+  @Deprecated @RequiresApi(19) public abstract class SliceChildView extends android.widget.FrameLayout {
+    ctor @Deprecated public SliceChildView(android.content.Context);
+    ctor @Deprecated public SliceChildView(android.content.Context, android.util.AttributeSet?);
+    method @Deprecated public abstract void resetView();
+    method @Deprecated public void setSliceItem(androidx.slice.widget.SliceContent?, boolean, int, int, androidx.slice.widget.SliceView.OnSliceActionListener?);
   }
 
-  @RequiresApi(19) public class SliceContent {
-    ctor public SliceContent(androidx.slice.Slice?);
+  @Deprecated @RequiresApi(19) public class SliceContent {
+    ctor @Deprecated public SliceContent(androidx.slice.Slice?);
   }
 
-  @RequiresApi(19) public final class SliceLiveData {
-    method public static androidx.slice.widget.SliceLiveData.CachedSliceLiveData fromCachedSlice(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent, androidx.slice.widget.SliceLiveData.OnErrorListener?);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromStream(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri, androidx.slice.widget.SliceLiveData.OnErrorListener?);
+  @Deprecated @RequiresApi(19) public final class SliceLiveData {
+    method @Deprecated public static androidx.slice.widget.SliceLiveData.CachedSliceLiveData fromCachedSlice(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent, androidx.slice.widget.SliceLiveData.OnErrorListener?);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromStream(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri, androidx.slice.widget.SliceLiveData.OnErrorListener?);
   }
 
-  public static class SliceLiveData.CachedSliceLiveData extends androidx.lifecycle.LiveData<androidx.slice.Slice> {
-    method public void goLive();
-    method public void parseStream();
+  @Deprecated public static class SliceLiveData.CachedSliceLiveData extends androidx.lifecycle.LiveData<androidx.slice.Slice> {
+    method @Deprecated public void goLive();
+    method @Deprecated public void parseStream();
   }
 
-  public static interface SliceLiveData.OnErrorListener {
-    method public void onSliceError(@androidx.slice.widget.SliceLiveData.OnErrorListener.ErrorType int, Throwable?);
-    field public static final int ERROR_INVALID_INPUT = 3; // 0x3
-    field public static final int ERROR_SLICE_NO_LONGER_PRESENT = 2; // 0x2
-    field public static final int ERROR_STRUCTURE_CHANGED = 1; // 0x1
-    field public static final int ERROR_UNKNOWN = 0; // 0x0
+  @Deprecated public static interface SliceLiveData.OnErrorListener {
+    method @Deprecated public void onSliceError(@androidx.slice.widget.SliceLiveData.OnErrorListener.ErrorType int, Throwable?);
+    field @Deprecated public static final int ERROR_INVALID_INPUT = 3; // 0x3
+    field @Deprecated public static final int ERROR_SLICE_NO_LONGER_PRESENT = 2; // 0x2
+    field @Deprecated public static final int ERROR_STRUCTURE_CHANGED = 1; // 0x1
+    field @Deprecated public static final int ERROR_UNKNOWN = 0; // 0x0
   }
 
-  @IntDef({androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_UNKNOWN, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_STRUCTURE_CHANGED, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_SLICE_NO_LONGER_PRESENT, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_INVALID_INPUT}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceLiveData.OnErrorListener.ErrorType {
+  @Deprecated @IntDef({androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_UNKNOWN, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_STRUCTURE_CHANGED, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_SLICE_NO_LONGER_PRESENT, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_INVALID_INPUT}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceLiveData.OnErrorListener.ErrorType {
   }
 
-  @RequiresApi(19) public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
-    ctor public SliceView(android.content.Context!);
-    ctor public SliceView(android.content.Context!, android.util.AttributeSet?);
-    ctor public SliceView(android.content.Context!, android.util.AttributeSet?, int);
-    ctor @RequiresApi(21) public SliceView(android.content.Context!, android.util.AttributeSet!, int, int);
-    method protected void configureViewPolicy(int);
-    method public int getHiddenItemCount();
-    method public int getMode();
-    method public androidx.slice.Slice? getSlice();
-    method public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
-    method public boolean isScrollable();
-    method public void onChanged(androidx.slice.Slice?);
-    method public void onClick(android.view.View!);
-    method public void setAccentColor(@ColorInt int);
-    method public void setCurrentView(androidx.slice.widget.SliceChildView);
-    method public void setMode(int);
-    method public void setOnSliceActionListener(androidx.slice.widget.SliceView.OnSliceActionListener?);
-    method public void setRowStyleFactory(androidx.slice.widget.RowStyleFactory?);
-    method public void setScrollable(boolean);
-    method public void setShowActionDividers(boolean);
-    method public void setShowHeaderDivider(boolean);
-    method public void setShowTitleItems(boolean);
-    method public void setSlice(androidx.slice.Slice?);
-    method public void setSliceActions(java.util.List<androidx.slice.core.SliceAction!>?);
-    field public static final int MODE_LARGE = 2; // 0x2
-    field public static final int MODE_SHORTCUT = 3; // 0x3
-    field public static final int MODE_SMALL = 1; // 0x1
+  @Deprecated @RequiresApi(19) public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
+    ctor @Deprecated public SliceView(android.content.Context!);
+    ctor @Deprecated public SliceView(android.content.Context!, android.util.AttributeSet?);
+    ctor @Deprecated public SliceView(android.content.Context!, android.util.AttributeSet?, int);
+    ctor @Deprecated @RequiresApi(21) public SliceView(android.content.Context!, android.util.AttributeSet!, int, int);
+    method @Deprecated protected void configureViewPolicy(int);
+    method @Deprecated public int getHiddenItemCount();
+    method @Deprecated public int getMode();
+    method @Deprecated public androidx.slice.Slice? getSlice();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
+    method @Deprecated public boolean isScrollable();
+    method @Deprecated public void onChanged(androidx.slice.Slice?);
+    method @Deprecated public void onClick(android.view.View!);
+    method @Deprecated public void setAccentColor(@ColorInt int);
+    method @Deprecated public void setCurrentView(androidx.slice.widget.SliceChildView);
+    method @Deprecated public void setMode(int);
+    method @Deprecated public void setOnSliceActionListener(androidx.slice.widget.SliceView.OnSliceActionListener?);
+    method @Deprecated public void setRowStyleFactory(androidx.slice.widget.RowStyleFactory?);
+    method @Deprecated public void setScrollable(boolean);
+    method @Deprecated public void setShowActionDividers(boolean);
+    method @Deprecated public void setShowHeaderDivider(boolean);
+    method @Deprecated public void setShowTitleItems(boolean);
+    method @Deprecated public void setSlice(androidx.slice.Slice?);
+    method @Deprecated public void setSliceActions(java.util.List<androidx.slice.core.SliceAction!>?);
+    field @Deprecated public static final int MODE_LARGE = 2; // 0x2
+    field @Deprecated public static final int MODE_SHORTCUT = 3; // 0x3
+    field @Deprecated public static final int MODE_SMALL = 1; // 0x1
   }
 
-  public static interface SliceView.OnSliceActionListener {
-    method public void onSliceAction(androidx.slice.widget.EventInfo, androidx.slice.SliceItem);
+  @Deprecated public static interface SliceView.OnSliceActionListener {
+    method @Deprecated public void onSliceAction(androidx.slice.widget.EventInfo, androidx.slice.SliceItem);
   }
 
-  @RequiresApi(19) public class TemplateView extends androidx.slice.widget.SliceChildView {
-    ctor public TemplateView(android.content.Context);
-    method public void onAttachedToWindow();
+  @Deprecated @RequiresApi(19) public class TemplateView extends androidx.slice.widget.SliceChildView {
+    ctor @Deprecated public TemplateView(android.content.Context);
+    method @Deprecated public void onAttachedToWindow();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void resetView();
-    method public void setAdapter(androidx.slice.widget.SliceAdapter);
+    method @Deprecated public void setAdapter(androidx.slice.widget.SliceAdapter);
   }
 
 }
diff --git a/slice/slice-view/api/removed_current.txt b/slice/slice-view/api/removed_current.txt
index 83b9996..c72daba 100644
--- a/slice/slice-view/api/removed_current.txt
+++ b/slice/slice-view/api/removed_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.slice.widget {
 
-  @RequiresApi(19) public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
+  @Deprecated @RequiresApi(19) public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
     method @Deprecated public void showActionDividers(boolean);
     method @Deprecated public void showHeaderDivider(boolean);
     method @Deprecated public void showTitleItems(boolean);
diff --git a/slice/slice-view/api/restricted_current.txt b/slice/slice-view/api/restricted_current.txt
index 9f3c43a..068889b 100644
--- a/slice/slice-view/api/restricted_current.txt
+++ b/slice/slice-view/api/restricted_current.txt
@@ -1,294 +1,294 @@
 // Signature format: 4.0
 package androidx.slice {
 
-  @RequiresApi(19) public class SliceMetadata {
-    method public static androidx.slice.SliceMetadata from(android.content.Context?, androidx.slice.Slice);
-    method public long getExpiry();
-    method public int getHeaderType();
-    method public android.os.Bundle getHostExtras();
-    method public android.app.PendingIntent? getInputRangeAction();
-    method public long getLastUpdatedTime();
-    method public int getLoadingState();
-    method public androidx.slice.core.SliceAction? getPrimaryAction();
-    method public androidx.core.util.Pair<java.lang.Integer!,java.lang.Integer!>? getRange();
-    method public int getRangeValue();
-    method public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
-    method public java.util.List<java.lang.String!>? getSliceKeywords();
-    method public CharSequence? getSubtitle();
-    method public CharSequence? getSummary();
-    method public CharSequence? getTitle();
-    method public java.util.List<androidx.slice.core.SliceAction!>! getToggles();
-    method public boolean hasLargeMode();
-    method public boolean isCachedSlice();
-    method public boolean isErrorSlice();
-    method public boolean isPermissionSlice();
-    method public boolean isSelection();
-    method public boolean sendInputRangeAction(int) throws android.app.PendingIntent.CanceledException;
-    method public boolean sendToggleAction(androidx.slice.core.SliceAction!, boolean) throws android.app.PendingIntent.CanceledException;
-    field public static final int LOADED_ALL = 2; // 0x2
-    field public static final int LOADED_NONE = 0; // 0x0
-    field public static final int LOADED_PARTIAL = 1; // 0x1
+  @Deprecated @RequiresApi(19) public class SliceMetadata {
+    method @Deprecated public static androidx.slice.SliceMetadata from(android.content.Context?, androidx.slice.Slice);
+    method @Deprecated public long getExpiry();
+    method @Deprecated public int getHeaderType();
+    method @Deprecated public android.os.Bundle getHostExtras();
+    method @Deprecated public android.app.PendingIntent? getInputRangeAction();
+    method @Deprecated public long getLastUpdatedTime();
+    method @Deprecated public int getLoadingState();
+    method @Deprecated public androidx.slice.core.SliceAction? getPrimaryAction();
+    method @Deprecated public androidx.core.util.Pair<java.lang.Integer!,java.lang.Integer!>? getRange();
+    method @Deprecated public int getRangeValue();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
+    method @Deprecated public java.util.List<java.lang.String!>? getSliceKeywords();
+    method @Deprecated public CharSequence? getSubtitle();
+    method @Deprecated public CharSequence? getSummary();
+    method @Deprecated public CharSequence? getTitle();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>! getToggles();
+    method @Deprecated public boolean hasLargeMode();
+    method @Deprecated public boolean isCachedSlice();
+    method @Deprecated public boolean isErrorSlice();
+    method @Deprecated public boolean isPermissionSlice();
+    method @Deprecated public boolean isSelection();
+    method @Deprecated public boolean sendInputRangeAction(int) throws android.app.PendingIntent.CanceledException;
+    method @Deprecated public boolean sendToggleAction(androidx.slice.core.SliceAction!, boolean) throws android.app.PendingIntent.CanceledException;
+    field @Deprecated public static final int LOADED_ALL = 2; // 0x2
+    field @Deprecated public static final int LOADED_NONE = 0; // 0x0
+    field @Deprecated public static final int LOADED_PARTIAL = 1; // 0x1
   }
 
-  @RequiresApi(19) public class SliceStructure {
-    ctor public SliceStructure(androidx.slice.Slice!);
+  @Deprecated @RequiresApi(19) public class SliceStructure {
+    ctor @Deprecated public SliceStructure(androidx.slice.Slice!);
   }
 
-  @RequiresApi(19) public class SliceUtils {
-    method public static androidx.slice.Slice parseSlice(android.content.Context, java.io.InputStream, String, androidx.slice.SliceUtils.SliceActionListener) throws java.io.IOException, androidx.slice.SliceUtils.SliceParseException;
-    method public static void serializeSlice(androidx.slice.Slice, android.content.Context, java.io.OutputStream, androidx.slice.SliceUtils.SerializeOptions) throws java.lang.IllegalArgumentException;
-    method public static androidx.slice.Slice stripSlice(androidx.slice.Slice, int, boolean);
+  @Deprecated @RequiresApi(19) public class SliceUtils {
+    method @Deprecated public static androidx.slice.Slice parseSlice(android.content.Context, java.io.InputStream, String, androidx.slice.SliceUtils.SliceActionListener) throws java.io.IOException, androidx.slice.SliceUtils.SliceParseException;
+    method @Deprecated public static void serializeSlice(androidx.slice.Slice, android.content.Context, java.io.OutputStream, androidx.slice.SliceUtils.SerializeOptions) throws java.lang.IllegalArgumentException;
+    method @Deprecated public static androidx.slice.Slice stripSlice(androidx.slice.Slice, int, boolean);
   }
 
-  public static class SliceUtils.SerializeOptions {
-    ctor public SliceUtils.SerializeOptions();
-    method public androidx.slice.SliceUtils.SerializeOptions! setActionMode(int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setImageConversionFormat(android.graphics.Bitmap.CompressFormat!, int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setImageMode(int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setMaxImageHeight(int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setMaxImageWidth(int);
-    field public static final int MODE_CONVERT = 2; // 0x2
-    field public static final int MODE_REMOVE = 1; // 0x1
-    field public static final int MODE_THROW = 0; // 0x0
+  @Deprecated public static class SliceUtils.SerializeOptions {
+    ctor @Deprecated public SliceUtils.SerializeOptions();
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setActionMode(int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setImageConversionFormat(android.graphics.Bitmap.CompressFormat!, int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setImageMode(int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setMaxImageHeight(int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setMaxImageWidth(int);
+    field @Deprecated public static final int MODE_CONVERT = 2; // 0x2
+    field @Deprecated public static final int MODE_REMOVE = 1; // 0x1
+    field @Deprecated public static final int MODE_THROW = 0; // 0x0
   }
 
-  public static interface SliceUtils.SliceActionListener {
-    method public void onSliceAction(android.net.Uri!, android.content.Context!, android.content.Intent!);
+  @Deprecated public static interface SliceUtils.SliceActionListener {
+    method @Deprecated public void onSliceAction(android.net.Uri!, android.content.Context!, android.content.Intent!);
   }
 
-  public static class SliceUtils.SliceParseException extends java.lang.Exception {
+  @Deprecated public static class SliceUtils.SliceParseException extends java.lang.Exception {
   }
 
-  @RequiresApi(19) public abstract class SliceViewManager {
-    method public abstract androidx.slice.Slice? bindSlice(android.content.Intent);
-    method public abstract androidx.slice.Slice? bindSlice(android.net.Uri);
-    method public static androidx.slice.SliceViewManager getInstance(android.content.Context);
-    method @WorkerThread public abstract java.util.Collection<android.net.Uri!> getSliceDescendants(android.net.Uri);
-    method public abstract android.net.Uri? mapIntentToUri(android.content.Intent);
-    method public abstract void pinSlice(android.net.Uri);
-    method public abstract void registerSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
-    method public abstract void registerSliceCallback(android.net.Uri, java.util.concurrent.Executor, androidx.slice.SliceViewManager.SliceCallback);
-    method public abstract void unpinSlice(android.net.Uri);
-    method public abstract void unregisterSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
+  @Deprecated @RequiresApi(19) public abstract class SliceViewManager {
+    method @Deprecated public abstract androidx.slice.Slice? bindSlice(android.content.Intent);
+    method @Deprecated public abstract androidx.slice.Slice? bindSlice(android.net.Uri);
+    method @Deprecated public static androidx.slice.SliceViewManager getInstance(android.content.Context);
+    method @Deprecated @WorkerThread public abstract java.util.Collection<android.net.Uri!> getSliceDescendants(android.net.Uri);
+    method @Deprecated public abstract android.net.Uri? mapIntentToUri(android.content.Intent);
+    method @Deprecated public abstract void pinSlice(android.net.Uri);
+    method @Deprecated public abstract void registerSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
+    method @Deprecated public abstract void registerSliceCallback(android.net.Uri, java.util.concurrent.Executor, androidx.slice.SliceViewManager.SliceCallback);
+    method @Deprecated public abstract void unpinSlice(android.net.Uri);
+    method @Deprecated public abstract void unregisterSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
   }
 
-  public static interface SliceViewManager.SliceCallback {
-    method public void onSliceUpdated(androidx.slice.Slice?);
+  @Deprecated public static interface SliceViewManager.SliceCallback {
+    method @Deprecated public void onSliceUpdated(androidx.slice.Slice?);
   }
 
 }
 
 package androidx.slice.widget {
 
-  @RequiresApi(19) public class EventInfo {
-    ctor public EventInfo(int, int, int, int);
-    method public void setPosition(int, int, int);
-    field public static final int ACTION_TYPE_BUTTON = 1; // 0x1
-    field public static final int ACTION_TYPE_CONTENT = 3; // 0x3
-    field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ACTION_TYPE_DATE_PICK = 6; // 0x6
-    field public static final int ACTION_TYPE_SEE_MORE = 4; // 0x4
-    field public static final int ACTION_TYPE_SELECTION = 5; // 0x5
-    field public static final int ACTION_TYPE_SLIDER = 2; // 0x2
-    field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ACTION_TYPE_TIME_PICK = 7; // 0x7
-    field public static final int ACTION_TYPE_TOGGLE = 0; // 0x0
-    field public static final int POSITION_CELL = 2; // 0x2
-    field public static final int POSITION_END = 1; // 0x1
-    field public static final int POSITION_START = 0; // 0x0
-    field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ROW_TYPE_DATE_PICK = 7; // 0x7
-    field public static final int ROW_TYPE_GRID = 1; // 0x1
-    field public static final int ROW_TYPE_LIST = 0; // 0x0
-    field public static final int ROW_TYPE_MESSAGING = 2; // 0x2
-    field public static final int ROW_TYPE_PROGRESS = 5; // 0x5
-    field public static final int ROW_TYPE_SELECTION = 6; // 0x6
-    field public static final int ROW_TYPE_SHORTCUT = -1; // 0xffffffff
-    field public static final int ROW_TYPE_SLIDER = 4; // 0x4
-    field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ROW_TYPE_TIME_PICK = 8; // 0x8
-    field public static final int ROW_TYPE_TOGGLE = 3; // 0x3
-    field public static final int STATE_OFF = 0; // 0x0
-    field public static final int STATE_ON = 1; // 0x1
-    field public int actionCount;
-    field public int actionIndex;
-    field public int actionPosition;
-    field public int actionType;
-    field public int rowIndex;
-    field public int rowTemplateType;
-    field public int sliceMode;
-    field public int state;
+  @Deprecated @RequiresApi(19) public class EventInfo {
+    ctor @Deprecated public EventInfo(int, int, int, int);
+    method @Deprecated public void setPosition(int, int, int);
+    field @Deprecated public static final int ACTION_TYPE_BUTTON = 1; // 0x1
+    field @Deprecated public static final int ACTION_TYPE_CONTENT = 3; // 0x3
+    field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ACTION_TYPE_DATE_PICK = 6; // 0x6
+    field @Deprecated public static final int ACTION_TYPE_SEE_MORE = 4; // 0x4
+    field @Deprecated public static final int ACTION_TYPE_SELECTION = 5; // 0x5
+    field @Deprecated public static final int ACTION_TYPE_SLIDER = 2; // 0x2
+    field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ACTION_TYPE_TIME_PICK = 7; // 0x7
+    field @Deprecated public static final int ACTION_TYPE_TOGGLE = 0; // 0x0
+    field @Deprecated public static final int POSITION_CELL = 2; // 0x2
+    field @Deprecated public static final int POSITION_END = 1; // 0x1
+    field @Deprecated public static final int POSITION_START = 0; // 0x0
+    field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ROW_TYPE_DATE_PICK = 7; // 0x7
+    field @Deprecated public static final int ROW_TYPE_GRID = 1; // 0x1
+    field @Deprecated public static final int ROW_TYPE_LIST = 0; // 0x0
+    field @Deprecated public static final int ROW_TYPE_MESSAGING = 2; // 0x2
+    field @Deprecated public static final int ROW_TYPE_PROGRESS = 5; // 0x5
+    field @Deprecated public static final int ROW_TYPE_SELECTION = 6; // 0x6
+    field @Deprecated public static final int ROW_TYPE_SHORTCUT = -1; // 0xffffffff
+    field @Deprecated public static final int ROW_TYPE_SLIDER = 4; // 0x4
+    field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ROW_TYPE_TIME_PICK = 8; // 0x8
+    field @Deprecated public static final int ROW_TYPE_TOGGLE = 3; // 0x3
+    field @Deprecated public static final int STATE_OFF = 0; // 0x0
+    field @Deprecated public static final int STATE_ON = 1; // 0x1
+    field @Deprecated public int actionCount;
+    field @Deprecated public int actionIndex;
+    field @Deprecated public int actionPosition;
+    field @Deprecated public int actionType;
+    field @Deprecated public int rowIndex;
+    field @Deprecated public int rowTemplateType;
+    field @Deprecated public int sliceMode;
+    field @Deprecated public int state;
   }
 
-  @RequiresApi(19) public class GridContent extends androidx.slice.widget.SliceContent {
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public GridContent(androidx.slice.SliceItem, int);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceItem? getContentIntent();
-    method public android.graphics.Point getFirstImageSize(android.content.Context);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public java.util.ArrayList<androidx.slice.widget.GridContent.CellContent!> getGridContent();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean getIsLastIndex();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public int getLargestImageMode();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public int getMaxCellLineCount();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceItem? getSeeMoreItem();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public CharSequence? getTitle();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean hasImage();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean isAllImages();
-    method public boolean isValid();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setIsLastIndex(boolean);
+  @Deprecated @RequiresApi(19) public class GridContent extends androidx.slice.widget.SliceContent {
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public GridContent(androidx.slice.SliceItem, int);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceItem? getContentIntent();
+    method @Deprecated public android.graphics.Point getFirstImageSize(android.content.Context);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public java.util.ArrayList<androidx.slice.widget.GridContent.CellContent!> getGridContent();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean getIsLastIndex();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public int getLargestImageMode();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public int getMaxCellLineCount();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceItem? getSeeMoreItem();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public CharSequence? getTitle();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean hasImage();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean isAllImages();
+    method @Deprecated public boolean isValid();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setIsLastIndex(boolean);
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static class GridContent.CellContent {
-    ctor public GridContent.CellContent(androidx.slice.SliceItem);
-    method public java.util.ArrayList<androidx.slice.SliceItem!> getCellItems();
-    method public CharSequence? getContentDescription();
-    method public androidx.slice.SliceItem? getContentIntent();
-    method public androidx.core.graphics.drawable.IconCompat? getImageIcon();
-    method public int getImageMode();
-    method public androidx.slice.SliceItem? getOverlayItem();
-    method public androidx.slice.SliceItem? getPicker();
-    method public int getTextCount();
-    method public androidx.slice.SliceItem? getTitleItem();
-    method public androidx.slice.SliceItem? getToggleItem();
-    method public boolean hasImage();
-    method public boolean isImageOnly();
-    method public boolean isValid();
-    method public boolean populate(androidx.slice.SliceItem);
+  @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static class GridContent.CellContent {
+    ctor @Deprecated public GridContent.CellContent(androidx.slice.SliceItem);
+    method @Deprecated public java.util.ArrayList<androidx.slice.SliceItem!> getCellItems();
+    method @Deprecated public CharSequence? getContentDescription();
+    method @Deprecated public androidx.slice.SliceItem? getContentIntent();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getImageIcon();
+    method @Deprecated public int getImageMode();
+    method @Deprecated public androidx.slice.SliceItem? getOverlayItem();
+    method @Deprecated public androidx.slice.SliceItem? getPicker();
+    method @Deprecated public int getTextCount();
+    method @Deprecated public androidx.slice.SliceItem? getTitleItem();
+    method @Deprecated public androidx.slice.SliceItem? getToggleItem();
+    method @Deprecated public boolean hasImage();
+    method @Deprecated public boolean isImageOnly();
+    method @Deprecated public boolean isValid();
+    method @Deprecated public boolean populate(androidx.slice.SliceItem);
   }
 
-  @RequiresApi(19) public class GridRowView extends androidx.slice.widget.SliceChildView implements android.view.View.OnClickListener android.view.View.OnTouchListener {
-    ctor public GridRowView(android.content.Context);
-    ctor public GridRowView(android.content.Context, android.util.AttributeSet?);
-    method protected boolean addImageItem(androidx.slice.SliceItem, androidx.slice.SliceItem?, int, android.view.ViewGroup, boolean);
-    method protected int getExtraBottomPadding();
-    method protected int getExtraTopPadding();
-    method protected int getMaxCells();
-    method protected int getTitleTextLayout();
+  @Deprecated @RequiresApi(19) public class GridRowView extends androidx.slice.widget.SliceChildView implements android.view.View.OnClickListener android.view.View.OnTouchListener {
+    ctor @Deprecated public GridRowView(android.content.Context);
+    ctor @Deprecated public GridRowView(android.content.Context, android.util.AttributeSet?);
+    method @Deprecated protected boolean addImageItem(androidx.slice.SliceItem, androidx.slice.SliceItem?, int, android.view.ViewGroup, boolean);
+    method @Deprecated protected int getExtraBottomPadding();
+    method @Deprecated protected int getExtraTopPadding();
+    method @Deprecated protected int getMaxCells();
+    method @Deprecated protected int getTitleTextLayout();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void onClick(android.view.View);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public boolean onTouch(android.view.View, android.view.MotionEvent);
-    method protected void populateViews();
-    method public void resetView();
-    method protected boolean scheduleMaxCellsUpdate();
+    method @Deprecated protected void populateViews();
+    method @Deprecated public void resetView();
+    method @Deprecated protected boolean scheduleMaxCellsUpdate();
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class RowContent extends androidx.slice.widget.SliceContent {
-    ctor public RowContent(androidx.slice.SliceItem!, int);
-    method public java.util.List<androidx.slice.SliceItem!>! getEndItems();
-    method public androidx.slice.SliceItem? getInputRangeThumb();
-    method public boolean getIsHeader();
-    method public int getLineCount();
-    method public androidx.slice.SliceItem? getPrimaryAction();
-    method public androidx.slice.SliceItem? getRange();
-    method public androidx.slice.SliceItem? getSelection();
-    method public androidx.slice.SliceItem? getStartItem();
-    method public androidx.slice.SliceItem? getSubtitleItem();
-    method public androidx.slice.SliceItem? getSummaryItem();
-    method public androidx.slice.SliceItem? getTitleItem();
-    method public java.util.List<androidx.slice.core.SliceAction!>! getToggleItems();
-    method public boolean hasActionDivider();
-    method public boolean hasBottomDivider();
-    method public boolean hasTitleItems();
-    method public boolean isDefaultSeeMore();
-    method public boolean isValid();
-    method public void setIsHeader(boolean);
-    method public void showActionDivider(boolean);
-    method public void showBottomDivider(boolean);
-    method public void showTitleItems(boolean);
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class RowContent extends androidx.slice.widget.SliceContent {
+    ctor @Deprecated public RowContent(androidx.slice.SliceItem!, int);
+    method @Deprecated public java.util.List<androidx.slice.SliceItem!>! getEndItems();
+    method @Deprecated public androidx.slice.SliceItem? getInputRangeThumb();
+    method @Deprecated public boolean getIsHeader();
+    method @Deprecated public int getLineCount();
+    method @Deprecated public androidx.slice.SliceItem? getPrimaryAction();
+    method @Deprecated public androidx.slice.SliceItem? getRange();
+    method @Deprecated public androidx.slice.SliceItem? getSelection();
+    method @Deprecated public androidx.slice.SliceItem? getStartItem();
+    method @Deprecated public androidx.slice.SliceItem? getSubtitleItem();
+    method @Deprecated public androidx.slice.SliceItem? getSummaryItem();
+    method @Deprecated public androidx.slice.SliceItem? getTitleItem();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>! getToggleItems();
+    method @Deprecated public boolean hasActionDivider();
+    method @Deprecated public boolean hasBottomDivider();
+    method @Deprecated public boolean hasTitleItems();
+    method @Deprecated public boolean isDefaultSeeMore();
+    method @Deprecated public boolean isValid();
+    method @Deprecated public void setIsHeader(boolean);
+    method @Deprecated public void showActionDivider(boolean);
+    method @Deprecated public void showBottomDivider(boolean);
+    method @Deprecated public void showTitleItems(boolean);
   }
 
-  public interface RowStyleFactory {
-    method @StyleRes public int getRowStyleRes(androidx.slice.SliceItem);
+  @Deprecated public interface RowStyleFactory {
+    method @Deprecated @StyleRes public int getRowStyleRes(androidx.slice.SliceItem);
   }
 
-  @RequiresApi(19) public class RowView extends androidx.slice.widget.SliceChildView implements android.widget.AdapterView.OnItemSelectedListener android.view.View.OnClickListener {
-    ctor public RowView(android.content.Context);
-    method protected java.util.List<java.lang.String!> getEndItemKeys();
-    method protected androidx.slice.SliceItem? getPrimaryActionItem();
-    method protected String? getPrimaryActionKey();
-    method public void onClick(android.view.View);
-    method public void onItemSelected(android.widget.AdapterView<?>, android.view.View, int, long);
-    method public void onNothingSelected(android.widget.AdapterView<?>);
+  @Deprecated @RequiresApi(19) public class RowView extends androidx.slice.widget.SliceChildView implements android.widget.AdapterView.OnItemSelectedListener android.view.View.OnClickListener {
+    ctor @Deprecated public RowView(android.content.Context);
+    method @Deprecated protected java.util.List<java.lang.String!> getEndItemKeys();
+    method @Deprecated protected androidx.slice.SliceItem? getPrimaryActionItem();
+    method @Deprecated protected String? getPrimaryActionKey();
+    method @Deprecated public void onClick(android.view.View);
+    method @Deprecated public void onItemSelected(android.widget.AdapterView<?>, android.view.View, int, long);
+    method @Deprecated public void onNothingSelected(android.widget.AdapterView<?>);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void resetView();
   }
 
-  @RequiresApi(19) public class SliceAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.slice.widget.SliceAdapter.SliceViewHolder> {
-    ctor public SliceAdapter(android.content.Context);
-    method public androidx.slice.widget.GridRowView getGridRowView();
-    method public int getItemCount();
-    method public androidx.slice.widget.RowView getRowView();
+  @Deprecated @RequiresApi(19) public class SliceAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.slice.widget.SliceAdapter.SliceViewHolder> {
+    ctor @Deprecated public SliceAdapter(android.content.Context);
+    method @Deprecated public androidx.slice.widget.GridRowView getGridRowView();
+    method @Deprecated public int getItemCount();
+    method @Deprecated public androidx.slice.widget.RowView getRowView();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void onBindViewHolder(androidx.slice.widget.SliceAdapter.SliceViewHolder, int);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public androidx.slice.widget.SliceAdapter.SliceViewHolder onCreateViewHolder(android.view.ViewGroup, int);
   }
 
-  @RequiresApi(19) public abstract class SliceChildView extends android.widget.FrameLayout {
-    ctor public SliceChildView(android.content.Context);
-    ctor public SliceChildView(android.content.Context, android.util.AttributeSet?);
-    method public abstract void resetView();
-    method public void setSliceItem(androidx.slice.widget.SliceContent?, boolean, int, int, androidx.slice.widget.SliceView.OnSliceActionListener?);
+  @Deprecated @RequiresApi(19) public abstract class SliceChildView extends android.widget.FrameLayout {
+    ctor @Deprecated public SliceChildView(android.content.Context);
+    ctor @Deprecated public SliceChildView(android.content.Context, android.util.AttributeSet?);
+    method @Deprecated public abstract void resetView();
+    method @Deprecated public void setSliceItem(androidx.slice.widget.SliceContent?, boolean, int, int, androidx.slice.widget.SliceView.OnSliceActionListener?);
   }
 
-  @RequiresApi(19) public class SliceContent {
-    ctor public SliceContent(androidx.slice.Slice?);
+  @Deprecated @RequiresApi(19) public class SliceContent {
+    ctor @Deprecated public SliceContent(androidx.slice.Slice?);
   }
 
-  @RequiresApi(19) public final class SliceLiveData {
-    method public static androidx.slice.widget.SliceLiveData.CachedSliceLiveData fromCachedSlice(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent, androidx.slice.widget.SliceLiveData.OnErrorListener?);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.slice.widget.SliceLiveData.CachedSliceLiveData fromStream(android.content.Context, androidx.slice.SliceViewManager!, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromStream(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri, androidx.slice.widget.SliceLiveData.OnErrorListener?);
+  @Deprecated @RequiresApi(19) public final class SliceLiveData {
+    method @Deprecated public static androidx.slice.widget.SliceLiveData.CachedSliceLiveData fromCachedSlice(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent, androidx.slice.widget.SliceLiveData.OnErrorListener?);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.slice.widget.SliceLiveData.CachedSliceLiveData fromStream(android.content.Context, androidx.slice.SliceViewManager!, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromStream(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri, androidx.slice.widget.SliceLiveData.OnErrorListener?);
   }
 
-  public static class SliceLiveData.CachedSliceLiveData extends androidx.lifecycle.LiveData<androidx.slice.Slice> {
-    method public void goLive();
-    method public void parseStream();
+  @Deprecated public static class SliceLiveData.CachedSliceLiveData extends androidx.lifecycle.LiveData<androidx.slice.Slice> {
+    method @Deprecated public void goLive();
+    method @Deprecated public void parseStream();
   }
 
-  public static interface SliceLiveData.OnErrorListener {
-    method public void onSliceError(@androidx.slice.widget.SliceLiveData.OnErrorListener.ErrorType int, Throwable?);
-    field public static final int ERROR_INVALID_INPUT = 3; // 0x3
-    field public static final int ERROR_SLICE_NO_LONGER_PRESENT = 2; // 0x2
-    field public static final int ERROR_STRUCTURE_CHANGED = 1; // 0x1
-    field public static final int ERROR_UNKNOWN = 0; // 0x0
+  @Deprecated public static interface SliceLiveData.OnErrorListener {
+    method @Deprecated public void onSliceError(@androidx.slice.widget.SliceLiveData.OnErrorListener.ErrorType int, Throwable?);
+    field @Deprecated public static final int ERROR_INVALID_INPUT = 3; // 0x3
+    field @Deprecated public static final int ERROR_SLICE_NO_LONGER_PRESENT = 2; // 0x2
+    field @Deprecated public static final int ERROR_STRUCTURE_CHANGED = 1; // 0x1
+    field @Deprecated public static final int ERROR_UNKNOWN = 0; // 0x0
   }
 
-  @IntDef({androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_UNKNOWN, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_STRUCTURE_CHANGED, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_SLICE_NO_LONGER_PRESENT, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_INVALID_INPUT}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceLiveData.OnErrorListener.ErrorType {
+  @Deprecated @IntDef({androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_UNKNOWN, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_STRUCTURE_CHANGED, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_SLICE_NO_LONGER_PRESENT, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_INVALID_INPUT}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceLiveData.OnErrorListener.ErrorType {
   }
 
-  @RequiresApi(19) public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
-    ctor public SliceView(android.content.Context!);
-    ctor public SliceView(android.content.Context!, android.util.AttributeSet?);
-    ctor public SliceView(android.content.Context!, android.util.AttributeSet?, int);
-    ctor @RequiresApi(21) public SliceView(android.content.Context!, android.util.AttributeSet!, int, int);
-    method protected void configureViewPolicy(int);
-    method public int getHiddenItemCount();
-    method public int getMode();
-    method public androidx.slice.Slice? getSlice();
-    method public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
-    method public boolean isScrollable();
-    method public void onChanged(androidx.slice.Slice?);
-    method public void onClick(android.view.View!);
-    method public void setAccentColor(@ColorInt int);
-    method public void setCurrentView(androidx.slice.widget.SliceChildView);
-    method public void setMode(int);
-    method public void setOnSliceActionListener(androidx.slice.widget.SliceView.OnSliceActionListener?);
-    method public void setRowStyleFactory(androidx.slice.widget.RowStyleFactory?);
-    method public void setScrollable(boolean);
-    method public void setShowActionDividers(boolean);
-    method public void setShowHeaderDivider(boolean);
-    method public void setShowTitleItems(boolean);
-    method public void setSlice(androidx.slice.Slice?);
-    method public void setSliceActions(java.util.List<androidx.slice.core.SliceAction!>?);
-    field public static final int MODE_LARGE = 2; // 0x2
-    field public static final int MODE_SHORTCUT = 3; // 0x3
-    field public static final int MODE_SMALL = 1; // 0x1
+  @Deprecated @RequiresApi(19) public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
+    ctor @Deprecated public SliceView(android.content.Context!);
+    ctor @Deprecated public SliceView(android.content.Context!, android.util.AttributeSet?);
+    ctor @Deprecated public SliceView(android.content.Context!, android.util.AttributeSet?, int);
+    ctor @Deprecated @RequiresApi(21) public SliceView(android.content.Context!, android.util.AttributeSet!, int, int);
+    method @Deprecated protected void configureViewPolicy(int);
+    method @Deprecated public int getHiddenItemCount();
+    method @Deprecated public int getMode();
+    method @Deprecated public androidx.slice.Slice? getSlice();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
+    method @Deprecated public boolean isScrollable();
+    method @Deprecated public void onChanged(androidx.slice.Slice?);
+    method @Deprecated public void onClick(android.view.View!);
+    method @Deprecated public void setAccentColor(@ColorInt int);
+    method @Deprecated public void setCurrentView(androidx.slice.widget.SliceChildView);
+    method @Deprecated public void setMode(int);
+    method @Deprecated public void setOnSliceActionListener(androidx.slice.widget.SliceView.OnSliceActionListener?);
+    method @Deprecated public void setRowStyleFactory(androidx.slice.widget.RowStyleFactory?);
+    method @Deprecated public void setScrollable(boolean);
+    method @Deprecated public void setShowActionDividers(boolean);
+    method @Deprecated public void setShowHeaderDivider(boolean);
+    method @Deprecated public void setShowTitleItems(boolean);
+    method @Deprecated public void setSlice(androidx.slice.Slice?);
+    method @Deprecated public void setSliceActions(java.util.List<androidx.slice.core.SliceAction!>?);
+    field @Deprecated public static final int MODE_LARGE = 2; // 0x2
+    field @Deprecated public static final int MODE_SHORTCUT = 3; // 0x3
+    field @Deprecated public static final int MODE_SMALL = 1; // 0x1
   }
 
-  public static interface SliceView.OnSliceActionListener {
-    method public void onSliceAction(androidx.slice.widget.EventInfo, androidx.slice.SliceItem);
+  @Deprecated public static interface SliceView.OnSliceActionListener {
+    method @Deprecated public void onSliceAction(androidx.slice.widget.EventInfo, androidx.slice.SliceItem);
   }
 
-  @RequiresApi(19) public class TemplateView extends androidx.slice.widget.SliceChildView {
-    ctor public TemplateView(android.content.Context);
-    method public void onAttachedToWindow();
+  @Deprecated @RequiresApi(19) public class TemplateView extends androidx.slice.widget.SliceChildView {
+    ctor @Deprecated public TemplateView(android.content.Context);
+    method @Deprecated public void onAttachedToWindow();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void resetView();
-    method public void setAdapter(androidx.slice.widget.SliceAdapter);
+    method @Deprecated public void setAdapter(androidx.slice.widget.SliceAdapter);
   }
 
 }
diff --git a/slice/slice-view/lint-baseline.xml b/slice/slice-view/lint-baseline.xml
index d89abc3..688d705 100644
--- a/slice/slice-view/lint-baseline.xml
+++ b/slice/slice-view/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-beta02" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta02)" variant="all" version="8.1.0-beta02">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="cli" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="BanSynchronizedMethods"
@@ -73,1345 +73,4 @@
             file="src/main/java/androidx/slice/widget/TemplateView.java"/>
     </issue>
 
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public ActionRow(Context context, boolean fullActions) {"
-        errorLine2="                     ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ActionRow.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getHeight(SliceStyle style, SliceViewPolicy policy) {"
-        errorLine2="                         ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ListContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getHeight(SliceStyle style, SliceViewPolicy policy) {"
-        errorLine2="                                           ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ListContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public DisplayedListItems getRowItems(int availableHeight, SliceStyle style,"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ListContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public DisplayedListItems getRowItems(int availableHeight, SliceStyle style,"
-        errorLine2="                                                               ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ListContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            SliceViewPolicy policy) {"
-        errorLine2="            ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ListContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceContent getSeeMoreItem() {"
-        errorLine2="           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ListContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static int getRowType(SliceContent content, boolean isHeader,"
-        errorLine2="                                 ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ListContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="                                 List&lt;SliceAction> actions) {"
-        errorLine2="                                 ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ListContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static int getListHeight(List&lt;SliceContent> listItems, SliceStyle style,"
-        errorLine2="                                    ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ListContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static int getListHeight(List&lt;SliceContent> listItems, SliceStyle style,"
-        errorLine2="                                                                  ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ListContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            SliceViewPolicy policy) {"
-        errorLine2="            ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ListContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static void trackInputFocused(ViewGroup parent) {"
-        errorLine2="                                         ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/LocationBasedViewTracker.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static void trackA11yFocus(ViewGroup parent) {"
-        errorLine2="                                      ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/LocationBasedViewTracker.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public MessageView(Context context) {"
-        errorLine2="                       ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/MessageView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceItem(SliceContent content, boolean isHeader, int index,"
-        errorLine2="                             ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/MessageView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            int rowCount, SliceView.OnSliceActionListener observer) {"
-        errorLine2="                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/MessageView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public RemoteInputView(Context context, AttributeSet attrs) {"
-        errorLine2="                           ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RemoteInputView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public RemoteInputView(Context context, AttributeSet attrs) {"
-        errorLine2="                                            ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RemoteInputView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static RemoteInputView inflate(Context context, ViewGroup root) {"
-        errorLine2="                  ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RemoteInputView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static RemoteInputView inflate(Context context, ViewGroup root) {"
-        errorLine2="                                          ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RemoteInputView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static RemoteInputView inflate(Context context, ViewGroup root) {"
-        errorLine2="                                                           ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RemoteInputView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setAction(SliceItem action) {"
-        errorLine2="                          ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RemoteInputView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {"
-        errorLine2="                               ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RemoteInputView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {"
-        errorLine2="                                                           ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RemoteInputView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public RemoteEditText(Context context, AttributeSet attrs) {"
-        errorLine2="                              ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RemoteInputView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public RemoteEditText(Context context, AttributeSet attrs) {"
-        errorLine2="                                               ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RemoteInputView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        protected void onVisibilityChanged(View changedView, int visibility) {"
-        errorLine2="                                           ~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RemoteInputView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {"
-        errorLine2="                                                                      ~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RemoteInputView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public RowContent(SliceItem rowSlice, int position) {"
-        errorLine2="                      ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RowContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public List&lt;SliceItem> getEndItems() {"
-        errorLine2="           ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RowContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public List&lt;SliceAction> getToggleItems() {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RowContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getHeight(SliceStyle style, SliceViewPolicy policy) {"
-        errorLine2="                         ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RowContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getHeight(SliceStyle style, SliceViewPolicy policy) {"
-        errorLine2="                                           ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RowContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public RowStyle(Context context, int resId, @NonNull SliceStyle sliceStyle) {"
-        errorLine2="                    ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RowStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected Set&lt;SliceItem> mLoadingActions = new HashSet&lt;>();"
-        errorLine2="              ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RowView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setStyle(SliceStyle styles, RowStyle rowStyle) {"
-        errorLine2="                         ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RowView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setStyle(SliceStyle styles, RowStyle rowStyle) {"
-        errorLine2="                                            ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RowView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceActions(List&lt;SliceAction> actions) {"
-        errorLine2="                                ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RowView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setLoadingActions(Set&lt;SliceItem> actions) {"
-        errorLine2="                                  ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/RowView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public ShortcutView(Context context) {"
-        errorLine2="                        ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ShortcutView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceContent(ListContent sliceContent) {"
-        errorLine2="                                ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ShortcutView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setLoadingActions(Set&lt;SliceItem> actions) {"
-        errorLine2="                                  ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ShortcutView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public Set&lt;SliceItem> getLoadingActions() {"
-        errorLine2="           ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/ShortcutView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceActionView(Context context, SliceStyle style, RowStyle rowStyle) {"
-        errorLine2="                           ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceActionView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceActionView(Context context, SliceStyle style, RowStyle rowStyle) {"
-        errorLine2="                                            ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceActionView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceActionView(Context context, SliceStyle style, RowStyle rowStyle) {"
-        errorLine2="                                                              ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceActionView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setAction(@NonNull SliceActionImpl action, EventInfo info,"
-        errorLine2="                                                           ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceActionView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            SliceView.OnSliceActionListener listener, int color,"
-        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceActionView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            SliceActionLoadingListener loadingListener) {"
-        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceActionView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setParents(SliceView parent, TemplateView templateView) {"
-        errorLine2="                           ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceAdapter.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setParents(SliceView parent, TemplateView templateView) {"
-        errorLine2="                                             ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceAdapter.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceObserver(SliceView.OnSliceActionListener observer) {"
-        errorLine2="                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceAdapter.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceActions(List&lt;SliceAction> actions) {"
-        errorLine2="                                ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceAdapter.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceItems(List&lt;SliceContent> slices, int color, int mode) {"
-        errorLine2="                              ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceAdapter.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setStyle(SliceStyle style) {"
-        errorLine2="                         ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceAdapter.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setPolicy(SliceViewPolicy p) {"
-        errorLine2="                          ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceAdapter.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setLoadingActions(Set&lt;SliceItem> actions) {"
-        errorLine2="                                  ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceAdapter.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public Set&lt;SliceItem> getLoadingActions() {"
-        errorLine2="           ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceAdapter.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void onSliceActionLoading(SliceItem actionItem, int position) {"
-        errorLine2="                                     ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceAdapter.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public SliceViewHolder(View itemView) {"
-        errorLine2="                               ~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceAdapter.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected SliceView.OnSliceActionListener mObserver;"
-        errorLine2="              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected SliceActionView.SliceActionLoadingListener mLoadingListener;"
-        errorLine2="              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected SliceStyle mSliceStyle;"
-        errorLine2="              ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected RowStyle mRowStyle;"
-        errorLine2="              ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected SliceViewPolicy mViewPolicy;"
-        errorLine2="              ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceContent(ListContent content) {"
-        errorLine2="                                ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceActions(List&lt;SliceAction> actions) {"
-        errorLine2="                                ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceActionListener(SliceView.OnSliceActionListener observer) {"
-        errorLine2="                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceActionLoadingListener(SliceActionView.SliceActionLoadingListener listener) {"
-        errorLine2="                                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setActionLoading(SliceItem item) {"
-        errorLine2="                                 ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setLoadingActions(Set&lt;SliceItem> loadingActions) {"
-        errorLine2="                                  ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public Set&lt;SliceItem> getLoadingActions() {"
-        errorLine2="           ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setStyle(SliceStyle styles, @NonNull RowStyle rowStyle) {"
-        errorLine2="                         ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceChildView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected SliceItem mSliceItem;"
-        errorLine2="              ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected SliceItem mColorItem;"
-        errorLine2="              ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected SliceItem mLayoutDirItem;"
-        errorLine2="              ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected SliceItem mContentDescr;"
-        errorLine2="              ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getHeight(SliceStyle style, SliceViewPolicy policy) {"
-        errorLine2="                         ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getHeight(SliceStyle style, SliceViewPolicy policy) {"
-        errorLine2="                                           ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceContent.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            @NonNull InputStream input, OnErrorListener listener) {"
-        errorLine2="                                        ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceLiveData.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            @NonNull InputStream input, OnErrorListener listener) {"
-        errorLine2="                                        ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceLiveData.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            SliceViewManager manager, @NonNull InputStream input, OnErrorListener listener) {"
-        errorLine2="            ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceLiveData.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            SliceViewManager manager, @NonNull InputStream input, OnErrorListener listener) {"
-        errorLine2="                                                                  ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceLiveData.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public List&lt;SliceAction> getToggles() {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceMetadata.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public boolean sendToggleAction(SliceAction toggleAction, boolean toggleValue)"
-        errorLine2="                                    ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceMetadata.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public ListContent getListContent() {"
-        errorLine2="           ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceMetadata.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceStructure(Slice s) {"
-        errorLine2="                          ~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceStructure.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceStructure(SliceItem s) {"
-        errorLine2="                          ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceStructure.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
-        errorLine2="                      ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
-        errorLine2="                                       ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getRowHeight(RowContent row, SliceViewPolicy policy) {"
-        errorLine2="                            ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getRowHeight(RowContent row, SliceViewPolicy policy) {"
-        errorLine2="                                            ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getGridHeight(GridContent grid, SliceViewPolicy policy) {"
-        errorLine2="                             ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getGridHeight(GridContent grid, SliceViewPolicy policy) {"
-        errorLine2="                                               ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getListHeight(ListContent list, SliceViewPolicy policy) {"
-        errorLine2="                             ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getListHeight(ListContent list, SliceViewPolicy policy) {"
-        errorLine2="                                               ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getListItemsHeight(List&lt;SliceContent> listItems, SliceViewPolicy policy) {"
-        errorLine2="                                  ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public int getListItemsHeight(List&lt;SliceContent> listItems, SliceViewPolicy policy) {"
-        errorLine2="                                                                ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public DisplayedListItems getListItemsForNonScrollingList(ListContent list,"
-        errorLine2="                                                              ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="                                                             SliceViewPolicy policy) {"
-        errorLine2="                                                             ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceStyle.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static IconCompat convert(Context context, IconCompat icon, SerializeOptions options) {"
-        errorLine2="                  ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static IconCompat convert(Context context, IconCompat icon, SerializeOptions options) {"
-        errorLine2="                                     ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static IconCompat convert(Context context, IconCompat icon, SerializeOptions options) {"
-        errorLine2="                                                      ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static IconCompat convert(Context context, IconCompat icon, SerializeOptions options) {"
-        errorLine2="                                                                       ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public void checkThrow(String format) {"
-        errorLine2="                               ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Bitmap.CompressFormat getFormat() {"
-        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public SerializeOptions setActionMode(@FormatMode int mode) {"
-        errorLine2="               ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public SerializeOptions setImageMode(@FormatMode int mode) {"
-        errorLine2="               ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public SerializeOptions setMaxImageWidth(int width) {"
-        errorLine2="               ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public SerializeOptions setMaxImageHeight(int height) {"
-        errorLine2="               ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public SerializeOptions setImageConversionFormat(Bitmap.CompressFormat format,"
-        errorLine2="               ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public SerializeOptions setImageConversionFormat(Bitmap.CompressFormat format,"
-        errorLine2="                                                         ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        void onSliceAction(Uri actionUri, Context context, Intent intent);"
-        errorLine2="                           ~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        void onSliceAction(Uri actionUri, Context context, Intent intent);"
-        errorLine2="                                          ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        void onSliceAction(Uri actionUri, Context context, Intent intent);"
-        errorLine2="                                                           ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public SliceParseException(String s, Throwable e) {"
-        errorLine2="                                   ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public SliceParseException(String s, Throwable e) {"
-        errorLine2="                                             ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public SliceParseException(String s) {"
-        errorLine2="                                   ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/SliceUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceView(Context context) {"
-        errorLine2="                     ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceView(Context context, @Nullable AttributeSet attrs) {"
-        errorLine2="                     ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {"
-        errorLine2="                     ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
-        errorLine2="                     ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
-        errorLine2="                                      ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setClickInfo(int[] info) {"
-        errorLine2="                             ~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setOnClickListener(View.OnClickListener listener) {"
-        errorLine2="                                   ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setOnLongClickListener(View.OnLongClickListener listener) {"
-        errorLine2="                                       ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static String modeToString(@SliceMode int mode) {"
-        errorLine2="                  ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected void onVisibilityChanged(View changedView, int visibility) {"
-        errorLine2="                                       ~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setListener(PolicyChangeListener listener) {"
-        errorLine2="                            ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceViewPolicy.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static Drawable getDrawable(@NonNull Context context, @AttrRes int attr) {"
-        errorLine2="                  ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceViewUtil.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static IconCompat createIconFromDrawable(Drawable d) {"
-        errorLine2="                  ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceViewUtil.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static IconCompat createIconFromDrawable(Drawable d) {"
-        errorLine2="                                                    ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceViewUtil.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            IconCompat icon, boolean isLarge, ViewGroup parent) {"
-        errorLine2="            ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceViewUtil.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            IconCompat icon, boolean isLarge, ViewGroup parent) {"
-        errorLine2="                                              ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceViewUtil.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static @NonNull Bitmap getCircularBitmap(Bitmap bitmap) {"
-        errorLine2="                                                    ~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceViewUtil.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static CharSequence getTimestampString(Context context, long time) {"
-        errorLine2="                  ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceViewUtil.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static CharSequence getTimestampString(Context context, long time) {"
-        errorLine2="                                                  ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceViewUtil.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static void tintIndeterminateProgressBar(Context context, ProgressBar bar) {"
-        errorLine2="                                                    ~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceViewUtil.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static void tintIndeterminateProgressBar(Context context, ProgressBar bar) {"
-        errorLine2="                                                                     ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/SliceViewUtil.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void onForegroundActivated(MotionEvent event) {"
-        errorLine2="                                      ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/TemplateView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setPolicy(SliceViewPolicy policy) {"
-        errorLine2="                          ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/TemplateView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setActionLoading(SliceItem item) {"
-        errorLine2="                                 ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/TemplateView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setLoadingActions(Set&lt;SliceItem> loadingActions) {"
-        errorLine2="                                  ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/TemplateView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public Set&lt;SliceItem> getLoadingActions() {"
-        errorLine2="           ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/TemplateView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceActionListener(SliceView.OnSliceActionListener observer) {"
-        errorLine2="                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/TemplateView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceActions(List&lt;SliceAction> actions) {"
-        errorLine2="                                ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/TemplateView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setSliceContent(ListContent sliceContent) {"
-        errorLine2="                                ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/TemplateView.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void setStyle(SliceStyle style, @NonNull RowStyle rowStyle) {"
-        errorLine2="                         ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slice/widget/TemplateView.java"/>
-    </issue>
-
 </issues>
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceMetadata.java b/slice/slice-view/src/main/java/androidx/slice/SliceMetadata.java
index edbc303..45009c3 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceMetadata.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceMetadata.java
@@ -71,8 +71,13 @@
 
 /**
  * Utility class to parse a {@link Slice} and provide access to information around its contents.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SliceMetadata {
 
     /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceStructure.java b/slice/slice-view/src/main/java/androidx/slice/SliceStructure.java
index f4f1f27..4b9f7cd 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceStructure.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceStructure.java
@@ -37,8 +37,13 @@
  * specific content such as text or icons.
  *
  * Two structures can be compared using {@link #equals(Object)}.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SliceStructure {
 
     private final String mStructure;
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceUtils.java b/slice/slice-view/src/main/java/androidx/slice/SliceUtils.java
index 46f1d35..d6209ce 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceUtils.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceUtils.java
@@ -63,8 +63,13 @@
 
 /**
  * Utilities for dealing with slices.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SliceUtils {
 
     private SliceUtils() {
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceViewManager.java b/slice/slice-view/src/main/java/androidx/slice/SliceViewManager.java
index 95c0d6d..3ebb7de 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceViewManager.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceViewManager.java
@@ -34,8 +34,13 @@
  * Class to handle interactions with {@link Slice}s.
  * <p>
  * The SliceViewManager manages permissions and pinned state for slices.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public abstract class SliceViewManager {
 
     /**
@@ -183,7 +188,12 @@
 
     /**
      * Class that listens to changes in {@link Slice}s.
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public interface SliceCallback {
 
         /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerBase.java b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerBase.java
index 217d0f17d..b7a5750 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerBase.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerBase.java
@@ -37,6 +37,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public abstract class SliceViewManagerBase extends SliceViewManager {
     private final ArrayMap<Pair<Uri, SliceCallback>, SliceListenerImpl> mListenerLookup =
             new ArrayMap<>();
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerCompat.java b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerCompat.java
index 1c1d436..02bfec0 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerCompat.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerCompat.java
@@ -36,6 +36,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 class SliceViewManagerCompat extends SliceViewManagerBase {
 
     SliceViewManagerCompat(Context context) {
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerWrapper.java b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerWrapper.java
index c4364d9..58a354a 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerWrapper.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerWrapper.java
@@ -43,6 +43,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(api = 28)
+@Deprecated
 class SliceViewManagerWrapper extends SliceViewManagerBase {
     private static final String TAG = "SliceViewManagerWrapper"; // exactly 23
 
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceXml.java b/slice/slice-view/src/main/java/androidx/slice/SliceXml.java
index 1251afb..b09499e 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceXml.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceXml.java
@@ -59,6 +59,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 class SliceXml {
 
     private static final String NAMESPACE = null;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/ActionRow.java b/slice/slice-view/src/main/java/androidx/slice/widget/ActionRow.java
index 15f0ac0..c32f815 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/ActionRow.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/ActionRow.java
@@ -54,6 +54,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class ActionRow extends FrameLayout {
 
     private static final int MAX_ACTIONS = 5;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/DisplayedListItems.java b/slice/slice-view/src/main/java/androidx/slice/widget/DisplayedListItems.java
index 4b5ee95..154d842 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/DisplayedListItems.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/DisplayedListItems.java
@@ -25,6 +25,7 @@
  *
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated
 class DisplayedListItems {
     private final List<SliceContent> mDisplayedItems;
     private final int mHiddenItemCount;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/EventInfo.java b/slice/slice-view/src/main/java/androidx/slice/widget/EventInfo.java
index db5231f..5e315a2 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/EventInfo.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/EventInfo.java
@@ -25,8 +25,13 @@
 
 /**
  * Represents information associated with a logged event on {@link SliceView}.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class EventInfo {
 
     /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/GridContent.java b/slice/slice-view/src/main/java/androidx/slice/widget/GridContent.java
index ee38115..1c8036e 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/GridContent.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/GridContent.java
@@ -55,8 +55,13 @@
 
 /**
  * Extracts information required to present content in a grid format from a slice.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class GridContent extends SliceContent {
 
     private boolean mAllImages;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/GridRowView.java b/slice/slice-view/src/main/java/androidx/slice/widget/GridRowView.java
index 9bbd5de..b48c559 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/GridRowView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/GridRowView.java
@@ -86,6 +86,12 @@
 import java.util.Iterator;
 import java.util.List;
 
+/**
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
+ */
+@Deprecated
 @RequiresApi(19)
 public class GridRowView extends SliceChildView implements View.OnClickListener,
         View.OnTouchListener {
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/ListContent.java b/slice/slice-view/src/main/java/androidx/slice/widget/ListContent.java
index 8151152..8f28a05 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/ListContent.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/ListContent.java
@@ -54,6 +54,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class ListContent extends SliceContent {
 
     private SliceAction mPrimaryAction;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/LocationBasedViewTracker.java b/slice/slice-view/src/main/java/androidx/slice/widget/LocationBasedViewTracker.java
index 0404633..b7c5639 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/LocationBasedViewTracker.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/LocationBasedViewTracker.java
@@ -34,6 +34,7 @@
  * Utility class to track view based on relative location to the parent.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
+@Deprecated
 public class LocationBasedViewTracker implements Runnable, View.OnLayoutChangeListener {
 
     private static final SelectionLogic INPUT_FOCUS = new SelectionLogic() {
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/MessageView.java b/slice/slice-view/src/main/java/androidx/slice/widget/MessageView.java
index 8e44e18..d14fbdb 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/MessageView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/MessageView.java
@@ -40,6 +40,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class MessageView extends SliceChildView {
 
     private TextView mDetails;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/RemoteInputView.java b/slice/slice-view/src/main/java/androidx/slice/widget/RemoteInputView.java
index 09392c9..5e89e45 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/RemoteInputView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/RemoteInputView.java
@@ -63,6 +63,7 @@
 @SuppressWarnings("AppCompatCustomView")
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(21)
+@Deprecated
 public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher {
 
     private static final String TAG = "RemoteInput";
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/RowContent.java b/slice/slice-view/src/main/java/androidx/slice/widget/RowContent.java
index eed88be..dcd403c 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/RowContent.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/RowContent.java
@@ -60,6 +60,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class RowContent extends SliceContent {
     private static final String TAG = "RowContent";
 
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/RowStyle.java b/slice/slice-view/src/main/java/androidx/slice/widget/RowStyle.java
index 02416ac..960bfcc 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/RowStyle.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/RowStyle.java
@@ -31,6 +31,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class RowStyle {
     public static final int UNBOUNDED = -1;
 
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/RowStyleFactory.java b/slice/slice-view/src/main/java/androidx/slice/widget/RowStyleFactory.java
index 39e9b74..4ada731 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/RowStyleFactory.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/RowStyleFactory.java
@@ -22,7 +22,12 @@
 
 /**
  * Factory to return different styles for child views of a slice.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public interface RowStyleFactory {
     /**
      * Returns the style resource to use for this child.
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/RowView.java b/slice/slice-view/src/main/java/androidx/slice/widget/RowView.java
index 34d23e5..84f7712 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/RowView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/RowView.java
@@ -119,8 +119,13 @@
 /**
  * Row item is in small template format and can be used to construct list items for use
  * with {@link TemplateView}.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class RowView extends SliceChildView implements View.OnClickListener,
         AdapterView.OnItemSelectedListener {
 
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/ShortcutView.java b/slice/slice-view/src/main/java/androidx/slice/widget/ShortcutView.java
index 89ae3d0..5fa1625 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/ShortcutView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/ShortcutView.java
@@ -42,6 +42,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class ShortcutView extends SliceChildView {
 
     private static final String TAG = "ShortcutView";
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceActionView.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceActionView.java
index e5b20d0..0ffdf27 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceActionView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceActionView.java
@@ -56,6 +56,7 @@
 @SuppressWarnings("AppCompatCustomView")
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SliceActionView extends FrameLayout implements View.OnClickListener,
         CompoundButton.OnCheckedChangeListener {
     private static final String TAG = "SliceActionView";
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceAdapter.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceAdapter.java
index f8ae73a..4fc45ab 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceAdapter.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceAdapter.java
@@ -47,9 +47,14 @@
 
 /**
  * RecyclerView.Adapter for the Slice components.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @SuppressWarnings("HiddenSuperclass")
 @RequiresApi(19)
+@Deprecated
 public class SliceAdapter extends RecyclerView.Adapter<SliceAdapter.SliceViewHolder>
         implements SliceActionView.SliceActionLoadingListener {
 
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceChildView.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceChildView.java
index b0795bf..9b1b11b 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceChildView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceChildView.java
@@ -35,8 +35,13 @@
 
 /**
  * Base class for children views of {@link SliceView}.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public abstract class SliceChildView extends FrameLayout {
 
     /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceContent.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceContent.java
index a56e1e9..0d03a82 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceContent.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceContent.java
@@ -53,8 +53,13 @@
 
 /**
  * Base class representing content that can be displayed.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SliceContent {
 
     /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceLiveData.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceLiveData.java
index 8fc3a85..ee2ca9e 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceLiveData.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceLiveData.java
@@ -55,8 +55,13 @@
  *
  * @see #fromUri(Context, Uri)
  * @see LiveData
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public final class SliceLiveData {
     private static final String TAG = "SliceLiveData";
 
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetrics.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetrics.java
index faa5e10..30178d3 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetrics.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetrics.java
@@ -29,6 +29,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 class SliceMetrics {
 
     public static @Nullable SliceMetrics getInstance(@NonNull Context context, @NonNull Uri uri) {
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetricsWrapper.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetricsWrapper.java
index adb43d8..fe4abcca 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetricsWrapper.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetricsWrapper.java
@@ -27,6 +27,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(api = 28)
+@Deprecated
 class SliceMetricsWrapper extends SliceMetrics {
 
     private final android.app.slice.SliceMetrics mSliceMetrics;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceStyle.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceStyle.java
index 2440e67..7d86b57 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceStyle.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceStyle.java
@@ -44,6 +44,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SliceStyle {
     private int mTintColor = -1;
     private final int mTitleColor;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceView.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceView.java
index 8c6fd44..43cbaf8 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceView.java
@@ -94,8 +94,13 @@
  *
  * @see Slice
  * @see SliceLiveData
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SliceView extends ViewGroup implements Observer<Slice>, View.OnClickListener {
 
     private static final String TAG = "SliceView";
@@ -104,7 +109,12 @@
      * Implement this interface to be notified of interactions with the slice displayed
      * in this view.
      * @see EventInfo
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public interface OnSliceActionListener {
         /**
          * Called when an interaction has occurred with an element in this view.
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewPolicy.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewPolicy.java
index 57dcf89..3c649ac 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewPolicy.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewPolicy.java
@@ -27,6 +27,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SliceViewPolicy {
 
     /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewUtil.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewUtil.java
index f4f0499..bb0eef3 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewUtil.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewUtil.java
@@ -54,6 +54,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SliceViewUtil {
 
     /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/TemplateView.java b/slice/slice-view/src/main/java/androidx/slice/widget/TemplateView.java
index b26282b..fca31f0 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/TemplateView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/TemplateView.java
@@ -36,9 +36,14 @@
 
 /**
  * Slice template containing all view components.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @SuppressWarnings("HiddenSuperclass")
 @RequiresApi(19)
+@Deprecated
 public class TemplateView extends SliceChildView implements
         SliceViewPolicy.PolicyChangeListener {
 
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index 032cd8a..c4276b5 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -754,8 +754,12 @@
                     bounds, swipeDirection, segment, speed, getDisplayId()).pause(250);
 
             // Perform the gesture and return early if we reached the end
-            if (mGestureController.performGestureAndWait(
-                    Until.scrollFinished(direction), SCROLL_TIMEOUT, swipe)) {
+            Boolean scrollFinishedResult = mGestureController.performGestureAndWait(
+                    Until.scrollFinished(direction), SCROLL_TIMEOUT, swipe);
+            if (!Boolean.FALSE.equals(scrollFinishedResult)) {
+                if (scrollFinishedResult == null) {
+                    Log.i(TAG, "No scroll event received after scroll.");
+                }
                 return false;
             }
         }
@@ -919,8 +923,12 @@
         // Perform the gesture and return true if we did not reach the end
         Log.d(TAG, String.format("Flinging %s (bounds=%s) at %dpx/s.",
                 direction.name().toLowerCase(), bounds, speed));
-        return !mGestureController.performGestureAndWait(
+        Boolean scrollFinishedResult = mGestureController.performGestureAndWait(
                 Until.scrollFinished(direction), FLING_TIMEOUT, swipe);
+        if (scrollFinishedResult == null) {
+            Log.i(TAG, "No scroll event received after fling.");
+        }
+        return Boolean.FALSE.equals(scrollFinishedResult);
     }
 
     /** Sets this object's text content if it is an editable field. */
diff --git a/transition/transition/build.gradle b/transition/transition/build.gradle
index e20a14c..693fbbd 100644
--- a/transition/transition/build.gradle
+++ b/transition/transition/build.gradle
@@ -8,9 +8,9 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.2.0")
-    api("androidx.core:core:1.2.0")
+    api("androidx.core:core:1.12.0")
     implementation("androidx.collection:collection:1.1.0")
-    compileOnly("androidx.fragment:fragment:1.2.5")
+    compileOnly(projectOrArtifact(":fragment:fragment"))
     compileOnly("androidx.appcompat:appcompat:1.0.1")
     implementation("androidx.dynamicanimation:dynamicanimation:1.0.0")
 
diff --git a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
new file mode 100644
index 0000000..868a511
--- /dev/null
+++ b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
@@ -0,0 +1,446 @@
+/*
+ * 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.transition
+
+import android.os.Build
+import android.window.BackEvent
+import androidx.activity.BackEventCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.waitForExecution
+import androidx.transition.test.R
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class FragmentTransitionSeekingTest {
+
+    @Suppress("DEPRECATION")
+    @get:Rule
+    val activityRule = androidx.test.rule.ActivityTestRule(
+        FragmentTransitionTestActivity::class.java
+    )
+
+    @Test
+    fun replaceOperationWithTransitionsThenGestureBack() {
+        val fm1 = activityRule.activity.supportFragmentManager
+
+        var startedEnter = false
+        val fragment1 = TransitionFragment(R.layout.scene1)
+        fragment1.setReenterTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    startedEnter = true
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment1, "1")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.waitForExecution()
+
+        val startedExitCountDownLatch = CountDownLatch(1)
+        val fragment2 = TransitionFragment()
+        fragment2.setReturnTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    startedExitCountDownLatch.countDown()
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment2, "2")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions()
+
+        fragment1.waitForTransition()
+        fragment2.waitForTransition()
+
+        val dispatcher = activityRule.activity.onBackPressedDispatcher
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackProgressed(
+                BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+            )
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(startedEnter).isTrue()
+        assertThat(startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        activityRule.runOnUiThread {
+            dispatcher.onBackPressed()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        fragment1.waitForTransition()
+
+        assertThat(fragment2.isAdded).isFalse()
+        assertThat(fm1.findFragmentByTag("2"))
+            .isEqualTo(null)
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment1.requireView().parent).isNotNull()
+    }
+
+    @Test
+    fun replaceOperationWithTransitionsThenBackCancelled() {
+        val fm1 = activityRule.activity.supportFragmentManager
+
+        var startedEnter = false
+        val fragment1 = TransitionFragment(R.layout.scene1)
+        fragment1.setReenterTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    startedEnter = true
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment1, "1")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.waitForExecution()
+
+        val startedExitCountDownLatch = CountDownLatch(1)
+        val fragment2 = TransitionFragment()
+        fragment2.setReturnTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    startedExitCountDownLatch.countDown()
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment2, "2")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions()
+
+        fragment1.waitForTransition()
+        fragment2.waitForTransition()
+
+        val dispatcher = activityRule.activity.onBackPressedDispatcher
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackProgressed(
+                BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+            )
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(startedEnter).isTrue()
+        assertThat(startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackCancelled()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        fragment1.waitForTransition()
+
+        assertThat(fragment2.isAdded).isTrue()
+        assertThat(fm1.findFragmentByTag("2")).isEqualTo(fragment2)
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment2.requireView()).isNotNull()
+    }
+
+    @Test
+    fun replaceOperationWithTransitionsThenGestureBackTwice() {
+        val fm1 = activityRule.activity.supportFragmentManager
+
+        var startedEnter = false
+        val fragment1 = TransitionFragment(R.layout.scene1)
+        fragment1.setReenterTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    startedEnter = true
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment1, "1")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.waitForExecution()
+
+        val fragment2startedExitCountDownLatch = CountDownLatch(1)
+        val fragment2 = TransitionFragment()
+        fragment2.setReenterTransition(Fade().apply { duration = 300 })
+        fragment2.setReturnTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    fragment2startedExitCountDownLatch.countDown()
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment2, "2")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions()
+
+        fragment1.waitForTransition()
+        fragment2.waitForTransition()
+
+        val fragment3startedExitCountDownLatch = CountDownLatch(1)
+        val fragment3 = TransitionFragment()
+        fragment3.setReturnTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    fragment3startedExitCountDownLatch.countDown()
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment3, "3")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions()
+
+        assertThat(fragment3.startTransitionCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
+            .isTrue()
+        // We need to wait for the exit animation to end
+        assertThat(fragment2.endTransitionCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
+            .isTrue()
+
+        val dispatcher = activityRule.activity.onBackPressedDispatcher
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackProgressed(
+                BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+            )
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(fragment3startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        activityRule.runOnUiThread {
+            dispatcher.onBackPressed()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        fragment2.waitForTransition()
+        fragment3.waitForTransition()
+
+        assertThat(fragment3.isAdded).isFalse()
+        assertThat(fm1.findFragmentByTag("3")).isEqualTo(null)
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment2.requireView().parent).isNotNull()
+
+        val fragment2ResumedLatch = CountDownLatch(1)
+        activityRule.runOnUiThread {
+            fragment2.lifecycle.addObserver(
+                object : LifecycleEventObserver {
+                    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+                        if (event.targetState == Lifecycle.State.RESUMED) {
+                            fragment2ResumedLatch.countDown()
+                        }
+                    }
+                }
+            )
+        }
+
+        assertThat(fragment2ResumedLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackProgressed(
+                BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+            )
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(startedEnter).isTrue()
+        assertThat(fragment2startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        activityRule.runOnUiThread {
+            dispatcher.onBackPressed()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        fragment1.waitForTransition()
+
+        assertThat(fragment2.isAdded).isFalse()
+        assertThat(fm1.findFragmentByTag("2")).isEqualTo(null)
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment1.requireView().parent).isNotNull()
+    }
+
+    @Test
+    fun replaceOperationWithTransitionsThenOnBackPressedTwice() {
+        val fm1 = activityRule.activity.supportFragmentManager
+
+        var startedEnter = false
+        val fragment1 = TransitionFragment(R.layout.scene1)
+        fragment1.setReenterTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    startedEnter = true
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment1, "1")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.waitForExecution()
+
+        val fragment2startedExitCountDownLatch = CountDownLatch(1)
+        val fragment2 = TransitionFragment()
+        fragment2.setReturnTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    fragment2startedExitCountDownLatch.countDown()
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment2, "2")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions()
+
+        fragment1.waitForTransition()
+        fragment2.waitForTransition()
+
+        val fragment3startedExitCountDownLatch = CountDownLatch(1)
+        val fragment3 = TransitionFragment()
+        fragment3.setReturnTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    fragment3startedExitCountDownLatch.countDown()
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment3, "3")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions()
+
+        assertThat(fragment3.startTransitionCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
+            .isTrue()
+        // We need to wait for the exit animation to end
+        assertThat(fragment2.endTransitionCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
+            .isTrue()
+
+        val dispatcher = activityRule.activity.onBackPressedDispatcher
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        activityRule.runOnUiThread {
+            dispatcher.onBackPressed()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(fragment3startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        fragment2.waitForTransition()
+        fragment3.waitForTransition()
+
+        assertThat(fragment3.isAdded).isFalse()
+        assertThat(fm1.findFragmentByTag("3")).isEqualTo(null)
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment2.requireView().parent).isNotNull()
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        activityRule.runOnUiThread {
+            dispatcher.onBackPressed()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(startedEnter).isTrue()
+        assertThat(fragment2startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        fragment1.waitForTransition()
+
+        assertThat(fragment2.isAdded).isFalse()
+        assertThat(fm1.findFragmentByTag("2")).isEqualTo(null)
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment1.requireView().parent).isNotNull()
+    }
+}
diff --git a/transition/transition/src/androidTest/java/androidx/transition/SlideEdgeTest.java b/transition/transition/src/androidTest/java/androidx/transition/SlideEdgeTest.java
index 0d7a373..e02c4b6 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/SlideEdgeTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/SlideEdgeTest.java
@@ -43,6 +43,7 @@
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.testutils.PollingCheck;
 
 import org.junit.Test;
 
@@ -217,13 +218,52 @@
         }
     }
 
+    @LargeTest
+    @Test
+    public void interruptSlidePosition() throws Throwable {
+        final Slide slide = new Slide(Gravity.LEFT);
+        slide.setDuration(1000);
+        slide.setInterpolator(new LinearInterpolator());
 
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+        final View redSquare = spy(new View(rule.getActivity()));
+        rule.runOnUiThread(() -> {
+            mRoot.addView(new View(mRoot.getContext()), 100, mRoot.getHeight() / 2);
+
+            redSquare.setBackgroundColor(Color.RED);
+            mRoot.addView(redSquare, ViewGroup.LayoutParams.MATCH_PARENT, 100);
+            redSquare.setVisibility(View.INVISIBLE);
+        });
+
+        // now slide in
+        rule.runOnUiThread(() -> {
+            TransitionManager.beginDelayedTransition(mRoot, slide);
+            redSquare.setVisibility(View.VISIBLE);
+        });
+
+        final float[] redStartX = new float[1];
+        rule.runOnUiThread(() -> redStartX[0] = redSquare.getTranslationX());
+        PollingCheck.waitFor(1000, () -> redSquare.getTranslationX() > redStartX[0] * 0.5f);
+        assertEquals(0f, redSquare.getTranslationY(), 0f);
+        final int[] interruptedPosition = new int[2];
+        rule.runOnUiThread(() -> {
+            TransitionManager.beginDelayedTransition(mRoot, slide);
+            redSquare.getLocationOnScreen(interruptedPosition);
+            mRoot.removeView(redSquare);
+        });
+
+        rule.runOnUiThread(() -> {
+            int[] position = new int[2];
+            mRoot.getLocationOnScreen(position);
+            position[0] += redSquare.getLeft() + redSquare.getTranslationX();
+            position[1] += redSquare.getTop() + redSquare.getTranslationY();
+            assertEquals(interruptedPosition[1], position[1]);
+            assertTrue(position[0] <= interruptedPosition[0]);
+        });
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     public void seekingSlideOut() throws Throwable {
-        if (Build.VERSION.SDK_INT < 34) {
-            return; // only supported on U+
-        }
         final TransitionActivity activity = rule.getActivity();
         TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
 
@@ -298,12 +338,9 @@
         });
     }
 
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     public void seekingSlideIn() throws Throwable {
-        if (Build.VERSION.SDK_INT < 34) {
-            return; // only supported on U+
-        }
         final TransitionActivity activity = rule.getActivity();
         TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
 
@@ -384,12 +421,9 @@
     }
 
 
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     public void seekWithTranslation() throws Throwable {
-        if (Build.VERSION.SDK_INT < 34) {
-            return; // only supported on U+
-        }
         final TransitionActivity activity = rule.getActivity();
         TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
         View redSquare = new View(activity);
diff --git a/transition/transition/src/main/java/androidx/transition/FragmentTransitionSupport.java b/transition/transition/src/main/java/androidx/transition/FragmentTransitionSupport.java
index 217c891..52dacee 100644
--- a/transition/transition/src/main/java/androidx/transition/FragmentTransitionSupport.java
+++ b/transition/transition/src/main/java/androidx/transition/FragmentTransitionSupport.java
@@ -20,6 +20,7 @@
 
 import android.annotation.SuppressLint;
 import android.graphics.Rect;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -228,6 +229,59 @@
     }
 
     @Override
+    public boolean isSeekingSupported() {
+        return true;
+    }
+
+    @Override
+    public boolean isSeekingSupported(@NonNull Object transition) {
+        boolean supported = ((Transition) transition).isSeekingSupported();
+        if (!supported) {
+            Log.v("FragmentManager",
+                    "Predictive back not available for AndroidX Transition "
+                            + transition + ". Please enable seeking support for the designated "
+                            + "transition by overriding isSeekingSupported().");
+        }
+        return supported;
+    }
+
+    @Override
+    @Nullable
+    public Object controlDelayedTransition(@NonNull ViewGroup sceneRoot,
+            @NonNull Object transition) {
+        return TransitionManager.controlDelayedTransition(sceneRoot, (Transition) transition);
+    }
+
+    @Override
+    public void setCurrentPlayTime(@NonNull Object transitionController, float progress) {
+        TransitionSeekController controller = (TransitionSeekController) transitionController;
+        if (controller.isReady()) {
+            long time = (long) (progress * controller.getDurationMillis());
+            // We cannot let the time get to 0 or the totalDuration to avoid
+            // completing the operation accidentally.
+            if (time == 0L) {
+                time = 1L;
+            }
+            if (time == controller.getDurationMillis()) {
+                time = controller.getDurationMillis() - 1;
+            }
+            controller.setCurrentPlayTimeMillis(time);
+        }
+    }
+
+    @Override
+    public void animateToEnd(@NonNull Object transitionController) {
+        TransitionSeekController controller = (TransitionSeekController) transitionController;
+        controller.animateToEnd();
+    }
+
+    @Override
+    public void animateToStart(@NonNull Object transitionController) {
+        TransitionSeekController controller = (TransitionSeekController) transitionController;
+        controller.animateToStart();
+    }
+
+    @Override
     public void scheduleRemoveTargets(final @NonNull Object overallTransitionObj,
             final @Nullable Object enterTransition, final @Nullable ArrayList<View> enteringViews,
             final @Nullable Object exitTransition, final @Nullable ArrayList<View> exitingViews,
@@ -269,11 +323,23 @@
     public void setListenerForTransitionEnd(@NonNull final Fragment outFragment,
             @NonNull final Object transition, @NonNull final CancellationSignal signal,
             @NonNull final Runnable transitionCompleteRunnable) {
+        setListenerForTransitionEnd(outFragment, transition, signal,
+                null, transitionCompleteRunnable);
+    }
+
+    @Override
+    public void setListenerForTransitionEnd(@NonNull Fragment outFragment,
+            @NonNull Object transition, @NonNull CancellationSignal signal,
+            @Nullable Runnable cancelRunnable, @NonNull Runnable transitionCompleteRunnable) {
         final Transition realTransition = ((Transition) transition);
         signal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
             @Override
             public void onCancel() {
-                realTransition.cancel();
+                if (cancelRunnable == null) {
+                    realTransition.cancel();
+                } else {
+                    cancelRunnable.run();
+                }
             }
         });
         realTransition.addListener(new Transition.TransitionListener() {
diff --git a/transition/transition/src/main/java/androidx/transition/Transition.java b/transition/transition/src/main/java/androidx/transition/Transition.java
index 9cdb387..529448e 100644
--- a/transition/transition/src/main/java/androidx/transition/Transition.java
+++ b/transition/transition/src/main/java/androidx/transition/Transition.java
@@ -527,7 +527,7 @@
      * Transition's progress. The Transition will begin without starting any of the
      * animations.
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @NonNull
     TransitionSeekController createSeekController() {
         mSeekController = new SeekController();
@@ -970,7 +970,7 @@
      * values. The duration is calculated. It also adds the animators to mCurrentAnimators so that
      * each animator can support seeking.
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     void prepareAnimatorsForSeeking() {
         ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
         // Now prepare every Animator that was previously created for this transition
@@ -2370,7 +2370,7 @@
      *                           than getTotalDurationMillis() to indicate that it is playing
      *                           backwards.
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     void setCurrentPlayTimeMillis(long playTimeMillis, long lastPlayTimeMillis) {
         long duration = getTotalDurationMillis();
         boolean isReversed = playTimeMillis < lastPlayTimeMillis;
@@ -2691,7 +2691,7 @@
     /**
      * Internal implementation of TransitionSeekController.
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     class SeekController extends TransitionListenerAdapter implements TransitionSeekController,
             DynamicAnimation.OnAnimationUpdateListener {
         // Animation calculations appear to work better with numbers that range greater than 1
diff --git a/transition/transition/src/main/java/androidx/transition/TransitionSet.java b/transition/transition/src/main/java/androidx/transition/TransitionSet.java
index d0ae436..6c52d23 100644
--- a/transition/transition/src/main/java/androidx/transition/TransitionSet.java
+++ b/transition/transition/src/main/java/androidx/transition/TransitionSet.java
@@ -528,7 +528,7 @@
         return false;
     }
 
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Override
     void prepareAnimatorsForSeeking() {
         mTotalDuration = 0;
@@ -572,7 +572,7 @@
         return mTransitions.size() - 1;
     }
 
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Override
     void setCurrentPlayTimeMillis(long playTimeMillis, long lastPlayTimeMillis) {
         long duration = getTotalDurationMillis();
diff --git a/transition/transition/src/main/java/androidx/transition/TranslationAnimationCreator.java b/transition/transition/src/main/java/androidx/transition/TranslationAnimationCreator.java
index 114674c..f37bc91 100644
--- a/transition/transition/src/main/java/androidx/transition/TranslationAnimationCreator.java
+++ b/transition/transition/src/main/java/androidx/transition/TranslationAnimationCreator.java
@@ -60,10 +60,6 @@
             startX = startPosition[0] - viewPosX + terminalX;
             startY = startPosition[1] - viewPosY + terminalY;
         }
-        // Initial position is at translation startX, startY, so position is offset by that amount
-        int startPosX = viewPosX + Math.round(startX - terminalX);
-        int startPosY = viewPosY + Math.round(startY - terminalY);
-
         view.setTranslationX(startX);
         view.setTranslationY(startY);
         if (startX == endX && startY == endY) {
@@ -74,7 +70,7 @@
                 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, startY, endY));
 
         TransitionPositionListener listener = new TransitionPositionListener(view, values.view,
-                startPosX, startPosY, terminalX, terminalY);
+                terminalX, terminalY);
         transition.addListener(listener);
         anim.addListener(listener);
         anim.setInterpolator(interpolator);
@@ -91,10 +87,10 @@
         private float mPausedY;
         private final float mTerminalX;
         private final float mTerminalY;
-        private boolean mIsAnimationCancelCalled;
+        private boolean mIsTransitionCanceled;
 
         TransitionPositionListener(View movingView, View viewInHierarchy,
-                int startX, int startY, float terminalX, float terminalY) {
+                float terminalX, float terminalY) {
             mMovingView = movingView;
             mViewInHierarchy = viewInHierarchy;
             mTerminalX = terminalX;
@@ -107,8 +103,9 @@
 
         @Override
         public void onAnimationCancel(Animator animation) {
-            setInterruptedPosition();
-            mIsAnimationCancelCalled = true;
+            mIsTransitionCanceled = true;
+            mMovingView.setTranslationX(mTerminalX);
+            mMovingView.setTranslationY(mTerminalY);
         }
 
         @Override
@@ -117,6 +114,9 @@
 
         @Override
         public void onTransitionEnd(@NonNull Transition transition, boolean isReverse) {
+            if (!mIsTransitionCanceled) {
+                mViewInHierarchy.setTag(R.id.transition_position, null);
+            }
             if (!isReverse) {
                 mMovingView.setTranslationX(mTerminalX);
                 mMovingView.setTranslationY(mTerminalY);
@@ -125,21 +125,19 @@
 
         @Override
         public void onTransitionEnd(@NonNull Transition transition) {
+            onTransitionEnd(transition, false);
         }
 
         @Override
         public void onTransitionCancel(@NonNull Transition transition) {
-            if (!mIsAnimationCancelCalled) {
-                setInterruptedPosition();
-            }
+            mIsTransitionCanceled = true;
             mMovingView.setTranslationX(mTerminalX);
             mMovingView.setTranslationY(mTerminalY);
-            int[] pos = new int[2];
-            mMovingView.getLocationOnScreen(pos);
         }
 
         @Override
         public void onTransitionPause(@NonNull Transition transition) {
+            setInterruptedPosition();
             mPausedX = mMovingView.getTranslationX();
             mPausedY = mMovingView.getTranslationY();
             mMovingView.setTranslationX(mTerminalX);
diff --git a/tv/integration-tests/macrobenchmark-target/build.gradle b/tv/integration-tests/macrobenchmark-target/build.gradle
deleted file mode 100644
index df1edfa..0000000
--- a/tv/integration-tests/macrobenchmark-target/build.gradle
+++ /dev/null
@@ -1,33 +0,0 @@
-plugins {
-    id("AndroidXPlugin")
-    id("com.android.application")
-    id("AndroidXComposePlugin")
-    id("org.jetbrains.kotlin.android")
-}
-
-android {
-    namespace 'androidx.tv.integration.macrobenchmark.target'
-
-    defaultConfig {
-        minSdkVersion 28
-    }
-
-    buildTypes {
-        release {
-            minifyEnabled true
-            shrinkResources true
-            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
-                    'proguard-rules.pro'
-        }
-        benchmark {
-            initWith release
-            signingConfig signingConfigs.debug
-            matchingFallbacks = ['release']
-            debuggable false
-        }
-    }
-}
-
-dependencies {
-    implementation(libs.kotlinStdlib)
-}
\ No newline at end of file
diff --git a/tv/integration-tests/macrobenchmark-target/proguard-rules.pro b/tv/integration-tests/macrobenchmark-target/proguard-rules.pro
deleted file mode 100644
index 481bb43..0000000
--- a/tv/integration-tests/macrobenchmark-target/proguard-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-#   http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-#   public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/tv/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
deleted file mode 100644
index dffaec2..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools">
-
-    <application
-        android:allowBackup="false"
-        android:label="@string/app_name"
-        android:supportsRtl="true"
-        android:theme="@android:style/Theme.DeviceDefault"
-        tools:ignore="MissingApplicationIcon" />
-
-</manifest>
\ No newline at end of file
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/drawable/ic_launcher_background.xml b/tv/integration-tests/macrobenchmark-target/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 8e2e905..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,186 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="108dp"
-    android:height="108dp"
-    android:viewportHeight="108"
-    android:viewportWidth="108">
-    <path
-        android:fillColor="#3DDC84"
-        android:pathData="M0,0h108v108h-108z" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M9,0L9,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,0L19,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M29,0L29,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M39,0L39,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M49,0L49,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M59,0L59,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M69,0L69,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M79,0L79,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M89,0L89,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M99,0L99,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,9L108,9"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,19L108,19"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,29L108,29"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,39L108,39"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,49L108,49"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,59L108,59"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,69L108,69"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,79L108,79"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,89L108,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,99L108,99"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,29L89,29"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,39L89,39"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,49L89,49"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,59L89,59"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,69L89,69"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,79L89,79"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M29,19L29,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M39,19L39,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M49,19L49,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M59,19L59,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M69,19L69,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M79,19L79,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-</vector>
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/drawable/ic_launcher_foreground.xml b/tv/integration-tests/macrobenchmark-target/src/main/res/drawable/ic_launcher_foreground.xml
deleted file mode 100644
index 6cadbfb..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/drawable/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,46 +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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:aapt="http://schemas.android.com/aapt"
-    android:width="108dp"
-    android:height="108dp"
-    android:viewportHeight="108"
-    android:viewportWidth="108">
-    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
-        <aapt:attr name="android:fillColor">
-            <gradient
-                android:endX="85.84757"
-                android:endY="92.4963"
-                android:startX="42.9492"
-                android:startY="49.59793"
-                android:type="linear">
-                <item
-                    android:color="#44000000"
-                    android:offset="0.0" />
-                <item
-                    android:color="#00000000"
-                    android:offset="1.0" />
-            </gradient>
-        </aapt:attr>
-    </path>
-    <path
-        android:fillColor="#FFFFFF"
-        android:fillType="nonZero"
-        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
-        android:strokeColor="#00000000"
-        android:strokeWidth="1" />
-</vector>
\ No newline at end of file
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-anydpi/ic_launcher.xml b/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-anydpi/ic_launcher.xml
deleted file mode 100644
index 7528704..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-anydpi/ic_launcher.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  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.
-  -->
-
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@drawable/ic_launcher_background" />
-    <foreground android:drawable="@drawable/ic_launcher_foreground" />
-    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
-</adaptive-icon>
\ No newline at end of file
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-anydpi/ic_launcher_round.xml
deleted file mode 100644
index 7528704..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-anydpi/ic_launcher_round.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  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.
-  -->
-
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@drawable/ic_launcher_background" />
-    <foreground android:drawable="@drawable/ic_launcher_foreground" />
-    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
-</adaptive-icon>
\ No newline at end of file
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-hdpi/ic_launcher.webp b/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-hdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-hdpi/ic_launcher_round.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-mdpi/ic_launcher.webp b/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d6..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-mdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611d..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-mdpi/ic_launcher_round.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xhdpi/ic_launcher.webp b/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a307..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xhdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a695..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxhdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f50..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d642..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae3..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/values/colors.xml b/tv/integration-tests/macrobenchmark-target/src/main/res/values/colors.xml
deleted file mode 100644
index ad6c63df..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  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.
-  -->
-
-<resources>
-    <color name="purple_200">#FFBB86FC</color>
-    <color name="purple_500">#FF6200EE</color>
-    <color name="purple_700">#FF3700B3</color>
-    <color name="teal_200">#FF03DAC5</color>
-    <color name="teal_700">#FF018786</color>
-    <color name="black">#FF000000</color>
-    <color name="white">#FFFFFFFF</color>
-</resources>
\ No newline at end of file
diff --git a/tv/integration-tests/macrobenchmark-target/src/main/res/values/strings.xml b/tv/integration-tests/macrobenchmark-target/src/main/res/values/strings.xml
deleted file mode 100644
index a37f548..0000000
--- a/tv/integration-tests/macrobenchmark-target/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-<resources>
-    <string name="app_name">TV Compose Macrobenchmark Target</string>
-</resources>
\ No newline at end of file
diff --git a/tv/integration-tests/macrobenchmark/build.gradle b/tv/integration-tests/macrobenchmark/build.gradle
deleted file mode 100644
index e1d220a..0000000
--- a/tv/integration-tests/macrobenchmark/build.gradle
+++ /dev/null
@@ -1,44 +0,0 @@
-plugins {
-    id("AndroidXPlugin")
-    id("com.android.test")
-    id("kotlin-android")
-}
-
-android {
-    namespace 'androidx.tv.integration.macrobenchmark'
-
-    defaultConfig {
-        minSdkVersion 24
-    }
-
-    buildTypes {
-        // This benchmark buildType is used for benchmarking, and should function like your
-        // release build (for example, with minification on). It's signed with a debug key
-        // for easy local/CI testing.
-        benchmark {
-            debuggable = true
-            signingConfig = debug.signingConfig
-            matchingFallbacks = ["release"]
-        }
-    }
-
-    targetProjectPath = ":tv:integration-tests:macrobenchmark-target"
-    experimentalProperties["android.experimental.self-instrumenting"] = true
-}
-
-dependencies {
-    implementation(project(":benchmark:benchmark-junit4"))
-    implementation(project(":benchmark:benchmark-macro-junit4"))
-    implementation(project(":internal-testutils-macrobenchmark"))
-    implementation(libs.testRules)
-    implementation(libs.testExtJunit)
-    implementation(libs.testCore)
-    implementation(libs.testRunner)
-    implementation(libs.testUiautomator)
-}
-
-androidComponents {
-    beforeVariants(selector().all()) {
-        enable = buildType == "benchmark"
-    }
-}
\ No newline at end of file
diff --git a/tv/integration-tests/macrobenchmark/src/main/AndroidManifest.xml b/tv/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
deleted file mode 100644
index 227314e..0000000
--- a/tv/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-<manifest />
\ No newline at end of file
diff --git a/tv/integration-tests/macrobenchmark/src/main/java/androidx/tv/integration/macrobenchmark/ExampleStartupBenchmark.kt b/tv/integration-tests/macrobenchmark/src/main/java/androidx/tv/integration/macrobenchmark/ExampleStartupBenchmark.kt
deleted file mode 100644
index 821b725..0000000
--- a/tv/integration-tests/macrobenchmark/src/main/java/androidx/tv/integration/macrobenchmark/ExampleStartupBenchmark.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package androidx.tv.integration.macrobenchmark
-
-import androidx.benchmark.macro.StartupMode
-import androidx.benchmark.macro.StartupTimingMetric
-import androidx.benchmark.macro.junit4.MacrobenchmarkRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * This is an example startup benchmark.
- *
- * It navigates to the device's home screen, and launches the default activity.
- *
- * Before running this benchmark:
- * 1) switch your app's active build variant in the Studio (affects Studio runs only)
- * 2) add `<profileable android:shell="true" />` to your app's manifest, within the `<application>` tag
- *
- * Run this benchmark from Studio to see startup measurements, and captured system traces
- * for investigating your app's performance.
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleStartupBenchmark {
-    @get:Rule
-    val benchmarkRule = MacrobenchmarkRule()
-
-    @Test
-    fun startup() = benchmarkRule.measureRepeated(
-        packageName = "androidx.tv.integration.macrobenchmark.target",
-        metrics = listOf(StartupTimingMetric()),
-        iterations = 5,
-        startupMode = StartupMode.COLD
-    ) {
-        pressHome()
-        startActivityAndWait()
-    }
-}
diff --git a/tv/integration-tests/playground/build.gradle b/tv/integration-tests/playground/build.gradle
index 4970492..0842d00 100644
--- a/tv/integration-tests/playground/build.gradle
+++ b/tv/integration-tests/playground/build.gradle
@@ -32,6 +32,8 @@
     implementation(project(":compose:material3:material3"))
     implementation(project(":navigation:navigation-compose"))
     implementation(project(":profileinstaller:profileinstaller"))
+    implementation(project(":compose:material:material-icons-core"))
+    implementation(project(":compose:material:material-icons-extended"))
 
     implementation(project(":tv:tv-foundation"))
     implementation(project(":tv:tv-material"))
diff --git a/tv/integration-tests/playground/src/main/baseline-prof.txt b/tv/integration-tests/playground/src/main/baseline-prof.txt
new file mode 100644
index 0000000..970cd8b
--- /dev/null
+++ b/tv/integration-tests/playground/src/main/baseline-prof.txt
@@ -0,0 +1,5623 @@
+HPLandroidx/collection/ArrayMap$EntrySet;-><init>(Ljava/util/Map;I)V
+HPLandroidx/compose/foundation/layout/SizeElement;->equals(Ljava/lang/Object;)Z
+HPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->unregisterComposer$runtime_release(Landroidx/compose/runtime/Composer;)V
+HPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->unregisterComposition$runtime_release(Landroidx/compose/runtime/CompositionImpl;)V
+HPLandroidx/compose/runtime/ComposerImpl;->dispose$runtime_release()V
+HPLandroidx/compose/runtime/CompositionImpl;->dispose()V
+HPLandroidx/compose/runtime/Recomposer;->unregisterComposition$runtime_release(Landroidx/compose/runtime/CompositionImpl;)V
+HPLandroidx/compose/runtime/SlotWriter$groupSlots$1;-><init>(IILandroidx/compose/runtime/SlotWriter;)V
+HPLandroidx/compose/runtime/SlotWriter$groupSlots$1;->hasNext()Z
+HPLandroidx/compose/runtime/SlotWriter$groupSlots$1;->next()Ljava/lang/Object;
+HPLandroidx/compose/runtime/SlotWriter;->removeGroup()Z
+HPLandroidx/compose/runtime/SlotWriter;->removeGroups(II)Z
+HPLandroidx/compose/runtime/SlotWriter;->removeSlots(III)V
+HPLandroidx/compose/runtime/SlotWriter;->skipGroup()I
+HPLandroidx/compose/runtime/internal/ComposableLambdaImpl;->trackRead(Landroidx/compose/runtime/Composer;)V
+HPLandroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;->getCurrentSnapshot()Landroidx/compose/runtime/snapshots/MutableSnapshot;
+HPLandroidx/compose/ui/focus/FocusOwnerImpl$moveFocus$foundNextItem$1;->invoke(Landroidx/compose/ui/layout/BeyondBoundsLayout$BeyondBoundsScope;)Ljava/lang/Boolean;
+HPLandroidx/compose/ui/layout/Placeable$PlacementScope;->placeWithLayer-aW-9-wM$default(Landroidx/compose/ui/layout/Placeable$PlacementScope;Landroidx/compose/ui/layout/Placeable;J)V
+HPLandroidx/compose/ui/modifier/BackwardsCompatLocalMap;->get$ui_release(Landroidx/compose/ui/modifier/ProvidableModifierLocal;)Ljava/lang/Object;
+HPLandroidx/compose/ui/node/LayoutNode;->detach$ui_release()V
+HPLandroidx/compose/ui/node/LayoutNode;->onChildRemoved(Landroidx/compose/ui/node/LayoutNode;)V
+HPLandroidx/compose/ui/node/LayoutNode;->removeAll$ui_release()V
+HPLandroidx/compose/ui/node/LayoutNode;->removeAt$ui_release(II)V
+HPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->requestRemeasure(Landroidx/compose/ui/node/LayoutNode;Z)Z
+HPLandroidx/compose/ui/node/UiApplier;->clear()V
+HPLandroidx/compose/ui/platform/AndroidComposeView;->getFocusedRect(Landroid/graphics/Rect;)V
+HPLandroidx/tv/foundation/lazy/layout/LazyLayoutBeyondBoundsInfo$Interval;-><init>(II)V
+HPLandroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsModifierLocal$layout$2;-><init>(Landroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsModifierLocal;Lkotlin/jvm/internal/Ref$ObjectRef;I)V
+HPLandroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsModifierLocal$layout$2;->getHasMoreContent()Z
+HPLandroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsModifierLocal;->hasMoreContent-FR3nfPY(Landroidx/tv/foundation/lazy/layout/LazyLayoutBeyondBoundsInfo$Interval;I)Z
+HPLandroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsModifierLocal;->isForward-4vf7U8o(I)Z
+HPLandroidx/tv/foundation/lazy/list/LazyListBeyondBoundsState;->getFirstPlacedIndex()I
+HPLandroidx/tv/foundation/lazy/list/LazyListBeyondBoundsState;->getHasVisibleItems()Z
+HPLandroidx/tv/foundation/lazy/list/LazyListBeyondBoundsState;->getItemCount()I
+HPLandroidx/tv/foundation/lazy/list/LazyListBeyondBoundsState;->getLastPlacedIndex()I
+HPLandroidx/tv/foundation/lazy/list/LazyListBeyondBoundsState;->remeasure()V
+HPLcom/example/tvcomposebasedtests/JankStatsAggregator;->issueJankReport(Ljava/lang/String;)V
+HPLcom/google/gson/Gson$3;->write(Lcom/google/gson/stream/JsonWriter;Ljava/lang/Boolean;)V
+HPLcom/google/gson/Gson$3;->write(Lcom/google/gson/stream/JsonWriter;Ljava/lang/Number;)V
+HPLcom/google/gson/Gson$3;->write(Lcom/google/gson/stream/JsonWriter;Ljava/lang/Object;)V
+HPLcom/google/gson/Gson;->getAdapter(Lcom/google/gson/reflect/TypeToken;)Lcom/google/gson/TypeAdapter;
+HPLcom/google/gson/JsonArray;-><init>()V
+HPLcom/google/gson/JsonArray;->iterator()Ljava/util/Iterator;
+HPLcom/google/gson/JsonObject;-><init>()V
+HPLcom/google/gson/JsonPrimitive;-><init>(Ljava/lang/Boolean;)V
+HPLcom/google/gson/JsonPrimitive;-><init>(Ljava/lang/Number;)V
+HPLcom/google/gson/JsonPrimitive;->getAsNumber()Ljava/lang/Number;
+HPLcom/google/gson/internal/LinkedTreeMap$LinkedTreeMapIterator;-><init>(Lcom/google/gson/internal/LinkedTreeMap;)V
+HPLcom/google/gson/internal/LinkedTreeMap$LinkedTreeMapIterator;->hasNext()Z
+HPLcom/google/gson/internal/LinkedTreeMap$Node;-><init>(Z)V
+HPLcom/google/gson/internal/LinkedTreeMap$Node;-><init>(ZLcom/google/gson/internal/LinkedTreeMap$Node;Ljava/lang/Object;Lcom/google/gson/internal/LinkedTreeMap$Node;Lcom/google/gson/internal/LinkedTreeMap$Node;)V
+HPLcom/google/gson/internal/LinkedTreeMap$Node;->getValue()Ljava/lang/Object;
+HPLcom/google/gson/internal/LinkedTreeMap;-><init>(Z)V
+HPLcom/google/gson/internal/LinkedTreeMap;->entrySet()Ljava/util/Set;
+HPLcom/google/gson/internal/LinkedTreeMap;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HPLcom/google/gson/internal/LinkedTreeMap;->rebalance(Lcom/google/gson/internal/LinkedTreeMap$Node;Z)V
+HPLcom/google/gson/internal/LinkedTreeMap;->replaceInParent(Lcom/google/gson/internal/LinkedTreeMap$Node;Lcom/google/gson/internal/LinkedTreeMap$Node;)V
+HPLcom/google/gson/internal/LinkedTreeMap;->rotateLeft(Lcom/google/gson/internal/LinkedTreeMap$Node;)V
+HPLcom/google/gson/internal/LinkedTreeMap;->rotateRight(Lcom/google/gson/internal/LinkedTreeMap$Node;)V
+HPLcom/google/gson/internal/bind/JsonTreeWriter;-><init>()V
+HPLcom/google/gson/internal/bind/JsonTreeWriter;->beginArray()V
+HPLcom/google/gson/internal/bind/JsonTreeWriter;->endArray()V
+HPLcom/google/gson/internal/bind/JsonTreeWriter;->endObject()V
+HPLcom/google/gson/internal/bind/JsonTreeWriter;->get()Lcom/google/gson/JsonElement;
+HPLcom/google/gson/internal/bind/JsonTreeWriter;->name(Ljava/lang/String;)V
+HPLcom/google/gson/internal/bind/JsonTreeWriter;->peek()Lcom/google/gson/JsonElement;
+HPLcom/google/gson/internal/bind/JsonTreeWriter;->put(Lcom/google/gson/JsonElement;)V
+HPLcom/google/gson/internal/bind/JsonTreeWriter;->value(J)V
+HPLcom/google/gson/internal/bind/ReflectiveTypeAdapterFactory$1;->write(Lcom/google/gson/stream/JsonWriter;Ljava/lang/Object;)V
+HPLcom/google/gson/internal/bind/ReflectiveTypeAdapterFactory$Adapter;->write(Lcom/google/gson/stream/JsonWriter;Ljava/lang/Object;)V
+HPLcom/google/gson/internal/bind/TypeAdapters$34$1;->write(Lcom/google/gson/stream/JsonWriter;Ljava/lang/Object;)V
+HPLcom/google/gson/internal/bind/TypeAdapters$EnumTypeAdapter;-><init>(Lcom/google/gson/Gson;Lcom/google/gson/TypeAdapter;Ljava/lang/reflect/Type;)V
+HPLcom/google/gson/reflect/TypeToken;-><init>(Ljava/lang/reflect/Type;)V
+HPLcom/google/gson/reflect/TypeToken;->equals(Ljava/lang/Object;)Z
+HPLcom/google/gson/reflect/TypeToken;->hashCode()I
+HPLcom/google/gson/stream/JsonWriter;-><init>(Ljava/io/Writer;)V
+HPLcom/google/gson/stream/JsonWriter;->beforeValue()V
+HPLcom/google/gson/stream/JsonWriter;->beginArray()V
+HPLcom/google/gson/stream/JsonWriter;->beginObject()V
+HPLcom/google/gson/stream/JsonWriter;->close(IIC)V
+HPLcom/google/gson/stream/JsonWriter;->endObject()V
+HPLcom/google/gson/stream/JsonWriter;->peek()I
+HPLcom/google/gson/stream/JsonWriter;->string(Ljava/lang/String;)V
+HPLcom/google/gson/stream/JsonWriter;->value(Ljava/lang/Number;)V
+HPLcom/google/gson/stream/JsonWriter;->value(Z)V
+HPLcom/google/gson/stream/JsonWriter;->writeDeferredName()V
+HPLkotlin/ResultKt;->canonicalize(Ljava/lang/reflect/Type;)Ljava/lang/reflect/Type;
+HPLkotlin/ResultKt;->getRawType(Ljava/lang/reflect/Type;)Ljava/lang/Class;
+HPLkotlin/TuplesKt;->fastFilter(Ljava/util/ArrayList;Lkotlin/jvm/functions/Function1;)Ljava/util/ArrayList;
+HPLkotlin/TuplesKt;->removeCurrentGroup(Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HPLkotlin/collections/ArrayDeque;->add(ILjava/lang/Object;)V
+HPLkotlin/collections/CollectionsKt___CollectionsKt;->first(Ljava/util/List;)Ljava/lang/Object;
+HSPL_COROUTINE/ArtificialStackFrames;-><init>(I)V
+HSPL_COROUTINE/ArtificialStackFrames;-><init>(II)V
+HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda0;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda1;-><init>(Landroidx/activity/ComponentActivity;)V
+HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda2;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda3;-><init>(Landroidx/activity/ComponentActivity;)V
+HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda3;->onContextAvailable()V
+HSPLandroidx/activity/ComponentActivity$1;-><init>(Landroid/view/KeyEvent$Callback;I)V
+HSPLandroidx/activity/ComponentActivity$2;-><init>()V
+HSPLandroidx/activity/ComponentActivity$3;-><init>(Landroidx/activity/ComponentActivity;)V
+HSPLandroidx/activity/ComponentActivity$3;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLandroidx/activity/ComponentActivity$4;-><init>(Landroidx/activity/ComponentActivity;)V
+HSPLandroidx/activity/ComponentActivity$4;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLandroidx/activity/ComponentActivity$5;-><init>(Landroidx/activity/ComponentActivity;)V
+HSPLandroidx/activity/ComponentActivity$5;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLandroidx/activity/ComponentActivity$ReportFullyDrawnExecutorApi16Impl;-><init>(Landroidx/activity/ComponentActivity;)V
+HSPLandroidx/activity/ComponentActivity$ReportFullyDrawnExecutorApi16Impl;->onDraw()V
+HSPLandroidx/activity/ComponentActivity$ReportFullyDrawnExecutorApi16Impl;->viewCreated(Landroid/view/View;)V
+HSPLandroidx/activity/ComponentActivity;-><init>()V
+HSPLandroidx/activity/ComponentActivity;->ensureViewModelStore()V
+HSPLandroidx/activity/ComponentActivity;->getLifecycle()Landroidx/lifecycle/LifecycleRegistry;
+HSPLandroidx/activity/ComponentActivity;->getViewModelStore()Landroidx/lifecycle/ViewModelStore;
+HSPLandroidx/activity/ComponentActivity;->initViewTreeOwners()V
+HSPLandroidx/activity/ComponentActivity;->onCreate(Landroid/os/Bundle;)V
+HSPLandroidx/activity/ComponentActivity;->setContentView(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V
+HSPLandroidx/activity/FullyDrawnReporter;-><init>(Landroidx/activity/ComponentActivity$ReportFullyDrawnExecutorApi16Impl;Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda1;)V
+HSPLandroidx/activity/OnBackPressedDispatcher;-><init>(Landroidx/activity/ComponentActivity$1;)V
+HSPLandroidx/activity/compose/ComponentActivityKt;-><clinit>()V
+HSPLandroidx/activity/compose/ComponentActivityKt;->setContent$default(Landroidx/activity/ComponentActivity;Landroidx/compose/runtime/internal/ComposableLambdaImpl;)V
+HSPLandroidx/activity/contextaware/ContextAwareHelper;-><init>()V
+HSPLandroidx/activity/result/ActivityResult$1;-><init>(I)V
+HSPLandroidx/arch/core/executor/ArchTaskExecutor;-><init>()V
+HSPLandroidx/arch/core/executor/ArchTaskExecutor;->getInstance()Landroidx/arch/core/executor/ArchTaskExecutor;
+HSPLandroidx/arch/core/executor/DefaultTaskExecutor$1;-><init>()V
+HSPLandroidx/arch/core/executor/DefaultTaskExecutor;-><init>()V
+HSPLandroidx/arch/core/internal/FastSafeIterableMap;-><init>()V
+HSPLandroidx/arch/core/internal/FastSafeIterableMap;->get(Ljava/lang/Object;)Landroidx/arch/core/internal/SafeIterableMap$Entry;
+HSPLandroidx/arch/core/internal/FastSafeIterableMap;->remove(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/arch/core/internal/SafeIterableMap$AscendingIterator;-><init>(Landroidx/arch/core/internal/SafeIterableMap$Entry;Landroidx/arch/core/internal/SafeIterableMap$Entry;I)V
+HSPLandroidx/arch/core/internal/SafeIterableMap$Entry;-><init>(Ljava/lang/Object;Ljava/lang/Object;)V
+HSPLandroidx/arch/core/internal/SafeIterableMap$Entry;->getKey()Ljava/lang/Object;
+HSPLandroidx/arch/core/internal/SafeIterableMap$Entry;->getValue()Ljava/lang/Object;
+HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;-><init>(Landroidx/arch/core/internal/SafeIterableMap;)V
+HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->hasNext()Z
+HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->next()Ljava/lang/Object;
+HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->supportRemove(Landroidx/arch/core/internal/SafeIterableMap$Entry;)V
+HSPLandroidx/arch/core/internal/SafeIterableMap$ListIterator;-><init>(Landroidx/arch/core/internal/SafeIterableMap$Entry;Landroidx/arch/core/internal/SafeIterableMap$Entry;)V
+HSPLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->hasNext()Z
+HSPLandroidx/arch/core/internal/SafeIterableMap;-><init>()V
+HSPLandroidx/arch/core/internal/SafeIterableMap;->get(Ljava/lang/Object;)Landroidx/arch/core/internal/SafeIterableMap$Entry;
+HSPLandroidx/arch/core/internal/SafeIterableMap;->iterator()Ljava/util/Iterator;
+HSPLandroidx/arch/core/internal/SafeIterableMap;->remove(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/collection/ArraySet;-><clinit>()V
+HSPLandroidx/collection/ArraySet;-><init>()V
+HSPLandroidx/collection/ArraySet;->add(Ljava/lang/Object;)Z
+HSPLandroidx/collection/ArraySet;->allocArrays(I)V
+HSPLandroidx/collection/ArraySet;->clear()V
+HSPLandroidx/collection/ArraySet;->freeArrays([I[Ljava/lang/Object;I)V
+HSPLandroidx/collection/ArraySet;->indexOf(ILjava/lang/Object;)I
+HSPLandroidx/collection/ArraySet;->toArray()[Ljava/lang/Object;
+HSPLandroidx/collection/LongSparseArray;-><clinit>()V
+HSPLandroidx/collection/LongSparseArray;-><init>(I)V
+HSPLandroidx/collection/SimpleArrayMap;-><init>()V
+HSPLandroidx/collection/SparseArrayCompat;-><clinit>()V
+HSPLandroidx/collection/SparseArrayCompat;-><init>()V
+HSPLandroidx/compose/animation/FlingCalculator;-><init>(FLandroidx/compose/ui/unit/Density;)V
+HSPLandroidx/compose/animation/FlingCalculatorKt;-><clinit>()V
+HSPLandroidx/compose/animation/SingleValueAnimationKt;-><clinit>()V
+HSPLandroidx/compose/animation/SingleValueAnimationKt;->animateColorAsState-euL9pac(JLjava/lang/String;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State;
+HSPLandroidx/compose/animation/SplineBasedFloatDecayAnimationSpec;-><init>(Landroidx/compose/ui/unit/Density;)V
+HSPLandroidx/compose/animation/SplineBasedFloatDecayAnimationSpec_androidKt;-><clinit>()V
+HSPLandroidx/compose/animation/core/Animatable$runAnimation$2;-><init>(Landroidx/compose/animation/core/Animatable;Ljava/lang/Object;Landroidx/compose/animation/core/Animation;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/animation/core/Animatable$runAnimation$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/Animatable$runAnimation$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/Animatable;-><init>(Ljava/lang/Object;Landroidx/compose/animation/core/TwoWayConverterImpl;Ljava/lang/Object;)V
+HSPLandroidx/compose/animation/core/Animatable;->animateTo$default(Landroidx/compose/animation/core/Animatable;Ljava/lang/Object;Landroidx/compose/animation/core/AnimationSpec;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/Animatable;->getValue()Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/AnimateAsStateKt$animateValueAsState$2;-><init>(Lkotlinx/coroutines/channels/Channel;Ljava/lang/Object;)V
+HSPLandroidx/compose/animation/core/AnimateAsStateKt$animateValueAsState$2;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/AnimateAsStateKt$animateValueAsState$3$1;-><init>(Ljava/lang/Object;Landroidx/compose/animation/core/Animatable;Landroidx/compose/runtime/State;Landroidx/compose/runtime/State;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/animation/core/AnimateAsStateKt$animateValueAsState$3$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/animation/core/AnimateAsStateKt$animateValueAsState$3$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/AnimateAsStateKt$animateValueAsState$3;-><init>(Lkotlinx/coroutines/channels/Channel;Landroidx/compose/animation/core/Animatable;Landroidx/compose/runtime/State;Landroidx/compose/runtime/State;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/animation/core/AnimateAsStateKt$animateValueAsState$3;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/animation/core/AnimateAsStateKt$animateValueAsState$3;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/AnimateAsStateKt;-><clinit>()V
+HSPLandroidx/compose/animation/core/AnimateAsStateKt;->animateDpAsState-AjpBEmI(FLjava/lang/String;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State;
+HSPLandroidx/compose/animation/core/AnimateAsStateKt;->animateFloatAsState(FLandroidx/compose/animation/core/TweenSpec;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State;
+HSPLandroidx/compose/animation/core/AnimateAsStateKt;->animateValueAsState(Ljava/lang/Object;Landroidx/compose/animation/core/TwoWayConverterImpl;Landroidx/compose/animation/core/AnimationSpec;Ljava/lang/Float;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State;
+HSPLandroidx/compose/animation/core/Animation;->isFinishedFromNanos(J)Z
+HSPLandroidx/compose/animation/core/AnimationEndReason$EnumUnboxingLocalUtility;-><clinit>()V
+HSPLandroidx/compose/animation/core/AnimationEndReason$EnumUnboxingLocalUtility;->compareTo(II)I
+HSPLandroidx/compose/animation/core/AnimationEndReason$EnumUnboxingLocalUtility;->ordinal(I)I
+HSPLandroidx/compose/animation/core/AnimationEndReason$EnumUnboxingLocalUtility;->values(I)[I
+HSPLandroidx/compose/animation/core/AnimationResult;-><init>(Landroidx/compose/animation/core/AnimationState;I)V
+HSPLandroidx/compose/animation/core/AnimationScope;-><init>(Ljava/lang/Object;Landroidx/compose/animation/core/TwoWayConverterImpl;Landroidx/compose/animation/core/AnimationVector;JLjava/lang/Object;JLandroidx/compose/animation/core/SuspendAnimationKt$animate$7;)V
+HSPLandroidx/compose/animation/core/AnimationScope;->getValue()Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/AnimationState;-><init>(Landroidx/compose/animation/core/TwoWayConverterImpl;Ljava/lang/Object;Landroidx/compose/animation/core/AnimationVector;I)V
+HSPLandroidx/compose/animation/core/AnimationState;-><init>(Landroidx/compose/animation/core/TwoWayConverterImpl;Ljava/lang/Object;Landroidx/compose/animation/core/AnimationVector;JJZ)V
+HSPLandroidx/compose/animation/core/AnimationState;->getValue()Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/AnimationVector1D;-><init>(F)V
+HSPLandroidx/compose/animation/core/AnimationVector1D;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/animation/core/AnimationVector1D;->get$animation_core_release(I)F
+HSPLandroidx/compose/animation/core/AnimationVector1D;->getSize$animation_core_release()I
+HSPLandroidx/compose/animation/core/AnimationVector1D;->newVector$animation_core_release()Landroidx/compose/animation/core/AnimationVector;
+HSPLandroidx/compose/animation/core/AnimationVector1D;->reset$animation_core_release()V
+HSPLandroidx/compose/animation/core/AnimationVector1D;->set$animation_core_release(FI)V
+HSPLandroidx/compose/animation/core/AnimationVector2D;-><init>(FF)V
+HSPLandroidx/compose/animation/core/AnimationVector3D;-><init>(FFF)V
+HSPLandroidx/compose/animation/core/AnimationVector4D;-><init>(FFFF)V
+HSPLandroidx/compose/animation/core/AnimationVector4D;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/animation/core/AnimationVector4D;->get$animation_core_release(I)F
+HSPLandroidx/compose/animation/core/AnimationVector4D;->getSize$animation_core_release()I
+HSPLandroidx/compose/animation/core/AnimationVector4D;->newVector$animation_core_release()Landroidx/compose/animation/core/AnimationVector;
+HSPLandroidx/compose/animation/core/AnimationVector4D;->reset$animation_core_release()V
+HSPLandroidx/compose/animation/core/AnimationVector4D;->set$animation_core_release(FI)V
+HSPLandroidx/compose/animation/core/ComplexDouble;-><init>(DD)V
+HSPLandroidx/compose/animation/core/CubicBezierEasing;-><init>(FFF)V
+HSPLandroidx/compose/animation/core/CubicBezierEasing;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/animation/core/CubicBezierEasing;->transform(F)F
+HSPLandroidx/compose/animation/core/DecayAnimationSpecImpl;-><init>(Landroidx/compose/animation/SplineBasedFloatDecayAnimationSpec;)V
+HSPLandroidx/compose/animation/core/EasingKt;-><clinit>()V
+HSPLandroidx/compose/animation/core/FloatSpringSpec;-><init>(FFF)V
+HSPLandroidx/compose/animation/core/FloatSpringSpec;->getDurationNanos(FFF)J
+HSPLandroidx/compose/animation/core/FloatSpringSpec;->getEndVelocity(FFF)F
+HSPLandroidx/compose/animation/core/FloatSpringSpec;->getValueFromNanos(JFFF)F
+HSPLandroidx/compose/animation/core/FloatSpringSpec;->getVelocityFromNanos(JFFF)F
+HSPLandroidx/compose/animation/core/FloatTweenSpec;-><init>(IILandroidx/compose/animation/core/Easing;)V
+HSPLandroidx/compose/animation/core/FloatTweenSpec;->getValueFromNanos(JFFF)F
+HSPLandroidx/compose/animation/core/FloatTweenSpec;->getVelocityFromNanos(JFFF)F
+HSPLandroidx/compose/animation/core/MutatorMutex$Mutator;-><init>(ILkotlinx/coroutines/Job;)V
+HSPLandroidx/compose/animation/core/MutatorMutex$mutate$2;-><init>(ILandroidx/compose/animation/core/MutatorMutex;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/animation/core/MutatorMutex$mutate$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/animation/core/MutatorMutex$mutate$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/MutatorMutex$mutate$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/MutatorMutex;-><init>()V
+HSPLandroidx/compose/animation/core/SpringSimulation;-><init>()V
+HSPLandroidx/compose/animation/core/SpringSimulation;->updateValues-IJZedt4$animation_core_release(FFJ)J
+HSPLandroidx/compose/animation/core/SpringSpec;-><init>(FFLjava/lang/Object;)V
+HSPLandroidx/compose/animation/core/SpringSpec;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/animation/core/SpringSpec;->vectorize(Landroidx/compose/animation/core/TwoWayConverterImpl;)Landroidx/compose/animation/core/VectorizedFiniteAnimationSpec;
+HSPLandroidx/compose/animation/core/SuspendAnimationKt$animate$4;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/SuspendAnimationKt$animate$6;-><init>(Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/lang/Object;Landroidx/compose/animation/core/Animation;Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationState;FLkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/animation/core/SuspendAnimationKt$animate$6;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/SuspendAnimationKt$animate$7;-><init>(Landroidx/compose/animation/core/AnimationState;I)V
+HSPLandroidx/compose/animation/core/SuspendAnimationKt$animate$9;-><init>(Lkotlin/jvm/internal/Ref$ObjectRef;FLandroidx/compose/animation/core/Animation;Landroidx/compose/animation/core/AnimationState;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/animation/core/SuspendAnimationKt$animate$9;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/TargetBasedAnimation;-><init>(Landroidx/compose/animation/core/AnimationSpec;Landroidx/compose/animation/core/TwoWayConverterImpl;Ljava/lang/Object;Ljava/lang/Object;Landroidx/compose/animation/core/AnimationVector;)V
+HSPLandroidx/compose/animation/core/TargetBasedAnimation;->getDurationNanos()J
+HSPLandroidx/compose/animation/core/TargetBasedAnimation;->getTargetValue()Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/TargetBasedAnimation;->getTypeConverter()Landroidx/compose/animation/core/TwoWayConverterImpl;
+HSPLandroidx/compose/animation/core/TargetBasedAnimation;->getValueFromNanos(J)Ljava/lang/Object;
+HSPLandroidx/compose/animation/core/TargetBasedAnimation;->getVelocityVectorFromNanos(J)Landroidx/compose/animation/core/AnimationVector;
+HSPLandroidx/compose/animation/core/TargetBasedAnimation;->isInfinite()Z
+HSPLandroidx/compose/animation/core/TweenSpec;-><init>(IILandroidx/compose/animation/core/Easing;)V
+HSPLandroidx/compose/animation/core/TweenSpec;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/animation/core/TweenSpec;->vectorize(Landroidx/compose/animation/core/TwoWayConverterImpl;)Landroidx/compose/animation/core/VectorizedFiniteAnimationSpec;
+HSPLandroidx/compose/animation/core/TwoWayConverterImpl;-><init>(Landroidx/compose/foundation/ImageKt$Image$1$1;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/animation/core/VectorConvertersKt;-><clinit>()V
+HSPLandroidx/compose/animation/core/VectorizedFloatAnimationSpec;-><init>(Landroidx/compose/animation/core/FloatAnimationSpec;)V
+HSPLandroidx/compose/animation/core/VectorizedFloatAnimationSpec;-><init>(Landroidx/compose/ui/input/pointer/util/PointerIdArray;)V
+HSPLandroidx/compose/animation/core/VectorizedFloatAnimationSpec;->getDurationNanos(Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;)J
+HSPLandroidx/compose/animation/core/VectorizedFloatAnimationSpec;->getEndVelocity(Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;)Landroidx/compose/animation/core/AnimationVector;
+HSPLandroidx/compose/animation/core/VectorizedFloatAnimationSpec;->getValueFromNanos(JLandroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;)Landroidx/compose/animation/core/AnimationVector;
+HSPLandroidx/compose/animation/core/VectorizedFloatAnimationSpec;->getVelocityFromNanos(JLandroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;)Landroidx/compose/animation/core/AnimationVector;
+HSPLandroidx/compose/animation/core/VectorizedSpringSpec;-><init>(FFLandroidx/compose/animation/core/AnimationVector;)V
+HSPLandroidx/compose/animation/core/VectorizedSpringSpec;->getDurationNanos(Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;)J
+HSPLandroidx/compose/animation/core/VectorizedSpringSpec;->getEndVelocity(Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;)Landroidx/compose/animation/core/AnimationVector;
+HSPLandroidx/compose/animation/core/VectorizedSpringSpec;->getValueFromNanos(JLandroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;)Landroidx/compose/animation/core/AnimationVector;
+HSPLandroidx/compose/animation/core/VectorizedSpringSpec;->getVelocityFromNanos(JLandroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;)Landroidx/compose/animation/core/AnimationVector;
+HSPLandroidx/compose/animation/core/VectorizedSpringSpec;->isInfinite()V
+HSPLandroidx/compose/animation/core/VectorizedTweenSpec;-><init>(IILandroidx/compose/animation/core/Easing;)V
+HSPLandroidx/compose/animation/core/VectorizedTweenSpec;->getValueFromNanos(JLandroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;)Landroidx/compose/animation/core/AnimationVector;
+HSPLandroidx/compose/animation/core/VectorizedTweenSpec;->getVelocityFromNanos(JLandroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;Landroidx/compose/animation/core/AnimationVector;)Landroidx/compose/animation/core/AnimationVector;
+HSPLandroidx/compose/animation/core/VisibilityThresholdsKt;-><clinit>()V
+HSPLandroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect$effectModifier$1;-><init>(Landroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect$onNewSize$1;-><init>(Landroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect;)V
+HSPLandroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect$onNewSize$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect;-><init>(Landroid/content/Context;Landroidx/compose/foundation/OverscrollConfiguration;)V
+HSPLandroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect;->animateToRelease()V
+HSPLandroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect;->getEffectModifier()Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect;->invalidateOverscroll()V
+HSPLandroidx/compose/foundation/AndroidOverscrollKt;-><clinit>()V
+HSPLandroidx/compose/foundation/Api31Impl;-><clinit>()V
+HSPLandroidx/compose/foundation/Api31Impl;->create(Landroid/content/Context;Landroid/util/AttributeSet;)Landroid/widget/EdgeEffect;
+HSPLandroidx/compose/foundation/Api31Impl;->getDistance(Landroid/widget/EdgeEffect;)F
+HSPLandroidx/compose/foundation/BackgroundElement;-><init>(JLandroidx/compose/ui/graphics/Shape;)V
+HSPLandroidx/compose/foundation/BackgroundElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/foundation/BackgroundElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/BackgroundElement;->update(Landroidx/compose/ui/Modifier$Node;)V
+HSPLandroidx/compose/foundation/BackgroundNode;-><init>(JLandroidx/compose/ui/graphics/Brush;FLandroidx/compose/ui/graphics/Shape;)V
+HSPLandroidx/compose/foundation/BackgroundNode;->draw(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V
+HSPLandroidx/compose/foundation/BorderKt$drawRectBorder$1;-><init>(Landroidx/compose/ui/graphics/Brush;JJLkotlin/ResultKt;)V
+HSPLandroidx/compose/foundation/BorderKt$drawRectBorder$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/BorderModifierNode;-><init>(FLandroidx/compose/ui/graphics/Brush;Landroidx/compose/ui/graphics/Shape;)V
+HSPLandroidx/compose/foundation/BorderModifierNodeElement;-><init>(FLandroidx/compose/ui/graphics/Brush;Landroidx/compose/ui/graphics/Shape;)V
+HSPLandroidx/compose/foundation/BorderModifierNodeElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/foundation/BorderModifierNodeElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/BorderModifierNodeElement;->update(Landroidx/compose/ui/Modifier$Node;)V
+HSPLandroidx/compose/foundation/BorderStroke;-><init>(FLandroidx/compose/ui/graphics/SolidColor;)V
+HSPLandroidx/compose/foundation/ClipScrollableContainerKt;-><clinit>()V
+HSPLandroidx/compose/foundation/DrawOverscrollModifier;-><init>(Landroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect;)V
+HSPLandroidx/compose/foundation/DrawOverscrollModifier;->draw(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V
+HSPLandroidx/compose/foundation/FocusableElement;-><init>(Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;)V
+HSPLandroidx/compose/foundation/FocusableElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/foundation/FocusableElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/FocusableInteractionNode$emitWithFallback$1;-><init>(Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Landroidx/compose/foundation/interaction/Interaction;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/FocusableInteractionNode$emitWithFallback$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/FocusableInteractionNode$emitWithFallback$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/FocusableInteractionNode;-><init>(Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;)V
+HSPLandroidx/compose/foundation/FocusableInteractionNode;->emitWithFallback(Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Landroidx/compose/foundation/interaction/Interaction;)V
+HSPLandroidx/compose/foundation/FocusableKt$FocusableInNonTouchModeElement$1;-><init>()V
+HSPLandroidx/compose/foundation/FocusableKt;-><clinit>()V
+HSPLandroidx/compose/foundation/FocusableKt;->focusable$default(Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;I)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/foundation/FocusableNode$onFocusEvent$1;-><init>(Landroidx/compose/foundation/FocusableNode;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/FocusableNode$onFocusEvent$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/FocusableNode$onFocusEvent$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/FocusableNode;-><init>(Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;)V
+HSPLandroidx/compose/foundation/FocusableNode;->onFocusEvent(Landroidx/compose/ui/focus/FocusStateImpl;)V
+HSPLandroidx/compose/foundation/FocusableNode;->onGloballyPositioned(Landroidx/compose/ui/node/NodeCoordinator;)V
+HSPLandroidx/compose/foundation/FocusableNode;->onPlaced(Landroidx/compose/ui/node/NodeCoordinator;)V
+HSPLandroidx/compose/foundation/FocusablePinnableContainerNode;->onReset()V
+HSPLandroidx/compose/foundation/FocusableSemanticsNode;-><init>()V
+HSPLandroidx/compose/foundation/FocusedBoundsKt;-><clinit>()V
+HSPLandroidx/compose/foundation/FocusedBoundsNode;->onGloballyPositioned(Landroidx/compose/ui/node/NodeCoordinator;)V
+HSPLandroidx/compose/foundation/FocusedBoundsObserverNode;-><init>(Lkotlin/collections/AbstractMap$toString$1;)V
+HSPLandroidx/compose/foundation/FocusedBoundsObserverNode;->getProvidedValues()Landroidx/tv/material3/TabKt;
+HSPLandroidx/compose/foundation/FocusedBoundsObserverNode;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/ImageKt$Image$1$1;-><clinit>()V
+HSPLandroidx/compose/foundation/ImageKt$Image$1$1;-><init>(I)V
+HSPLandroidx/compose/foundation/ImageKt$Image$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/ImageKt$Image$1;-><clinit>()V
+HSPLandroidx/compose/foundation/ImageKt$Image$1;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Ljava/util/List;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/ImageKt;->Image(Landroidx/compose/ui/graphics/painter/Painter;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;FLandroidx/compose/ui/graphics/BlendModeColorFilter;Landroidx/compose/runtime/Composer;II)V
+HSPLandroidx/compose/foundation/ImageKt;->background-bw27NRU(Landroidx/compose/ui/Modifier;JLandroidx/compose/ui/graphics/Shape;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/foundation/ImageKt;->checkScrollableContainerConstraints-K40F9xA(JLandroidx/compose/foundation/gestures/Orientation;)V
+HSPLandroidx/compose/foundation/ImageKt;->create(Landroid/content/Context;)Landroid/widget/EdgeEffect;
+HSPLandroidx/compose/foundation/ImageKt;->getDistanceCompat(Landroid/widget/EdgeEffect;)F
+HSPLandroidx/compose/foundation/IndicationKt$indication$2;-><init>(Ljava/lang/Object;ILjava/lang/Object;)V
+HSPLandroidx/compose/foundation/IndicationKt$indication$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/IndicationKt;-><clinit>()V
+HSPLandroidx/compose/foundation/IndicationModifier;-><init>(Landroidx/compose/foundation/IndicationInstance;)V
+HSPLandroidx/compose/foundation/IndicationModifier;->draw(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V
+HSPLandroidx/compose/foundation/MutatePriority;-><clinit>()V
+HSPLandroidx/compose/foundation/MutatePriority;-><init>(ILjava/lang/String;)V
+HSPLandroidx/compose/foundation/MutatorMutex$Mutator;-><init>(Landroidx/compose/foundation/MutatePriority;Lkotlinx/coroutines/Job;)V
+HSPLandroidx/compose/foundation/MutatorMutex$mutateWith$2;-><init>(Landroidx/compose/foundation/MutatePriority;Landroidx/compose/foundation/MutatorMutex;Lkotlin/jvm/functions/Function2;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/MutatorMutex$mutateWith$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/MutatorMutex$mutateWith$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/MutatorMutex$mutateWith$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/MutatorMutex;-><init>()V
+HSPLandroidx/compose/foundation/OverscrollConfiguration;-><init>()V
+HSPLandroidx/compose/foundation/OverscrollConfigurationKt;-><clinit>()V
+HSPLandroidx/compose/foundation/ScrollKt$rememberScrollState$1$1;-><init>(I)V
+HSPLandroidx/compose/foundation/ScrollKt$rememberScrollState$1$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/foundation/ScrollKt$scroll$2$semantics$1$1;-><init>(ZLkotlinx/coroutines/CoroutineScope;Landroidx/tv/foundation/lazy/layout/LazyLayoutSemanticState;)V
+HSPLandroidx/compose/foundation/ScrollKt$scroll$2$semantics$1;-><init>(ZZZLandroidx/compose/foundation/ScrollState;Lkotlinx/coroutines/CoroutineScope;)V
+HSPLandroidx/compose/foundation/ScrollKt$scroll$2;-><init>(Landroidx/compose/foundation/ScrollState;Landroidx/compose/foundation/gestures/FlingBehavior;ZZ)V
+HSPLandroidx/compose/foundation/ScrollKt$scroll$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/ScrollState$canScrollForward$2;-><init>(Landroidx/compose/foundation/ScrollState;I)V
+HSPLandroidx/compose/foundation/ScrollState;-><clinit>()V
+HSPLandroidx/compose/foundation/ScrollState;-><init>(I)V
+HSPLandroidx/compose/foundation/ScrollState;->getValue()I
+HSPLandroidx/compose/foundation/ScrollingLayoutElement;-><init>(Landroidx/compose/foundation/ScrollState;ZZ)V
+HSPLandroidx/compose/foundation/ScrollingLayoutElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/foundation/ScrollingLayoutNode;-><init>(Landroidx/compose/foundation/ScrollState;ZZ)V
+HSPLandroidx/compose/foundation/ScrollingLayoutNode;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/gestures/BringIntoViewRequestPriorityQueue;-><init>()V
+HSPLandroidx/compose/foundation/gestures/BringIntoViewRequestPriorityQueue;->cancelAndRemoveAll(Ljava/util/concurrent/CancellationException;)V
+HSPLandroidx/compose/foundation/gestures/BringIntoViewSpec$Companion$DefaultBringIntoViewSpec$1;-><init>()V
+HSPLandroidx/compose/foundation/gestures/BringIntoViewSpec$Companion$DefaultBringIntoViewSpec$1;->calculateScrollDistance(FFF)F
+HSPLandroidx/compose/foundation/gestures/BringIntoViewSpec$Companion$DefaultBringIntoViewSpec$1;->getScrollAnimationSpec()Landroidx/compose/animation/core/AnimationSpec;
+HSPLandroidx/compose/foundation/gestures/BringIntoViewSpec$Companion;-><clinit>()V
+HSPLandroidx/compose/foundation/gestures/BringIntoViewSpec;-><clinit>()V
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode$Request;-><init>(Landroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2$1$1;Lkotlinx/coroutines/CancellableContinuationImpl;)V
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode$launchAnimation$2$1;-><init>(Landroidx/compose/foundation/gestures/ContentInViewNode;Lkotlinx/coroutines/Job;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode$launchAnimation$2$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode$launchAnimation$2$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode$launchAnimation$2$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode$launchAnimation$2;-><init>(Landroidx/compose/foundation/gestures/ContentInViewNode;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode$launchAnimation$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode$launchAnimation$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode$launchAnimation$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode;-><init>(Landroidx/compose/foundation/gestures/Orientation;Landroidx/compose/foundation/gestures/ScrollableState;ZLandroidx/compose/foundation/gestures/BringIntoViewSpec;)V
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode;->access$calculateScrollDelta(Landroidx/compose/foundation/gestures/ContentInViewNode;)F
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode;->isMaxVisible-O0kMr_c(Landroidx/compose/ui/geometry/Rect;J)Z
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode;->launchAnimation()V
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode;->onPlaced(Landroidx/compose/ui/node/NodeCoordinator;)V
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode;->onRemeasured-ozmzZPI(J)V
+HSPLandroidx/compose/foundation/gestures/ContentInViewNode;->relocationOffset-BMxPBkI(Landroidx/compose/ui/geometry/Rect;J)J
+HSPLandroidx/compose/foundation/gestures/DefaultFlingBehavior;-><init>(Landroidx/compose/animation/core/DecayAnimationSpecImpl;)V
+HSPLandroidx/compose/foundation/gestures/DefaultScrollableState$scroll$2$1;-><init>(Landroidx/compose/foundation/gestures/DefaultScrollableState;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/gestures/DefaultScrollableState$scroll$2$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/gestures/DefaultScrollableState$scroll$2$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/DefaultScrollableState$scroll$2$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/DefaultScrollableState$scroll$2;-><init>(Landroidx/compose/foundation/gestures/DefaultScrollableState;Landroidx/compose/foundation/MutatePriority;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/gestures/DefaultScrollableState$scroll$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/gestures/DefaultScrollableState$scroll$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/DefaultScrollableState$scroll$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/DefaultScrollableState$scrollScope$1;-><init>(Landroidx/compose/foundation/gestures/DefaultScrollableState;)V
+HSPLandroidx/compose/foundation/gestures/DefaultScrollableState$scrollScope$1;->scrollBy(F)F
+HSPLandroidx/compose/foundation/gestures/DefaultScrollableState;-><init>(Lkotlin/collections/AbstractMap$toString$1;)V
+HSPLandroidx/compose/foundation/gestures/DefaultScrollableState;->scroll(Landroidx/compose/foundation/MutatePriority;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/DraggableKt$awaitDrag$2;-><init>(ILjava/lang/Object;Ljava/lang/Object;Z)V
+HSPLandroidx/compose/foundation/gestures/DraggableKt$awaitDrag$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/DraggableNode$onAttach$1;-><init>(Landroidx/compose/foundation/gestures/DraggableNode;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/gestures/DraggableNode$onAttach$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/gestures/DraggableNode$onAttach$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/DraggableNode$pointerInputNode$1;-><init>(Landroidx/compose/foundation/gestures/DraggableNode;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/gestures/DraggableNode;-><init>(Landroidx/compose/foundation/gestures/ScrollDraggableState;Landroidx/compose/foundation/gestures/Orientation;ZLandroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Landroidx/compose/ui/node/LayoutNode$_foldedChildren$1;Landroidx/compose/foundation/gestures/ScrollableKt$NoOpOnDragStarted$1;Landroidx/compose/foundation/gestures/ScrollableGesturesNode$onDragStopped$1;)V
+HSPLandroidx/compose/foundation/gestures/DraggableNode;->onAttach()V
+HSPLandroidx/compose/foundation/gestures/ModifierLocalScrollableContainerProvider;-><init>(Z)V
+HSPLandroidx/compose/foundation/gestures/ModifierLocalScrollableContainerProvider;->getProvidedValues()Landroidx/tv/material3/TabKt;
+HSPLandroidx/compose/foundation/gestures/MouseWheelScrollNode$1;-><init>(Landroidx/compose/foundation/gestures/MouseWheelScrollNode;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/gestures/MouseWheelScrollNode;-><init>(Landroidx/compose/foundation/gestures/ScrollingLogic;)V
+HSPLandroidx/compose/foundation/gestures/MouseWheelScrollNode;->onAttach()V
+HSPLandroidx/compose/foundation/gestures/Orientation;-><clinit>()V
+HSPLandroidx/compose/foundation/gestures/Orientation;-><init>(ILjava/lang/String;)V
+HSPLandroidx/compose/foundation/gestures/ScrollDraggableState;-><init>(Landroidx/compose/foundation/gestures/ScrollingLogic;)V
+HSPLandroidx/compose/foundation/gestures/ScrollableElement;-><init>(Landroidx/compose/foundation/gestures/ScrollableState;Landroidx/compose/foundation/gestures/Orientation;Landroidx/compose/foundation/OverscrollEffect;ZZLandroidx/compose/foundation/gestures/FlingBehavior;Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Landroidx/compose/foundation/gestures/BringIntoViewSpec;)V
+HSPLandroidx/compose/foundation/gestures/ScrollableElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/foundation/gestures/ScrollableGesturesNode$onDragStopped$1;-><init>(Landroidx/compose/foundation/gestures/ScrollableGesturesNode;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/gestures/ScrollableGesturesNode;-><init>(Landroidx/compose/foundation/gestures/ScrollingLogic;Landroidx/compose/foundation/gestures/Orientation;ZLandroidx/compose/ui/input/nestedscroll/NestedScrollDispatcher;Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;)V
+HSPLandroidx/compose/foundation/gestures/ScrollableKt$NoOpOnDragStarted$1;-><init>(ILkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/gestures/ScrollableKt$UnityDensity$1;->getDensity()F
+HSPLandroidx/compose/foundation/gestures/ScrollableKt;-><clinit>()V
+HSPLandroidx/compose/foundation/gestures/ScrollableKt;->scrollable$default(Landroidx/compose/foundation/gestures/ScrollableState;Landroidx/compose/foundation/gestures/Orientation;Landroidx/compose/foundation/OverscrollEffect;ZZLandroidx/compose/foundation/gestures/FlingBehavior;Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Landroidx/tv/foundation/TvBringIntoViewSpec;I)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/foundation/gestures/ScrollableNestedScrollConnection;-><init>(Landroidx/compose/foundation/gestures/ScrollingLogic;Z)V
+HSPLandroidx/compose/foundation/gestures/ScrollableNode;-><init>(Landroidx/compose/foundation/gestures/ScrollableState;Landroidx/compose/foundation/gestures/Orientation;Landroidx/compose/foundation/OverscrollEffect;ZZLandroidx/compose/foundation/gestures/FlingBehavior;Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Landroidx/compose/foundation/gestures/BringIntoViewSpec;)V
+HSPLandroidx/compose/foundation/gestures/ScrollableNode;->applyFocusProperties(Landroidx/compose/ui/focus/FocusProperties;)V
+HSPLandroidx/compose/foundation/gestures/ScrollableNode;->onAttach()V
+HSPLandroidx/compose/foundation/gestures/ScrollableState;->scroll$default(Landroidx/compose/foundation/gestures/ScrollableState;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/ScrollingLogic;-><init>(Landroidx/compose/foundation/gestures/ScrollableState;Landroidx/compose/foundation/gestures/Orientation;Landroidx/compose/foundation/OverscrollEffect;ZLandroidx/compose/foundation/gestures/FlingBehavior;Landroidx/compose/ui/input/nestedscroll/NestedScrollDispatcher;)V
+HSPLandroidx/compose/foundation/gestures/UpdatableAnimationState$animateToZero$1;-><init>(Landroidx/compose/foundation/gestures/UpdatableAnimationState;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/gestures/UpdatableAnimationState$animateToZero$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/UpdatableAnimationState$animateToZero$4;-><init>(Landroidx/compose/foundation/gestures/UpdatableAnimationState;FLkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/foundation/gestures/UpdatableAnimationState$animateToZero$4;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/gestures/UpdatableAnimationState;-><clinit>()V
+HSPLandroidx/compose/foundation/gestures/UpdatableAnimationState;-><init>(Landroidx/compose/animation/core/AnimationSpec;)V
+HSPLandroidx/compose/foundation/gestures/UpdatableAnimationState;->animateToZero(Landroidx/compose/foundation/layout/OffsetNode$measure$1;Landroidx/compose/ui/node/LayoutNode$_foldedChildren$1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/interaction/FocusInteraction$Unfocus;-><init>(Landroidx/compose/foundation/interaction/FocusInteraction$Focus;)V
+HSPLandroidx/compose/foundation/interaction/FocusInteractionKt$collectIsFocusedAsState$1$1$1;-><init>(Ljava/util/ArrayList;Landroidx/compose/runtime/MutableState;I)V
+HSPLandroidx/compose/foundation/interaction/FocusInteractionKt$collectIsFocusedAsState$1$1$1;->emit(Landroidx/compose/foundation/interaction/Interaction;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/interaction/FocusInteractionKt$collectIsFocusedAsState$1$1$1;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/interaction/FocusInteractionKt$collectIsFocusedAsState$1$1;-><init>(Landroidx/compose/foundation/interaction/InteractionSource;Landroidx/compose/runtime/MutableState;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/interaction/FocusInteractionKt$collectIsFocusedAsState$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/interaction/FocusInteractionKt$collectIsFocusedAsState$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/interaction/MutableInteractionSourceImpl;-><init>()V
+HSPLandroidx/compose/foundation/interaction/MutableInteractionSourceImpl;->emit(Landroidx/compose/foundation/interaction/Interaction;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/interaction/PressInteraction$Press;-><init>(J)V
+HSPLandroidx/compose/foundation/interaction/PressInteractionKt$collectIsPressedAsState$1$1;-><init>(Landroidx/compose/foundation/interaction/InteractionSource;Landroidx/compose/runtime/MutableState;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/interaction/PressInteractionKt$collectIsPressedAsState$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/interaction/PressInteractionKt$collectIsPressedAsState$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/layout/Arrangement$Center$1;-><init>(I)V
+HSPLandroidx/compose/foundation/layout/Arrangement$Center$1;->arrange(ILandroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;[I[I)V
+HSPLandroidx/compose/foundation/layout/Arrangement$Center$1;->getSpacing-D9Ej5fM()F
+HSPLandroidx/compose/foundation/layout/Arrangement$End$1;-><init>(I)V
+HSPLandroidx/compose/foundation/layout/Arrangement$SpacedAligned;-><init>(F)V
+HSPLandroidx/compose/foundation/layout/Arrangement$SpacedAligned;->arrange(ILandroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;[I[I)V
+HSPLandroidx/compose/foundation/layout/Arrangement$SpacedAligned;->arrange(Landroidx/compose/ui/unit/Density;I[I[I)V
+HSPLandroidx/compose/foundation/layout/Arrangement$SpacedAligned;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/layout/Arrangement$SpacedAligned;->getSpacing-D9Ej5fM()F
+HSPLandroidx/compose/foundation/layout/Arrangement$Top$1;-><init>(I)V
+HSPLandroidx/compose/foundation/layout/Arrangement$Top$1;->arrange(Landroidx/compose/ui/unit/Density;I[I[I)V
+HSPLandroidx/compose/foundation/layout/Arrangement;-><clinit>()V
+HSPLandroidx/compose/foundation/layout/Arrangement;->placeCenter$foundation_layout_release(I[I[IZ)V
+HSPLandroidx/compose/foundation/layout/Arrangement;->placeLeftOrTop$foundation_layout_release([I[IZ)V
+HSPLandroidx/compose/foundation/layout/BoxKt$Box$2;-><init>(IILjava/lang/Object;)V
+HSPLandroidx/compose/foundation/layout/BoxKt$Box$2;->invoke(Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/compose/foundation/layout/BoxKt$Box$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/layout/BoxKt$EmptyBoxMeasurePolicy$1;-><clinit>()V
+HSPLandroidx/compose/foundation/layout/BoxKt$EmptyBoxMeasurePolicy$1;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Ljava/util/List;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/layout/BoxKt$boxMeasurePolicy$1$2;-><init>(Landroidx/compose/ui/layout/Placeable;Landroidx/compose/ui/layout/Measurable;Landroidx/compose/ui/layout/MeasureScope;IILandroidx/compose/ui/Alignment;)V
+HSPLandroidx/compose/foundation/layout/BoxKt$boxMeasurePolicy$1$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/layout/BoxKt$boxMeasurePolicy$1;-><init>(Z)V
+HSPLandroidx/compose/foundation/layout/BoxKt$boxMeasurePolicy$1;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Ljava/util/List;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/layout/BoxKt;-><clinit>()V
+HSPLandroidx/compose/foundation/layout/BoxKt;->Box(Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/compose/foundation/layout/BoxKt;->access$placeInBox(Landroidx/compose/ui/layout/Placeable$PlacementScope;Landroidx/compose/ui/layout/Placeable;Landroidx/compose/ui/layout/Measurable;Landroidx/compose/ui/unit/LayoutDirection;IILandroidx/compose/ui/Alignment;)V
+HSPLandroidx/compose/foundation/layout/BoxKt;->rememberBoxMeasurePolicy(ZLandroidx/compose/runtime/Composer;)Landroidx/compose/ui/layout/MeasurePolicy;
+HSPLandroidx/compose/foundation/layout/BoxScopeInstance;-><clinit>()V
+HSPLandroidx/compose/foundation/layout/ColumnKt;-><clinit>()V
+HSPLandroidx/compose/foundation/layout/ColumnKt;->columnMeasurePolicy(Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/runtime/Composer;)Landroidx/compose/ui/layout/MeasurePolicy;
+HSPLandroidx/compose/foundation/layout/CrossAxisAlignment$VerticalCrossAxisAlignment;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/foundation/layout/CrossAxisAlignment$VerticalCrossAxisAlignment;->align$foundation_layout_release(ILandroidx/compose/ui/unit/LayoutDirection;)I
+HSPLandroidx/compose/foundation/layout/FillElement;-><init>(IF)V
+HSPLandroidx/compose/foundation/layout/FillElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/foundation/layout/FillElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/layout/FillNode;-><init>(IF)V
+HSPLandroidx/compose/foundation/layout/FillNode;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/layout/HorizontalAlignElement;-><init>()V
+HSPLandroidx/compose/foundation/layout/HorizontalAlignElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/foundation/layout/HorizontalAlignElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/layout/HorizontalAlignNode;-><init>(Landroidx/compose/ui/Alignment$Horizontal;)V
+HSPLandroidx/compose/foundation/layout/HorizontalAlignNode;->modifyParentData(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/layout/OffsetElement;-><init>(FF)V
+HSPLandroidx/compose/foundation/layout/OffsetElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/foundation/layout/OffsetElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/layout/OffsetKt;->Spacer(Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;)V
+HSPLandroidx/compose/foundation/layout/OffsetKt;->access$intrinsicSize(Ljava/util/List;Landroidx/compose/ui/CombinedModifier$toString$1;Landroidx/compose/ui/CombinedModifier$toString$1;IIII)I
+HSPLandroidx/compose/foundation/layout/OffsetKt;->getRowColumnParentData(Landroidx/compose/ui/layout/Measurable;)Landroidx/compose/foundation/layout/RowColumnParentData;
+HSPLandroidx/compose/foundation/layout/OffsetKt;->getWeight(Landroidx/compose/foundation/layout/RowColumnParentData;)F
+HSPLandroidx/compose/foundation/layout/OffsetKt;->offset-VpY3zN4(Landroidx/compose/ui/Modifier;FF)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/foundation/layout/OffsetKt;->padding-3ABfNKs(Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/foundation/layout/OffsetKt;->padding-VpY3zN4(FF)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/foundation/layout/OffsetKt;->size(Landroidx/compose/ui/Alignment;Z)Landroidx/compose/foundation/layout/WrapContentElement;
+HSPLandroidx/compose/foundation/layout/OffsetKt;->toBoxConstraints-OenEA2s(JI)J
+HSPLandroidx/compose/foundation/layout/OffsetNode$measure$1;-><init>(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;I)V
+HSPLandroidx/compose/foundation/layout/OffsetNode$measure$1;->invoke(Landroidx/compose/ui/layout/Placeable$PlacementScope;)V
+HSPLandroidx/compose/foundation/layout/OffsetNode$measure$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/layout/OffsetNode;-><init>(FFZ)V
+HSPLandroidx/compose/foundation/layout/OffsetNode;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/layout/PaddingElement;-><init>(FFFF)V
+HSPLandroidx/compose/foundation/layout/PaddingElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/foundation/layout/PaddingElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/layout/PaddingNode;-><init>(FFFFZ)V
+HSPLandroidx/compose/foundation/layout/PaddingNode;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/layout/PaddingValuesImpl;-><init>(FFFF)V
+HSPLandroidx/compose/foundation/layout/RowColumnImplKt$rowColumnMeasurePolicy$1;-><init>(ILkotlin/jvm/functions/Function5;FLandroidx/compose/foundation/layout/CrossAxisAlignment$VerticalCrossAxisAlignment;)V
+HSPLandroidx/compose/foundation/layout/RowColumnImplKt$rowColumnMeasurePolicy$1;->maxIntrinsicHeight(Landroidx/compose/ui/node/NodeCoordinator;Ljava/util/List;I)I
+HSPLandroidx/compose/foundation/layout/RowColumnImplKt$rowColumnMeasurePolicy$1;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Ljava/util/List;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/layout/RowColumnMeasureHelperResult;-><init>(III[I)V
+HSPLandroidx/compose/foundation/layout/RowColumnMeasurementHelper;-><init>(ILkotlin/jvm/functions/Function5;FILandroidx/compose/foundation/layout/OffsetKt;Ljava/util/List;[Landroidx/compose/ui/layout/Placeable;)V
+HSPLandroidx/compose/foundation/layout/RowColumnParentData;-><init>()V
+HSPLandroidx/compose/foundation/layout/RowKt$DefaultRowMeasurePolicy$1;-><clinit>()V
+HSPLandroidx/compose/foundation/layout/RowKt$DefaultRowMeasurePolicy$1;-><init>(I)V
+HSPLandroidx/compose/foundation/layout/RowKt$DefaultRowMeasurePolicy$1;->invoke(ILandroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;[I[I)V
+HSPLandroidx/compose/foundation/layout/RowKt$DefaultRowMeasurePolicy$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/io/Serializable;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/layout/RowKt$rowMeasurePolicy$1$1;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/foundation/layout/RowKt$rowMeasurePolicy$1$1;->invoke(ILandroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;[I[I)V
+HSPLandroidx/compose/foundation/layout/RowKt$rowMeasurePolicy$1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/io/Serializable;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/layout/RowKt;-><clinit>()V
+HSPLandroidx/compose/foundation/layout/RowKt;->rowMeasurePolicy(Landroidx/compose/foundation/layout/Arrangement$Horizontal;Landroidx/compose/ui/BiasAlignment$Vertical;Landroidx/compose/runtime/Composer;)Landroidx/compose/ui/layout/MeasurePolicy;
+HSPLandroidx/compose/foundation/layout/RowScopeInstance;-><clinit>()V
+HSPLandroidx/compose/foundation/layout/SizeElement;-><init>(FFFFI)V
+HSPLandroidx/compose/foundation/layout/SizeElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/foundation/layout/SizeKt;-><clinit>()V
+HSPLandroidx/compose/foundation/layout/SizeKt;->fillMaxWidth$default(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/foundation/layout/SizeKt;->height-3ABfNKs(Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/foundation/layout/SizeKt;->width-3ABfNKs(Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/foundation/layout/SizeKt;->wrapContentSize$default(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/foundation/layout/SizeNode;-><init>(FFFFZ)V
+HSPLandroidx/compose/foundation/layout/SizeNode;->getTargetConstraints-OenEA2s(Landroidx/compose/ui/unit/Density;)J
+HSPLandroidx/compose/foundation/layout/SizeNode;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/layout/SpacerMeasurePolicy;-><clinit>()V
+HSPLandroidx/compose/foundation/layout/SpacerMeasurePolicy;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Ljava/util/List;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/layout/WrapContentElement;-><init>(IZLcom/example/tvcomposebasedtests/tvComponents/AppKt$App$1;Ljava/lang/Object;)V
+HSPLandroidx/compose/foundation/layout/WrapContentElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/foundation/layout/WrapContentElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/layout/WrapContentNode$measure$1;-><init>(Landroidx/compose/foundation/layout/WrapContentNode;ILandroidx/compose/ui/layout/Placeable;ILandroidx/compose/ui/layout/MeasureScope;)V
+HSPLandroidx/compose/foundation/layout/WrapContentNode$measure$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/layout/WrapContentNode;-><init>(IZLkotlin/jvm/functions/Function2;)V
+HSPLandroidx/compose/foundation/layout/WrapContentNode;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/lazy/layout/DefaultLazyKey;-><clinit>()V
+HSPLandroidx/compose/foundation/lazy/layout/DefaultLazyKey;-><init>(I)V
+HSPLandroidx/compose/foundation/lazy/layout/DefaultLazyKey;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/lazy/layout/DefaultLazyKey;->hashCode()I
+HSPLandroidx/compose/foundation/lazy/layout/IntervalList$Interval;-><init>(IILandroidx/compose/foundation/lazy/layout/LazyLayoutIntervalContent$Interval;)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory$CachedItemContent;-><init>(Landroidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory;ILjava/lang/Object;Ljava/lang/Object;)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory$CachedItemContent;->getContent()Lkotlin/jvm/functions/Function2;
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory;-><init>(Landroidx/compose/runtime/saveable/SaveableStateHolder;Landroidx/compose/foundation/lazy/layout/LazyLayoutKt$LazyLayout$3$itemContentFactory$1$1;)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory;->getContent(Ljava/lang/Object;ILjava/lang/Object;)Lkotlin/jvm/functions/Function2;
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory;->getContentType(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutItemReusePolicy;-><init>(Landroidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory;)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutItemReusePolicy;->areCompatible(Ljava/lang/Object;Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutItemReusePolicy;->getSlotsToRetain(Landroidx/compose/ui/layout/SubcomposeSlotReusePolicy$SlotIdsSet;)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutKt$LazyLayout$3$2$1;-><init>(Ljava/lang/Object;ILjava/lang/Object;)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutKt$LazyLayout$3$2$1;->invoke(Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutKt$LazyLayout$3$2$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutKt$LazyLayout$3$itemContentFactory$1$1;-><init>(Landroidx/compose/runtime/State;I)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutKt$LazyLayout$3$itemContentFactory$1$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutKt$LazyLayout$3;-><init>(Landroidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;ILandroidx/compose/runtime/MutableState;)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutKt$LazyLayout$3;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutMeasureScopeImpl;-><init>(Landroidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory;Landroidx/compose/ui/layout/SubcomposeMeasureScope;)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutMeasureScopeImpl;->getLayoutDirection()Landroidx/compose/ui/unit/LayoutDirection;
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutMeasureScopeImpl;->isLookingAhead()Z
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutMeasureScopeImpl;->layout(IILjava/util/Map;Lkotlin/jvm/functions/Function1;)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutMeasureScopeImpl;->measure-0kLqBqw(JI)Ljava/util/List;
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutMeasureScopeImpl;->roundToPx-0680j_4(F)I
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPinnableItem;-><init>(Ljava/lang/Object;Landroidx/compose/foundation/lazy/layout/LazyLayoutPinnedItemList;)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPinnableItem;->getPinsCount()I
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPinnableItem;->pin()Landroidx/compose/foundation/lazy/layout/LazyLayoutPinnableItem;
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPinnableItem;->release()V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPinnedItemList;-><init>()V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPinnedItemList;->get(I)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPinnedItemList;->isEmpty()Z
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPinnedItemList;->size()I
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState;->schedulePrefetch-0kLqBqw(JI)Landroidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState$PrefetchHandle;
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher$PrefetchRequest;-><init>(IJ)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher$PrefetchRequest;->cancel()V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher;-><init>(Landroidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState;Landroidx/compose/ui/layout/SubcomposeLayoutState;Landroidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory;Landroid/view/View;)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher;->doFrame(J)V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher;->onRemembered()V
+HSPLandroidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher;->run()V
+HSPLandroidx/compose/foundation/lazy/layout/LazySaveableStateHolder$1;-><init>(Landroidx/compose/runtime/saveable/SaveableStateRegistry;I)V
+HSPLandroidx/compose/foundation/lazy/layout/LazySaveableStateHolder$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/lazy/layout/LazySaveableStateHolder$SaveableStateProvider$2$invoke$$inlined$onDispose$1;-><init>(Ljava/lang/Object;ILjava/lang/Object;)V
+HSPLandroidx/compose/foundation/lazy/layout/LazySaveableStateHolder$SaveableStateProvider$2$invoke$$inlined$onDispose$1;->dispose()V
+HSPLandroidx/compose/foundation/lazy/layout/LazySaveableStateHolder;-><init>(Landroidx/compose/runtime/saveable/SaveableStateRegistry;Ljava/util/Map;)V
+HSPLandroidx/compose/foundation/lazy/layout/LazySaveableStateHolder;->SaveableStateProvider(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/compose/foundation/lazy/layout/LazySaveableStateHolder;->canBeSaved(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/lazy/layout/LazySaveableStateHolder;->consumeRestored(Ljava/lang/String;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/lazy/layout/LazySaveableStateHolder;->performSave()Ljava/util/Map;
+HSPLandroidx/compose/foundation/lazy/layout/LazySaveableStateHolder;->registerProvider(Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Landroidx/compose/runtime/saveable/SaveableStateRegistryImpl$registerProvider$3;
+HSPLandroidx/compose/foundation/lazy/layout/MutableIntervalList;-><init>()V
+HSPLandroidx/compose/foundation/lazy/layout/MutableIntervalList;->addInterval(ILandroidx/compose/foundation/lazy/layout/LazyLayoutIntervalContent$Interval;)V
+HSPLandroidx/compose/foundation/lazy/layout/MutableIntervalList;->checkIndexBounds(I)V
+HSPLandroidx/compose/foundation/lazy/layout/MutableIntervalList;->get(I)Landroidx/compose/foundation/lazy/layout/IntervalList$Interval;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewChildNode;-><init>()V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewChildNode;->getLayoutCoordinates()Landroidx/compose/ui/layout/LayoutCoordinates;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewChildNode;->onPlaced(Landroidx/compose/ui/node/NodeCoordinator;)V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewKt;-><clinit>()V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewRequesterImpl$bringIntoView$1;-><init>(Landroidx/compose/foundation/relocation/BringIntoViewRequesterImpl;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewRequesterImpl$bringIntoView$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewRequesterImpl;-><init>()V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewRequesterImpl;->bringIntoView(Landroidx/compose/ui/geometry/Rect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewRequesterNode;-><init>(Landroidx/compose/foundation/relocation/BringIntoViewRequesterImpl;)V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewRequesterNode;->onAttach()V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewRequesterNode;->onDetach()V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2$1$1;-><init>(Landroidx/compose/foundation/relocation/BringIntoViewResponderNode;Landroidx/compose/ui/layout/LayoutCoordinates;Lkotlin/jvm/functions/Function0;)V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2$1$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2$1;-><init>(Landroidx/compose/foundation/relocation/BringIntoViewResponderNode;Landroidx/compose/ui/layout/LayoutCoordinates;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2$2;-><init>(Landroidx/compose/foundation/relocation/BringIntoViewResponderNode;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2;-><init>(Landroidx/compose/foundation/relocation/BringIntoViewResponderNode;Landroidx/compose/ui/layout/LayoutCoordinates;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$parentRect$1;-><init>(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;I)V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$parentRect$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode;-><init>(Landroidx/compose/foundation/gestures/ContentInViewNode;)V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode;->access$bringChildIntoView$localRect(Landroidx/compose/foundation/relocation/BringIntoViewResponderNode;Landroidx/compose/ui/layout/LayoutCoordinates;Lkotlin/jvm/functions/Function0;)Landroidx/compose/ui/geometry/Rect;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode;->bringChildIntoView(Landroidx/compose/ui/layout/LayoutCoordinates;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponderNode;->getProvidedValues()Landroidx/tv/material3/TabKt;
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponder_androidKt$defaultBringIntoViewParent$1;-><init>(Landroidx/compose/ui/node/CompositionLocalConsumerModifierNode;)V
+HSPLandroidx/compose/foundation/relocation/BringIntoViewResponder_androidKt$defaultBringIntoViewParent$1;->bringChildIntoView(Landroidx/compose/ui/layout/LayoutCoordinates;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/foundation/shape/CornerBasedShape;-><init>(Landroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;)V
+HSPLandroidx/compose/foundation/shape/CornerBasedShape;->createOutline-Pq9zytI(JLandroidx/compose/ui/unit/LayoutDirection;Landroidx/compose/ui/unit/Density;)Landroidx/compose/ui/graphics/BrushKt;
+HSPLandroidx/compose/foundation/shape/DpCornerSize;-><init>(F)V
+HSPLandroidx/compose/foundation/shape/PercentCornerSize;-><init>(F)V
+HSPLandroidx/compose/foundation/shape/PercentCornerSize;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/shape/PercentCornerSize;->toPx-TmRCtEA(JLandroidx/compose/ui/unit/Density;)F
+HSPLandroidx/compose/foundation/shape/RoundedCornerShape;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/shape/RoundedCornerShapeKt;-><clinit>()V
+HSPLandroidx/compose/foundation/shape/RoundedCornerShapeKt;->RoundedCornerShape-0680j_4(F)Landroidx/compose/foundation/shape/RoundedCornerShape;
+HSPLandroidx/compose/foundation/text/EmptyMeasurePolicy;-><clinit>()V
+HSPLandroidx/compose/foundation/text/EmptyMeasurePolicy;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Ljava/util/List;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/text/modifiers/InlineDensity;-><clinit>()V
+HSPLandroidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache;-><init>(Landroidx/compose/ui/text/AnnotatedString;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/font/FontFamily$Resolver;IZIILjava/util/List;)V
+HSPLandroidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache;->intrinsicHeight(ILandroidx/compose/ui/unit/LayoutDirection;)I
+HSPLandroidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache;->layoutText-K40F9xA(JLandroidx/compose/ui/unit/LayoutDirection;)Landroidx/compose/ui/text/MultiParagraph;
+HSPLandroidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache;->setDensity$foundation_release(Landroidx/compose/ui/unit/Density;)V
+HSPLandroidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache;->setLayoutDirection(Landroidx/compose/ui/unit/LayoutDirection;)Landroidx/compose/ui/text/MultiParagraphIntrinsics;
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringElement;-><init>(Landroidx/compose/ui/text/AnnotatedString;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/font/FontFamily$Resolver;Lkotlin/jvm/functions/Function1;IZII)V
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringElement;->update(Landroidx/compose/ui/Modifier$Node;)V
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode;-><init>(Landroidx/compose/ui/text/AnnotatedString;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/font/FontFamily$Resolver;Lkotlin/jvm/functions/Function1;IZIILjava/util/List;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode;->doInvalidations(ZZZZ)V
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode;->draw(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode;->getLayoutCache()Landroidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache;
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode;->getLayoutCache(Landroidx/compose/ui/unit/Density;)Landroidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache;
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode;->getTextSubstitution()Landroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode$TextSubstitutionValue;
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode;->maxIntrinsicHeight(Landroidx/compose/ui/layout/IntrinsicMeasureScope;Landroidx/compose/ui/layout/Measurable;I)I
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode;->maxIntrinsicWidth(Landroidx/compose/ui/layout/IntrinsicMeasureScope;Landroidx/compose/ui/layout/Measurable;I)I
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode;->updateCallbacks(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Z
+HSPLandroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode;->updateLayoutRelatedArgs-MPT68mk(Landroidx/compose/ui/text/TextStyle;Ljava/util/List;IIZLandroidx/compose/ui/text/font/FontFamily$Resolver;I)Z
+HSPLandroidx/compose/foundation/text/selection/SelectionRegistrarKt;-><clinit>()V
+HSPLandroidx/compose/foundation/text/selection/TextSelectionColors;-><init>(JJ)V
+HSPLandroidx/compose/foundation/text/selection/TextSelectionColorsKt;-><clinit>()V
+HSPLandroidx/compose/material/ripple/PlatformRipple;-><init>(ZFLandroidx/compose/runtime/MutableState;)V
+HSPLandroidx/compose/material/ripple/RippleAlpha;-><init>(FFFF)V
+HSPLandroidx/compose/material/ripple/RippleKt;-><clinit>()V
+HSPLandroidx/compose/material/ripple/RippleThemeKt;-><clinit>()V
+HSPLandroidx/compose/material3/ColorScheme;-><init>(JJJJJJJJJJJJJJJJJJJJJJJJJJJJJ)V
+HSPLandroidx/compose/material3/ColorScheme;->getBackground-0d7_KjU()J
+HSPLandroidx/compose/material3/ColorScheme;->getPrimary-0d7_KjU()J
+HSPLandroidx/compose/material3/ColorScheme;->getSurface-0d7_KjU()J
+HSPLandroidx/compose/material3/ColorSchemeKt;-><clinit>()V
+HSPLandroidx/compose/material3/ColorSchemeKt;->darkColorScheme-G1PFc-w$default(JJJI)Landroidx/compose/material3/ColorScheme;
+HSPLandroidx/compose/material3/MaterialRippleTheme;-><clinit>()V
+HSPLandroidx/compose/material3/MaterialThemeKt$MaterialTheme$2;-><init>(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;III)V
+HSPLandroidx/compose/material3/ShapeDefaults;-><clinit>()V
+HSPLandroidx/compose/material3/Shapes;-><init>(Landroidx/compose/foundation/shape/RoundedCornerShape;Landroidx/compose/foundation/shape/RoundedCornerShape;Landroidx/compose/foundation/shape/RoundedCornerShape;I)V
+HSPLandroidx/compose/material3/ShapesKt$LocalShapes$1;-><clinit>()V
+HSPLandroidx/compose/material3/ShapesKt$LocalShapes$1;-><init>(I)V
+HSPLandroidx/compose/material3/ShapesKt$LocalShapes$1;->invoke()Landroidx/compose/ui/node/LayoutNode;
+HSPLandroidx/compose/material3/ShapesKt$LocalShapes$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/material3/ShapesKt;-><clinit>()V
+HSPLandroidx/compose/material3/TextKt$ProvideTextStyle$1;-><init>(IILjava/lang/Object;Ljava/lang/Object;)V
+HSPLandroidx/compose/material3/TextKt$ProvideTextStyle$1;->invoke(Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/compose/material3/TextKt$ProvideTextStyle$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/material3/TextKt$Text$1;-><clinit>()V
+HSPLandroidx/compose/material3/TextKt$Text$1;-><init>()V
+HSPLandroidx/compose/material3/TextKt$Text$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/material3/TextKt;-><clinit>()V
+HSPLandroidx/compose/material3/TextKt;->ProvideTextStyle(Landroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/compose/material3/TextKt;->Text-fLXpl1I(Ljava/lang/String;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontFamily;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/text/style/TextAlign;JIZILkotlin/jvm/functions/Function1;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/runtime/Composer;III)V
+HSPLandroidx/compose/material3/Typography;-><init>(Landroidx/compose/ui/text/TextStyle;I)V
+HSPLandroidx/compose/material3/TypographyKt;-><clinit>()V
+HSPLandroidx/compose/material3/tokens/ColorDarkTokens;-><clinit>()V
+HSPLandroidx/compose/material3/tokens/PaletteTokens;-><clinit>()V
+HSPLandroidx/compose/material3/tokens/ShapeTokens;-><clinit>()V
+HSPLandroidx/compose/material3/tokens/TypeScaleTokens;-><clinit>()V
+HSPLandroidx/compose/material3/tokens/TypefaceTokens;-><clinit>()V
+HSPLandroidx/compose/material3/tokens/TypographyTokens;-><clinit>()V
+HSPLandroidx/compose/runtime/Anchor;-><init>(I)V
+HSPLandroidx/compose/runtime/BroadcastFrameClock$FrameAwaiter;-><init>(Lkotlin/jvm/functions/Function1;Lkotlinx/coroutines/CancellableContinuationImpl;)V
+HSPLandroidx/compose/runtime/BroadcastFrameClock;-><init>(Landroidx/compose/runtime/Pending$keyMap$2;)V
+HSPLandroidx/compose/runtime/BroadcastFrameClock;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/BroadcastFrameClock;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
+HSPLandroidx/compose/runtime/BroadcastFrameClock;->sendFrame(J)V
+HSPLandroidx/compose/runtime/BroadcastFrameClock;->withFrameNanos(Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/ComposableSingletons$CompositionKt;-><clinit>()V
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextHolder;-><init>(Landroidx/compose/runtime/ComposerImpl$CompositionContextImpl;)V
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextHolder;->onRemembered()V
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;-><init>(Landroidx/compose/runtime/ComposerImpl;IZLandroidx/compose/runtime/CompositionObserverHolder;)V
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->composeInitial$runtime_release(Landroidx/compose/runtime/CompositionImpl;Landroidx/compose/runtime/internal/ComposableLambdaImpl;)V
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->doneComposing$runtime_release()V
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->getCollectingParameterInformation$runtime_release()Z
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->getCompositionLocalScope$runtime_release()Landroidx/compose/runtime/PersistentCompositionLocalMap;
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->getCompoundHashKey$runtime_release()I
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->getEffectCoroutineContext()Lkotlin/coroutines/CoroutineContext;
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->getObserverHolder$runtime_release()Landroidx/compose/runtime/CompositionObserverHolder;
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->invalidate$runtime_release(Landroidx/compose/runtime/CompositionImpl;)V
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->registerComposer$runtime_release(Landroidx/compose/runtime/ComposerImpl;)V
+HSPLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->startComposing$runtime_release()V
+HSPLandroidx/compose/runtime/ComposerImpl$derivedStateObserver$1;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/runtime/ComposerImpl$derivedStateObserver$1;->done()V
+HSPLandroidx/compose/runtime/ComposerImpl$derivedStateObserver$1;->start()V
+HSPLandroidx/compose/runtime/ComposerImpl;-><init>(Landroidx/compose/ui/node/UiApplier;Landroidx/compose/runtime/CompositionContext;Landroidx/compose/runtime/SlotTable;Ljava/util/HashSet;Landroidx/compose/runtime/changelist/ChangeList;Landroidx/compose/runtime/changelist/ChangeList;Landroidx/compose/runtime/CompositionImpl;)V
+HSPLandroidx/compose/runtime/ComposerImpl;->apply(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V
+HSPLandroidx/compose/runtime/ComposerImpl;->changed(F)Z
+HSPLandroidx/compose/runtime/ComposerImpl;->changed(I)Z
+HSPLandroidx/compose/runtime/ComposerImpl;->changed(J)Z
+HSPLandroidx/compose/runtime/ComposerImpl;->changed(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/ComposerImpl;->changed(Z)Z
+HSPLandroidx/compose/runtime/ComposerImpl;->changedInstance(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/ComposerImpl;->cleanUpCompose()V
+HSPLandroidx/compose/runtime/ComposerImpl;->compoundKeyOf(III)I
+HSPLandroidx/compose/runtime/ComposerImpl;->consume(Landroidx/compose/runtime/ProvidableCompositionLocal;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/ComposerImpl;->createFreshInsertTable()V
+HSPLandroidx/compose/runtime/ComposerImpl;->createNode(Lkotlin/jvm/functions/Function0;)V
+HSPLandroidx/compose/runtime/ComposerImpl;->currentCompositionLocalScope()Landroidx/compose/runtime/PersistentCompositionLocalMap;
+HSPLandroidx/compose/runtime/ComposerImpl;->deactivateToEndGroup(Z)V
+HSPLandroidx/compose/runtime/ComposerImpl;->doCompose(Landroidx/core/content/res/ComplexColorCompat;Landroidx/compose/runtime/internal/ComposableLambdaImpl;)V
+HSPLandroidx/compose/runtime/ComposerImpl;->doRecordDownsFor(II)V
+HSPLandroidx/compose/runtime/ComposerImpl;->endDefaults()V
+HSPLandroidx/compose/runtime/ComposerImpl;->endRestartGroup()Landroidx/compose/runtime/RecomposeScopeImpl;
+HSPLandroidx/compose/runtime/ComposerImpl;->endReusableGroup()V
+HSPLandroidx/compose/runtime/ComposerImpl;->endRoot()V
+HSPLandroidx/compose/runtime/ComposerImpl;->enterGroup(ZLandroidx/compose/runtime/Pending;)V
+HSPLandroidx/compose/runtime/ComposerImpl;->getCurrentRecomposeScope$runtime_release()Landroidx/compose/runtime/RecomposeScopeImpl;
+HSPLandroidx/compose/runtime/ComposerImpl;->getDefaultsInvalid()Z
+HSPLandroidx/compose/runtime/ComposerImpl;->getSkipping()Z
+HSPLandroidx/compose/runtime/ComposerImpl;->nextSlot()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/ComposerImpl;->recompose$runtime_release(Landroidx/core/content/res/ComplexColorCompat;)Z
+HSPLandroidx/compose/runtime/ComposerImpl;->recomposeToGroupEnd()V
+HSPLandroidx/compose/runtime/ComposerImpl;->recordUpsAndDowns(III)V
+HSPLandroidx/compose/runtime/ComposerImpl;->skipCurrentGroup()V
+HSPLandroidx/compose/runtime/ComposerImpl;->skipReaderToGroupEnd()V
+HSPLandroidx/compose/runtime/ComposerImpl;->skipToGroupEnd()V
+HSPLandroidx/compose/runtime/ComposerImpl;->start-BaiHCIY(IILandroidx/compose/runtime/OpaqueKey;Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/ComposerImpl;->startDefaults()V
+HSPLandroidx/compose/runtime/ComposerImpl;->startGroup(ILandroidx/compose/runtime/OpaqueKey;)V
+HSPLandroidx/compose/runtime/ComposerImpl;->startReaderGroup(Ljava/lang/Object;Z)V
+HSPLandroidx/compose/runtime/ComposerImpl;->startReplaceableGroup(I)V
+HSPLandroidx/compose/runtime/ComposerImpl;->startRestartGroup(I)Landroidx/compose/runtime/ComposerImpl;
+HSPLandroidx/compose/runtime/ComposerImpl;->startReusableGroup(Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/ComposerImpl;->startReusableNode()V
+HSPLandroidx/compose/runtime/ComposerImpl;->startRoot()V
+HSPLandroidx/compose/runtime/ComposerImpl;->tryImminentInvalidation$runtime_release(Landroidx/compose/runtime/RecomposeScopeImpl;Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/ComposerImpl;->updateCompoundKeyWhenWeEnterGroup(Ljava/lang/Object;ILjava/lang/Object;)V
+HSPLandroidx/compose/runtime/ComposerImpl;->updateCompoundKeyWhenWeEnterGroupKeyHash(I)V
+HSPLandroidx/compose/runtime/ComposerImpl;->updateCompoundKeyWhenWeExitGroup(Ljava/lang/Object;ILjava/lang/Object;)V
+HSPLandroidx/compose/runtime/ComposerImpl;->updateCompoundKeyWhenWeExitGroupKeyHash(I)V
+HSPLandroidx/compose/runtime/ComposerImpl;->updateNodeCount(II)V
+HSPLandroidx/compose/runtime/ComposerImpl;->updateNodeCountOverrides(II)V
+HSPLandroidx/compose/runtime/ComposerImpl;->updateProviderMapGroup(Landroidx/compose/runtime/PersistentCompositionLocalMap;Landroidx/compose/runtime/internal/PersistentCompositionLocalHashMap;)Landroidx/compose/runtime/internal/PersistentCompositionLocalHashMap;
+HSPLandroidx/compose/runtime/ComposerImpl;->updatedNodeCount(I)I
+HSPLandroidx/compose/runtime/ComposerImpl;->useNode()V
+HSPLandroidx/compose/runtime/CompositionContext;->doneComposing$runtime_release()V
+HSPLandroidx/compose/runtime/CompositionContext;->getCompositionLocalScope$runtime_release()Landroidx/compose/runtime/PersistentCompositionLocalMap;
+HSPLandroidx/compose/runtime/CompositionContext;->getObserverHolder$runtime_release()Landroidx/compose/runtime/CompositionObserverHolder;
+HSPLandroidx/compose/runtime/CompositionContext;->registerComposer$runtime_release(Landroidx/compose/runtime/ComposerImpl;)V
+HSPLandroidx/compose/runtime/CompositionContext;->startComposing$runtime_release()V
+HSPLandroidx/compose/runtime/CompositionContextKt;-><clinit>()V
+HSPLandroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;-><init>(Ljava/util/HashSet;)V
+HSPLandroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;->dispatchAbandons()V
+HSPLandroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;->dispatchRememberObservers()V
+HSPLandroidx/compose/runtime/CompositionImpl;-><init>(Landroidx/compose/runtime/CompositionContext;Landroidx/compose/ui/node/UiApplier;)V
+HSPLandroidx/compose/runtime/CompositionImpl;->addPendingInvalidationsLocked(Ljava/util/HashSet;Ljava/lang/Object;Z)Ljava/util/HashSet;
+HSPLandroidx/compose/runtime/CompositionImpl;->addPendingInvalidationsLocked(Ljava/util/Set;Z)V
+HSPLandroidx/compose/runtime/CompositionImpl;->applyChanges()V
+HSPLandroidx/compose/runtime/CompositionImpl;->applyChangesInLocked(Landroidx/compose/runtime/changelist/ChangeList;)V
+HSPLandroidx/compose/runtime/CompositionImpl;->applyLateChanges()V
+HSPLandroidx/compose/runtime/CompositionImpl;->changesApplied()V
+HSPLandroidx/compose/runtime/CompositionImpl;->cleanUpDerivedStateObservations()V
+HSPLandroidx/compose/runtime/CompositionImpl;->composeContent(Landroidx/compose/runtime/internal/ComposableLambdaImpl;)V
+HSPLandroidx/compose/runtime/CompositionImpl;->drainPendingModificationsForCompositionLocked()V
+HSPLandroidx/compose/runtime/CompositionImpl;->drainPendingModificationsLocked()V
+HSPLandroidx/compose/runtime/CompositionImpl;->getHasInvalidations()Z
+HSPLandroidx/compose/runtime/CompositionImpl;->invalidate$enumunboxing$(Landroidx/compose/runtime/RecomposeScopeImpl;Ljava/lang/Object;)I
+HSPLandroidx/compose/runtime/CompositionImpl;->invalidateChecked$enumunboxing$(Landroidx/compose/runtime/RecomposeScopeImpl;Landroidx/compose/runtime/Anchor;Ljava/lang/Object;)I
+HSPLandroidx/compose/runtime/CompositionImpl;->invalidateScopeOfLocked(Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/CompositionImpl;->isDisposed()Z
+HSPLandroidx/compose/runtime/CompositionImpl;->observer()V
+HSPLandroidx/compose/runtime/CompositionImpl;->observesAnyOf(Landroidx/compose/runtime/collection/IdentityArraySet;)Z
+HSPLandroidx/compose/runtime/CompositionImpl;->recompose()Z
+HSPLandroidx/compose/runtime/CompositionImpl;->recomposeScopeReleased()V
+HSPLandroidx/compose/runtime/CompositionImpl;->recordModificationsOf(Landroidx/compose/runtime/collection/IdentityArraySet;)V
+HSPLandroidx/compose/runtime/CompositionImpl;->recordReadOf(Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/CompositionImpl;->recordWriteOf(Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/CompositionImpl;->setContent(Lkotlin/jvm/functions/Function2;)V
+HSPLandroidx/compose/runtime/CompositionKt;-><clinit>()V
+HSPLandroidx/compose/runtime/CompositionLocal;-><init>(Lkotlin/jvm/functions/Function0;)V
+HSPLandroidx/compose/runtime/CompositionLocalMap$Companion;-><clinit>()V
+HSPLandroidx/compose/runtime/CompositionLocalMap;-><clinit>()V
+HSPLandroidx/compose/runtime/CompositionObserverHolder;-><init>()V
+HSPLandroidx/compose/runtime/CompositionScopedCoroutineScopeCanceller;-><init>(Lkotlinx/coroutines/internal/ContextScope;)V
+HSPLandroidx/compose/runtime/CompositionScopedCoroutineScopeCanceller;->onRemembered()V
+HSPLandroidx/compose/runtime/DerivedSnapshotState$ResultRecord;-><clinit>()V
+HSPLandroidx/compose/runtime/DerivedSnapshotState$ResultRecord;-><init>()V
+HSPLandroidx/compose/runtime/DerivedSnapshotState$ResultRecord;->assign(Landroidx/compose/runtime/snapshots/StateRecord;)V
+HSPLandroidx/compose/runtime/DerivedSnapshotState$ResultRecord;->create()Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/DerivedSnapshotState$ResultRecord;->isValid(Landroidx/compose/runtime/DerivedSnapshotState;Landroidx/compose/runtime/snapshots/Snapshot;)Z
+HSPLandroidx/compose/runtime/DerivedSnapshotState$ResultRecord;->readableHash(Landroidx/compose/runtime/DerivedSnapshotState;Landroidx/compose/runtime/snapshots/Snapshot;)I
+HSPLandroidx/compose/runtime/DerivedSnapshotState;-><init>(Landroidx/compose/runtime/ReferentialEqualityPolicy;Lkotlin/jvm/functions/Function0;)V
+HSPLandroidx/compose/runtime/DerivedSnapshotState;->currentRecord(Landroidx/compose/runtime/DerivedSnapshotState$ResultRecord;Landroidx/compose/runtime/snapshots/Snapshot;ZLkotlin/jvm/functions/Function0;)Landroidx/compose/runtime/DerivedSnapshotState$ResultRecord;
+HSPLandroidx/compose/runtime/DerivedSnapshotState;->getCurrentRecord()Landroidx/compose/runtime/DerivedSnapshotState$ResultRecord;
+HSPLandroidx/compose/runtime/DerivedSnapshotState;->getFirstStateRecord()Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/DerivedSnapshotState;->getValue()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/DerivedSnapshotState;->prependStateRecord(Landroidx/compose/runtime/snapshots/StateRecord;)V
+HSPLandroidx/compose/runtime/DisposableEffectImpl;-><init>(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/runtime/DisposableEffectImpl;->onForgotten()V
+HSPLandroidx/compose/runtime/DisposableEffectImpl;->onRemembered()V
+HSPLandroidx/compose/runtime/DynamicProvidableCompositionLocal;-><init>(Landroidx/compose/runtime/SnapshotMutationPolicy;Lkotlin/jvm/functions/Function0;)V
+HSPLandroidx/compose/runtime/DynamicProvidableCompositionLocal;->updatedStateOf$runtime_release(Ljava/lang/Object;Landroidx/compose/runtime/State;)Landroidx/compose/runtime/State;
+HSPLandroidx/compose/runtime/GroupInfo;-><init>(III)V
+HSPLandroidx/compose/runtime/IntStack;-><init>()V
+HSPLandroidx/compose/runtime/IntStack;-><init>(I)V
+HSPLandroidx/compose/runtime/IntStack;->getSize()I
+HSPLandroidx/compose/runtime/IntStack;->pop()I
+HSPLandroidx/compose/runtime/IntStack;->push(I)V
+HSPLandroidx/compose/runtime/IntStack;->pushDiagonal(III)V
+HSPLandroidx/compose/runtime/IntStack;->pushRange(IIII)V
+HSPLandroidx/compose/runtime/IntStack;->quickSort(II)V
+HSPLandroidx/compose/runtime/IntStack;->swapDiagonal(II)V
+HSPLandroidx/compose/runtime/Invalidation;-><init>(Landroidx/compose/runtime/RecomposeScopeImpl;ILandroidx/compose/runtime/collection/IdentityArraySet;)V
+HSPLandroidx/compose/runtime/KeyInfo;-><init>(ILjava/lang/Object;II)V
+HSPLandroidx/compose/runtime/Latch$await$2$2;-><init>(Ljava/lang/Object;ILjava/lang/Object;)V
+HSPLandroidx/compose/runtime/Latch$await$2$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Latch$await$2$2;->invoke(Ljava/lang/Throwable;)V
+HSPLandroidx/compose/runtime/Latch;-><init>()V
+HSPLandroidx/compose/runtime/Latch;-><init>(I)V
+HSPLandroidx/compose/runtime/Latch;->add(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/runtime/Latch;->contains(Landroidx/compose/ui/node/LayoutNode;)Z
+HSPLandroidx/compose/runtime/Latch;->pop()Landroidx/compose/ui/node/LayoutNode;
+HSPLandroidx/compose/runtime/Latch;->remove(Landroidx/compose/ui/node/LayoutNode;)Z
+HSPLandroidx/compose/runtime/LaunchedEffectImpl;-><init>(Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V
+HSPLandroidx/compose/runtime/LaunchedEffectImpl;->onForgotten()V
+HSPLandroidx/compose/runtime/LaunchedEffectImpl;->onRemembered()V
+HSPLandroidx/compose/runtime/LazyValueHolder;-><init>(Lkotlin/jvm/functions/Function0;)V
+HSPLandroidx/compose/runtime/LazyValueHolder;->getValue()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/MonotonicFrameClock;->getKey()Lkotlin/coroutines/CoroutineContext$Key;
+HSPLandroidx/compose/runtime/OpaqueKey;-><init>(Ljava/lang/String;)V
+HSPLandroidx/compose/runtime/OpaqueKey;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/OpaqueKey;->hashCode()I
+HSPLandroidx/compose/runtime/ParcelableSnapshotMutableFloatState;-><clinit>()V
+HSPLandroidx/compose/runtime/ParcelableSnapshotMutableIntState;-><clinit>()V
+HSPLandroidx/compose/runtime/ParcelableSnapshotMutableState;-><clinit>()V
+HSPLandroidx/compose/runtime/PausableMonotonicFrameClock$withFrameNanos$1;-><init>(Landroidx/compose/runtime/PausableMonotonicFrameClock;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/runtime/PausableMonotonicFrameClock$withFrameNanos$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/PausableMonotonicFrameClock;-><init>(Landroidx/compose/runtime/MonotonicFrameClock;)V
+HSPLandroidx/compose/runtime/PausableMonotonicFrameClock;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/PausableMonotonicFrameClock;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
+HSPLandroidx/compose/runtime/PausableMonotonicFrameClock;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext;
+HSPLandroidx/compose/runtime/PausableMonotonicFrameClock;->withFrameNanos(Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Pending$keyMap$2;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/runtime/Pending$keyMap$2;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Pending;-><init>(ILjava/util/ArrayList;)V
+HSPLandroidx/compose/runtime/ProduceStateScopeImpl;-><init>(Landroidx/compose/runtime/MutableState;Lkotlin/coroutines/CoroutineContext;)V
+HSPLandroidx/compose/runtime/ProduceStateScopeImpl;->setValue(Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/ProvidableCompositionLocal;->provides(Ljava/lang/Object;)Landroidx/compose/runtime/ProvidedValue;
+HSPLandroidx/compose/runtime/ProvidedValue;-><init>(Landroidx/compose/runtime/CompositionLocal;Ljava/lang/Object;Z)V
+HSPLandroidx/compose/runtime/RecomposeScopeImpl$end$1$2;-><init>(IILjava/lang/Object;Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/RecomposeScopeImpl$end$1$2;-><init>(Landroidx/compose/runtime/DerivedSnapshotState;Landroidx/core/content/res/ComplexColorCompat;I)V
+HSPLandroidx/compose/runtime/RecomposeScopeImpl$end$1$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/RecomposeScopeImpl;-><init>(Landroidx/compose/runtime/CompositionImpl;)V
+HSPLandroidx/compose/runtime/RecomposeScopeImpl;->invalidateForResult$enumunboxing$(Ljava/lang/Object;)I
+HSPLandroidx/compose/runtime/Recomposer$State;-><clinit>()V
+HSPLandroidx/compose/runtime/Recomposer$State;-><init>(ILjava/lang/String;)V
+HSPLandroidx/compose/runtime/Recomposer$effectJob$1$1;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/runtime/Recomposer$effectJob$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Recomposer$join$2;-><init>(Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/runtime/Recomposer$join$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/runtime/Recomposer$join$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Recomposer$join$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Recomposer$performRecompose$1$1;-><init>(Ljava/lang/Object;ILjava/lang/Object;)V
+HSPLandroidx/compose/runtime/Recomposer$performRecompose$1$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Recomposer$performRecompose$1$1;->invoke()V
+HSPLandroidx/compose/runtime/Recomposer$recompositionRunner$2$3;-><init>(Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/MonotonicFrameClock;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/runtime/Recomposer$recompositionRunner$2$3;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/runtime/Recomposer$recompositionRunner$2$3;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Recomposer$recompositionRunner$2$3;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Recomposer$recompositionRunner$2$unregisterApplyObserver$1;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/runtime/Recomposer$recompositionRunner$2$unregisterApplyObserver$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Recomposer$recompositionRunner$2$unregisterApplyObserver$1;->invoke(Ljava/util/Set;)V
+HSPLandroidx/compose/runtime/Recomposer$recompositionRunner$2;-><init>(Landroidx/compose/runtime/Recomposer;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/MonotonicFrameClock;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/runtime/Recomposer$recompositionRunner$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/runtime/Recomposer$recompositionRunner$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Recomposer$recompositionRunner$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Recomposer$runRecomposeAndApplyChanges$2$1;-><init>(Landroidx/compose/runtime/Recomposer;Landroidx/compose/runtime/collection/IdentityArraySet;Landroidx/compose/runtime/collection/IdentityArraySet;Ljava/util/List;Ljava/util/List;Ljava/util/Set;Ljava/util/List;Ljava/util/Set;)V
+HSPLandroidx/compose/runtime/Recomposer$runRecomposeAndApplyChanges$2$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Recomposer$runRecomposeAndApplyChanges$2;-><init>(Landroidx/compose/runtime/Recomposer;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/runtime/Recomposer$runRecomposeAndApplyChanges$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Recomposer$runRecomposeAndApplyChanges$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Recomposer;-><clinit>()V
+HSPLandroidx/compose/runtime/Recomposer;-><init>(Lkotlin/coroutines/CoroutineContext;)V
+HSPLandroidx/compose/runtime/Recomposer;->access$performRecompose(Landroidx/compose/runtime/Recomposer;Landroidx/compose/runtime/CompositionImpl;Landroidx/compose/runtime/collection/IdentityArraySet;)Landroidx/compose/runtime/CompositionImpl;
+HSPLandroidx/compose/runtime/Recomposer;->access$recordComposerModifications(Landroidx/compose/runtime/Recomposer;)Z
+HSPLandroidx/compose/runtime/Recomposer;->applyAndCheck(Landroidx/compose/runtime/snapshots/MutableSnapshot;)V
+HSPLandroidx/compose/runtime/Recomposer;->composeInitial$runtime_release(Landroidx/compose/runtime/CompositionImpl;Landroidx/compose/runtime/internal/ComposableLambdaImpl;)V
+HSPLandroidx/compose/runtime/Recomposer;->deriveStateLocked()Lkotlinx/coroutines/CancellableContinuation;
+HSPLandroidx/compose/runtime/Recomposer;->getCollectingParameterInformation$runtime_release()Z
+HSPLandroidx/compose/runtime/Recomposer;->getCompoundHashKey$runtime_release()I
+HSPLandroidx/compose/runtime/Recomposer;->getEffectCoroutineContext()Lkotlin/coroutines/CoroutineContext;
+HSPLandroidx/compose/runtime/Recomposer;->getHasBroadcastFrameClockAwaitersLocked()Z
+HSPLandroidx/compose/runtime/Recomposer;->getHasSchedulingWork()Z
+HSPLandroidx/compose/runtime/Recomposer;->getKnownCompositions()Ljava/util/List;
+HSPLandroidx/compose/runtime/Recomposer;->invalidate$runtime_release(Landroidx/compose/runtime/CompositionImpl;)V
+HSPLandroidx/compose/runtime/Recomposer;->performInitialMovableContentInserts(Landroidx/compose/runtime/CompositionImpl;)V
+HSPLandroidx/compose/runtime/ReferentialEqualityPolicy;-><clinit>()V
+HSPLandroidx/compose/runtime/ReferentialEqualityPolicy;->equivalent(Ljava/lang/Object;Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/SkippableUpdater;-><init>(Landroidx/compose/runtime/Composer;)V
+HSPLandroidx/compose/runtime/SlotReader;-><init>(Landroidx/compose/runtime/SlotTable;)V
+HSPLandroidx/compose/runtime/SlotReader;->anchor(I)Landroidx/compose/runtime/Anchor;
+HSPLandroidx/compose/runtime/SlotReader;->aux([II)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/SlotReader;->close()V
+HSPLandroidx/compose/runtime/SlotReader;->endGroup()V
+HSPLandroidx/compose/runtime/SlotReader;->getGroupAux()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/SlotReader;->getGroupKey()I
+HSPLandroidx/compose/runtime/SlotReader;->groupGet(II)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/SlotReader;->groupSize(I)I
+HSPLandroidx/compose/runtime/SlotReader;->isNode(I)Z
+HSPLandroidx/compose/runtime/SlotReader;->node(I)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/SlotReader;->nodeCount(I)I
+HSPLandroidx/compose/runtime/SlotReader;->objectKey([II)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/SlotReader;->parent(I)I
+HSPLandroidx/compose/runtime/SlotReader;->reposition(I)V
+HSPLandroidx/compose/runtime/SlotReader;->skipGroup()I
+HSPLandroidx/compose/runtime/SlotReader;->skipToGroupEnd()V
+HSPLandroidx/compose/runtime/SlotReader;->startGroup()V
+HSPLandroidx/compose/runtime/SlotTable;-><init>()V
+HSPLandroidx/compose/runtime/SlotTable;->anchorIndex(Landroidx/compose/runtime/Anchor;)I
+HSPLandroidx/compose/runtime/SlotTable;->openReader()Landroidx/compose/runtime/SlotReader;
+HSPLandroidx/compose/runtime/SlotTable;->openWriter()Landroidx/compose/runtime/SlotWriter;
+HSPLandroidx/compose/runtime/SlotTable;->ownsAnchor(Landroidx/compose/runtime/Anchor;)Z
+HSPLandroidx/compose/runtime/SlotWriter;-><clinit>()V
+HSPLandroidx/compose/runtime/SlotWriter;-><init>(Landroidx/compose/runtime/SlotTable;)V
+HSPLandroidx/compose/runtime/SlotWriter;->advanceBy(I)V
+HSPLandroidx/compose/runtime/SlotWriter;->anchor(I)Landroidx/compose/runtime/Anchor;
+HSPLandroidx/compose/runtime/SlotWriter;->auxIndex([II)I
+HSPLandroidx/compose/runtime/SlotWriter;->beginInsert()V
+HSPLandroidx/compose/runtime/SlotWriter;->close()V
+HSPLandroidx/compose/runtime/SlotWriter;->dataIndex([II)I
+HSPLandroidx/compose/runtime/SlotWriter;->dataIndexToDataAddress(I)I
+HSPLandroidx/compose/runtime/SlotWriter;->endInsert()V
+HSPLandroidx/compose/runtime/SlotWriter;->getSize$runtime_release()I
+HSPLandroidx/compose/runtime/SlotWriter;->groupIndexToAddress(I)I
+HSPLandroidx/compose/runtime/SlotWriter;->groupSize(I)I
+HSPLandroidx/compose/runtime/SlotWriter;->markGroup$default(Landroidx/compose/runtime/SlotWriter;)V
+HSPLandroidx/compose/runtime/SlotWriter;->moveFrom(Landroidx/compose/runtime/SlotTable;I)V
+HSPLandroidx/compose/runtime/SlotWriter;->moveGroupGapTo(I)V
+HSPLandroidx/compose/runtime/SlotWriter;->moveSlotGapTo(II)V
+HSPLandroidx/compose/runtime/SlotWriter;->node(I)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/SlotWriter;->parent(I)I
+HSPLandroidx/compose/runtime/SlotWriter;->parent([II)I
+HSPLandroidx/compose/runtime/SlotWriter;->recalculateMarks()V
+HSPLandroidx/compose/runtime/SlotWriter;->set(IILjava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/SlotWriter;->skipToGroupEnd()V
+HSPLandroidx/compose/runtime/SlotWriter;->slotIndex([II)I
+HSPLandroidx/compose/runtime/SlotWriter;->updateAux(Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/SlotWriter;->updateContainsMark(I)V
+HSPLandroidx/compose/runtime/SlotWriter;->updateNodeOfGroup(ILjava/lang/Object;)V
+HSPLandroidx/compose/runtime/SnapshotMutableFloatStateImpl$FloatStateStateRecord;-><init>(F)V
+HSPLandroidx/compose/runtime/SnapshotMutableFloatStateImpl;-><init>(F)V
+HSPLandroidx/compose/runtime/SnapshotMutableFloatStateImpl;->setFloatValue(F)V
+HSPLandroidx/compose/runtime/SnapshotMutableIntStateImpl$IntStateStateRecord;-><init>(I)V
+HSPLandroidx/compose/runtime/SnapshotMutableIntStateImpl$IntStateStateRecord;->assign(Landroidx/compose/runtime/snapshots/StateRecord;)V
+HSPLandroidx/compose/runtime/SnapshotMutableIntStateImpl$IntStateStateRecord;->create()Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/SnapshotMutableIntStateImpl;-><init>(I)V
+HSPLandroidx/compose/runtime/SnapshotMutableIntStateImpl;->getFirstStateRecord()Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/SnapshotMutableIntStateImpl;->getIntValue()I
+HSPLandroidx/compose/runtime/SnapshotMutableIntStateImpl;->prependStateRecord(Landroidx/compose/runtime/snapshots/StateRecord;)V
+HSPLandroidx/compose/runtime/SnapshotMutableIntStateImpl;->setIntValue(I)V
+HSPLandroidx/compose/runtime/SnapshotMutableStateImpl$StateStateRecord;-><init>(Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/SnapshotMutableStateImpl$StateStateRecord;->assign(Landroidx/compose/runtime/snapshots/StateRecord;)V
+HSPLandroidx/compose/runtime/SnapshotMutableStateImpl$StateStateRecord;->create()Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/SnapshotMutableStateImpl;-><init>(Ljava/lang/Object;Landroidx/compose/runtime/SnapshotMutationPolicy;)V
+HSPLandroidx/compose/runtime/SnapshotMutableStateImpl;->getFirstStateRecord()Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/SnapshotMutableStateImpl;->getValue()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/SnapshotMutableStateImpl;->prependStateRecord(Landroidx/compose/runtime/snapshots/StateRecord;)V
+HSPLandroidx/compose/runtime/SnapshotMutableStateImpl;->setValue(Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/SnapshotStateKt__DerivedStateKt;-><clinit>()V
+HSPLandroidx/compose/runtime/SnapshotStateKt__ProduceStateKt$produceState$3;-><init>(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/MutableState;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/runtime/SnapshotStateKt__ProduceStateKt$produceState$3;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/runtime/SnapshotStateKt__ProduceStateKt$produceState$3;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/SnapshotStateKt__SnapshotFlowKt$collectAsState$1$1;-><init>(Landroidx/compose/runtime/ProduceStateScopeImpl;I)V
+HSPLandroidx/compose/runtime/SnapshotStateKt__SnapshotFlowKt$collectAsState$1$1;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/SnapshotStateKt__SnapshotFlowKt$collectAsState$1;-><init>(Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/runtime/SnapshotStateKt__SnapshotFlowKt$collectAsState$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/runtime/SnapshotStateKt__SnapshotFlowKt$collectAsState$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/SnapshotStateKt__SnapshotFlowKt$collectAsState$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Stack;-><init>()V
+HSPLandroidx/compose/runtime/Stack;-><init>(I)V
+HSPLandroidx/compose/runtime/Stack;-><init>(II)V
+HSPLandroidx/compose/runtime/Stack;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/runtime/Stack;-><init>(Landroid/content/Context;)V
+HSPLandroidx/compose/runtime/Stack;-><init>(Ljava/nio/ByteBuffer;)V
+HSPLandroidx/compose/runtime/Stack;->clear()V
+HSPLandroidx/compose/runtime/Stack;->load(Lokhttp3/MediaType;)V
+HSPLandroidx/compose/runtime/Stack;->pop()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/Stack;->readUnsignedInt()J
+HSPLandroidx/compose/runtime/Stack;->skip(I)V
+HSPLandroidx/compose/runtime/StaticProvidableCompositionLocal;->updatedStateOf$runtime_release(Ljava/lang/Object;Landroidx/compose/runtime/State;)Landroidx/compose/runtime/State;
+HSPLandroidx/compose/runtime/StaticValueHolder;-><init>(Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/StaticValueHolder;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/StaticValueHolder;->getValue()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/StructuralEqualityPolicy;-><clinit>()V
+HSPLandroidx/compose/runtime/StructuralEqualityPolicy;->equivalent(Ljava/lang/Object;Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/changelist/ChangeList;-><init>()V
+HSPLandroidx/compose/runtime/changelist/ChangeList;->executeAndFlushAllPendingChanges(Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/ChangeList;->isEmpty()Z
+HSPLandroidx/compose/runtime/changelist/ComposerChangeListWriter;-><init>(Landroidx/compose/runtime/ComposerImpl;Landroidx/compose/runtime/changelist/ChangeList;)V
+HSPLandroidx/compose/runtime/changelist/ComposerChangeListWriter;->moveUp()V
+HSPLandroidx/compose/runtime/changelist/ComposerChangeListWriter;->pushPendingUpsAndDowns()V
+HSPLandroidx/compose/runtime/changelist/ComposerChangeListWriter;->realizeNodeMovementOperations()V
+HSPLandroidx/compose/runtime/changelist/ComposerChangeListWriter;->realizeOperationLocation(Z)V
+HSPLandroidx/compose/runtime/changelist/FixupList;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$AdvanceSlotsBy;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$AdvanceSlotsBy;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$AdvanceSlotsBy;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation$DeactivateCurrentGroup;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$DeactivateCurrentGroup;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$DeactivateCurrentGroup;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation$Downs;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$Downs;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$Downs;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation$InsertNodeFixup;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$InsertNodeFixup;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$InsertNodeFixup;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation$InsertSlotsWithFixups;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$InsertSlotsWithFixups;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$InsertSlotsWithFixups;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation$PostInsertNodeFixup;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$PostInsertNodeFixup;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$PostInsertNodeFixup;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation$Remember;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$Remember;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$Remember;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation$SideEffect;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$SideEffect;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$SideEffect;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation$UpdateAuxData;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$UpdateAuxData;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$UpdateAuxData;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation$UpdateNode;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$UpdateNode;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$UpdateNode;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation$UpdateValue;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$UpdateValue;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$UpdateValue;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation$Ups;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$Ups;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$Ups;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation$UseCurrentNode;-><clinit>()V
+HSPLandroidx/compose/runtime/changelist/Operation$UseCurrentNode;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operation$UseCurrentNode;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operation;-><init>(II)V
+HSPLandroidx/compose/runtime/changelist/Operation;-><init>(III)V
+HSPLandroidx/compose/runtime/changelist/Operations$OpIterator;-><init>(Landroidx/compose/runtime/changelist/Operations;)V
+HSPLandroidx/compose/runtime/changelist/Operations$OpIterator;->getInt-w8GmfQM(I)I
+HSPLandroidx/compose/runtime/changelist/Operations$OpIterator;->getObject-31yXWZQ(I)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/changelist/Operations;-><init>()V
+HSPLandroidx/compose/runtime/changelist/Operations;->access$createExpectedArgMask(Landroidx/compose/runtime/changelist/Operations;I)I
+HSPLandroidx/compose/runtime/changelist/Operations;->clear()V
+HSPLandroidx/compose/runtime/changelist/Operations;->executeAndFlushAllPendingOperations(Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+HSPLandroidx/compose/runtime/changelist/Operations;->peekOperation()Landroidx/compose/runtime/changelist/Operation;
+HSPLandroidx/compose/runtime/changelist/Operations;->push(Landroidx/compose/runtime/changelist/Operation;)V
+HSPLandroidx/compose/runtime/changelist/Operations;->pushOp(Landroidx/compose/runtime/changelist/Operation;)V
+HSPLandroidx/compose/runtime/collection/IdentityArrayIntMap;-><init>()V
+HSPLandroidx/compose/runtime/collection/IdentityArrayIntMap;->add(ILjava/lang/Object;)I
+HSPLandroidx/compose/runtime/collection/IdentityArrayMap$asMap$1$entries$1$iterator$1$1;-><init>(Ljava/lang/Object;Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/collection/IdentityArrayMap$asMap$1$entries$1$iterator$1$1;->getKey()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/collection/IdentityArrayMap$asMap$1$entries$1$iterator$1$1;->getValue()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/collection/IdentityArraySet;-><init>()V
+HSPLandroidx/compose/runtime/collection/IdentityArraySet;->add(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/collection/IdentityArraySet;->addAll(Ljava/util/Collection;)V
+HSPLandroidx/compose/runtime/collection/IdentityArraySet;->clear()V
+HSPLandroidx/compose/runtime/collection/IdentityArraySet;->contains(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/collection/IdentityArraySet;->find(Ljava/lang/Object;)I
+HSPLandroidx/compose/runtime/collection/IdentityArraySet;->isEmpty()Z
+HSPLandroidx/compose/runtime/collection/IdentityArraySet;->isNotEmpty()Z
+HSPLandroidx/compose/runtime/collection/IdentityArraySet;->iterator()Ljava/util/Iterator;
+HSPLandroidx/compose/runtime/collection/IdentityArraySet;->remove(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/collection/MutableVector$MutableVectorList;-><init>(Landroidx/compose/runtime/collection/MutableVector;)V
+HSPLandroidx/compose/runtime/collection/MutableVector$MutableVectorList;->get(I)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/collection/MutableVector$MutableVectorList;->indexOf(Ljava/lang/Object;)I
+HSPLandroidx/compose/runtime/collection/MutableVector$MutableVectorList;->isEmpty()Z
+HSPLandroidx/compose/runtime/collection/MutableVector$MutableVectorList;->iterator()Ljava/util/Iterator;
+HSPLandroidx/compose/runtime/collection/MutableVector$MutableVectorList;->size()I
+HSPLandroidx/compose/runtime/collection/MutableVector$VectorListIterator;-><init>(ILjava/util/List;)V
+HSPLandroidx/compose/runtime/collection/MutableVector$VectorListIterator;->hasNext()Z
+HSPLandroidx/compose/runtime/collection/MutableVector$VectorListIterator;->next()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/collection/MutableVector;-><init>([Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/collection/MutableVector;->add(ILjava/lang/Object;)V
+HSPLandroidx/compose/runtime/collection/MutableVector;->add(Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/collection/MutableVector;->addAll(ILandroidx/compose/runtime/collection/MutableVector;)V
+HSPLandroidx/compose/runtime/collection/MutableVector;->asMutableList()Ljava/util/List;
+HSPLandroidx/compose/runtime/collection/MutableVector;->clear()V
+HSPLandroidx/compose/runtime/collection/MutableVector;->contains(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/collection/MutableVector;->ensureCapacity(I)V
+HSPLandroidx/compose/runtime/collection/MutableVector;->indexOf(Ljava/lang/Object;)I
+HSPLandroidx/compose/runtime/collection/MutableVector;->isEmpty()Z
+HSPLandroidx/compose/runtime/collection/MutableVector;->isNotEmpty()Z
+HSPLandroidx/compose/runtime/collection/MutableVector;->remove(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/collection/MutableVector;->removeAt(I)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/collection/MutableVector;->removeRange(II)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/SmallPersistentVector;-><clinit>()V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/SmallPersistentVector;-><init>([Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/SmallPersistentVector;->add(Ljava/lang/Object;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/PersistentList;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/SmallPersistentVector;->get(I)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/SmallPersistentVector;->getSize()I
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/SmallPersistentVector;->indexOf(Ljava/lang/Object;)I
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/SmallPersistentVector;->removeAt(I)Landroidx/compose/runtime/external/kotlinx/collections/immutable/PersistentList;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap;-><clinit>()V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap;-><init>(Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;I)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap;->containsKey(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap;->get(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap;->put(Ljava/lang/Object;Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/persistentOrderedSet/Links;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBaseIterator;-><init>(Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;[Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNodeBaseIterator;)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBaseIterator;->ensureNextEntryIsReady()V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBaseIterator;->hasNext()Z
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBaseIterator;->moveToNextNodeWithData(I)I
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBaseIterator;->next()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder;-><init>(Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap;)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder;->putAll(Ljava/util/Map;)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder;->setSize(I)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapKeys;-><init>(Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap;I)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapKeys;->getSize()I
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapKeys;->iterator()Ljava/util/Iterator;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapKeysIterator;-><init>(Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;I)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;-><clinit>()V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;-><init>(II[Ljava/lang/Object;L_COROUTINE/ArtificialStackFrames;)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->bufferMoveEntryToNode(IIILjava/lang/Object;Ljava/lang/Object;IL_COROUTINE/ArtificialStackFrames;)[Ljava/lang/Object;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->containsKey(IILjava/lang/Object;)Z
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->elementsIdentityEquals(Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;)Z
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->entryKeyIndex$runtime_release(I)I
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->get(IILjava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->hasEntryAt$runtime_release(I)Z
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->hasNodeAt(I)Z
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->makeNode(ILjava/lang/Object;Ljava/lang/Object;ILjava/lang/Object;Ljava/lang/Object;IL_COROUTINE/ArtificialStackFrames;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->mutablePut(ILjava/lang/Object;Ljava/lang/Object;ILandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->mutablePutAll(Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;ILandroidx/compose/runtime/external/kotlinx/collections/immutable/internal/DeltaCounter;Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->nodeAtIndex$runtime_release(I)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->nodeIndex$runtime_release(I)I
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->put(IILjava/lang/Object;Ljava/lang/Object;)Landroidx/compose/ui/input/pointer/util/PointerIdArray;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->valueAtKeyIndex(I)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNodeBaseIterator;-><init>()V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNodeBaseIterator;->reset(II[Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNodeKeysIterator;-><init>(I)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNodeKeysIterator;->next()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/persistentOrderedSet/Links;-><init>(Ljava/lang/Object;Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/persistentOrderedSet/PersistentOrderedSet;-><clinit>()V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/persistentOrderedSet/PersistentOrderedSet;-><init>(Ljava/lang/Object;Ljava/lang/Object;Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap;)V
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/persistentOrderedSet/PersistentOrderedSet;->getSize()I
+HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/internal/DeltaCounter;-><init>()V
+HSPLandroidx/compose/runtime/internal/ComposableLambdaImpl;-><init>(IZ)V
+HSPLandroidx/compose/runtime/internal/ComposableLambdaImpl;->invoke(Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/internal/ComposableLambdaImpl;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/internal/ComposableLambdaImpl;->invoke(Ljava/lang/Object;Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/internal/ComposableLambdaImpl;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/internal/ComposableLambdaImpl;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/internal/ComposableLambdaImpl;->update(Lkotlin/jvm/internal/Lambda;)V
+HSPLandroidx/compose/runtime/internal/PersistentCompositionLocalHashMap$Builder;-><init>(Landroidx/compose/runtime/internal/PersistentCompositionLocalHashMap;)V
+HSPLandroidx/compose/runtime/internal/PersistentCompositionLocalHashMap$Builder;->build()Landroidx/compose/runtime/internal/PersistentCompositionLocalHashMap;
+HSPLandroidx/compose/runtime/internal/PersistentCompositionLocalHashMap;-><clinit>()V
+HSPLandroidx/compose/runtime/internal/PersistentCompositionLocalHashMap;->containsKey(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/internal/PersistentCompositionLocalHashMap;->get(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/internal/PersistentCompositionLocalHashMap;->putValue(Landroidx/compose/runtime/CompositionLocal;Landroidx/compose/runtime/State;)Landroidx/compose/runtime/internal/PersistentCompositionLocalHashMap;
+HSPLandroidx/compose/runtime/internal/ThreadMap;-><init>(I[J[Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/internal/ThreadMap;->find(J)I
+HSPLandroidx/compose/runtime/internal/ThreadMap;->newWith(JLjava/lang/Object;)Landroidx/compose/runtime/internal/ThreadMap;
+HSPLandroidx/compose/runtime/saveable/ListSaverKt$listSaver$1;-><init>(Lkotlin/coroutines/CoroutineContext$plus$1;)V
+HSPLandroidx/compose/runtime/saveable/ListSaverKt$listSaver$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/saveable/RememberSaveableKt$rememberSaveable$1;-><init>(Landroidx/compose/runtime/saveable/SaveableHolder;Landroidx/compose/runtime/saveable/SaverKt$Saver$1;Landroidx/compose/runtime/saveable/SaveableStateRegistry;Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/saveable/RememberSaveableKt$rememberSaveable$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/saveable/SaveableHolder;-><init>(Landroidx/compose/runtime/saveable/SaverKt$Saver$1;Landroidx/compose/runtime/saveable/SaveableStateRegistry;Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/saveable/SaveableHolder;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/runtime/saveable/SaveableHolder;->onRemembered()V
+HSPLandroidx/compose/runtime/saveable/SaveableHolder;->register()V
+HSPLandroidx/compose/runtime/saveable/SaveableStateHolderImpl$RegistryHolder$registry$1;-><init>(Landroidx/compose/runtime/saveable/SaveableStateHolderImpl;)V
+HSPLandroidx/compose/runtime/saveable/SaveableStateHolderImpl$RegistryHolder;-><init>(Landroidx/compose/runtime/saveable/SaveableStateHolderImpl;Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/saveable/SaveableStateHolderImpl$SaveableStateProvider$1$1$invoke$$inlined$onDispose$1;-><init>(Landroidx/compose/runtime/saveable/SaveableStateHolderImpl$RegistryHolder;Landroidx/compose/runtime/saveable/SaveableStateHolderImpl;Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/saveable/SaveableStateHolderImpl$SaveableStateProvider$1$1$invoke$$inlined$onDispose$1;->dispose()V
+HSPLandroidx/compose/runtime/saveable/SaveableStateHolderImpl;-><clinit>()V
+HSPLandroidx/compose/runtime/saveable/SaveableStateHolderImpl;-><init>(Ljava/util/Map;)V
+HSPLandroidx/compose/runtime/saveable/SaveableStateHolderImpl;->SaveableStateProvider(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/compose/runtime/saveable/SaveableStateRegistryImpl$registerProvider$3;-><init>(Landroidx/compose/runtime/saveable/SaveableStateRegistryImpl;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V
+HSPLandroidx/compose/runtime/saveable/SaveableStateRegistryImpl;-><init>(Ljava/util/Map;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/runtime/saveable/SaveableStateRegistryImpl;->canBeSaved(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/saveable/SaveableStateRegistryImpl;->consumeRestored(Ljava/lang/String;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/saveable/SaveableStateRegistryImpl;->performSave()Ljava/util/Map;
+HSPLandroidx/compose/runtime/saveable/SaveableStateRegistryImpl;->registerProvider(Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Landroidx/compose/runtime/saveable/SaveableStateRegistryImpl$registerProvider$3;
+HSPLandroidx/compose/runtime/saveable/SaveableStateRegistryKt;-><clinit>()V
+HSPLandroidx/compose/runtime/saveable/SaverKt$Saver$1;-><init>(Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/runtime/saveable/SaverKt;-><clinit>()V
+HSPLandroidx/compose/runtime/snapshots/GlobalSnapshot$1$1$1;-><init>(ILjava/util/List;)V
+HSPLandroidx/compose/runtime/snapshots/GlobalSnapshot$1$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/snapshots/GlobalSnapshot;-><init>(ILandroidx/compose/runtime/snapshots/SnapshotIdSet;)V
+HSPLandroidx/compose/runtime/snapshots/GlobalSnapshot;->dispose()V
+HSPLandroidx/compose/runtime/snapshots/GlobalSnapshot;->notifyObjectsInitialized$runtime_release()V
+HSPLandroidx/compose/runtime/snapshots/GlobalSnapshot;->takeNestedMutableSnapshot(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Landroidx/compose/runtime/snapshots/MutableSnapshot;
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;-><clinit>()V
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;-><init>(ILandroidx/compose/runtime/snapshots/SnapshotIdSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->advance$runtime_release()V
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->apply()Lokhttp3/MediaType;
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->closeLocked$runtime_release()V
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->dispose()V
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->getModified$runtime_release()Landroidx/compose/runtime/collection/IdentityArraySet;
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->getReadObserver$runtime_release()Lkotlin/jvm/functions/Function1;
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->getReadOnly()Z
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->getWriteCount$runtime_release()I
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->getWriteObserver$runtime_release()Lkotlin/jvm/functions/Function1;
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->innerApplyLocked$runtime_release(ILjava/util/HashMap;Landroidx/compose/runtime/snapshots/SnapshotIdSet;)Lokhttp3/MediaType;
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->nestedDeactivated$runtime_release()V
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->notifyObjectsInitialized$runtime_release()V
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->recordModified$runtime_release(Landroidx/compose/runtime/snapshots/StateObject;)V
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->recordPrevious$runtime_release(I)V
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->releasePinnedSnapshotsForCloseLocked$runtime_release()V
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->setModified(Landroidx/compose/runtime/collection/IdentityArraySet;)V
+HSPLandroidx/compose/runtime/snapshots/MutableSnapshot;->setWriteCount$runtime_release(I)V
+HSPLandroidx/compose/runtime/snapshots/Snapshot$Companion$$ExternalSyntheticLambda0;-><init>(Lkotlin/jvm/internal/Lambda;I)V
+HSPLandroidx/compose/runtime/snapshots/Snapshot;-><init>(ILandroidx/compose/runtime/snapshots/SnapshotIdSet;)V
+HSPLandroidx/compose/runtime/snapshots/Snapshot;->getId()I
+HSPLandroidx/compose/runtime/snapshots/Snapshot;->getInvalid$runtime_release()Landroidx/compose/runtime/snapshots/SnapshotIdSet;
+HSPLandroidx/compose/runtime/snapshots/Snapshot;->makeCurrent()Landroidx/compose/runtime/snapshots/Snapshot;
+HSPLandroidx/compose/runtime/snapshots/Snapshot;->restoreCurrent(Landroidx/compose/runtime/snapshots/Snapshot;)V
+HSPLandroidx/compose/runtime/snapshots/Snapshot;->setId$runtime_release(I)V
+HSPLandroidx/compose/runtime/snapshots/Snapshot;->setInvalid$runtime_release(Landroidx/compose/runtime/snapshots/SnapshotIdSet;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotApplyResult$Success;-><clinit>()V
+HSPLandroidx/compose/runtime/snapshots/SnapshotDoubleIndexHeap;-><init>()V
+HSPLandroidx/compose/runtime/snapshots/SnapshotDoubleIndexHeap;->add(I)I
+HSPLandroidx/compose/runtime/snapshots/SnapshotDoubleIndexHeap;->swap(II)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotIdSet;-><clinit>()V
+HSPLandroidx/compose/runtime/snapshots/SnapshotIdSet;-><init>(JJI[I)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotIdSet;->andNot(Landroidx/compose/runtime/snapshots/SnapshotIdSet;)Landroidx/compose/runtime/snapshots/SnapshotIdSet;
+HSPLandroidx/compose/runtime/snapshots/SnapshotIdSet;->clear(I)Landroidx/compose/runtime/snapshots/SnapshotIdSet;
+HSPLandroidx/compose/runtime/snapshots/SnapshotIdSet;->get(I)Z
+HSPLandroidx/compose/runtime/snapshots/SnapshotIdSet;->or(Landroidx/compose/runtime/snapshots/SnapshotIdSet;)Landroidx/compose/runtime/snapshots/SnapshotIdSet;
+HSPLandroidx/compose/runtime/snapshots/SnapshotIdSet;->set(I)Landroidx/compose/runtime/snapshots/SnapshotIdSet;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt$mergedReadObserver$1;-><init>(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;I)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt$mergedReadObserver$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt$mergedReadObserver$1;->invoke(Ljava/lang/Object;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;-><clinit>()V
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->access$advanceGlobalSnapshot()V
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->access$mergedWriteObserver(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->access$optimisticMerges(Landroidx/compose/runtime/snapshots/MutableSnapshot;Landroidx/compose/runtime/snapshots/MutableSnapshot;Landroidx/compose/runtime/snapshots/SnapshotIdSet;)Ljava/util/HashMap;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->access$validateOpen(Landroidx/compose/runtime/snapshots/Snapshot;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->advanceGlobalSnapshot(Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->checkAndOverwriteUnusedRecordsLocked()V
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->createTransparentSnapshotWithNoParentReadObserver(Landroidx/compose/runtime/snapshots/Snapshot;Lkotlin/jvm/functions/Function1;Z)Landroidx/compose/runtime/snapshots/Snapshot;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->current(Landroidx/compose/runtime/snapshots/StateRecord;)Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->currentSnapshot()Landroidx/compose/runtime/snapshots/Snapshot;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->mergedReadObserver(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Z)Lkotlin/jvm/functions/Function1;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->newOverwritableRecordLocked(Landroidx/compose/runtime/snapshots/StateRecord;Landroidx/compose/runtime/snapshots/StateObject;)Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->notifyWrite(Landroidx/compose/runtime/snapshots/Snapshot;Landroidx/compose/runtime/snapshots/StateObject;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->overwritableRecord(Landroidx/compose/runtime/snapshots/StateRecord;Landroidx/compose/runtime/snapshots/StateObject;Landroidx/compose/runtime/snapshots/Snapshot;Landroidx/compose/runtime/snapshots/StateRecord;)Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->overwriteUnusedRecordsLocked(Landroidx/compose/runtime/snapshots/StateObject;)Z
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->processForUnusedRecordsLocked(Landroidx/compose/runtime/snapshots/StateObject;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->readable(Landroidx/compose/runtime/snapshots/StateRecord;ILandroidx/compose/runtime/snapshots/SnapshotIdSet;)Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->readable(Landroidx/compose/runtime/snapshots/StateRecord;Landroidx/compose/runtime/snapshots/StateObject;)Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->releasePinningLocked(I)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->takeNewGlobalSnapshot(Landroidx/compose/runtime/snapshots/Snapshot;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/snapshots/SnapshotKt;->writableRecord(Landroidx/compose/runtime/snapshots/StateRecord;Landroidx/compose/runtime/snapshots/StateObject;Landroidx/compose/runtime/snapshots/Snapshot;)Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList$StateListStateRecord;-><init>(Landroidx/compose/runtime/external/kotlinx/collections/immutable/PersistentList;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList$StateListStateRecord;->assign(Landroidx/compose/runtime/snapshots/StateRecord;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList$StateListStateRecord;->create()Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList$addAll$1;-><init>(Landroidx/compose/ui/layout/Placeable;II)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList$addAll$1;->invoke(Landroidx/compose/ui/layout/Placeable$PlacementScope;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList$addAll$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList;-><init>()V
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList;->add(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList;->get(I)Ljava/lang/Object;
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList;->getFirstStateRecord()Landroidx/compose/runtime/snapshots/StateRecord;
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList;->getReadable$runtime_release()Landroidx/compose/runtime/snapshots/SnapshotStateList$StateListStateRecord;
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList;->isEmpty()Z
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList;->prependStateRecord(Landroidx/compose/runtime/snapshots/StateRecord;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList;->remove(Ljava/lang/Object;)Z
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateList;->size()I
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateListKt;-><clinit>()V
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateObserver$ObservedScopeMap;-><init>(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateObserver$ObservedScopeMap;->observe(Ljava/lang/Object;Lkotlin/collections/AbstractMap$toString$1;Lkotlin/jvm/functions/Function0;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateObserver$ObservedScopeMap;->recordInvalidation(Ljava/util/Set;)Z
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateObserver$ObservedScopeMap;->recordRead(Ljava/lang/Object;ILjava/lang/Object;Landroidx/compose/runtime/collection/IdentityArrayIntMap;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateObserver$ObservedScopeMap;->removeScopeIf()V
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateObserver;-><init>(Landroidx/compose/ui/platform/AndroidComposeView$focusOwner$1;)V
+HSPLandroidx/compose/runtime/snapshots/SnapshotStateObserver;->access$drainChanges(Landroidx/compose/runtime/snapshots/SnapshotStateObserver;)Z
+HSPLandroidx/compose/runtime/snapshots/StateRecord;-><init>()V
+HSPLandroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;-><init>(Landroidx/compose/runtime/snapshots/MutableSnapshot;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZ)V
+HSPLandroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;->apply()Lokhttp3/MediaType;
+HSPLandroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;->dispose()V
+HSPLandroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;->getId()I
+HSPLandroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;->getInvalid$runtime_release()Landroidx/compose/runtime/snapshots/SnapshotIdSet;
+HSPLandroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;->getReadOnly()Z
+HSPLandroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;->getWriteCount$runtime_release()I
+HSPLandroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;->notifyObjectsInitialized$runtime_release()V
+HSPLandroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;->recordModified$runtime_release(Landroidx/compose/runtime/snapshots/StateObject;)V
+HSPLandroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;->setWriteCount$runtime_release(I)V
+HSPLandroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;->takeNestedMutableSnapshot(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Landroidx/compose/runtime/snapshots/MutableSnapshot;
+HSPLandroidx/compose/runtime/tooling/InspectionTablesKt;-><clinit>()V
+HSPLandroidx/compose/ui/BiasAlignment$Horizontal;-><init>(F)V
+HSPLandroidx/compose/ui/BiasAlignment$Horizontal;->align(IILandroidx/compose/ui/unit/LayoutDirection;)I
+HSPLandroidx/compose/ui/BiasAlignment$Horizontal;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/BiasAlignment$Vertical;-><init>(F)V
+HSPLandroidx/compose/ui/BiasAlignment$Vertical;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/BiasAlignment;-><init>(FF)V
+HSPLandroidx/compose/ui/BiasAlignment;->align-KFBX0sM(JJLandroidx/compose/ui/unit/LayoutDirection;)J
+HSPLandroidx/compose/ui/BiasAlignment;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/CombinedModifier$toString$1;-><clinit>()V
+HSPLandroidx/compose/ui/CombinedModifier$toString$1;-><init>(I)V
+HSPLandroidx/compose/ui/CombinedModifier$toString$1;->invoke(Landroidx/compose/ui/layout/Measurable;I)Ljava/lang/Integer;
+HSPLandroidx/compose/ui/CombinedModifier$toString$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/CombinedModifier;-><init>(Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;)V
+HSPLandroidx/compose/ui/CombinedModifier;->all(Lkotlin/jvm/functions/Function1;)Z
+HSPLandroidx/compose/ui/CombinedModifier;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/CombinedModifier;->foldIn(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/ComposedModifier;-><init>(Lkotlin/jvm/functions/Function3;)V
+HSPLandroidx/compose/ui/Modifier$Companion;-><clinit>()V
+HSPLandroidx/compose/ui/Modifier$Companion;->all(Lkotlin/jvm/functions/Function1;)Z
+HSPLandroidx/compose/ui/Modifier$Companion;->then(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/Modifier$Element;->all(Lkotlin/jvm/functions/Function1;)Z
+HSPLandroidx/compose/ui/Modifier$Element;->foldIn(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/Modifier$Node;-><init>()V
+HSPLandroidx/compose/ui/Modifier$Node;->getCoroutineScope()Lkotlinx/coroutines/CoroutineScope;
+HSPLandroidx/compose/ui/Modifier$Node;->getShouldAutoInvalidate()Z
+HSPLandroidx/compose/ui/Modifier$Node;->markAsAttached$ui_release()V
+HSPLandroidx/compose/ui/Modifier$Node;->markAsDetached$ui_release()V
+HSPLandroidx/compose/ui/Modifier$Node;->onAttach()V
+HSPLandroidx/compose/ui/Modifier$Node;->onDetach()V
+HSPLandroidx/compose/ui/Modifier$Node;->onReset()V
+HSPLandroidx/compose/ui/Modifier$Node;->reset$ui_release()V
+HSPLandroidx/compose/ui/Modifier$Node;->runAttachLifecycle$ui_release()V
+HSPLandroidx/compose/ui/Modifier$Node;->runDetachLifecycle$ui_release()V
+HSPLandroidx/compose/ui/Modifier$Node;->updateCoordinator$ui_release(Landroidx/compose/ui/node/NodeCoordinator;)V
+HSPLandroidx/compose/ui/Modifier;->then(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/MotionDurationScale;->getKey()Lkotlin/coroutines/CoroutineContext$Key;
+HSPLandroidx/compose/ui/ZIndexElement;-><init>()V
+HSPLandroidx/compose/ui/ZIndexElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/ZIndexElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/ZIndexNode$measure$1;-><init>(Ljava/lang/Object;ILjava/lang/Object;)V
+HSPLandroidx/compose/ui/ZIndexNode$measure$1;->invoke(Landroidx/compose/ui/layout/Placeable$PlacementScope;)V
+HSPLandroidx/compose/ui/ZIndexNode$measure$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/ZIndexNode$measure$1;->invoke(Ljava/lang/Throwable;)V
+HSPLandroidx/compose/ui/ZIndexNode;-><init>(F)V
+HSPLandroidx/compose/ui/ZIndexNode;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/ui/autofill/AndroidAutofill;-><init>(Landroid/view/View;Landroidx/compose/ui/autofill/AutofillTree;)V
+HSPLandroidx/compose/ui/autofill/AutofillCallback;-><clinit>()V
+HSPLandroidx/compose/ui/autofill/AutofillCallback;->register(Landroidx/compose/ui/autofill/AndroidAutofill;)V
+HSPLandroidx/compose/ui/autofill/AutofillTree;-><init>()V
+HSPLandroidx/compose/ui/draw/CacheDrawModifierNodeImpl;-><init>(Landroidx/compose/ui/draw/CacheDrawScope;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/draw/CacheDrawModifierNodeImpl;->draw(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V
+HSPLandroidx/compose/ui/draw/CacheDrawModifierNodeImpl;->getDensity()Landroidx/compose/ui/unit/Density;
+HSPLandroidx/compose/ui/draw/CacheDrawModifierNodeImpl;->getLayoutDirection()Landroidx/compose/ui/unit/LayoutDirection;
+HSPLandroidx/compose/ui/draw/CacheDrawModifierNodeImpl;->getSize-NH-jbRc()J
+HSPLandroidx/compose/ui/draw/CacheDrawModifierNodeImpl;->invalidateDrawCache()V
+HSPLandroidx/compose/ui/draw/CacheDrawModifierNodeImpl;->onMeasureResultChanged()V
+HSPLandroidx/compose/ui/draw/CacheDrawScope$onDrawBehind$1;-><init>(ILkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/draw/CacheDrawScope$onDrawBehind$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/draw/CacheDrawScope;-><init>()V
+HSPLandroidx/compose/ui/draw/CacheDrawScope;->getDensity()F
+HSPLandroidx/compose/ui/draw/CacheDrawScope;->getSize-NH-jbRc()J
+HSPLandroidx/compose/ui/draw/CacheDrawScope;->onDrawWithContent(Lkotlin/jvm/functions/Function1;)Landroidx/compose/ui/draw/DrawResult;
+HSPLandroidx/compose/ui/draw/ClipKt;->clip(Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/draw/ClipKt;->clipToBounds(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/draw/ClipKt;->drawWithCache(Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/draw/ClipKt;->paint$default(Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;FLandroidx/compose/ui/graphics/BlendModeColorFilter;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/draw/DrawResult;-><init>(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/draw/DrawWithCacheElement;-><init>(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/draw/DrawWithCacheElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/draw/DrawWithCacheElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/draw/EmptyBuildDrawCacheParams;-><clinit>()V
+HSPLandroidx/compose/ui/draw/PainterElement;-><init>(Landroidx/compose/ui/graphics/painter/Painter;ZLandroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;FLandroidx/compose/ui/graphics/BlendModeColorFilter;)V
+HSPLandroidx/compose/ui/draw/PainterElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/draw/PainterElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/draw/PainterElement;->update(Landroidx/compose/ui/Modifier$Node;)V
+HSPLandroidx/compose/ui/draw/PainterNode$measure$1;-><init>(Landroidx/compose/ui/layout/Placeable;I)V
+HSPLandroidx/compose/ui/draw/PainterNode$measure$1;->invoke(Landroidx/compose/ui/layout/Placeable$PlacementScope;)V
+HSPLandroidx/compose/ui/draw/PainterNode$measure$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/draw/PainterNode;-><init>(Landroidx/compose/ui/graphics/painter/Painter;ZLandroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;FLandroidx/compose/ui/graphics/BlendModeColorFilter;)V
+HSPLandroidx/compose/ui/draw/PainterNode;->draw(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V
+HSPLandroidx/compose/ui/draw/PainterNode;->getUseIntrinsicSize()Z
+HSPLandroidx/compose/ui/draw/PainterNode;->hasSpecifiedAndFiniteHeight-uvyYCjk(J)Z
+HSPLandroidx/compose/ui/draw/PainterNode;->hasSpecifiedAndFiniteWidth-uvyYCjk(J)Z
+HSPLandroidx/compose/ui/draw/PainterNode;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/ui/draw/PainterNode;->modifyConstraints-ZezNO4M(J)J
+HSPLandroidx/compose/ui/focus/FocusChangedElement;-><init>(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/focus/FocusChangedElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/focus/FocusChangedElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/focus/FocusChangedElement;->update(Landroidx/compose/ui/Modifier$Node;)V
+HSPLandroidx/compose/ui/focus/FocusChangedNode;-><init>(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/focus/FocusChangedNode;->onFocusEvent(Landroidx/compose/ui/focus/FocusStateImpl;)V
+HSPLandroidx/compose/ui/focus/FocusDirection;-><init>(I)V
+HSPLandroidx/compose/ui/focus/FocusInvalidationManager;-><init>(Landroidx/compose/ui/platform/AndroidComposeView$focusOwner$1;)V
+HSPLandroidx/compose/ui/focus/FocusInvalidationManager;->scheduleInvalidation(Ljava/util/LinkedHashSet;Ljava/lang/Object;)V
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->beamBeats-I7lrPNg(Landroidx/compose/ui/geometry/Rect;Landroidx/compose/ui/geometry/Rect;Landroidx/compose/ui/geometry/Rect;I)Z
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->beamBeats_I7lrPNg$inSourceBeam(ILandroidx/compose/ui/geometry/Rect;Landroidx/compose/ui/geometry/Rect;)Z
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->clearFocus(Landroidx/compose/ui/focus/FocusTargetNode;ZZ)Z
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->collectAccessibleChildren(Landroidx/compose/ui/node/DelegatableNode;Landroidx/compose/runtime/collection/MutableVector;)V
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->findActiveFocusNode(Landroidx/compose/ui/focus/FocusTargetNode;)Landroidx/compose/ui/focus/FocusTargetNode;
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->findBestCandidate-4WY_MpI(Landroidx/compose/runtime/collection/MutableVector;Landroidx/compose/ui/geometry/Rect;I)Landroidx/compose/ui/focus/FocusTargetNode;
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->findChildCorrespondingToFocusEnter--OM-vw8(Landroidx/compose/ui/focus/FocusTargetNode;ILkotlin/jvm/functions/Function1;)Z
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->focusRect(Landroidx/compose/ui/focus/FocusTargetNode;)Landroidx/compose/ui/geometry/Rect;
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->generateAndSearchChildren-4C6V_qg$1(Landroidx/compose/ui/focus/FocusTargetNode;Landroidx/compose/ui/focus/FocusTargetNode;ILkotlin/jvm/functions/Function1;)Z
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->getActiveChild(Landroidx/compose/ui/focus/FocusTargetNode;)Landroidx/compose/ui/focus/FocusTargetNode;
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->getFocusState(Landroidx/compose/ui/focus/FocusEventModifierNode;)Landroidx/compose/ui/focus/FocusStateImpl;
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->grantFocus(Landroidx/compose/ui/focus/FocusTargetNode;)V
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->invalidateFocusEvent(Landroidx/compose/ui/focus/FocusEventModifierNode;)V
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->isBetterCandidate_I7lrPNg$isCandidate(ILandroidx/compose/ui/geometry/Rect;Landroidx/compose/ui/geometry/Rect;)Z
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->isBetterCandidate_I7lrPNg$weightedDistance(ILandroidx/compose/ui/geometry/Rect;Landroidx/compose/ui/geometry/Rect;)J
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->isEligibleForFocusSearch(Landroidx/compose/ui/focus/FocusTargetNode;)Z
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->onFocusChanged(Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->performCustomClearFocus-Mxy_nc0(Landroidx/compose/ui/focus/FocusTargetNode;I)I
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->performCustomEnter-Mxy_nc0(Landroidx/compose/ui/focus/FocusTargetNode;I)I
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->performCustomRequestFocus-Mxy_nc0(Landroidx/compose/ui/focus/FocusTargetNode;I)I
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->performRequestFocus(Landroidx/compose/ui/focus/FocusTargetNode;)Z
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->refreshFocusEventNodes(Landroidx/compose/ui/focus/FocusTargetNode;)V
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->requestFocusForChild(Landroidx/compose/ui/focus/FocusTargetNode;Landroidx/compose/ui/focus/FocusTargetNode;)Z
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->requireActiveChild(Landroidx/compose/ui/focus/FocusTargetNode;)Landroidx/compose/ui/focus/FocusTargetNode;
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->requireTransactionManager(Landroidx/compose/ui/focus/FocusTargetNode;)Lcom/google/gson/internal/ConstructorConstructor;
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->searchBeyondBounds--OM-vw8(Landroidx/compose/ui/focus/FocusTargetNode;ILandroidx/compose/ui/focus/FocusOwnerImpl$moveFocus$foundNextItem$1;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->searchChildren-4C6V_qg$1(Landroidx/compose/ui/focus/FocusTargetNode;Landroidx/compose/ui/focus/FocusTargetNode;ILkotlin/jvm/functions/Function1;)Z
+HSPLandroidx/compose/ui/focus/FocusModifierKt;->twoDimensionalFocusSearch--OM-vw8(Landroidx/compose/ui/focus/FocusTargetNode;ILandroidx/compose/ui/focus/FocusOwnerImpl$moveFocus$foundNextItem$1;)Ljava/lang/Boolean;
+HSPLandroidx/compose/ui/focus/FocusOwnerImpl$modifier$1;-><init>(Landroidx/compose/ui/focus/FocusOwnerImpl;)V
+HSPLandroidx/compose/ui/focus/FocusOwnerImpl$modifier$1;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/focus/FocusOwnerImpl$moveFocus$foundNextItem$1;-><init>(Landroidx/compose/ui/focus/FocusTargetNode;Ljava/lang/Object;ILjava/lang/Object;I)V
+HSPLandroidx/compose/ui/focus/FocusOwnerImpl$moveFocus$foundNextItem$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/focus/FocusOwnerImpl;-><init>(Landroidx/compose/ui/platform/AndroidComposeView$focusOwner$1;)V
+HSPLandroidx/compose/ui/focus/FocusOwnerImpl;->moveFocus-3ESFkO8(I)Z
+HSPLandroidx/compose/ui/focus/FocusProperties$exit$1;-><clinit>()V
+HSPLandroidx/compose/ui/focus/FocusProperties$exit$1;-><init>(I)V
+HSPLandroidx/compose/ui/focus/FocusProperties$exit$1;->invoke(Landroidx/compose/ui/node/AlignmentLinesOwner;)V
+HSPLandroidx/compose/ui/focus/FocusProperties$exit$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/focus/FocusProperties$exit$1;->invoke-3ESFkO8()Landroidx/compose/ui/focus/FocusRequester;
+HSPLandroidx/compose/ui/focus/FocusPropertiesImpl;-><init>()V
+HSPLandroidx/compose/ui/focus/FocusPropertiesImpl;->setCanFocus(Z)V
+HSPLandroidx/compose/ui/focus/FocusRequester;-><clinit>()V
+HSPLandroidx/compose/ui/focus/FocusRequester;-><init>()V
+HSPLandroidx/compose/ui/focus/FocusStateImpl;-><clinit>()V
+HSPLandroidx/compose/ui/focus/FocusStateImpl;-><init>(ILjava/lang/String;)V
+HSPLandroidx/compose/ui/focus/FocusStateImpl;->isFocused()Z
+HSPLandroidx/compose/ui/focus/FocusTargetNode$FocusTargetElement;-><clinit>()V
+HSPLandroidx/compose/ui/focus/FocusTargetNode$FocusTargetElement;-><init>()V
+HSPLandroidx/compose/ui/focus/FocusTargetNode$FocusTargetElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/focus/FocusTargetNode$FocusTargetElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/focus/FocusTargetNode;-><init>()V
+HSPLandroidx/compose/ui/focus/FocusTargetNode;->fetchFocusProperties$ui_release()Landroidx/compose/ui/focus/FocusPropertiesImpl;
+HSPLandroidx/compose/ui/focus/FocusTargetNode;->getFocusState()Landroidx/compose/ui/focus/FocusStateImpl;
+HSPLandroidx/compose/ui/focus/FocusTargetNode;->invalidateFocus$ui_release()V
+HSPLandroidx/compose/ui/focus/FocusTargetNode;->onReset()V
+HSPLandroidx/compose/ui/focus/FocusTargetNode;->scheduleInvalidationForFocusEvents$ui_release()V
+HSPLandroidx/compose/ui/focus/FocusTargetNode;->setFocusState(Landroidx/compose/ui/focus/FocusStateImpl;)V
+HSPLandroidx/compose/ui/geometry/CornerRadius;-><clinit>()V
+HSPLandroidx/compose/ui/geometry/CornerRadius;->getX-impl(J)F
+HSPLandroidx/compose/ui/geometry/CornerRadius;->getY-impl(J)F
+HSPLandroidx/compose/ui/geometry/MutableRect;-><init>()V
+HSPLandroidx/compose/ui/geometry/MutableRect;->isEmpty()Z
+HSPLandroidx/compose/ui/geometry/Offset;-><clinit>()V
+HSPLandroidx/compose/ui/geometry/Offset;->getX-impl(J)F
+HSPLandroidx/compose/ui/geometry/Offset;->getY-impl(J)F
+HSPLandroidx/compose/ui/geometry/Rect;-><clinit>()V
+HSPLandroidx/compose/ui/geometry/Rect;-><init>(FFFF)V
+HSPLandroidx/compose/ui/geometry/Rect;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/geometry/Rect;->intersect(Landroidx/compose/ui/geometry/Rect;)Landroidx/compose/ui/geometry/Rect;
+HSPLandroidx/compose/ui/geometry/Rect;->translate(FF)Landroidx/compose/ui/geometry/Rect;
+HSPLandroidx/compose/ui/geometry/Rect;->translate-k-4lQ0M(J)Landroidx/compose/ui/geometry/Rect;
+HSPLandroidx/compose/ui/geometry/RoundRect;-><clinit>()V
+HSPLandroidx/compose/ui/geometry/RoundRect;-><init>(FFFFJJJJ)V
+HSPLandroidx/compose/ui/geometry/Size;-><clinit>()V
+HSPLandroidx/compose/ui/geometry/Size;-><init>(J)V
+HSPLandroidx/compose/ui/geometry/Size;->equals-impl0(JJ)Z
+HSPLandroidx/compose/ui/geometry/Size;->getHeight-impl(J)F
+HSPLandroidx/compose/ui/geometry/Size;->getMinDimension-impl(J)F
+HSPLandroidx/compose/ui/geometry/Size;->getWidth-impl(J)F
+HSPLandroidx/compose/ui/geometry/Size;->isEmpty-impl(J)Z
+HSPLandroidx/compose/ui/graphics/AndroidCanvas;-><init>()V
+HSPLandroidx/compose/ui/graphics/AndroidCanvas;->drawImageRect-HPBpro0(Landroidx/compose/ui/graphics/ImageBitmap;JJJJLandroidx/compose/ui/graphics/AndroidPaint;)V
+HSPLandroidx/compose/ui/graphics/AndroidCanvas;->drawRect(FFFFLandroidx/compose/ui/graphics/AndroidPaint;)V
+HSPLandroidx/compose/ui/graphics/AndroidCanvas;->drawRoundRect(FFFFFFLandroidx/compose/ui/graphics/AndroidPaint;)V
+HSPLandroidx/compose/ui/graphics/AndroidCanvas;->restore()V
+HSPLandroidx/compose/ui/graphics/AndroidCanvas;->save()V
+HSPLandroidx/compose/ui/graphics/AndroidCanvas;->scale(FF)V
+HSPLandroidx/compose/ui/graphics/AndroidCanvas;->translate(FF)V
+HSPLandroidx/compose/ui/graphics/AndroidCanvas_androidKt;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/AndroidImageBitmap;-><init>(Landroid/graphics/Bitmap;)V
+HSPLandroidx/compose/ui/graphics/AndroidPaint;-><init>(Landroid/graphics/Paint;)V
+HSPLandroidx/compose/ui/graphics/AndroidPaint;->setAlpha(F)V
+HSPLandroidx/compose/ui/graphics/AndroidPaint;->setBlendMode-s9anfk8(I)V
+HSPLandroidx/compose/ui/graphics/AndroidPaint;->setColor-8_81llA(J)V
+HSPLandroidx/compose/ui/graphics/AndroidPaint;->setStyle-k9PVt8s(I)V
+HSPLandroidx/compose/ui/graphics/AndroidPaint_androidKt$WhenMappings;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/BlockGraphicsLayerElement;-><init>(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/graphics/BlockGraphicsLayerElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/graphics/BlockGraphicsLayerElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/graphics/BlockGraphicsLayerModifier;-><init>(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/graphics/BlockGraphicsLayerModifier;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/ui/graphics/Brush;-><init>()V
+HSPLandroidx/compose/ui/graphics/BrushKt;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/BrushKt;->Color$default(III)J
+HSPLandroidx/compose/ui/graphics/BrushKt;->Color(FFFFLandroidx/compose/ui/graphics/colorspace/ColorSpace;)J
+HSPLandroidx/compose/ui/graphics/BrushKt;->Color(I)J
+HSPLandroidx/compose/ui/graphics/BrushKt;->Color(J)J
+HSPLandroidx/compose/ui/graphics/BrushKt;->Paint()Landroidx/compose/ui/graphics/AndroidPaint;
+HSPLandroidx/compose/ui/graphics/BrushKt;->drawOutline-wDX37Ww$default(Landroidx/compose/ui/graphics/drawscope/DrawScope;Landroidx/compose/ui/graphics/BrushKt;J)V
+HSPLandroidx/compose/ui/graphics/BrushKt;->graphicsLayer(Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/graphics/BrushKt;->graphicsLayer-Ap8cVGQ$default(Landroidx/compose/ui/Modifier;FFLandroidx/compose/ui/graphics/Shape;ZI)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/graphics/BrushKt;->setFrom-tU-YjHk(Landroid/graphics/Matrix;[F)V
+HSPLandroidx/compose/ui/graphics/BrushKt;->toArgb-8_81llA(J)I
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m$1(Landroid/graphics/RenderNode;)I
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m$1(Landroid/graphics/RenderNode;F)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m$1(Landroid/graphics/RenderNode;I)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m$2(Landroid/graphics/RenderNode;F)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m$2(Landroid/graphics/RenderNode;I)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m$3(Landroid/graphics/RenderNode;F)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m$4(Landroid/graphics/RenderNode;F)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m$5(Landroid/graphics/RenderNode;F)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m$6(Landroid/graphics/RenderNode;F)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m$7(Landroid/graphics/RenderNode;F)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m$8(Landroid/graphics/RenderNode;F)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/Canvas;Landroid/graphics/RenderNode;)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;)I
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;)Z
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;F)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;I)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;Z)V
+HSPLandroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;->m(Landroid/view/View;)V
+HSPLandroidx/compose/ui/graphics/Color;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/Color;-><init>(J)V
+HSPLandroidx/compose/ui/graphics/Color;->convert-vNxB06k(JLandroidx/compose/ui/graphics/colorspace/ColorSpace;)J
+HSPLandroidx/compose/ui/graphics/Color;->copy-wmQWz5c$default(JF)J
+HSPLandroidx/compose/ui/graphics/Color;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/graphics/Color;->equals-impl0(JJ)Z
+HSPLandroidx/compose/ui/graphics/Color;->getAlpha-impl(J)F
+HSPLandroidx/compose/ui/graphics/Color;->getBlue-impl(J)F
+HSPLandroidx/compose/ui/graphics/Color;->getColorSpace-impl(J)Landroidx/compose/ui/graphics/colorspace/ColorSpace;
+HSPLandroidx/compose/ui/graphics/Color;->getGreen-impl(J)F
+HSPLandroidx/compose/ui/graphics/Color;->getRed-impl(J)F
+HSPLandroidx/compose/ui/graphics/ColorSpaceVerificationHelper$$ExternalSyntheticLambda1;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/ui/graphics/Float16;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/Float16;->constructor-impl(F)S
+HSPLandroidx/compose/ui/graphics/Float16;->toFloat-impl(S)F
+HSPLandroidx/compose/ui/graphics/GraphicsLayerElement;-><init>(FFFFFFFFFFJLandroidx/compose/ui/graphics/Shape;ZJJI)V
+HSPLandroidx/compose/ui/graphics/GraphicsLayerElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/graphics/GraphicsLayerElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/graphics/GraphicsLayerElement;->update(Landroidx/compose/ui/Modifier$Node;)V
+HSPLandroidx/compose/ui/graphics/GraphicsLayerScopeKt;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/Matrix;->constructor-impl$default()[F
+HSPLandroidx/compose/ui/graphics/Matrix;->map-MK-Hz9U([FJ)J
+HSPLandroidx/compose/ui/graphics/Matrix;->map-impl([FLandroidx/compose/ui/geometry/MutableRect;)V
+HSPLandroidx/compose/ui/graphics/Outline$Rectangle;-><init>(Landroidx/compose/ui/geometry/Rect;)V
+HSPLandroidx/compose/ui/graphics/Outline$Rounded;-><init>(Landroidx/compose/ui/geometry/RoundRect;)V
+HSPLandroidx/compose/ui/graphics/RectangleShapeKt$RectangleShape$1;-><init>(I)V
+HSPLandroidx/compose/ui/graphics/RectangleShapeKt$RectangleShape$1;->createOutline-Pq9zytI(JLandroidx/compose/ui/unit/LayoutDirection;Landroidx/compose/ui/unit/Density;)Landroidx/compose/ui/graphics/BrushKt;
+HSPLandroidx/compose/ui/graphics/ReusableGraphicsLayerScope;-><init>()V
+HSPLandroidx/compose/ui/graphics/Shadow;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/Shadow;-><init>(JJF)V
+HSPLandroidx/compose/ui/graphics/Shadow;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/graphics/SimpleGraphicsLayerModifier$layerBlock$1;-><init>(Landroidx/compose/ui/graphics/SimpleGraphicsLayerModifier;)V
+HSPLandroidx/compose/ui/graphics/SimpleGraphicsLayerModifier$layerBlock$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/graphics/SimpleGraphicsLayerModifier;-><init>(FFFFFFFFFFJLandroidx/compose/ui/graphics/Shape;ZJJI)V
+HSPLandroidx/compose/ui/graphics/SimpleGraphicsLayerModifier;->getShouldAutoInvalidate()Z
+HSPLandroidx/compose/ui/graphics/SimpleGraphicsLayerModifier;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/ui/graphics/SolidColor;-><init>(J)V
+HSPLandroidx/compose/ui/graphics/SolidColor;->applyTo-Pq9zytI(FJLandroidx/compose/ui/graphics/AndroidPaint;)V
+HSPLandroidx/compose/ui/graphics/SolidColor;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/graphics/TransformOrigin;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/colorspace/Adaptation;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/colorspace/Adaptation;-><init>([F)V
+HSPLandroidx/compose/ui/graphics/colorspace/ColorModel;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/colorspace/ColorModel;->equals-impl0(JJ)Z
+HSPLandroidx/compose/ui/graphics/colorspace/ColorSpace;-><init>(Ljava/lang/String;JI)V
+HSPLandroidx/compose/ui/graphics/colorspace/ColorSpace;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/graphics/colorspace/ColorSpace;->isSrgb()Z
+HSPLandroidx/compose/ui/graphics/colorspace/ColorSpaces;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/colorspace/Connector$Companion$identity$1;-><init>(Landroidx/compose/ui/graphics/colorspace/ColorSpace;)V
+HSPLandroidx/compose/ui/graphics/colorspace/Connector;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/colorspace/Connector;-><init>(Landroidx/compose/ui/graphics/colorspace/ColorSpace;Landroidx/compose/ui/graphics/colorspace/ColorSpace;I)V
+HSPLandroidx/compose/ui/graphics/colorspace/Connector;-><init>(Landroidx/compose/ui/graphics/colorspace/ColorSpace;Landroidx/compose/ui/graphics/colorspace/ColorSpace;Landroidx/compose/ui/graphics/colorspace/ColorSpace;[F)V
+HSPLandroidx/compose/ui/graphics/colorspace/Connector;->transformToColor-wmQWz5c$ui_graphics_release(FFFF)J
+HSPLandroidx/compose/ui/graphics/colorspace/Lab;-><init>()V
+HSPLandroidx/compose/ui/graphics/colorspace/Oklab;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/colorspace/Oklab;-><init>()V
+HSPLandroidx/compose/ui/graphics/colorspace/Oklab;->getMaxValue(I)F
+HSPLandroidx/compose/ui/graphics/colorspace/Oklab;->getMinValue(I)F
+HSPLandroidx/compose/ui/graphics/colorspace/Oklab;->toXy$ui_graphics_release(FFF)J
+HSPLandroidx/compose/ui/graphics/colorspace/Oklab;->toZ$ui_graphics_release(FFF)F
+HSPLandroidx/compose/ui/graphics/colorspace/Oklab;->xyzaToColor-JlNiLsg$ui_graphics_release(FFFFLandroidx/compose/ui/graphics/colorspace/ColorSpace;)J
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb$$ExternalSyntheticLambda0;-><init>(Landroidx/compose/ui/graphics/colorspace/Rgb;I)V
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb$$ExternalSyntheticLambda0;->invoke(D)D
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb$$ExternalSyntheticLambda1;-><init>(Landroidx/compose/ui/graphics/colorspace/TransferParameters;I)V
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb$$ExternalSyntheticLambda1;->invoke(D)D
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb$$ExternalSyntheticLambda2;-><init>(DI)V
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb$eotf$1;-><init>(Landroidx/compose/ui/graphics/colorspace/Rgb;I)V
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb;-><init>(Ljava/lang/String;[FLandroidx/compose/ui/graphics/colorspace/WhitePoint;DFFI)V
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb;-><init>(Ljava/lang/String;[FLandroidx/compose/ui/graphics/colorspace/WhitePoint;Landroidx/compose/ui/graphics/colorspace/TransferParameters;I)V
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb;-><init>(Ljava/lang/String;[FLandroidx/compose/ui/graphics/colorspace/WhitePoint;[FLandroidx/compose/ui/graphics/colorspace/DoubleFunction;Landroidx/compose/ui/graphics/colorspace/DoubleFunction;FFLandroidx/compose/ui/graphics/colorspace/TransferParameters;I)V
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb;->getMaxValue(I)F
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb;->getMinValue(I)F
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb;->isSrgb()Z
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb;->toXy$ui_graphics_release(FFF)J
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb;->toZ$ui_graphics_release(FFF)F
+HSPLandroidx/compose/ui/graphics/colorspace/Rgb;->xyzaToColor-JlNiLsg$ui_graphics_release(FFFFLandroidx/compose/ui/graphics/colorspace/ColorSpace;)J
+HSPLandroidx/compose/ui/graphics/colorspace/TransferParameters;-><init>(DDDDD)V
+HSPLandroidx/compose/ui/graphics/colorspace/TransferParameters;-><init>(DDDDDDD)V
+HSPLandroidx/compose/ui/graphics/colorspace/WhitePoint;-><init>(FF)V
+HSPLandroidx/compose/ui/graphics/colorspace/WhitePoint;->toXyz$ui_graphics_release()[F
+HSPLandroidx/compose/ui/graphics/colorspace/Xyz;-><init>()V
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope$DrawParams;-><init>()V
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope$drawContext$1;-><init>(Landroidx/compose/ui/graphics/drawscope/CanvasDrawScope;)V
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope$drawContext$1;->getCanvas()Landroidx/compose/ui/graphics/Canvas;
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope$drawContext$1;->getSize-NH-jbRc()J
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope$drawContext$1;->setSize-uvyYCjk(J)V
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope;-><init>()V
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope;->configurePaint-2qPWKa0$default(Landroidx/compose/ui/graphics/drawscope/CanvasDrawScope;JLkotlin/ResultKt;FLandroidx/compose/ui/graphics/BlendModeColorFilter;I)Landroidx/compose/ui/graphics/AndroidPaint;
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope;->configurePaint-swdJneE$default(Landroidx/compose/ui/graphics/drawscope/CanvasDrawScope;Landroidx/compose/ui/graphics/Brush;Lkotlin/ResultKt;FLandroidx/compose/ui/graphics/BlendModeColorFilter;I)Landroidx/compose/ui/graphics/AndroidPaint;
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope;->configurePaint-swdJneE(Landroidx/compose/ui/graphics/Brush;Lkotlin/ResultKt;FLandroidx/compose/ui/graphics/BlendModeColorFilter;II)Landroidx/compose/ui/graphics/AndroidPaint;
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope;->drawImage-AZ2fEMs(Landroidx/compose/ui/graphics/ImageBitmap;JJJJFLkotlin/ResultKt;Landroidx/compose/ui/graphics/BlendModeColorFilter;II)V
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope;->drawRect-AsUm42w(Landroidx/compose/ui/graphics/Brush;JJFLkotlin/ResultKt;Landroidx/compose/ui/graphics/BlendModeColorFilter;I)V
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope;->drawRect-n-J9OG0(JJJFLkotlin/ResultKt;Landroidx/compose/ui/graphics/BlendModeColorFilter;I)V
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope;->drawRoundRect-u-Aw5IA(JJJJLkotlin/ResultKt;FLandroidx/compose/ui/graphics/BlendModeColorFilter;I)V
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope;->getDensity()F
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope;->getDrawContext()Landroidx/compose/ui/graphics/drawscope/CanvasDrawScope$drawContext$1;
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope;->getFontScale()F
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScope;->selectPaint(Lkotlin/ResultKt;)Landroidx/compose/ui/graphics/AndroidPaint;
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScopeKt$asDrawTransform$1;-><init>(Landroidx/compose/ui/graphics/drawscope/CanvasDrawScope$drawContext$1;)V
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScopeKt$asDrawTransform$1;->inset(FFFF)V
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScopeKt$asDrawTransform$1;->scale-0AR0LA0(FFJ)V
+HSPLandroidx/compose/ui/graphics/drawscope/CanvasDrawScopeKt$asDrawTransform$1;->translate(FF)V
+HSPLandroidx/compose/ui/graphics/drawscope/DrawScope;->drawImage-AZ2fEMs$default(Landroidx/compose/ui/graphics/drawscope/DrawScope;Landroidx/compose/ui/graphics/ImageBitmap;JJJFLandroidx/compose/ui/graphics/BlendModeColorFilter;II)V
+HSPLandroidx/compose/ui/graphics/drawscope/DrawScope;->drawRect-AsUm42w$default(Landroidx/compose/ui/graphics/drawscope/DrawScope;Landroidx/compose/ui/graphics/Brush;JJFLkotlin/ResultKt;I)V
+HSPLandroidx/compose/ui/graphics/drawscope/DrawScope;->drawRect-n-J9OG0$default(Landroidx/compose/ui/graphics/drawscope/DrawScope;JJI)V
+HSPLandroidx/compose/ui/graphics/drawscope/DrawScope;->getCenter-F1C5BW0()J
+HSPLandroidx/compose/ui/graphics/drawscope/DrawScope;->getSize-NH-jbRc()J
+HSPLandroidx/compose/ui/graphics/drawscope/DrawScope;->offsetSize-PENXr5M(JJ)J
+HSPLandroidx/compose/ui/graphics/drawscope/Fill;-><clinit>()V
+HSPLandroidx/compose/ui/graphics/drawscope/Stroke;-><init>(FFIII)V
+HSPLandroidx/compose/ui/graphics/drawscope/Stroke;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/graphics/painter/BitmapPainter;-><init>(Landroidx/compose/ui/graphics/ImageBitmap;)V
+HSPLandroidx/compose/ui/graphics/painter/BitmapPainter;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/graphics/painter/BitmapPainter;->getIntrinsicSize-NH-jbRc()J
+HSPLandroidx/compose/ui/graphics/painter/BitmapPainter;->onDraw(Landroidx/compose/ui/graphics/drawscope/DrawScope;)V
+HSPLandroidx/compose/ui/graphics/painter/Painter;-><init>()V
+HSPLandroidx/compose/ui/input/InputMode;-><init>(I)V
+HSPLandroidx/compose/ui/input/InputMode;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/input/InputModeManagerImpl;-><init>(I)V
+HSPLandroidx/compose/ui/input/key/Key;-><clinit>()V
+HSPLandroidx/compose/ui/input/key/Key;->equals-impl0(JJ)Z
+HSPLandroidx/compose/ui/input/key/KeyEvent;-><init>(Landroid/view/KeyEvent;)V
+HSPLandroidx/compose/ui/input/key/KeyInputElement;-><init>(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/input/key/KeyInputElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/input/key/KeyInputElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/input/key/KeyInputElement;->update(Landroidx/compose/ui/Modifier$Node;)V
+HSPLandroidx/compose/ui/input/key/KeyInputNode;-><init>(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/input/key/Key_androidKt;->Key(I)J
+HSPLandroidx/compose/ui/input/key/Key_androidKt;->onKeyEvent(Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/input/nestedscroll/NestedScrollDispatcher;-><init>()V
+HSPLandroidx/compose/ui/input/nestedscroll/NestedScrollNode;-><init>(Landroidx/compose/foundation/gestures/ScrollableNestedScrollConnection;Landroidx/compose/ui/input/nestedscroll/NestedScrollDispatcher;)V
+HSPLandroidx/compose/ui/input/nestedscroll/NestedScrollNode;->getProvidedValues()Landroidx/tv/material3/TabKt;
+HSPLandroidx/compose/ui/input/nestedscroll/NestedScrollNode;->onAttach()V
+HSPLandroidx/compose/ui/input/nestedscroll/NestedScrollNodeKt;-><clinit>()V
+HSPLandroidx/compose/ui/input/pointer/AndroidPointerIconType;-><init>(I)V
+HSPLandroidx/compose/ui/input/pointer/MotionEventAdapter;-><init>()V
+HSPLandroidx/compose/ui/input/pointer/NodeParent;-><init>()V
+HSPLandroidx/compose/ui/input/pointer/PointerEvent;-><init>(Ljava/util/List;Lcom/google/gson/internal/ConstructorConstructor;)V
+HSPLandroidx/compose/ui/input/pointer/PointerIcon;-><clinit>()V
+HSPLandroidx/compose/ui/input/pointer/PointerKeyboardModifiers;-><init>(I)V
+HSPLandroidx/compose/ui/input/pointer/PointerKeyboardModifiers;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/input/pointer/SuspendPointerInputElement;-><init>(Ljava/lang/Object;Lokhttp3/MediaType;Lkotlin/jvm/functions/Function2;I)V
+HSPLandroidx/compose/ui/input/pointer/SuspendPointerInputElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/input/pointer/SuspendingPointerInputFilterKt;-><clinit>()V
+HSPLandroidx/compose/ui/input/pointer/SuspendingPointerInputFilterKt;->pointerInput(Landroidx/compose/ui/Modifier;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/input/pointer/SuspendingPointerInputModifierNodeImpl;-><init>(Lkotlin/jvm/functions/Function2;)V
+HSPLandroidx/compose/ui/input/pointer/util/PointerIdArray;-><init>(FF)V
+HSPLandroidx/compose/ui/input/pointer/util/PointerIdArray;-><init>(FFLandroidx/compose/animation/core/AnimationVector;)V
+HSPLandroidx/compose/ui/input/pointer/util/PointerIdArray;-><init>(I[Landroidx/core/provider/FontsContractCompat$FontInfo;)V
+HSPLandroidx/compose/ui/input/pointer/util/PointerIdArray;-><init>(Landroidx/compose/animation/core/FloatAnimationSpec;)V
+HSPLandroidx/compose/ui/input/pointer/util/PointerIdArray;-><init>(Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;I)V
+HSPLandroidx/compose/ui/input/pointer/util/PointerIdArray;->get(I)Landroidx/compose/animation/core/FloatAnimationSpec;
+HSPLandroidx/compose/ui/input/pointer/util/VelocityTracker1D;-><init>()V
+HSPLandroidx/compose/ui/input/pointer/util/VelocityTracker;-><init>()V
+HSPLandroidx/compose/ui/input/rotary/RotaryInputElement;-><init>()V
+HSPLandroidx/compose/ui/input/rotary/RotaryInputElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/input/rotary/RotaryInputModifierKt;->onRotaryScrollEvent()Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/input/rotary/RotaryInputNode;-><init>(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/layout/AlignmentLine;-><init>(Lkotlin/jvm/functions/Function2;)V
+HSPLandroidx/compose/ui/layout/AlignmentLineKt$FirstBaseline$1;-><clinit>()V
+HSPLandroidx/compose/ui/layout/AlignmentLineKt$FirstBaseline$1;-><init>()V
+HSPLandroidx/compose/ui/layout/AlignmentLineKt$LastBaseline$1;-><clinit>()V
+HSPLandroidx/compose/ui/layout/AlignmentLineKt$LastBaseline$1;-><init>()V
+HSPLandroidx/compose/ui/layout/AlignmentLineKt;-><clinit>()V
+HSPLandroidx/compose/ui/layout/BeyondBoundsLayoutKt;-><clinit>()V
+HSPLandroidx/compose/ui/layout/ComposableSingletons$SubcomposeLayoutKt;-><clinit>()V
+HSPLandroidx/compose/ui/layout/DefaultIntrinsicMeasurable;-><init>(Landroidx/compose/ui/layout/Measurable;Ljava/lang/Enum;Ljava/lang/Enum;I)V
+HSPLandroidx/compose/ui/layout/DefaultIntrinsicMeasurable;->getParentData()Ljava/lang/Object;
+HSPLandroidx/compose/ui/layout/FixedSizeIntrinsicsPlaceable;-><init>(III)V
+HSPLandroidx/compose/ui/layout/IntrinsicMinMax;-><clinit>()V
+HSPLandroidx/compose/ui/layout/IntrinsicMinMax;-><init>(ILjava/lang/String;)V
+HSPLandroidx/compose/ui/layout/IntrinsicWidthHeight;-><clinit>()V
+HSPLandroidx/compose/ui/layout/IntrinsicWidthHeight;-><init>(ILjava/lang/String;)V
+HSPLandroidx/compose/ui/layout/IntrinsicsMeasureScope;-><init>(Landroidx/compose/ui/layout/IntrinsicMeasureScope;Landroidx/compose/ui/unit/LayoutDirection;)V
+HSPLandroidx/compose/ui/layout/IntrinsicsMeasureScope;->roundToPx-0680j_4(F)I
+HSPLandroidx/compose/ui/layout/LayoutElement;-><init>(Lkotlin/jvm/functions/Function3;)V
+HSPLandroidx/compose/ui/layout/LayoutElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/layout/LayoutElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/layout/LayoutKt$materializerOf$1;-><init>(Landroidx/compose/ui/Modifier;I)V
+HSPLandroidx/compose/ui/layout/LayoutKt$materializerOf$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/layout/LayoutKt$materializerOf$1;->invoke-Deg8D_g(Landroidx/compose/runtime/Composer;Landroidx/compose/runtime/Composer;)V
+HSPLandroidx/compose/ui/layout/LayoutKt;->ScaleFactor(FF)J
+HSPLandroidx/compose/ui/layout/LayoutKt;->SubcomposeLayout(Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
+HSPLandroidx/compose/ui/layout/LayoutKt;->SubcomposeLayout(Landroidx/compose/ui/layout/SubcomposeLayoutState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
+HSPLandroidx/compose/ui/layout/LayoutKt;->findRootCoordinates(Landroidx/compose/ui/node/NodeCoordinator;)Landroidx/compose/ui/layout/LayoutCoordinates;
+HSPLandroidx/compose/ui/layout/LayoutKt;->layout(Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;)Landroidx/compose/ui/Modifier;
+HSPLandroidx/compose/ui/layout/LayoutKt;->modifierMaterializerOf(Landroidx/compose/ui/Modifier;)Landroidx/compose/runtime/internal/ComposableLambdaImpl;
+HSPLandroidx/compose/ui/layout/LayoutKt;->times-UQTWf7w(JJ)J
+HSPLandroidx/compose/ui/layout/LayoutModifierImpl;-><init>(Lkotlin/jvm/functions/Function3;)V
+HSPLandroidx/compose/ui/layout/LayoutModifierImpl;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$NodeState;-><init>(Ljava/lang/Object;Landroidx/compose/runtime/internal/ComposableLambdaImpl;)V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$PostLookaheadMeasureScopeImpl;-><init>(Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState;)V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$Scope;-><init>(Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState;)V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$Scope;->getDensity()F
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$Scope;->getLayoutDirection()Landroidx/compose/ui/unit/LayoutDirection;
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$Scope;->isLookingAhead()Z
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$Scope;->subcompose(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/util/List;
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$createMeasurePolicy$1$measure-3p2s80s$$inlined$createMeasureResult$1;-><init>(Landroidx/compose/ui/layout/MeasureResult;Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState;ILandroidx/compose/ui/layout/MeasureResult;I)V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$createMeasurePolicy$1$measure-3p2s80s$$inlined$createMeasureResult$1;->getAlignmentLines()Ljava/util/Map;
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$createMeasurePolicy$1$measure-3p2s80s$$inlined$createMeasureResult$1;->getHeight()I
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$createMeasurePolicy$1$measure-3p2s80s$$inlined$createMeasureResult$1;->getWidth()I
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$createMeasurePolicy$1$measure-3p2s80s$$inlined$createMeasureResult$1;->placeChildren()V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$createMeasurePolicy$1;-><init>(Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState;Lkotlin/jvm/functions/Function2;Ljava/lang/String;)V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$createMeasurePolicy$1;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Ljava/util/List;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$precompose$1;-><init>(Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState;Ljava/lang/Object;)V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$precompose$1;->dispose()V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState$precompose$1;->premeasure-0kLqBqw(JI)V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState;-><init>(Landroidx/compose/ui/node/LayoutNode;Landroidx/compose/ui/layout/SubcomposeSlotReusePolicy;)V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState;->disposeOrReuseStartingFromIndex(I)V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState;->makeSureStateIsConsistent()V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState;->precompose(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState$precompose$1;
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState;->subcompose(Landroidx/compose/ui/node/LayoutNode;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V
+HSPLandroidx/compose/ui/layout/LayoutNodeSubcompositionsState;->takeNodeFromReusables(Ljava/lang/Object;)Landroidx/compose/ui/node/LayoutNode;
+HSPLandroidx/compose/ui/layout/MeasurePolicy;->maxIntrinsicHeight(Landroidx/compose/ui/node/NodeCoordinator;Ljava/util/List;I)I
+HSPLandroidx/compose/ui/layout/MeasureScope$layout$1;-><init>(IILjava/util/Map;Landroidx/compose/ui/layout/MeasureScope;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/layout/MeasureScope$layout$1;->getAlignmentLines()Ljava/util/Map;
+HSPLandroidx/compose/ui/layout/MeasureScope$layout$1;->getHeight()I
+HSPLandroidx/compose/ui/layout/MeasureScope$layout$1;->getWidth()I
+HSPLandroidx/compose/ui/layout/MeasureScope$layout$1;->placeChildren()V
+HSPLandroidx/compose/ui/layout/MeasureScope;->layout$default(Landroidx/compose/ui/layout/MeasureScope;IILkotlin/jvm/functions/Function1;)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/ui/layout/MeasureScope;->layout(IILjava/util/Map;Lkotlin/jvm/functions/Function1;)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/ui/layout/OnSizeChangedModifier;-><init>(Landroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect$onNewSize$1;)V
+HSPLandroidx/compose/ui/layout/PinnableContainerKt;-><clinit>()V
+HSPLandroidx/compose/ui/layout/Placeable$PlacementScope$Companion;->access$configureForPlacingForAlignment(Landroidx/compose/ui/node/LookaheadCapablePlaceable;)Z
+HSPLandroidx/compose/ui/layout/Placeable$PlacementScope;-><clinit>()V
+HSPLandroidx/compose/ui/layout/Placeable$PlacementScope;->place(Landroidx/compose/ui/layout/Placeable;IIF)V
+HSPLandroidx/compose/ui/layout/Placeable$PlacementScope;->place-70tqf50$default(Landroidx/compose/ui/layout/Placeable$PlacementScope;Landroidx/compose/ui/layout/Placeable;J)V
+HSPLandroidx/compose/ui/layout/Placeable$PlacementScope;->place-70tqf50(Landroidx/compose/ui/layout/Placeable;JF)V
+HSPLandroidx/compose/ui/layout/Placeable$PlacementScope;->placeRelative$default(Landroidx/compose/ui/layout/Placeable$PlacementScope;Landroidx/compose/ui/layout/Placeable;II)V
+HSPLandroidx/compose/ui/layout/Placeable$PlacementScope;->placeRelativeWithLayer$default(Landroidx/compose/ui/layout/Placeable$PlacementScope;Landroidx/compose/ui/layout/Placeable;II)V
+HSPLandroidx/compose/ui/layout/Placeable$PlacementScope;->placeRelativeWithLayer-aW-9-wM$default(Landroidx/compose/ui/layout/Placeable$PlacementScope;Landroidx/compose/ui/layout/Placeable;J)V
+HSPLandroidx/compose/ui/layout/Placeable$PlacementScope;->placeWithLayer$default(Landroidx/compose/ui/layout/Placeable$PlacementScope;Landroidx/compose/ui/layout/Placeable;IILkotlin/jvm/functions/Function1;I)V
+HSPLandroidx/compose/ui/layout/Placeable;-><init>()V
+HSPLandroidx/compose/ui/layout/Placeable;->getMeasuredHeight()I
+HSPLandroidx/compose/ui/layout/Placeable;->getMeasuredWidth()I
+HSPLandroidx/compose/ui/layout/Placeable;->onMeasuredSizeChanged()V
+HSPLandroidx/compose/ui/layout/Placeable;->setMeasuredSize-ozmzZPI(J)V
+HSPLandroidx/compose/ui/layout/Placeable;->setMeasurementConstraints-BRTryo0(J)V
+HSPLandroidx/compose/ui/layout/PlaceableKt;-><clinit>()V
+HSPLandroidx/compose/ui/layout/RootMeasurePolicy$measure$2;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/ui/layout/RootMeasurePolicy$measure$2;->invoke(Landroidx/compose/ui/layout/Placeable$PlacementScope;)V
+HSPLandroidx/compose/ui/layout/RootMeasurePolicy$measure$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/layout/RootMeasurePolicy;-><clinit>()V
+HSPLandroidx/compose/ui/layout/RootMeasurePolicy;-><init>()V
+HSPLandroidx/compose/ui/layout/RootMeasurePolicy;->measure-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Ljava/util/List;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/ui/layout/ScaleFactor;-><clinit>()V
+HSPLandroidx/compose/ui/layout/SubcomposeLayoutKt$SubcomposeLayout$2;-><init>(Ljava/lang/Object;ILjava/lang/Object;II)V
+HSPLandroidx/compose/ui/layout/SubcomposeLayoutKt$SubcomposeLayout$2;->invoke(Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/compose/ui/layout/SubcomposeLayoutKt$SubcomposeLayout$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/layout/SubcomposeLayoutKt$SubcomposeLayout$4;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/ui/layout/SubcomposeLayoutKt$SubcomposeLayout$4;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/ui/layout/SubcomposeLayoutKt$SubcomposeLayout$5$1$invoke$$inlined$onDispose$1;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/ui/layout/SubcomposeLayoutKt$SubcomposeLayout$5$1$invoke$$inlined$onDispose$1;->dispose()V
+HSPLandroidx/compose/ui/layout/SubcomposeLayoutState$setRoot$1;-><init>(Landroidx/compose/ui/layout/SubcomposeLayoutState;I)V
+HSPLandroidx/compose/ui/layout/SubcomposeLayoutState$setRoot$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/layout/SubcomposeLayoutState;-><init>(Landroidx/compose/ui/layout/SubcomposeSlotReusePolicy;)V
+HSPLandroidx/compose/ui/layout/SubcomposeLayoutState;->getState()Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState;
+HSPLandroidx/compose/ui/layout/SubcomposeSlotReusePolicy$SlotIdsSet;-><init>()V
+HSPLandroidx/compose/ui/layout/SubcomposeSlotReusePolicy$SlotIdsSet;->clear()V
+HSPLandroidx/compose/ui/layout/SubcomposeSlotReusePolicy$SlotIdsSet;->contains(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/layout/SubcomposeSlotReusePolicy$SlotIdsSet;->iterator()Ljava/util/Iterator;
+HSPLandroidx/compose/ui/modifier/BackwardsCompatLocalMap;-><init>(Landroidx/compose/ui/modifier/ModifierLocalProvider;)V
+HSPLandroidx/compose/ui/modifier/BackwardsCompatLocalMap;->contains$ui_release(Landroidx/compose/ui/modifier/ModifierLocal;)Z
+HSPLandroidx/compose/ui/modifier/EmptyMap;-><clinit>()V
+HSPLandroidx/compose/ui/modifier/EmptyMap;->contains$ui_release(Landroidx/compose/ui/modifier/ModifierLocal;)Z
+HSPLandroidx/compose/ui/modifier/ModifierLocal;-><init>(Landroidx/compose/material3/ShapesKt$LocalShapes$1;)V
+HSPLandroidx/compose/ui/modifier/ModifierLocalManager;-><init>(Landroidx/compose/ui/node/Owner;)V
+HSPLandroidx/compose/ui/modifier/ModifierLocalModifierNode;->getCurrent(Landroidx/compose/ui/modifier/ProvidableModifierLocal;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/modifier/ModifierLocalModifierNode;->getProvidedValues()Landroidx/tv/material3/TabKt;
+HSPLandroidx/compose/ui/modifier/SingleLocalMap;-><init>(Landroidx/compose/ui/modifier/ModifierLocal;)V
+HSPLandroidx/compose/ui/modifier/SingleLocalMap;->contains$ui_release(Landroidx/compose/ui/modifier/ModifierLocal;)Z
+HSPLandroidx/compose/ui/modifier/SingleLocalMap;->get$ui_release(Landroidx/compose/ui/modifier/ProvidableModifierLocal;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/node/AlignmentLines;-><init>(Landroidx/compose/ui/node/AlignmentLinesOwner;)V
+HSPLandroidx/compose/ui/node/AlignmentLines;->getQueried$ui_release()Z
+HSPLandroidx/compose/ui/node/AlignmentLines;->getRequired$ui_release()Z
+HSPLandroidx/compose/ui/node/AlignmentLines;->onAlignmentsChanged()V
+HSPLandroidx/compose/ui/node/AlignmentLines;->recalculateQueryOwner()V
+HSPLandroidx/compose/ui/node/BackwardsCompatNode;-><init>(Landroidx/compose/ui/Modifier$Element;)V
+HSPLandroidx/compose/ui/node/BackwardsCompatNode;->draw(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V
+HSPLandroidx/compose/ui/node/BackwardsCompatNode;->getProvidedValues()Landroidx/tv/material3/TabKt;
+HSPLandroidx/compose/ui/node/BackwardsCompatNode;->initializeModifier(Z)V
+HSPLandroidx/compose/ui/node/BackwardsCompatNode;->onAttach()V
+HSPLandroidx/compose/ui/node/BackwardsCompatNode;->onGloballyPositioned(Landroidx/compose/ui/node/NodeCoordinator;)V
+HSPLandroidx/compose/ui/node/BackwardsCompatNode;->onMeasureResultChanged()V
+HSPLandroidx/compose/ui/node/BackwardsCompatNode;->onPlaced(Landroidx/compose/ui/node/NodeCoordinator;)V
+HSPLandroidx/compose/ui/node/BackwardsCompatNode;->onRemeasured-ozmzZPI(J)V
+HSPLandroidx/compose/ui/node/BackwardsCompatNode;->unInitializeModifier()V
+HSPLandroidx/compose/ui/node/CanFocusChecker;-><clinit>()V
+HSPLandroidx/compose/ui/node/CanFocusChecker;->setCanFocus(Z)V
+HSPLandroidx/compose/ui/node/ComposeUiNode$Companion;-><clinit>()V
+HSPLandroidx/compose/ui/node/ComposeUiNode;-><clinit>()V
+HSPLandroidx/compose/ui/node/DelegatingNode;-><init>()V
+HSPLandroidx/compose/ui/node/DelegatingNode;->delegate(Landroidx/compose/ui/Modifier$Node;)V
+HSPLandroidx/compose/ui/node/DelegatingNode;->markAsAttached$ui_release()V
+HSPLandroidx/compose/ui/node/DelegatingNode;->markAsDetached$ui_release()V
+HSPLandroidx/compose/ui/node/DelegatingNode;->reset$ui_release()V
+HSPLandroidx/compose/ui/node/DelegatingNode;->runAttachLifecycle$ui_release()V
+HSPLandroidx/compose/ui/node/DelegatingNode;->runDetachLifecycle$ui_release()V
+HSPLandroidx/compose/ui/node/DelegatingNode;->updateCoordinator$ui_release(Landroidx/compose/ui/node/NodeCoordinator;)V
+HSPLandroidx/compose/ui/node/DrawModifierNode;->onMeasureResultChanged()V
+HSPLandroidx/compose/ui/node/HitTestResult;-><init>()V
+HSPLandroidx/compose/ui/node/InnerNodeCoordinator;-><clinit>()V
+HSPLandroidx/compose/ui/node/InnerNodeCoordinator;-><init>(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/InnerNodeCoordinator;->getTail()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/node/InnerNodeCoordinator;->maxIntrinsicHeight(I)I
+HSPLandroidx/compose/ui/node/InnerNodeCoordinator;->measure-BRTryo0(J)Landroidx/compose/ui/layout/Placeable;
+HSPLandroidx/compose/ui/node/InnerNodeCoordinator;->performDraw(Landroidx/compose/ui/graphics/Canvas;)V
+HSPLandroidx/compose/ui/node/InnerNodeCoordinator;->placeAt-f8xVGno(JFLkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/node/IntrinsicsPolicy;-><init>(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/IntrinsicsPolicy;->measurePolicyFromState()Landroidx/compose/ui/layout/MeasurePolicy;
+HSPLandroidx/compose/ui/node/LayerPositionalProperties;-><init>()V
+HSPLandroidx/compose/ui/node/LayoutAwareModifierNode;->onRemeasured-ozmzZPI(J)V
+HSPLandroidx/compose/ui/node/LayoutModifierNode;->maxIntrinsicHeight(Landroidx/compose/ui/layout/IntrinsicMeasureScope;Landroidx/compose/ui/layout/Measurable;I)I
+HSPLandroidx/compose/ui/node/LayoutModifierNode;->maxIntrinsicWidth(Landroidx/compose/ui/layout/IntrinsicMeasureScope;Landroidx/compose/ui/layout/Measurable;I)I
+HSPLandroidx/compose/ui/node/LayoutModifierNodeCoordinator;-><clinit>()V
+HSPLandroidx/compose/ui/node/LayoutModifierNodeCoordinator;-><init>(Landroidx/compose/ui/node/LayoutNode;Landroidx/compose/ui/node/LayoutModifierNode;)V
+HSPLandroidx/compose/ui/node/LayoutModifierNodeCoordinator;->getTail()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/node/LayoutModifierNodeCoordinator;->maxIntrinsicHeight(I)I
+HSPLandroidx/compose/ui/node/LayoutModifierNodeCoordinator;->maxIntrinsicWidth(I)I
+HSPLandroidx/compose/ui/node/LayoutModifierNodeCoordinator;->measure-BRTryo0(J)Landroidx/compose/ui/layout/Placeable;
+HSPLandroidx/compose/ui/node/LayoutModifierNodeCoordinator;->performDraw(Landroidx/compose/ui/graphics/Canvas;)V
+HSPLandroidx/compose/ui/node/LayoutModifierNodeCoordinator;->placeAt-f8xVGno(JFLkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/node/LayoutNode$$ExternalSyntheticLambda0;-><init>(I)V
+HSPLandroidx/compose/ui/node/LayoutNode$$ExternalSyntheticLambda0;->compare(Ljava/lang/Object;Ljava/lang/Object;)I
+HSPLandroidx/compose/ui/node/LayoutNode$Companion$ErrorMeasurePolicy$1;-><init>()V
+HSPLandroidx/compose/ui/node/LayoutNode$NoIntrinsicsMeasurePolicy;-><init>(Ljava/lang/String;)V
+HSPLandroidx/compose/ui/node/LayoutNode$WhenMappings;-><clinit>()V
+HSPLandroidx/compose/ui/node/LayoutNode$_foldedChildren$1;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/ui/node/LayoutNode$_foldedChildren$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/ui/node/LayoutNode$_foldedChildren$1;->invoke()V
+HSPLandroidx/compose/ui/node/LayoutNode;-><clinit>()V
+HSPLandroidx/compose/ui/node/LayoutNode;-><init>(IZ)V
+HSPLandroidx/compose/ui/node/LayoutNode;-><init>(ZI)V
+HSPLandroidx/compose/ui/node/LayoutNode;->attach$ui_release(Landroidx/compose/ui/node/Owner;)V
+HSPLandroidx/compose/ui/node/LayoutNode;->clearSubtreeIntrinsicsUsage$ui_release()V
+HSPLandroidx/compose/ui/node/LayoutNode;->clearSubtreePlacementIntrinsicsUsage()V
+HSPLandroidx/compose/ui/node/LayoutNode;->draw$ui_release(Landroidx/compose/ui/graphics/Canvas;)V
+HSPLandroidx/compose/ui/node/LayoutNode;->forceRemeasure()V
+HSPLandroidx/compose/ui/node/LayoutNode;->getChildMeasurables$ui_release()Ljava/util/List;
+HSPLandroidx/compose/ui/node/LayoutNode;->getChildren$ui_release()Ljava/util/List;
+HSPLandroidx/compose/ui/node/LayoutNode;->getFoldedChildren$ui_release()Ljava/util/List;
+HSPLandroidx/compose/ui/node/LayoutNode;->getMeasuredByParent$ui_release$enumunboxing$()I
+HSPLandroidx/compose/ui/node/LayoutNode;->getParent$ui_release()Landroidx/compose/ui/node/LayoutNode;
+HSPLandroidx/compose/ui/node/LayoutNode;->getPlaceOrder$ui_release()I
+HSPLandroidx/compose/ui/node/LayoutNode;->getZSortedChildren()Landroidx/compose/runtime/collection/MutableVector;
+HSPLandroidx/compose/ui/node/LayoutNode;->get_children$ui_release()Landroidx/compose/runtime/collection/MutableVector;
+HSPLandroidx/compose/ui/node/LayoutNode;->insertAt$ui_release(ILandroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/LayoutNode;->invalidateLayer$ui_release()V
+HSPLandroidx/compose/ui/node/LayoutNode;->invalidateLayers$ui_release()V
+HSPLandroidx/compose/ui/node/LayoutNode;->invalidateMeasurements$ui_release()V
+HSPLandroidx/compose/ui/node/LayoutNode;->invalidateSemantics$ui_release()V
+HSPLandroidx/compose/ui/node/LayoutNode;->invalidateUnfoldedVirtualChildren()V
+HSPLandroidx/compose/ui/node/LayoutNode;->isAttached()Z
+HSPLandroidx/compose/ui/node/LayoutNode;->isPlaced()Z
+HSPLandroidx/compose/ui/node/LayoutNode;->isValidOwnerScope()Z
+HSPLandroidx/compose/ui/node/LayoutNode;->move$ui_release(III)V
+HSPLandroidx/compose/ui/node/LayoutNode;->onZSortedChildrenInvalidated$ui_release()V
+HSPLandroidx/compose/ui/node/LayoutNode;->requestRelayout$ui_release(Z)V
+HSPLandroidx/compose/ui/node/LayoutNode;->requestRemeasure$ui_release$default(Landroidx/compose/ui/node/LayoutNode;ZI)V
+HSPLandroidx/compose/ui/node/LayoutNode;->rescheduleRemeasureOrRelayout$ui_release(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/LayoutNode;->resetSubtreeIntrinsicsUsage$ui_release()V
+HSPLandroidx/compose/ui/node/LayoutNode;->setCompositionLocalMap(Landroidx/compose/runtime/CompositionLocalMap;)V
+HSPLandroidx/compose/ui/node/LayoutNode;->setDensity(Landroidx/compose/ui/unit/Density;)V
+HSPLandroidx/compose/ui/node/LayoutNode;->setLookaheadRoot(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/LayoutNode;->setMeasurePolicy(Landroidx/compose/ui/layout/MeasurePolicy;)V
+HSPLandroidx/compose/ui/node/LayoutNode;->setModifier(Landroidx/compose/ui/Modifier;)V
+HSPLandroidx/compose/ui/node/LayoutNode;->setViewConfiguration(Landroidx/compose/ui/platform/ViewConfiguration;)V
+HSPLandroidx/compose/ui/node/LayoutNode;->updateChildrenIfDirty$ui_release()V
+HSPLandroidx/compose/ui/node/LayoutNodeDrawScope;-><init>()V
+HSPLandroidx/compose/ui/node/LayoutNodeDrawScope;->drawContent()V
+HSPLandroidx/compose/ui/node/LayoutNodeDrawScope;->drawDirect-x_KDEd0$ui_release(Landroidx/compose/ui/graphics/Canvas;JLandroidx/compose/ui/node/NodeCoordinator;Landroidx/compose/ui/node/DrawModifierNode;)V
+HSPLandroidx/compose/ui/node/LayoutNodeDrawScope;->drawImage-AZ2fEMs(Landroidx/compose/ui/graphics/ImageBitmap;JJJJFLkotlin/ResultKt;Landroidx/compose/ui/graphics/BlendModeColorFilter;II)V
+HSPLandroidx/compose/ui/node/LayoutNodeDrawScope;->drawRect-AsUm42w(Landroidx/compose/ui/graphics/Brush;JJFLkotlin/ResultKt;Landroidx/compose/ui/graphics/BlendModeColorFilter;I)V
+HSPLandroidx/compose/ui/node/LayoutNodeDrawScope;->drawRect-n-J9OG0(JJJFLkotlin/ResultKt;Landroidx/compose/ui/graphics/BlendModeColorFilter;I)V
+HSPLandroidx/compose/ui/node/LayoutNodeDrawScope;->drawRoundRect-u-Aw5IA(JJJJLkotlin/ResultKt;FLandroidx/compose/ui/graphics/BlendModeColorFilter;I)V
+HSPLandroidx/compose/ui/node/LayoutNodeDrawScope;->getCenter-F1C5BW0()J
+HSPLandroidx/compose/ui/node/LayoutNodeDrawScope;->getDensity()F
+HSPLandroidx/compose/ui/node/LayoutNodeDrawScope;->getFontScale()F
+HSPLandroidx/compose/ui/node/LayoutNodeDrawScope;->getLayoutDirection()Landroidx/compose/ui/unit/LayoutDirection;
+HSPLandroidx/compose/ui/node/LayoutNodeDrawScope;->getSize-NH-jbRc()J
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate$placeOuterCoordinator$1;-><init>(Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/node/LayoutNodeLayoutDelegate;JF)V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate$placeOuterCoordinator$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;-><init>(Landroidx/compose/ui/node/LayoutNodeLayoutDelegate;)V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->forEachChildAlignmentLinesOwner(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->getAlignmentLines()Landroidx/compose/ui/node/LookaheadAlignmentLines;
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->getChildDelegates$ui_release()Ljava/util/List;
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->getInnerCoordinator()Landroidx/compose/ui/node/InnerNodeCoordinator;
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->getMeasuredWidth()I
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->getParentAlignmentLinesOwner()Landroidx/compose/ui/node/AlignmentLinesOwner;
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->getParentData()Ljava/lang/Object;
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->layoutChildren()V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->markNodeAndSubtreeAsPlaced()V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->markSubtreeAsNotPlaced()V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->maxIntrinsicHeight(I)I
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->maxIntrinsicWidth(I)I
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->measure-BRTryo0(J)Landroidx/compose/ui/layout/Placeable;
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->notifyChildrenUsingCoordinatesWhilePlacing()V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->onIntrinsicsQueried()V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->onNodePlaced$ui_release()V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->placeAt-f8xVGno(JFLkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->placeOuterCoordinator-f8xVGno(JFLkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->remeasure-BRTryo0(J)Z
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;->replace()V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$performMeasure$2;-><init>(Landroidx/compose/ui/node/LayoutNodeLayoutDelegate;JI)V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$performMeasure$2;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate$performMeasure$2;->invoke()V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate;-><init>(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate;->getOuterCoordinator()Landroidx/compose/ui/node/NodeCoordinator;
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate;->isOutMostLookaheadRoot(Landroidx/compose/ui/node/LayoutNode;)Z
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate;->setCoordinatesAccessedDuringModifierPlacement(Z)V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate;->setCoordinatesAccessedDuringPlacement(Z)V
+HSPLandroidx/compose/ui/node/LayoutNodeLayoutDelegate;->updateParentData()V
+HSPLandroidx/compose/ui/node/LookaheadAlignmentLines;-><init>(Landroidx/compose/ui/node/AlignmentLinesOwner;I)V
+HSPLandroidx/compose/ui/node/LookaheadCapablePlaceable;->invalidateAlignmentLinesFromPositionChange(Landroidx/compose/ui/node/NodeCoordinator;)V
+HSPLandroidx/compose/ui/node/LookaheadCapablePlaceable;->isLookingAhead()Z
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;-><init>(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->dispatchOnPositionedCallbacks(Z)V
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->doLookaheadRemeasure-sdFAvZA(Landroidx/compose/ui/node/LayoutNode;Landroidx/compose/ui/unit/Constraints;)Z
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->doRemeasure-sdFAvZA(Landroidx/compose/ui/node/LayoutNode;Landroidx/compose/ui/unit/Constraints;)Z
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->forceMeasureTheSubtree(Landroidx/compose/ui/node/LayoutNode;Z)V
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->measureAndLayout(Lkotlin/jvm/functions/Function0;)Z
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->measureAndLayout-0kLqBqw(Landroidx/compose/ui/node/LayoutNode;J)V
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->measureOnly()V
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->recurseRemeasure(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->remeasureAndRelayoutIfNeeded(Landroidx/compose/ui/node/LayoutNode;Z)Z
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->remeasureOnly(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->requestRelayout(Landroidx/compose/ui/node/LayoutNode;Z)Z
+HSPLandroidx/compose/ui/node/MeasureAndLayoutDelegate;->updateRootConstraints-BRTryo0(J)V
+HSPLandroidx/compose/ui/node/NodeChain$Differ;-><init>(Landroidx/compose/ui/node/NodeChain;Landroidx/compose/ui/Modifier$Node;ILandroidx/compose/runtime/collection/MutableVector;Landroidx/compose/runtime/collection/MutableVector;Z)V
+HSPLandroidx/compose/ui/node/NodeChain$Differ;->areItemsTheSame(II)Z
+HSPLandroidx/compose/ui/node/NodeChain;-><init>(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/NodeChain;->access$propagateCoordinator(Landroidx/compose/ui/node/NodeChain;Landroidx/compose/ui/Modifier$Node;Landroidx/compose/ui/node/NodeCoordinator;)V
+HSPLandroidx/compose/ui/node/NodeChain;->createAndInsertNodeAsChild(Landroidx/compose/ui/Modifier$Element;Landroidx/compose/ui/Modifier$Node;)Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/node/NodeChain;->detachAndRemoveNode(Landroidx/compose/ui/Modifier$Node;)Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/node/NodeChain;->has-H91voCI$ui_release(I)Z
+HSPLandroidx/compose/ui/node/NodeChain;->runAttachLifecycle()V
+HSPLandroidx/compose/ui/node/NodeChain;->syncCoordinators()V
+HSPLandroidx/compose/ui/node/NodeChain;->updateNode(Landroidx/compose/ui/Modifier$Element;Landroidx/compose/ui/Modifier$Element;Landroidx/compose/ui/Modifier$Node;)V
+HSPLandroidx/compose/ui/node/NodeChainKt;-><clinit>()V
+HSPLandroidx/compose/ui/node/NodeChainKt;->actionForModifiers(Landroidx/compose/ui/Modifier$Element;Landroidx/compose/ui/Modifier$Element;)I
+HSPLandroidx/compose/ui/node/NodeCoordinator$invoke$1;-><init>(Ljava/lang/Object;ILjava/lang/Object;)V
+HSPLandroidx/compose/ui/node/NodeCoordinator$invoke$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/ui/node/NodeCoordinator$invoke$1;->invoke()V
+HSPLandroidx/compose/ui/node/NodeCoordinator;-><clinit>()V
+HSPLandroidx/compose/ui/node/NodeCoordinator;-><init>(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/NodeCoordinator;->ancestorToLocal(Landroidx/compose/ui/node/NodeCoordinator;Landroidx/compose/ui/geometry/MutableRect;Z)V
+HSPLandroidx/compose/ui/node/NodeCoordinator;->draw(Landroidx/compose/ui/graphics/Canvas;)V
+HSPLandroidx/compose/ui/node/NodeCoordinator;->drawContainedDrawModifiers(Landroidx/compose/ui/graphics/Canvas;)V
+HSPLandroidx/compose/ui/node/NodeCoordinator;->findCommonAncestor$ui_release(Landroidx/compose/ui/node/NodeCoordinator;)Landroidx/compose/ui/node/NodeCoordinator;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->getAlignmentLinesOwner()Landroidx/compose/ui/node/AlignmentLinesOwner;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->getCoordinates()Landroidx/compose/ui/layout/LayoutCoordinates;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->getDensity()F
+HSPLandroidx/compose/ui/node/NodeCoordinator;->getFontScale()F
+HSPLandroidx/compose/ui/node/NodeCoordinator;->getLayoutDirection()Landroidx/compose/ui/unit/LayoutDirection;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->getLayoutNode()Landroidx/compose/ui/node/LayoutNode;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->getMeasureResult$ui_release()Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->getParent()Landroidx/compose/ui/node/LookaheadCapablePlaceable;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->getParentData()Ljava/lang/Object;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->getParentLayoutCoordinates()Landroidx/compose/ui/layout/LayoutCoordinates;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->getSize-YbymL2g()J
+HSPLandroidx/compose/ui/node/NodeCoordinator;->head-H91voCI(I)Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->headNode(Z)Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->invalidateLayer()V
+HSPLandroidx/compose/ui/node/NodeCoordinator;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->isAttached()Z
+HSPLandroidx/compose/ui/node/NodeCoordinator;->isValidOwnerScope()Z
+HSPLandroidx/compose/ui/node/NodeCoordinator;->localBoundingBoxOf(Landroidx/compose/ui/layout/LayoutCoordinates;Z)Landroidx/compose/ui/geometry/Rect;
+HSPLandroidx/compose/ui/node/NodeCoordinator;->localToRoot-MK-Hz9U(J)J
+HSPLandroidx/compose/ui/node/NodeCoordinator;->onCoordinatesUsed$ui_release()V
+HSPLandroidx/compose/ui/node/NodeCoordinator;->onMeasured()V
+HSPLandroidx/compose/ui/node/NodeCoordinator;->onPlaced()V
+HSPLandroidx/compose/ui/node/NodeCoordinator;->placeSelf-f8xVGno(JFLkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/node/NodeCoordinator;->rectInParent$ui_release(Landroidx/compose/ui/geometry/MutableRect;ZZ)V
+HSPLandroidx/compose/ui/node/NodeCoordinator;->setMeasureResult$ui_release(Landroidx/compose/ui/layout/MeasureResult;)V
+HSPLandroidx/compose/ui/node/NodeCoordinator;->toParentPosition-MK-Hz9U(J)J
+HSPLandroidx/compose/ui/node/NodeCoordinator;->updateLayerParameters(Z)V
+HSPLandroidx/compose/ui/node/NodeMeasuringIntrinsics$IntrinsicMinMax;-><clinit>()V
+HSPLandroidx/compose/ui/node/NodeMeasuringIntrinsics$IntrinsicMinMax;-><init>(ILjava/lang/String;)V
+HSPLandroidx/compose/ui/node/NodeMeasuringIntrinsics$IntrinsicWidthHeight;-><clinit>()V
+HSPLandroidx/compose/ui/node/NodeMeasuringIntrinsics$IntrinsicWidthHeight;-><init>(ILjava/lang/String;)V
+HSPLandroidx/compose/ui/node/ObserverNodeOwnerScope;-><init>(Landroidx/compose/ui/node/ObserverModifierNode;)V
+HSPLandroidx/compose/ui/node/ObserverNodeOwnerScope;->isValidOwnerScope()Z
+HSPLandroidx/compose/ui/node/OnPositionedDispatcher$Companion$DepthComparator;-><clinit>()V
+HSPLandroidx/compose/ui/node/OnPositionedDispatcher$Companion$DepthComparator;->compare(Ljava/lang/Object;Ljava/lang/Object;)I
+HSPLandroidx/compose/ui/node/OnPositionedDispatcher;-><init>()V
+HSPLandroidx/compose/ui/node/OnPositionedDispatcher;->dispatchHierarchy(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/Owner;->measureAndLayout$default(Landroidx/compose/ui/node/Owner;)V
+HSPLandroidx/compose/ui/node/OwnerSnapshotObserver;-><init>(Landroidx/compose/ui/platform/AndroidComposeView$focusOwner$1;)V
+HSPLandroidx/compose/ui/node/OwnerSnapshotObserver;->observeLayoutModifierSnapshotReads$ui_release(Landroidx/compose/ui/node/LayoutNode;ZLkotlin/jvm/functions/Function0;)V
+HSPLandroidx/compose/ui/node/OwnerSnapshotObserver;->observeReads$ui_release(Landroidx/compose/ui/node/OwnerScope;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)V
+HSPLandroidx/compose/ui/node/TailModifierNode;-><init>()V
+HSPLandroidx/compose/ui/node/TailModifierNode;->onAttach()V
+HSPLandroidx/compose/ui/node/TailModifierNode;->onDetach()V
+HSPLandroidx/compose/ui/node/UiApplier;-><init>(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/node/UiApplier;->down(Ljava/lang/Object;)V
+HSPLandroidx/compose/ui/node/UiApplier;->getCurrent()Ljava/lang/Object;
+HSPLandroidx/compose/ui/node/UiApplier;->insertBottomUp(ILjava/lang/Object;)V
+HSPLandroidx/compose/ui/node/UiApplier;->insertTopDown(ILjava/lang/Object;)V
+HSPLandroidx/compose/ui/node/UiApplier;->onEndChanges()V
+HSPLandroidx/compose/ui/node/UiApplier;->up()V
+HSPLandroidx/compose/ui/platform/AbstractComposeView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->addView(Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->addView(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->checkAddView()V
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->ensureCompositionCreated()V
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->isAlive(Landroidx/compose/runtime/CompositionContext;)Z
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->onAttachedToWindow()V
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->onLayout(ZIIII)V
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->onMeasure(II)V
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->onRtlPropertiesChanged(I)V
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->resolveParentCompositionContext()Landroidx/compose/runtime/CompositionContext;
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->setParentCompositionContext(Landroidx/compose/runtime/CompositionContext;)V
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->setParentContext(Landroidx/compose/runtime/CompositionContext;)V
+HSPLandroidx/compose/ui/platform/AbstractComposeView;->setPreviousAttachedWindowToken(Landroid/os/IBinder;)V
+HSPLandroidx/compose/ui/platform/AndroidAccessibilityManager;-><init>(Landroid/content/Context;)V
+HSPLandroidx/compose/ui/platform/AndroidClipboardManager;-><init>(Landroid/content/Context;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticApiModelOutline0;->m(Landroid/content/res/Configuration;)I
+HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticApiModelOutline0;->m(Landroid/view/View;Landroid/view/translation/ViewTranslationCallback;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda1;-><init>(Landroidx/compose/ui/platform/AndroidComposeView;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda1;->onGlobalLayout()V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda2;-><init>(Landroidx/compose/ui/platform/AndroidComposeView;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda3;-><init>(Landroidx/compose/ui/platform/AndroidComposeView;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda3;->onTouchModeChanged(Z)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$ViewTreeOwners;-><init>(Landroidx/lifecycle/LifecycleOwner;Landroidx/savedstate/SavedStateRegistryOwner;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$focusOwner$1;-><init>(Landroidx/compose/ui/platform/AndroidComposeView;I)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$focusOwner$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/AndroidComposeView$focusOwner$1;->invoke(Lkotlin/jvm/functions/Function0;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$pointerIconService$1;-><init>(Landroidx/compose/ui/platform/AndroidComposeView;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$viewTreeOwners$2;-><init>(Landroidx/compose/ui/platform/AndroidComposeView;I)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView$viewTreeOwners$2;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;-><clinit>()V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;-><init>(Landroid/content/Context;Lkotlin/coroutines/CoroutineContext;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->access$get_viewTreeOwners(Landroidx/compose/ui/platform/AndroidComposeView;)Landroidx/compose/ui/platform/AndroidComposeView$ViewTreeOwners;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->convertMeasureSpec-I7RO_PI(I)J
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->dispatchDraw(Landroid/graphics/Canvas;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->dispatchKeyEvent(Landroid/view/KeyEvent;)Z
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->dispatchKeyEventPreIme(Landroid/view/KeyEvent;)Z
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->forceMeasureTheSubtree(Landroidx/compose/ui/node/LayoutNode;Z)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getAccessibilityManager()Landroidx/compose/ui/platform/AccessibilityManager;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getAccessibilityManager()Landroidx/compose/ui/platform/AndroidAccessibilityManager;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getAutofill()Landroidx/compose/ui/autofill/Autofill;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getAutofillTree()Landroidx/compose/ui/autofill/AutofillTree;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getClipboardManager()Landroidx/compose/ui/platform/AndroidClipboardManager;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getClipboardManager()Landroidx/compose/ui/platform/ClipboardManager;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getCoroutineContext()Lkotlin/coroutines/CoroutineContext;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getDensity()Landroidx/compose/ui/unit/Density;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getFocusOwner()Landroidx/compose/ui/focus/FocusOwner;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getFontFamilyResolver()Landroidx/compose/ui/text/font/FontFamily$Resolver;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getFontLoader()Landroidx/compose/ui/text/font/Font$ResourceLoader;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getHapticFeedBack()Landroidx/compose/ui/hapticfeedback/HapticFeedback;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getInputModeManager()Landroidx/compose/ui/input/InputModeManager;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getLayoutDirection()Landroidx/compose/ui/unit/LayoutDirection;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getPointerIconService()Landroidx/compose/ui/input/pointer/PointerIconService;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getRoot()Landroidx/compose/ui/node/LayoutNode;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getSemanticsOwner()Landroidx/compose/ui/semantics/SemanticsOwner;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getSharedDrawScope()Landroidx/compose/ui/node/LayoutNodeDrawScope;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getShowLayoutBounds()Z
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getSnapshotObserver()Landroidx/compose/ui/node/OwnerSnapshotObserver;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getSoftwareKeyboardController()Landroidx/compose/ui/platform/SoftwareKeyboardController;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getTextInputService()Landroidx/compose/ui/text/input/TextInputService;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getTextToolbar()Landroidx/compose/ui/platform/TextToolbar;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getView()Landroid/view/View;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getViewConfiguration()Landroidx/compose/ui/platform/ViewConfiguration;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getViewTreeOwners()Landroidx/compose/ui/platform/AndroidComposeView$ViewTreeOwners;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->getWindowInfo()Landroidx/compose/ui/platform/WindowInfo;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->get_viewTreeOwners()Landroidx/compose/ui/platform/AndroidComposeView$ViewTreeOwners;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->invalidateLayers(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->invalidateLayoutNodeMeasurement(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->measureAndLayout(Z)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->measureAndLayout-0kLqBqw(Landroidx/compose/ui/node/LayoutNode;J)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->notifyLayerIsDirty$ui_release(Landroidx/compose/ui/node/OwnedLayer;Z)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onAttachedToWindow()V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onCheckIsTextEditor()Z
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onCreateInputConnection(Landroid/view/inputmethod/EditorInfo;)Landroid/view/inputmethod/InputConnection;
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onDraw(Landroid/graphics/Canvas;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onEndApplyChanges()V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onFocusChanged(ZILandroid/graphics/Rect;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onLayout(ZIIII)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onLayoutChange(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onMeasure(II)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onRequestRelayout(Landroidx/compose/ui/node/LayoutNode;ZZ)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onResume(Landroidx/lifecycle/LifecycleOwner;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onRtlPropertiesChanged(I)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onSemanticsChange()V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->onWindowFocusChanged(Z)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->pack-ZIaKswc(II)J
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->recycle$ui_release(Landroidx/compose/ui/node/OwnedLayer;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->scheduleMeasureAndLayout(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->setConfigurationChangeObserver(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->setLayoutDirection(Landroidx/compose/ui/unit/LayoutDirection;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->setOnViewTreeOwnersAvailable(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->setShowLayoutBounds(Z)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->set_viewTreeOwners(Landroidx/compose/ui/platform/AndroidComposeView$ViewTreeOwners;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeView;->updatePositionCacheAndDispatch()V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$$ExternalSyntheticLambda1;-><init>(Landroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$$ExternalSyntheticLambda2;-><init>(Landroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$1;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$1;->onViewAttachedToWindow(Landroid/view/View;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$MyNodeProvider;-><init>(Landroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$SemanticsNodeCopy;-><init>(Landroidx/compose/ui/semantics/SemanticsNode;Ljava/util/Map;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$boundsUpdatesEventLoop$1;-><init>(Landroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat;-><clinit>()V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat;-><init>(Landroidx/compose/ui/platform/AndroidComposeView;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat;->boundsUpdatesEventLoop(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat;->getAccessibilityNodeProvider(Landroid/view/View;)Landroidx/core/view/accessibility/AccessibilityNodeProviderCompat;
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat;->isEnabledForAccessibility()Z
+HSPLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat;->onStart(Landroidx/lifecycle/LifecycleOwner;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewForceDarkModeQ;-><clinit>()V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewForceDarkModeQ;->disallowForceDark(Landroid/view/View;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewTranslationCallbackS;-><clinit>()V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewTranslationCallbackS;->setViewTranslationCallback(Landroid/view/View;Landroid/view/translation/ViewTranslationCallback;)V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewVerificationHelperMethodsO;-><clinit>()V
+HSPLandroidx/compose/ui/platform/AndroidComposeViewVerificationHelperMethodsO;->focusable(Landroid/view/View;IZ)V
+HSPLandroidx/compose/ui/platform/AndroidCompositionLocals_androidKt$obtainImageVectorCache$callbacks$1$1;-><init>(Landroid/content/res/Configuration;Landroidx/compose/ui/res/ImageVectorCache;)V
+HSPLandroidx/compose/ui/platform/AndroidCompositionLocals_androidKt;-><clinit>()V
+HSPLandroidx/compose/ui/platform/AndroidCompositionLocals_androidKt;->ProvideAndroidCompositionLocals(Landroidx/compose/ui/platform/AndroidComposeView;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/compose/ui/platform/AndroidUiDispatcher$dispatchCallback$1;-><init>(Landroidx/compose/ui/platform/AndroidUiDispatcher;)V
+HSPLandroidx/compose/ui/platform/AndroidUiDispatcher$dispatchCallback$1;->doFrame(J)V
+HSPLandroidx/compose/ui/platform/AndroidUiDispatcher$dispatchCallback$1;->run()V
+HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;-><clinit>()V
+HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;-><init>(Landroid/view/Choreographer;Landroid/os/Handler;)V
+HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$performTrampolineDispatch(Landroidx/compose/ui/platform/AndroidUiDispatcher;)V
+HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->dispatch(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
+HSPLandroidx/compose/ui/platform/AndroidUiFrameClock$withFrameNanos$2$callback$1;-><init>(Lkotlinx/coroutines/CancellableContinuationImpl;Landroidx/compose/ui/platform/AndroidUiFrameClock;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/platform/AndroidUiFrameClock$withFrameNanos$2$callback$1;->doFrame(J)V
+HSPLandroidx/compose/ui/platform/AndroidUiFrameClock;-><init>(Landroid/view/Choreographer;Landroidx/compose/ui/platform/AndroidUiDispatcher;)V
+HSPLandroidx/compose/ui/platform/AndroidUiFrameClock;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/AndroidUiFrameClock;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
+HSPLandroidx/compose/ui/platform/AndroidUiFrameClock;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext;
+HSPLandroidx/compose/ui/platform/AndroidUiFrameClock;->withFrameNanos(Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/AndroidViewConfiguration;-><init>(Landroid/view/ViewConfiguration;)V
+HSPLandroidx/compose/ui/platform/CalculateMatrixToWindowApi29;-><init>()V
+HSPLandroidx/compose/ui/platform/ComposableSingletons$Wrapper_androidKt;-><clinit>()V
+HSPLandroidx/compose/ui/platform/ComposeView;-><init>(Landroid/content/Context;)V
+HSPLandroidx/compose/ui/platform/ComposeView;->Content(Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/compose/ui/platform/ComposeView;->getShouldCreateCompositionOnAttachedToWindow()Z
+HSPLandroidx/compose/ui/platform/ComposeView;->setContent(Lkotlin/jvm/functions/Function2;)V
+HSPLandroidx/compose/ui/platform/CompositionLocalsKt;-><clinit>()V
+HSPLandroidx/compose/ui/platform/CompositionLocalsKt;->ProvideCommonCompositionLocals(Landroidx/compose/ui/node/Owner;Landroidx/compose/ui/platform/UriHandler;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/compose/ui/platform/DisposableSaveableStateRegistry;-><init>(Landroidx/compose/runtime/saveable/SaveableStateRegistryImpl;Landroidx/compose/ui/platform/DisposableSaveableStateRegistry_androidKt$DisposableSaveableStateRegistry$1;)V
+HSPLandroidx/compose/ui/platform/DisposableSaveableStateRegistry;->canBeSaved(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/platform/DisposableSaveableStateRegistry;->consumeRestored(Ljava/lang/String;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/DisposableSaveableStateRegistry;->registerProvider(Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Landroidx/compose/runtime/saveable/SaveableStateRegistryImpl$registerProvider$3;
+HSPLandroidx/compose/ui/platform/DisposableSaveableStateRegistry_androidKt$DisposableSaveableStateRegistry$1;-><init>(ZLandroidx/savedstate/SavedStateRegistry;Ljava/lang/String;)V
+HSPLandroidx/compose/ui/platform/GlobalSnapshotManager$ensureStarted$1;-><init>(Lkotlinx/coroutines/channels/Channel;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/ui/platform/GlobalSnapshotManager$ensureStarted$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/ui/platform/GlobalSnapshotManager$ensureStarted$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/GlobalSnapshotManager;-><clinit>()V
+HSPLandroidx/compose/ui/platform/InspectableModifier;-><init>()V
+HSPLandroidx/compose/ui/platform/LayerMatrixCache;-><init>(Landroidx/compose/ui/text/SaversKt$ColorSaver$1;)V
+HSPLandroidx/compose/ui/platform/LayerMatrixCache;->calculateMatrix-GrdbGEg(Ljava/lang/Object;)[F
+HSPLandroidx/compose/ui/platform/LayerMatrixCache;->invalidate()V
+HSPLandroidx/compose/ui/platform/MotionDurationScaleImpl;-><init>()V
+HSPLandroidx/compose/ui/platform/MotionDurationScaleImpl;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/MotionDurationScaleImpl;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
+HSPLandroidx/compose/ui/platform/MotionDurationScaleImpl;->getScaleFactor()F
+HSPLandroidx/compose/ui/platform/MotionDurationScaleImpl;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext;
+HSPLandroidx/compose/ui/platform/OutlineResolver;-><init>(Landroidx/compose/ui/unit/Density;)V
+HSPLandroidx/compose/ui/platform/OutlineResolver;->getOutline()Landroid/graphics/Outline;
+HSPLandroidx/compose/ui/platform/OutlineResolver;->update(Landroidx/compose/ui/graphics/Shape;FZFLandroidx/compose/ui/unit/LayoutDirection;Landroidx/compose/ui/unit/Density;)Z
+HSPLandroidx/compose/ui/platform/OutlineResolver;->updateCache()V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;-><init>()V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->discardDisplayList()V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->drawInto(Landroid/graphics/Canvas;)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->getAlpha()F
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->getClipToOutline()Z
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->getElevation()F
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->getHasDisplayList()Z
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->getHeight()I
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->getLeft()I
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->getMatrix(Landroid/graphics/Matrix;)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->getTop()I
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->getWidth()I
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->offsetLeftAndRight(I)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->offsetTopAndBottom(I)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setAlpha(F)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setAmbientShadowColor(I)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setCameraDistance(F)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setClipToBounds(Z)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setClipToOutline(Z)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setCompositingStrategy-aDBOjCE(I)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setElevation(F)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setHasOverlappingRendering()Z
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setOutline(Landroid/graphics/Outline;)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setPivotX(F)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setPivotY(F)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setPosition(IIII)Z
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setRenderEffect()V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setRotationX(F)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setRotationY(F)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setRotationZ(F)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setScaleX(F)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setScaleY(F)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setSpotShadowColor(I)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setTranslationX(F)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29;->setTranslationY(F)V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29VerificationHelper;-><clinit>()V
+HSPLandroidx/compose/ui/platform/RenderNodeApi29VerificationHelper;->setRenderEffect(Landroid/graphics/RenderNode;Landroidx/compose/ui/graphics/RenderEffect;)V
+HSPLandroidx/compose/ui/platform/RenderNodeLayer;-><init>(Landroidx/compose/ui/platform/AndroidComposeView;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/node/LayoutNode$_foldedChildren$1;)V
+HSPLandroidx/compose/ui/platform/RenderNodeLayer;->destroy()V
+HSPLandroidx/compose/ui/platform/RenderNodeLayer;->drawLayer(Landroidx/compose/ui/graphics/Canvas;)V
+HSPLandroidx/compose/ui/platform/RenderNodeLayer;->invalidate()V
+HSPLandroidx/compose/ui/platform/RenderNodeLayer;->mapBounds(Landroidx/compose/ui/geometry/MutableRect;Z)V
+HSPLandroidx/compose/ui/platform/RenderNodeLayer;->mapOffset-8S9VItk(JZ)J
+HSPLandroidx/compose/ui/platform/RenderNodeLayer;->move--gyyYBs(J)V
+HSPLandroidx/compose/ui/platform/RenderNodeLayer;->resize-ozmzZPI(J)V
+HSPLandroidx/compose/ui/platform/RenderNodeLayer;->reuseLayer(Landroidx/compose/ui/node/LayoutNode$_foldedChildren$1;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/platform/RenderNodeLayer;->setDirty(Z)V
+HSPLandroidx/compose/ui/platform/RenderNodeLayer;->updateDisplayList()V
+HSPLandroidx/compose/ui/platform/RenderNodeLayer;->updateLayerProperties-dDxr-wY(FFFFFFFFFFJLandroidx/compose/ui/graphics/Shape;ZJJILandroidx/compose/ui/unit/LayoutDirection;Landroidx/compose/ui/unit/Density;)V
+HSPLandroidx/compose/ui/platform/ViewLayer;-><clinit>()V
+HSPLandroidx/compose/ui/platform/WeakCache;-><init>()V
+HSPLandroidx/compose/ui/platform/WeakCache;-><init>(I)V
+HSPLandroidx/compose/ui/platform/WeakCache;-><init>(Landroidx/compose/ui/node/InnerNodeCoordinator;)V
+HSPLandroidx/compose/ui/platform/WeakCache;-><init>(Ljava/lang/Object;ILjava/lang/Object;)V
+HSPLandroidx/compose/ui/platform/WeakCache;->add(Landroidx/compose/ui/node/LayoutNode;Z)V
+HSPLandroidx/compose/ui/platform/WeakCache;->clearWeakReferences()V
+HSPLandroidx/compose/ui/platform/WeakCache;->get$1()Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/WeakCache;->set(Ljava/lang/Object;)V
+HSPLandroidx/compose/ui/platform/WindowInfoImpl;-><clinit>()V
+HSPLandroidx/compose/ui/platform/WindowInfoImpl;-><init>()V
+HSPLandroidx/compose/ui/platform/WindowRecomposerPolicy$createAndInstallWindowRecomposer$unsetJob$1;-><init>(Landroidx/compose/runtime/Recomposer;Landroid/view/View;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/ui/platform/WindowRecomposerPolicy$createAndInstallWindowRecomposer$unsetJob$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/ui/platform/WindowRecomposerPolicy$createAndInstallWindowRecomposer$unsetJob$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/WindowRecomposerPolicy;-><clinit>()V
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$1;-><init>(Landroid/view/View;Landroidx/compose/runtime/Recomposer;)V
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$1;->onViewAttachedToWindow(Landroid/view/View;)V
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2$WhenMappings;-><clinit>()V
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2$onStateChanged$1$1$1;-><init>(Lkotlinx/coroutines/flow/StateFlow;Landroidx/compose/ui/platform/MotionDurationScaleImpl;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2$onStateChanged$1$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2$onStateChanged$1$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2$onStateChanged$1;-><init>(Lkotlin/jvm/internal/Ref$ObjectRef;Landroidx/compose/runtime/Recomposer;Landroidx/lifecycle/LifecycleOwner;Landroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2;Landroid/view/View;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2$onStateChanged$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2$onStateChanged$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2$onStateChanged$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2;-><init>(Lkotlinx/coroutines/internal/ContextScope;Landroidx/compose/runtime/PausableMonotonicFrameClock;Landroidx/compose/runtime/Recomposer;Lkotlin/jvm/internal/Ref$ObjectRef;Landroid/view/View;)V
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$getAnimationScaleFlowFor$1$1$1;-><init>(Landroid/content/ContentResolver;Landroid/net/Uri;Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$1;Lkotlinx/coroutines/channels/Channel;Landroid/content/Context;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$getAnimationScaleFlowFor$1$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$getAnimationScaleFlowFor$1$1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt$getAnimationScaleFlowFor$1$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt;-><clinit>()V
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt;->access$getAnimationScaleFlowFor(Landroid/content/Context;)Lkotlinx/coroutines/flow/StateFlow;
+HSPLandroidx/compose/ui/platform/WindowRecomposer_androidKt;->getCompositionContext(Landroid/view/View;)Landroidx/compose/runtime/CompositionContext;
+HSPLandroidx/compose/ui/platform/WrappedComposition$setContent$1$1$1;-><init>(Landroidx/compose/ui/platform/WrappedComposition;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/compose/ui/platform/WrappedComposition$setContent$1$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLandroidx/compose/ui/platform/WrappedComposition$setContent$1$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/WrappedComposition$setContent$1$1;-><init>(Landroidx/compose/ui/platform/WrappedComposition;Lkotlin/jvm/functions/Function2;I)V
+HSPLandroidx/compose/ui/platform/WrappedComposition$setContent$1$1;->invoke(Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/compose/ui/platform/WrappedComposition$setContent$1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/WrappedComposition$setContent$1;-><init>(Ljava/lang/Object;ILjava/lang/Object;)V
+HSPLandroidx/compose/ui/platform/WrappedComposition$setContent$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/platform/WrappedComposition;-><init>(Landroidx/compose/ui/platform/AndroidComposeView;Landroidx/compose/runtime/CompositionImpl;)V
+HSPLandroidx/compose/ui/platform/WrappedComposition;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLandroidx/compose/ui/platform/WrappedComposition;->setContent(Lkotlin/jvm/functions/Function2;)V
+HSPLandroidx/compose/ui/platform/WrapperRenderNodeLayerHelperMethods;-><clinit>()V
+HSPLandroidx/compose/ui/platform/WrapperRenderNodeLayerHelperMethods;->onDescendantInvalidated(Landroidx/compose/ui/platform/AndroidComposeView;)V
+HSPLandroidx/compose/ui/platform/WrapperVerificationHelperMethods;-><clinit>()V
+HSPLandroidx/compose/ui/platform/WrapperVerificationHelperMethods;->attributeSourceResourceMap(Landroid/view/View;)Ljava/util/Map;
+HSPLandroidx/compose/ui/platform/Wrapper_androidKt;-><clinit>()V
+HSPLandroidx/compose/ui/platform/Wrapper_androidKt;->setContent(Landroidx/compose/ui/platform/AbstractComposeView;Landroidx/compose/runtime/CompositionContext;Landroidx/compose/runtime/internal/ComposableLambdaImpl;)Landroidx/compose/runtime/Composition;
+HSPLandroidx/compose/ui/res/ImageVectorCache;-><init>()V
+HSPLandroidx/compose/ui/semantics/AppendedSemanticsElement;-><init>(Lkotlin/jvm/functions/Function1;Z)V
+HSPLandroidx/compose/ui/semantics/AppendedSemanticsElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/semantics/AppendedSemanticsElement;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/semantics/AppendedSemanticsElement;->update(Landroidx/compose/ui/Modifier$Node;)V
+HSPLandroidx/compose/ui/semantics/CollectionInfo;-><init>(II)V
+HSPLandroidx/compose/ui/semantics/CoreSemanticsModifierNode;-><init>(ZLkotlin/jvm/functions/Function1;)V
+HSPLandroidx/compose/ui/semantics/EmptySemanticsElement;-><clinit>()V
+HSPLandroidx/compose/ui/semantics/EmptySemanticsElement;-><init>()V
+HSPLandroidx/compose/ui/semantics/EmptySemanticsElement;->create()Landroidx/compose/ui/Modifier$Node;
+HSPLandroidx/compose/ui/semantics/ScrollAxisRange;-><init>(Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Z)V
+HSPLandroidx/compose/ui/semantics/SemanticsConfiguration;-><init>()V
+HSPLandroidx/compose/ui/semantics/SemanticsConfiguration;->contains(Landroidx/compose/ui/semantics/SemanticsPropertyKey;)Z
+HSPLandroidx/compose/ui/semantics/SemanticsModifierKt;-><clinit>()V
+HSPLandroidx/compose/ui/semantics/SemanticsNode;-><init>(Landroidx/compose/ui/Modifier$Node;ZLandroidx/compose/ui/node/LayoutNode;Landroidx/compose/ui/semantics/SemanticsConfiguration;)V
+HSPLandroidx/compose/ui/semantics/SemanticsNode;->fillOneLayerOfSemanticsWrappers(Landroidx/compose/ui/node/LayoutNode;Ljava/util/ArrayList;)V
+HSPLandroidx/compose/ui/semantics/SemanticsNode;->getChildren(ZZ)Ljava/util/List;
+HSPLandroidx/compose/ui/semantics/SemanticsNode;->getReplacedChildren$ui_release()Ljava/util/List;
+HSPLandroidx/compose/ui/semantics/SemanticsNode;->isMergingSemanticsOfDescendants()Z
+HSPLandroidx/compose/ui/semantics/SemanticsNode;->unmergedChildren$ui_release(Z)Ljava/util/List;
+HSPLandroidx/compose/ui/semantics/SemanticsOwner;-><init>(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/semantics/SemanticsOwner;->getUnmergedRootSemanticsNode()Landroidx/compose/ui/semantics/SemanticsNode;
+HSPLandroidx/compose/ui/semantics/SemanticsProperties;-><clinit>()V
+HSPLandroidx/compose/ui/semantics/SemanticsPropertyKey;-><init>(Ljava/lang/String;)V
+HSPLandroidx/compose/ui/semantics/SemanticsPropertyKey;-><init>(Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V
+HSPLandroidx/compose/ui/text/AndroidParagraph;-><init>(Landroidx/compose/ui/text/platform/AndroidParagraphIntrinsics;IZJ)V
+HSPLandroidx/compose/ui/text/AndroidParagraph;->constructTextLayout(IILandroid/text/TextUtils$TruncateAt;IIIII)Landroidx/compose/ui/text/android/TextLayout;
+HSPLandroidx/compose/ui/text/AndroidParagraph;->getHeight()F
+HSPLandroidx/compose/ui/text/AndroidParagraph;->getWidth()F
+HSPLandroidx/compose/ui/text/AndroidParagraph;->paint(Landroidx/compose/ui/graphics/Canvas;)V
+HSPLandroidx/compose/ui/text/AndroidParagraph;->paint-LG529CI(Landroidx/compose/ui/graphics/Canvas;JLandroidx/compose/ui/graphics/Shadow;Landroidx/compose/ui/text/style/TextDecoration;Lkotlin/ResultKt;I)V
+HSPLandroidx/compose/ui/text/AnnotatedString$Range;-><init>(IILjava/lang/Object;)V
+HSPLandroidx/compose/ui/text/AnnotatedString$Range;-><init>(Ljava/lang/Object;IILjava/lang/String;)V
+HSPLandroidx/compose/ui/text/AnnotatedString;-><init>(Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;)V
+HSPLandroidx/compose/ui/text/AnnotatedString;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/AnnotatedStringKt;-><clinit>()V
+HSPLandroidx/compose/ui/text/EmojiSupportMatch;-><init>(I)V
+HSPLandroidx/compose/ui/text/MultiParagraph;->paint-LG529CI$default(Landroidx/compose/ui/text/MultiParagraph;Landroidx/compose/ui/graphics/Canvas;JLandroidx/compose/ui/graphics/Shadow;Landroidx/compose/ui/text/style/TextDecoration;Lkotlin/ResultKt;)V
+HSPLandroidx/compose/ui/text/MultiParagraphIntrinsics$maxIntrinsicWidth$2;-><init>(Landroidx/compose/ui/text/MultiParagraphIntrinsics;I)V
+HSPLandroidx/compose/ui/text/MultiParagraphIntrinsics$maxIntrinsicWidth$2;->invoke$1()Ljava/lang/Float;
+HSPLandroidx/compose/ui/text/MultiParagraphIntrinsics$maxIntrinsicWidth$2;->invoke()Ljava/lang/Object;
+HSPLandroidx/compose/ui/text/MultiParagraphIntrinsics;-><init>(Landroidx/compose/ui/text/AnnotatedString;Landroidx/compose/ui/text/TextStyle;Ljava/util/List;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/text/font/FontFamily$Resolver;)V
+HSPLandroidx/compose/ui/text/MultiParagraphIntrinsics;->getHasStaleResolvedFonts()Z
+HSPLandroidx/compose/ui/text/MultiParagraphIntrinsics;->getMaxIntrinsicWidth()F
+HSPLandroidx/compose/ui/text/ParagraphInfo;-><init>(Landroidx/compose/ui/text/AndroidParagraph;IIIIFF)V
+HSPLandroidx/compose/ui/text/ParagraphIntrinsicInfo;-><init>(Landroidx/compose/ui/text/platform/AndroidParagraphIntrinsics;II)V
+HSPLandroidx/compose/ui/text/ParagraphStyle;-><init>(Landroidx/compose/ui/text/style/TextAlign;Landroidx/compose/ui/text/style/TextDirection;JLandroidx/compose/ui/text/style/TextIndent;Landroidx/compose/ui/text/PlatformParagraphStyle;Landroidx/compose/ui/text/style/LineHeightStyle;Landroidx/compose/ui/text/style/LineBreak;Landroidx/compose/ui/text/style/Hyphens;Landroidx/compose/ui/text/style/TextMotion;)V
+HSPLandroidx/compose/ui/text/ParagraphStyle;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/ParagraphStyle;->merge(Landroidx/compose/ui/text/ParagraphStyle;)Landroidx/compose/ui/text/ParagraphStyle;
+HSPLandroidx/compose/ui/text/ParagraphStyleKt;-><clinit>()V
+HSPLandroidx/compose/ui/text/ParagraphStyleKt;->fastMerge-HtYhynw(Landroidx/compose/ui/text/ParagraphStyle;Landroidx/compose/ui/text/style/TextAlign;Landroidx/compose/ui/text/style/TextDirection;JLandroidx/compose/ui/text/style/TextIndent;Landroidx/compose/ui/text/PlatformParagraphStyle;Landroidx/compose/ui/text/style/LineHeightStyle;Landroidx/compose/ui/text/style/LineBreak;Landroidx/compose/ui/text/style/Hyphens;Landroidx/compose/ui/text/style/TextMotion;)Landroidx/compose/ui/text/ParagraphStyle;
+HSPLandroidx/compose/ui/text/PlatformParagraphStyle;-><init>(I)V
+HSPLandroidx/compose/ui/text/PlatformParagraphStyle;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/PlatformTextStyle;-><init>(Landroidx/compose/ui/text/PlatformParagraphStyle;)V
+HSPLandroidx/compose/ui/text/SaversKt$ColorSaver$1;-><clinit>()V
+HSPLandroidx/compose/ui/text/SaversKt$ColorSaver$1;-><init>(I)V
+HSPLandroidx/compose/ui/text/SaversKt$ColorSaver$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/text/SaversKt$ColorSaver$2;-><clinit>()V
+HSPLandroidx/compose/ui/text/SaversKt$ColorSaver$2;-><init>(I)V
+HSPLandroidx/compose/ui/text/SaversKt$ColorSaver$2;->invoke(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/compose/ui/text/SaversKt$ColorSaver$2;->invoke(Ljava/lang/Object;)Ljava/lang/Boolean;
+HSPLandroidx/compose/ui/text/SaversKt$ColorSaver$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/text/SpanStyle;-><init>(JJLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontSynthesis;Landroidx/compose/ui/text/font/FontFamily;Ljava/lang/String;JLandroidx/compose/ui/text/style/BaselineShift;Landroidx/compose/ui/text/style/TextGeometricTransform;Landroidx/compose/ui/text/intl/LocaleList;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/graphics/Shadow;I)V
+HSPLandroidx/compose/ui/text/SpanStyle;-><init>(JJLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontSynthesis;Landroidx/compose/ui/text/font/FontFamily;Ljava/lang/String;JLandroidx/compose/ui/text/style/BaselineShift;Landroidx/compose/ui/text/style/TextGeometricTransform;Landroidx/compose/ui/text/intl/LocaleList;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/graphics/Shadow;Lkotlin/ResultKt;)V
+HSPLandroidx/compose/ui/text/SpanStyle;-><init>(Landroidx/compose/ui/text/style/TextForegroundStyle;JLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontSynthesis;Landroidx/compose/ui/text/font/FontFamily;Ljava/lang/String;JLandroidx/compose/ui/text/style/BaselineShift;Landroidx/compose/ui/text/style/TextGeometricTransform;Landroidx/compose/ui/text/intl/LocaleList;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/graphics/Shadow;Lkotlin/ResultKt;)V
+HSPLandroidx/compose/ui/text/SpanStyle;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/SpanStyle;->getColor-0d7_KjU()J
+HSPLandroidx/compose/ui/text/SpanStyle;->hasSameLayoutAffectingAttributes$ui_text_release(Landroidx/compose/ui/text/SpanStyle;)Z
+HSPLandroidx/compose/ui/text/SpanStyle;->hasSameNonLayoutAttributes$ui_text_release(Landroidx/compose/ui/text/SpanStyle;)Z
+HSPLandroidx/compose/ui/text/SpanStyle;->merge(Landroidx/compose/ui/text/SpanStyle;)Landroidx/compose/ui/text/SpanStyle;
+HSPLandroidx/compose/ui/text/SpanStyleKt;-><clinit>()V
+HSPLandroidx/compose/ui/text/SpanStyleKt;->fastMerge-dSHsh3o(Landroidx/compose/ui/text/SpanStyle;JLandroidx/compose/ui/graphics/Brush;FJLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontSynthesis;Landroidx/compose/ui/text/font/FontFamily;Ljava/lang/String;JLandroidx/compose/ui/text/style/BaselineShift;Landroidx/compose/ui/text/style/TextGeometricTransform;Landroidx/compose/ui/text/intl/LocaleList;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/graphics/Shadow;Lkotlin/ResultKt;)Landroidx/compose/ui/text/SpanStyle;
+HSPLandroidx/compose/ui/text/TextLayoutInput;-><init>(Landroidx/compose/ui/text/AnnotatedString;Landroidx/compose/ui/text/TextStyle;Ljava/util/List;IZILandroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Landroidx/compose/ui/text/font/FontFamily$Resolver;J)V
+HSPLandroidx/compose/ui/text/TextLayoutResult;-><init>(Landroidx/compose/ui/text/TextLayoutInput;Landroidx/compose/ui/text/MultiParagraph;J)V
+HSPLandroidx/compose/ui/text/TextRange;-><clinit>()V
+HSPLandroidx/compose/ui/text/TextRange;->getEnd-impl(J)I
+HSPLandroidx/compose/ui/text/TextStyle;-><clinit>()V
+HSPLandroidx/compose/ui/text/TextStyle;-><init>(JJLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontFamily;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/text/style/TextAlign;JI)V
+HSPLandroidx/compose/ui/text/TextStyle;-><init>(JLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/DefaultFontFamily;I)V
+HSPLandroidx/compose/ui/text/TextStyle;-><init>(Landroidx/compose/ui/text/SpanStyle;Landroidx/compose/ui/text/ParagraphStyle;)V
+HSPLandroidx/compose/ui/text/TextStyle;-><init>(Landroidx/compose/ui/text/SpanStyle;Landroidx/compose/ui/text/ParagraphStyle;Landroidx/compose/ui/text/PlatformTextStyle;)V
+HSPLandroidx/compose/ui/text/TextStyle;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/TextStyle;->getColor-0d7_KjU()J
+HSPLandroidx/compose/ui/text/TextStyle;->merge(Landroidx/compose/ui/text/TextStyle;)Landroidx/compose/ui/text/TextStyle;
+HSPLandroidx/compose/ui/text/TextStyle;->merge-Z1GrekI$default(IJJJJLandroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/font/FontFamily;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/style/TextAlign;Landroidx/compose/ui/text/style/TextDecoration;)Landroidx/compose/ui/text/TextStyle;
+HSPLandroidx/compose/ui/text/android/BoringLayoutFactoryDefault;->create(Ljava/lang/CharSequence;Landroid/text/TextPaint;ILandroid/text/Layout$Alignment;FFLandroid/text/BoringLayout$Metrics;ZLandroid/text/TextUtils$TruncateAt;I)Landroid/text/BoringLayout;
+HSPLandroidx/compose/ui/text/android/BoringLayoutFactoryDefault;->isBoring(Ljava/lang/CharSequence;Landroid/text/TextPaint;Landroid/text/TextDirectionHeuristic;)Landroid/text/BoringLayout$Metrics;
+HSPLandroidx/compose/ui/text/android/LayoutIntrinsics;-><init>(Ljava/lang/CharSequence;Landroidx/compose/ui/text/platform/AndroidTextPaint;I)V
+HSPLandroidx/compose/ui/text/android/LayoutIntrinsics;->getBoringMetrics()Landroid/text/BoringLayout$Metrics;
+HSPLandroidx/compose/ui/text/android/LayoutIntrinsics;->getMaxIntrinsicWidth()F
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m$1(Landroid/graphics/RenderNode;)F
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m$1(Landroid/graphics/RenderNode;)Z
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m$1(Landroid/graphics/RenderNode;F)V
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m$2(Landroid/graphics/RenderNode;)V
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m$2(Landroid/graphics/RenderNode;F)V
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m$3(Landroid/graphics/RenderNode;)I
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m$3(Landroid/graphics/RenderNode;)V
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m()Landroid/graphics/RenderNode;
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/Paint;Ljava/lang/CharSequence;IILandroid/graphics/Rect;)V
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;)F
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;)I
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;)Landroid/graphics/RecordingCanvas;
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;)V
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;)Z
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;F)V
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;I)V
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;IIII)Z
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;Landroid/graphics/Matrix;)V
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;Landroid/graphics/Outline;)V
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/RenderNode;Z)V
+HSPLandroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;->m(Landroid/view/View;)Ljava/util/Map;
+HSPLandroidx/compose/ui/text/android/Paint29;->getTextBounds(Landroid/graphics/Paint;Ljava/lang/CharSequence;IILandroid/graphics/Rect;)V
+HSPLandroidx/compose/ui/text/android/StaticLayoutFactory23;->create(Landroidx/compose/ui/text/android/StaticLayoutParams;)Landroid/text/StaticLayout;
+HSPLandroidx/compose/ui/text/android/StaticLayoutFactory26;->setJustificationMode(Landroid/text/StaticLayout$Builder;I)V
+HSPLandroidx/compose/ui/text/android/StaticLayoutFactory28;->setUseLineSpacingFromFallbacks(Landroid/text/StaticLayout$Builder;Z)V
+HSPLandroidx/compose/ui/text/android/StaticLayoutParams;-><init>(Ljava/lang/CharSequence;IILandroidx/compose/ui/text/platform/AndroidTextPaint;ILandroid/text/TextDirectionHeuristic;Landroid/text/Layout$Alignment;ILandroid/text/TextUtils$TruncateAt;IFFIZZIIII[I[I)V
+HSPLandroidx/compose/ui/text/android/TextAlignmentAdapter;-><clinit>()V
+HSPLandroidx/compose/ui/text/android/TextAndroidCanvas;->drawText(Ljava/lang/String;FFLandroid/graphics/Paint;)V
+HSPLandroidx/compose/ui/text/android/TextAndroidCanvas;->drawTextRun(Ljava/lang/CharSequence;IIIIFFZLandroid/graphics/Paint;)V
+HSPLandroidx/compose/ui/text/android/TextAndroidCanvas;->getClipBounds(Landroid/graphics/Rect;)Z
+HSPLandroidx/compose/ui/text/android/TextLayout;-><init>(Ljava/lang/CharSequence;FLandroidx/compose/ui/text/platform/AndroidTextPaint;ILandroid/text/TextUtils$TruncateAt;IZIIIIIILandroidx/compose/ui/text/android/LayoutIntrinsics;)V
+HSPLandroidx/compose/ui/text/android/TextLayout;->getHeight()I
+HSPLandroidx/compose/ui/text/android/TextLayout;->getLineBaseline(I)F
+HSPLandroidx/compose/ui/text/android/TextLayout;->getText()Ljava/lang/CharSequence;
+HSPLandroidx/compose/ui/text/android/TextLayoutKt;-><clinit>()V
+HSPLandroidx/compose/ui/text/android/TextLayoutKt;->getTextDirectionHeuristic(I)Landroid/text/TextDirectionHeuristic;
+HSPLandroidx/compose/ui/text/android/style/LineHeightStyleSpan;-><init>(FIZZF)V
+HSPLandroidx/compose/ui/text/android/style/LineHeightStyleSpan;->chooseHeight(Ljava/lang/CharSequence;IIIILandroid/graphics/Paint$FontMetricsInt;)V
+HSPLandroidx/compose/ui/text/caches/LruCache;-><init>()V
+HSPLandroidx/compose/ui/text/caches/LruCache;->get(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/text/caches/LruCache;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/text/caches/LruCache;->size()I
+HSPLandroidx/compose/ui/text/caches/SimpleArrayMap;-><init>()V
+HSPLandroidx/compose/ui/text/font/AndroidFontResolveInterceptor;-><init>(I)V
+HSPLandroidx/compose/ui/text/font/AndroidFontResolveInterceptor;->interceptFontWeight(Landroidx/compose/ui/text/font/FontWeight;)Landroidx/compose/ui/text/font/FontWeight;
+HSPLandroidx/compose/ui/text/font/AsyncTypefaceCache;-><init>()V
+HSPLandroidx/compose/ui/text/font/FontFamily;-><clinit>()V
+HSPLandroidx/compose/ui/text/font/FontFamilyResolverImpl;-><init>(Landroidx/compose/ui/unit/Dp$Companion;Landroidx/compose/ui/text/font/AndroidFontResolveInterceptor;)V
+HSPLandroidx/compose/ui/text/font/FontFamilyResolverImpl;->resolve(Landroidx/compose/ui/text/font/TypefaceRequest;)Landroidx/compose/ui/text/font/TypefaceResult;
+HSPLandroidx/compose/ui/text/font/FontFamilyResolverImpl;->resolve-DPcqOEQ(Landroidx/compose/ui/text/font/FontFamily;Landroidx/compose/ui/text/font/FontWeight;II)Landroidx/compose/ui/text/font/TypefaceResult;
+HSPLandroidx/compose/ui/text/font/FontFamilyResolverKt;-><clinit>()V
+HSPLandroidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter$special$$inlined$CoroutineExceptionHandler$1;-><init>()V
+HSPLandroidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter;-><clinit>()V
+HSPLandroidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter;-><init>(Landroidx/compose/ui/text/font/AsyncTypefaceCache;)V
+HSPLandroidx/compose/ui/text/font/FontStyle;-><init>(I)V
+HSPLandroidx/compose/ui/text/font/FontSynthesis;-><init>(I)V
+HSPLandroidx/compose/ui/text/font/FontWeight;-><clinit>()V
+HSPLandroidx/compose/ui/text/font/FontWeight;-><init>(I)V
+HSPLandroidx/compose/ui/text/font/FontWeight;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/font/PlatformResolveInterceptor$Companion;-><clinit>()V
+HSPLandroidx/compose/ui/text/font/PlatformResolveInterceptor;-><clinit>()V
+HSPLandroidx/compose/ui/text/font/TypefaceRequest;-><init>(Landroidx/compose/ui/text/font/FontFamily;Landroidx/compose/ui/text/font/FontWeight;IILjava/lang/Object;)V
+HSPLandroidx/compose/ui/text/font/TypefaceRequest;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/font/TypefaceRequest;->hashCode()I
+HSPLandroidx/compose/ui/text/font/TypefaceResult$Immutable;-><init>(Ljava/lang/Object;Z)V
+HSPLandroidx/compose/ui/text/input/InputMethodManagerImpl;-><init>(Landroid/view/View;)V
+HSPLandroidx/compose/ui/text/input/TextFieldValue;-><clinit>()V
+HSPLandroidx/compose/ui/text/input/TextFieldValue;-><init>(Landroidx/compose/ui/text/AnnotatedString;JLandroidx/compose/ui/text/TextRange;)V
+HSPLandroidx/compose/ui/text/input/TextInputService;-><init>()V
+HSPLandroidx/compose/ui/text/input/TextInputServiceAndroid;-><init>(Landroid/view/View;Landroidx/compose/ui/input/pointer/PositionCalculator;)V
+HSPLandroidx/compose/ui/text/input/TextInputServiceAndroid_androidKt$$ExternalSyntheticLambda0;-><init>(Ljava/lang/Runnable;I)V
+HSPLandroidx/compose/ui/text/input/TextInputServiceAndroid_androidKt$$ExternalSyntheticLambda0;->doFrame(J)V
+HSPLandroidx/compose/ui/text/intl/AndroidLocale;-><init>(Ljava/util/Locale;)V
+HSPLandroidx/compose/ui/text/intl/AndroidLocaleDelegateAPI24;-><init>()V
+HSPLandroidx/compose/ui/text/intl/AndroidLocaleDelegateAPI24;->getCurrent()Landroidx/compose/ui/text/intl/LocaleList;
+HSPLandroidx/compose/ui/text/intl/Locale;-><init>(Landroidx/compose/ui/text/intl/AndroidLocale;)V
+HSPLandroidx/compose/ui/text/intl/LocaleList;-><init>(Ljava/util/List;)V
+HSPLandroidx/compose/ui/text/intl/LocaleList;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/intl/PlatformLocaleKt;-><clinit>()V
+HSPLandroidx/compose/ui/text/platform/AndroidParagraphHelper_androidKt;-><clinit>()V
+HSPLandroidx/compose/ui/text/platform/AndroidParagraphIntrinsics$resolveTypeface$1;-><init>(Landroidx/compose/ui/text/platform/AndroidParagraphIntrinsics;)V
+HSPLandroidx/compose/ui/text/platform/AndroidParagraphIntrinsics;-><init>(Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/font/FontFamily$Resolver;Landroidx/compose/ui/unit/Density;Ljava/lang/String;Ljava/util/List;Ljava/util/List;)V
+HSPLandroidx/compose/ui/text/platform/AndroidParagraphIntrinsics;->getHasStaleResolvedFonts()Z
+HSPLandroidx/compose/ui/text/platform/AndroidParagraphIntrinsics;->getMaxIntrinsicWidth()F
+HSPLandroidx/compose/ui/text/platform/AndroidTextPaint;-><init>(F)V
+HSPLandroidx/compose/ui/text/platform/AndroidTextPaint;->setBrush-12SF9DM(Landroidx/compose/ui/graphics/Brush;JF)V
+HSPLandroidx/compose/ui/text/platform/AndroidTextPaint;->setDrawStyle(Lkotlin/ResultKt;)V
+HSPLandroidx/compose/ui/text/platform/AndroidTextPaint;->setShadow(Landroidx/compose/ui/graphics/Shadow;)V
+HSPLandroidx/compose/ui/text/platform/AndroidTextPaint;->setTextDecoration(Landroidx/compose/ui/text/style/TextDecoration;)V
+HSPLandroidx/compose/ui/text/platform/DefaultImpl$getFontLoadState$initCallback$1;-><init>(Landroidx/compose/runtime/ParcelableSnapshotMutableState;Landroidx/compose/ui/text/platform/DefaultImpl;)V
+HSPLandroidx/compose/ui/text/platform/DefaultImpl;-><init>()V
+HSPLandroidx/compose/ui/text/platform/DefaultImpl;->getFontLoadState()Landroidx/compose/runtime/State;
+HSPLandroidx/compose/ui/text/platform/EmojiCompatStatus;-><clinit>()V
+HSPLandroidx/compose/ui/text/platform/ImmutableBool;-><init>(Z)V
+HSPLandroidx/compose/ui/text/platform/ImmutableBool;->getValue()Ljava/lang/Object;
+HSPLandroidx/compose/ui/text/platform/URLSpanCache;-><init>()V
+HSPLandroidx/compose/ui/text/platform/extensions/LocaleListHelperMethods;-><clinit>()V
+HSPLandroidx/compose/ui/text/style/BaselineShift;-><init>(F)V
+HSPLandroidx/compose/ui/text/style/ColorStyle;-><init>(J)V
+HSPLandroidx/compose/ui/text/style/ColorStyle;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/style/ColorStyle;->getAlpha()F
+HSPLandroidx/compose/ui/text/style/ColorStyle;->getBrush()Landroidx/compose/ui/graphics/Brush;
+HSPLandroidx/compose/ui/text/style/ColorStyle;->getColor-0d7_KjU()J
+HSPLandroidx/compose/ui/text/style/Hyphens;-><init>(I)V
+HSPLandroidx/compose/ui/text/style/Hyphens;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/style/LineBreak$Strategy;-><init>(I)V
+HSPLandroidx/compose/ui/text/style/LineBreak$Strictness;-><init>(I)V
+HSPLandroidx/compose/ui/text/style/LineBreak$WordBreak;-><init>(I)V
+HSPLandroidx/compose/ui/text/style/LineBreak;-><init>(I)V
+HSPLandroidx/compose/ui/text/style/LineBreak;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/style/LineHeightStyle$Alignment;-><clinit>()V
+HSPLandroidx/compose/ui/text/style/LineHeightStyle$Alignment;->constructor-impl(F)V
+HSPLandroidx/compose/ui/text/style/LineHeightStyle;-><clinit>()V
+HSPLandroidx/compose/ui/text/style/LineHeightStyle;-><init>(F)V
+HSPLandroidx/compose/ui/text/style/TextAlign;-><init>(I)V
+HSPLandroidx/compose/ui/text/style/TextAlign;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/style/TextDecoration;-><clinit>()V
+HSPLandroidx/compose/ui/text/style/TextDecoration;-><init>(I)V
+HSPLandroidx/compose/ui/text/style/TextDecoration;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/style/TextDirection;-><init>(I)V
+HSPLandroidx/compose/ui/text/style/TextDirection;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/style/TextForegroundStyle$Unspecified;-><clinit>()V
+HSPLandroidx/compose/ui/text/style/TextForegroundStyle$Unspecified;->getAlpha()F
+HSPLandroidx/compose/ui/text/style/TextForegroundStyle$Unspecified;->getBrush()Landroidx/compose/ui/graphics/Brush;
+HSPLandroidx/compose/ui/text/style/TextForegroundStyle$Unspecified;->getColor-0d7_KjU()J
+HSPLandroidx/compose/ui/text/style/TextGeometricTransform;-><clinit>()V
+HSPLandroidx/compose/ui/text/style/TextGeometricTransform;-><init>(FF)V
+HSPLandroidx/compose/ui/text/style/TextGeometricTransform;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/style/TextIndent;-><clinit>()V
+HSPLandroidx/compose/ui/text/style/TextIndent;-><init>(JJ)V
+HSPLandroidx/compose/ui/text/style/TextIndent;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/text/style/TextMotion;-><clinit>()V
+HSPLandroidx/compose/ui/text/style/TextMotion;-><init>(IZ)V
+HSPLandroidx/compose/ui/text/style/TextMotion;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/unit/Constraints;-><clinit>()V
+HSPLandroidx/compose/ui/unit/Constraints;-><init>(J)V
+HSPLandroidx/compose/ui/unit/Constraints;->copy-Zbe2FdA$default(JIIIII)J
+HSPLandroidx/compose/ui/unit/Constraints;->equals-impl0(JJ)Z
+HSPLandroidx/compose/ui/unit/Constraints;->getHasBoundedHeight-impl(J)Z
+HSPLandroidx/compose/ui/unit/Constraints;->getHasBoundedWidth-impl(J)Z
+HSPLandroidx/compose/ui/unit/Constraints;->getHasFixedHeight-impl(J)Z
+HSPLandroidx/compose/ui/unit/Constraints;->getHasFixedWidth-impl(J)Z
+HSPLandroidx/compose/ui/unit/Constraints;->getMaxHeight-impl(J)I
+HSPLandroidx/compose/ui/unit/Constraints;->getMaxWidth-impl(J)I
+HSPLandroidx/compose/ui/unit/Constraints;->getMinHeight-impl(J)I
+HSPLandroidx/compose/ui/unit/Constraints;->getMinWidth-impl(J)I
+HSPLandroidx/compose/ui/unit/Density;->roundToPx-0680j_4(F)I
+HSPLandroidx/compose/ui/unit/Density;->toDp-u2uoSUM(I)F
+HSPLandroidx/compose/ui/unit/Density;->toPx--R2X_6o(J)F
+HSPLandroidx/compose/ui/unit/Density;->toPx-0680j_4(F)F
+HSPLandroidx/compose/ui/unit/DensityImpl;-><init>(FF)V
+HSPLandroidx/compose/ui/unit/DensityImpl;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/unit/DensityImpl;->getDensity()F
+HSPLandroidx/compose/ui/unit/DensityImpl;->getFontScale()F
+HSPLandroidx/compose/ui/unit/Dp$Companion;-><clinit>()V
+HSPLandroidx/compose/ui/unit/Dp$Companion;-><init>(Landroid/content/Context;)V
+HSPLandroidx/compose/ui/unit/Dp$Companion;->access$getIsShowingLayoutBounds()Z
+HSPLandroidx/compose/ui/unit/Dp$Companion;->area([F)F
+HSPLandroidx/compose/ui/unit/Dp$Companion;->bitsNeedForSize(I)I
+HSPLandroidx/compose/ui/unit/Dp$Companion;->createAndroidTypefaceApi28-RetOiIg(Ljava/lang/String;Landroidx/compose/ui/text/font/FontWeight;I)Landroid/graphics/Typeface;
+HSPLandroidx/compose/ui/unit/Dp$Companion;->createConstraints-Zbe2FdA$ui_unit_release(IIII)J
+HSPLandroidx/compose/ui/unit/Dp$Companion;->fixed-JhjzzOo(II)J
+HSPLandroidx/compose/ui/unit/Dp$Companion;->observe(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
+HSPLandroidx/compose/ui/unit/Dp;-><init>(F)V
+HSPLandroidx/compose/ui/unit/Dp;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/unit/Dp;->equals-impl0(FF)Z
+HSPLandroidx/compose/ui/unit/DpOffset;-><clinit>()V
+HSPLandroidx/compose/ui/unit/DpRect;-><init>(FFFF)V
+HSPLandroidx/compose/ui/unit/DpRect;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/compose/ui/unit/IntOffset;-><clinit>()V
+HSPLandroidx/compose/ui/unit/IntOffset;-><init>(J)V
+HSPLandroidx/compose/ui/unit/IntOffset;->getY-impl(J)I
+HSPLandroidx/compose/ui/unit/IntSize;-><init>(J)V
+HSPLandroidx/compose/ui/unit/IntSize;->equals-impl0(JJ)Z
+HSPLandroidx/compose/ui/unit/IntSize;->getHeight-impl(J)I
+HSPLandroidx/compose/ui/unit/LayoutDirection;-><clinit>()V
+HSPLandroidx/compose/ui/unit/LayoutDirection;-><init>(ILjava/lang/String;)V
+HSPLandroidx/compose/ui/unit/TextUnit;-><clinit>()V
+HSPLandroidx/compose/ui/unit/TextUnit;->equals-impl0(JJ)Z
+HSPLandroidx/compose/ui/unit/TextUnit;->getType-UIouoOA(J)J
+HSPLandroidx/compose/ui/unit/TextUnit;->getValue-impl(J)F
+HSPLandroidx/compose/ui/unit/TextUnitType;-><init>(J)V
+HSPLandroidx/compose/ui/unit/TextUnitType;->equals-impl0(JJ)Z
+HSPLandroidx/core/app/ComponentActivity;-><init>()V
+HSPLandroidx/core/app/ComponentActivity;->dispatchKeyEvent(Landroid/view/KeyEvent;)Z
+HSPLandroidx/core/app/ComponentActivity;->onCreate(Landroid/os/Bundle;)V
+HSPLandroidx/core/app/ComponentActivity;->superDispatchKeyEvent(Landroid/view/KeyEvent;)Z
+HSPLandroidx/core/app/CoreComponentFactory;-><init>()V
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateActivity(Ljava/lang/ClassLoader;Ljava/lang/String;Landroid/content/Intent;)Landroid/app/Activity;
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateApplication(Ljava/lang/ClassLoader;Ljava/lang/String;)Landroid/app/Application;
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateProvider(Ljava/lang/ClassLoader;Ljava/lang/String;)Landroid/content/ContentProvider;
+HSPLandroidx/core/content/res/ComplexColorCompat;-><init>()V
+HSPLandroidx/core/content/res/ComplexColorCompat;-><init>(I)V
+HSPLandroidx/core/content/res/ComplexColorCompat;-><init>(Ljava/lang/Object;)V
+HSPLandroidx/core/content/res/ComplexColorCompat;->find(Ljava/lang/Object;)I
+HSPLandroidx/core/content/res/ComplexColorCompat;->get(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/core/content/res/ComplexColorCompat;->set(Ljava/lang/Object;Ljava/lang/Object;)V
+HSPLandroidx/core/graphics/TypefaceCompat;-><clinit>()V
+HSPLandroidx/core/graphics/TypefaceCompatApi29Impl;-><init>()V
+HSPLandroidx/core/graphics/TypefaceCompatApi29Impl;->createFromFontInfo(Landroid/content/Context;[Landroidx/core/provider/FontsContractCompat$FontInfo;I)Landroid/graphics/Typeface;
+HSPLandroidx/core/graphics/TypefaceCompatApi29Impl;->findBaseFont(Landroid/graphics/fonts/FontFamily;I)Landroid/graphics/fonts/Font;
+HSPLandroidx/core/graphics/TypefaceCompatApi29Impl;->getMatchScore(Landroid/graphics/fonts/FontStyle;Landroid/graphics/fonts/FontStyle;)I
+HSPLandroidx/core/graphics/TypefaceCompatUtil$Api19Impl;->openFileDescriptor(Landroid/content/ContentResolver;Landroid/net/Uri;Ljava/lang/String;Landroid/os/CancellationSignal;)Landroid/os/ParcelFileDescriptor;
+HSPLandroidx/core/os/BuildCompat$Extensions30Impl$$ExternalSyntheticApiModelOutline0;->m$1()I
+HSPLandroidx/core/os/BuildCompat$Extensions30Impl$$ExternalSyntheticApiModelOutline0;->m$2()I
+HSPLandroidx/core/os/BuildCompat$Extensions30Impl$$ExternalSyntheticApiModelOutline0;->m$3()I
+HSPLandroidx/core/os/BuildCompat$Extensions30Impl$$ExternalSyntheticApiModelOutline0;->m()I
+HSPLandroidx/core/os/BuildCompat$Extensions30Impl;-><clinit>()V
+HSPLandroidx/core/os/BuildCompat;-><clinit>()V
+HSPLandroidx/core/os/BuildCompat;->isAtLeastT()Z
+HSPLandroidx/core/os/TraceCompat$Api18Impl;->beginSection(Ljava/lang/String;)V
+HSPLandroidx/core/os/TraceCompat$Api18Impl;->endSection()V
+HSPLandroidx/core/os/TraceCompat;-><clinit>()V
+HSPLandroidx/core/provider/CallbackWithHandler$2;-><init>(ILjava/util/ArrayList;)V
+HSPLandroidx/core/provider/CallbackWithHandler$2;-><init>(Ljava/util/List;ILjava/lang/Throwable;)V
+HSPLandroidx/core/provider/CallbackWithHandler$2;->run()V
+HSPLandroidx/core/provider/FontProvider$Api16Impl;->query(Landroid/content/ContentResolver;Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)Landroid/database/Cursor;
+HSPLandroidx/core/provider/FontRequest;-><init>(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V
+HSPLandroidx/core/provider/FontsContractCompat$FontInfo;-><init>(Landroid/net/Uri;IIZI)V
+HSPLandroidx/core/view/AccessibilityDelegateCompat$AccessibilityDelegateAdapter;-><init>(Landroidx/core/view/AccessibilityDelegateCompat;)V
+HSPLandroidx/core/view/AccessibilityDelegateCompat$AccessibilityDelegateAdapter;->dispatchPopulateAccessibilityEvent(Landroid/view/View;Landroid/view/accessibility/AccessibilityEvent;)Z
+HSPLandroidx/core/view/AccessibilityDelegateCompat$AccessibilityDelegateAdapter;->getAccessibilityNodeProvider(Landroid/view/View;)Landroid/view/accessibility/AccessibilityNodeProvider;
+HSPLandroidx/core/view/AccessibilityDelegateCompat$AccessibilityDelegateAdapter;->onInitializeAccessibilityEvent(Landroid/view/View;Landroid/view/accessibility/AccessibilityEvent;)V
+HSPLandroidx/core/view/AccessibilityDelegateCompat$AccessibilityDelegateAdapter;->onPopulateAccessibilityEvent(Landroid/view/View;Landroid/view/accessibility/AccessibilityEvent;)V
+HSPLandroidx/core/view/AccessibilityDelegateCompat$AccessibilityDelegateAdapter;->sendAccessibilityEvent(Landroid/view/View;I)V
+HSPLandroidx/core/view/AccessibilityDelegateCompat$AccessibilityDelegateAdapter;->sendAccessibilityEventUnchecked(Landroid/view/View;Landroid/view/accessibility/AccessibilityEvent;)V
+HSPLandroidx/core/view/AccessibilityDelegateCompat;-><clinit>()V
+HSPLandroidx/core/view/AccessibilityDelegateCompat;-><init>()V
+HSPLandroidx/core/view/MenuHostHelper;-><init>(Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda0;)V
+HSPLandroidx/core/view/MenuHostHelper;-><init>(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V
+HSPLandroidx/core/view/ViewCompat$$ExternalSyntheticLambda0;-><init>(I)V
+HSPLandroidx/core/view/ViewCompat$$ExternalSyntheticLambda0;->invoke(D)D
+HSPLandroidx/core/view/ViewCompat$Api29Impl;->getContentCaptureSession(Landroid/view/View;)Landroid/view/contentcapture/ContentCaptureSession;
+HSPLandroidx/core/view/ViewCompat$Api30Impl;->setImportantForContentCapture(Landroid/view/View;I)V
+HSPLandroidx/core/view/ViewCompat;-><clinit>()V
+HSPLandroidx/core/view/accessibility/AccessibilityNodeProviderCompat;-><init>(Ljava/lang/Object;)V
+HSPLandroidx/customview/poolingcontainer/PoolingContainerListenerHolder;-><init>()V
+HSPLandroidx/emoji2/text/ConcurrencyHelpers$$ExternalSyntheticLambda0;-><init>(Ljava/lang/String;)V
+HSPLandroidx/emoji2/text/ConcurrencyHelpers$$ExternalSyntheticLambda0;->newThread(Ljava/lang/Runnable;)Ljava/lang/Thread;
+HSPLandroidx/emoji2/text/ConcurrencyHelpers$Handler28Impl;->createAsync(Landroid/os/Looper;)Landroid/os/Handler;
+HSPLandroidx/emoji2/text/DefaultGlyphChecker;-><clinit>()V
+HSPLandroidx/emoji2/text/DefaultGlyphChecker;-><init>()V
+HSPLandroidx/emoji2/text/EmojiCompat$CompatInternal19$1;-><init>(Landroidx/emoji2/text/EmojiCompat$CompatInternal19;)V
+HSPLandroidx/emoji2/text/EmojiCompat$CompatInternal19$1;->onLoaded(Landroidx/emoji2/text/MetadataRepo;)V
+HSPLandroidx/emoji2/text/EmojiCompat$CompatInternal19;-><init>(Landroidx/emoji2/text/EmojiCompat;)V
+HSPLandroidx/emoji2/text/EmojiCompat$CompatInternal19;->process(Ljava/lang/CharSequence;IZ)Ljava/lang/CharSequence;
+HSPLandroidx/emoji2/text/EmojiCompat$Config;-><init>(Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoader;)V
+HSPLandroidx/emoji2/text/EmojiCompat;-><clinit>()V
+HSPLandroidx/emoji2/text/EmojiCompat;-><init>(Landroidx/emoji2/text/FontRequestEmojiCompatConfig;)V
+HSPLandroidx/emoji2/text/EmojiCompat;->get()Landroidx/emoji2/text/EmojiCompat;
+HSPLandroidx/emoji2/text/EmojiCompat;->getLoadState()I
+HSPLandroidx/emoji2/text/EmojiCompat;->load()V
+HSPLandroidx/emoji2/text/EmojiCompat;->onMetadataLoadSuccess()V
+HSPLandroidx/emoji2/text/EmojiCompatInitializer$1;-><init>(Landroidx/emoji2/text/EmojiCompatInitializer;Landroidx/lifecycle/Lifecycle;)V
+HSPLandroidx/emoji2/text/EmojiCompatInitializer$1;->onResume(Landroidx/lifecycle/LifecycleOwner;)V
+HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$$ExternalSyntheticLambda0;-><init>(Landroidx/compose/runtime/Stack;Lokhttp3/MediaType;Ljava/util/concurrent/ThreadPoolExecutor;)V
+HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$$ExternalSyntheticLambda0;->run()V
+HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$1;-><init>(Lokhttp3/MediaType;Ljava/util/concurrent/ThreadPoolExecutor;)V
+HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$1;->onLoaded(Landroidx/emoji2/text/MetadataRepo;)V
+HSPLandroidx/emoji2/text/EmojiCompatInitializer$LoadEmojiCompatRunnable;->run()V
+HSPLandroidx/emoji2/text/EmojiCompatInitializer;-><init>()V
+HSPLandroidx/emoji2/text/EmojiCompatInitializer;->create(Landroid/content/Context;)Ljava/lang/Boolean;
+HSPLandroidx/emoji2/text/EmojiCompatInitializer;->create(Landroid/content/Context;)Ljava/lang/Object;
+HSPLandroidx/emoji2/text/EmojiCompatInitializer;->dependencies()Ljava/util/List;
+HSPLandroidx/emoji2/text/EmojiProcessor$EmojiProcessAddSpanCallback;-><init>(Landroidx/emoji2/text/UnprecomputeTextOnModificationSpannable;Lkotlin/ULong$Companion;)V
+HSPLandroidx/emoji2/text/EmojiProcessor$EmojiProcessAddSpanCallback;->getResult()Ljava/lang/Object;
+HSPLandroidx/emoji2/text/EmojiProcessor$ProcessorSm;-><init>(Landroidx/emoji2/text/MetadataRepo$Node;Z[I)V
+HSPLandroidx/emoji2/text/EmojiProcessor$ProcessorSm;->shouldUseEmojiPresentationStyleForSingleCodepoint()Z
+HSPLandroidx/emoji2/text/EmojiProcessor;-><init>(Landroidx/compose/ui/node/LayoutNode;)V
+HSPLandroidx/emoji2/text/EmojiProcessor;-><init>(Landroidx/emoji2/text/MetadataRepo;Lkotlin/ULong$Companion;Landroidx/emoji2/text/DefaultGlyphChecker;Ljava/util/Set;)V
+HSPLandroidx/emoji2/text/EmojiProcessor;->process(Ljava/lang/String;IIIZLandroidx/emoji2/text/EmojiProcessor$EmojiProcessCallback;)Ljava/lang/Object;
+HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0;-><init>(Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;I)V
+HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0;->run()V
+HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$1;-><init>(Lkotlinx/coroutines/channels/BufferedChannel;Landroid/os/Handler;I)V
+HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;-><init>(Landroid/content/Context;Landroidx/core/provider/FontRequest;)V
+HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->cleanUp()V
+HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->load(Lokhttp3/MediaType;)V
+HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->loadInternal()V
+HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->retrieveFontInfo()Landroidx/core/provider/FontsContractCompat$FontInfo;
+HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig;-><clinit>()V
+HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig;-><init>(Landroid/content/Context;)V
+HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig;-><init>(Landroid/content/Context;Landroidx/core/provider/FontRequest;)V
+HSPLandroidx/emoji2/text/MetadataRepo$Node;-><init>(I)V
+HSPLandroidx/emoji2/text/MetadataRepo$Node;->put(Landroidx/emoji2/text/TypefaceEmojiRasterizer;II)V
+HSPLandroidx/emoji2/text/MetadataRepo;-><init>(Landroid/graphics/Typeface;Landroidx/emoji2/text/flatbuffer/MetadataList;)V
+HSPLandroidx/emoji2/text/TypefaceEmojiRasterizer;-><clinit>()V
+HSPLandroidx/emoji2/text/TypefaceEmojiRasterizer;-><init>(Landroidx/emoji2/text/MetadataRepo;I)V
+HSPLandroidx/emoji2/text/TypefaceEmojiRasterizer;->getCodepointAt(I)I
+HSPLandroidx/emoji2/text/TypefaceEmojiRasterizer;->getCodepointsLength()I
+HSPLandroidx/emoji2/text/TypefaceEmojiRasterizer;->getMetadataItem()Landroidx/emoji2/text/flatbuffer/MetadataItem;
+HSPLandroidx/emoji2/text/flatbuffer/Table;-><init>()V
+HSPLandroidx/emoji2/text/flatbuffer/Table;->__offset(I)I
+HSPLandroidx/lifecycle/DefaultLifecycleObserver;->onResume(Landroidx/lifecycle/LifecycleOwner;)V
+HSPLandroidx/lifecycle/DefaultLifecycleObserver;->onStart(Landroidx/lifecycle/LifecycleOwner;)V
+HSPLandroidx/lifecycle/DefaultLifecycleObserverAdapter$WhenMappings;-><clinit>()V
+HSPLandroidx/lifecycle/DefaultLifecycleObserverAdapter;-><init>(Landroidx/lifecycle/DefaultLifecycleObserver;Landroidx/lifecycle/LifecycleEventObserver;)V
+HSPLandroidx/lifecycle/DefaultLifecycleObserverAdapter;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLandroidx/lifecycle/EmptyActivityLifecycleCallbacks;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
+HSPLandroidx/lifecycle/EmptyActivityLifecycleCallbacks;->onActivityResumed(Landroid/app/Activity;)V
+HSPLandroidx/lifecycle/EmptyActivityLifecycleCallbacks;->onActivityStarted(Landroid/app/Activity;)V
+HSPLandroidx/lifecycle/Lifecycle$Event$Companion;->upFrom(Landroidx/lifecycle/Lifecycle$State;)Landroidx/lifecycle/Lifecycle$Event;
+HSPLandroidx/lifecycle/Lifecycle$Event$WhenMappings;-><clinit>()V
+HSPLandroidx/lifecycle/Lifecycle$Event;-><clinit>()V
+HSPLandroidx/lifecycle/Lifecycle$Event;-><init>(ILjava/lang/String;)V
+HSPLandroidx/lifecycle/Lifecycle$Event;->getTargetState()Landroidx/lifecycle/Lifecycle$State;
+HSPLandroidx/lifecycle/Lifecycle$Event;->values()[Landroidx/lifecycle/Lifecycle$Event;
+HSPLandroidx/lifecycle/Lifecycle$State;-><clinit>()V
+HSPLandroidx/lifecycle/Lifecycle$State;-><init>(ILjava/lang/String;)V
+HSPLandroidx/lifecycle/Lifecycle;-><init>()V
+HSPLandroidx/lifecycle/LifecycleDestroyedException;-><init>(I)V
+HSPLandroidx/lifecycle/LifecycleDestroyedException;-><init>(II)V
+HSPLandroidx/lifecycle/LifecycleDestroyedException;->fillInStackTrace()Ljava/lang/Throwable;
+HSPLandroidx/lifecycle/LifecycleDispatcher$DispatcherActivityCallback;-><init>()V
+HSPLandroidx/lifecycle/LifecycleDispatcher$DispatcherActivityCallback;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
+HSPLandroidx/lifecycle/LifecycleDispatcher;-><clinit>()V
+HSPLandroidx/lifecycle/LifecycleRegistry$ObserverWithState;-><init>(Landroidx/lifecycle/LifecycleObserver;Landroidx/lifecycle/Lifecycle$State;)V
+HSPLandroidx/lifecycle/LifecycleRegistry$ObserverWithState;->dispatchEvent(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLandroidx/lifecycle/LifecycleRegistry;-><init>(Landroidx/lifecycle/LifecycleOwner;)V
+HSPLandroidx/lifecycle/LifecycleRegistry;->addObserver(Landroidx/lifecycle/LifecycleObserver;)V
+HSPLandroidx/lifecycle/LifecycleRegistry;->calculateTargetState(Landroidx/lifecycle/LifecycleObserver;)Landroidx/lifecycle/Lifecycle$State;
+HSPLandroidx/lifecycle/LifecycleRegistry;->enforceMainThreadIfNeeded(Ljava/lang/String;)V
+HSPLandroidx/lifecycle/LifecycleRegistry;->handleLifecycleEvent(Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLandroidx/lifecycle/LifecycleRegistry;->moveToState(Landroidx/lifecycle/Lifecycle$State;)V
+HSPLandroidx/lifecycle/LifecycleRegistry;->removeObserver(Landroidx/lifecycle/LifecycleObserver;)V
+HSPLandroidx/lifecycle/LifecycleRegistry;->sync()V
+HSPLandroidx/lifecycle/Lifecycling;-><clinit>()V
+HSPLandroidx/lifecycle/ProcessLifecycleInitializer;-><init>()V
+HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->create(Landroid/content/Context;)Ljava/lang/Object;
+HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->dependencies()Ljava/util/List;
+HSPLandroidx/lifecycle/ProcessLifecycleOwner$Api29Impl;->registerActivityLifecycleCallbacks(Landroid/app/Activity;Landroid/app/Application$ActivityLifecycleCallbacks;)V
+HSPLandroidx/lifecycle/ProcessLifecycleOwner$attach$1$onActivityPreCreated$1;-><init>(Landroidx/lifecycle/ProcessLifecycleOwner;)V
+HSPLandroidx/lifecycle/ProcessLifecycleOwner$attach$1$onActivityPreCreated$1;->onActivityPostResumed(Landroid/app/Activity;)V
+HSPLandroidx/lifecycle/ProcessLifecycleOwner$attach$1$onActivityPreCreated$1;->onActivityPostStarted(Landroid/app/Activity;)V
+HSPLandroidx/lifecycle/ProcessLifecycleOwner$attach$1;-><init>(Landroidx/lifecycle/ProcessLifecycleOwner;)V
+HSPLandroidx/lifecycle/ProcessLifecycleOwner$attach$1;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
+HSPLandroidx/lifecycle/ProcessLifecycleOwner$attach$1;->onActivityPreCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
+HSPLandroidx/lifecycle/ProcessLifecycleOwner$initializationListener$1;-><init>(Landroidx/lifecycle/ProcessLifecycleOwner;)V
+HSPLandroidx/lifecycle/ProcessLifecycleOwner;-><clinit>()V
+HSPLandroidx/lifecycle/ProcessLifecycleOwner;-><init>()V
+HSPLandroidx/lifecycle/ProcessLifecycleOwner;->activityResumed$lifecycle_process_release()V
+HSPLandroidx/lifecycle/ProcessLifecycleOwner;->getLifecycle()Landroidx/lifecycle/LifecycleRegistry;
+HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;-><clinit>()V
+HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;-><init>()V
+HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
+HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPostCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
+HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPostResumed(Landroid/app/Activity;)V
+HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPostStarted(Landroid/app/Activity;)V
+HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityResumed(Landroid/app/Activity;)V
+HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityStarted(Landroid/app/Activity;)V
+HSPLandroidx/lifecycle/ReportFragment;-><init>()V
+HSPLandroidx/lifecycle/ReportFragment;->dispatch(Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLandroidx/lifecycle/ReportFragment;->onActivityCreated(Landroid/os/Bundle;)V
+HSPLandroidx/lifecycle/ReportFragment;->onResume()V
+HSPLandroidx/lifecycle/ReportFragment;->onStart()V
+HSPLandroidx/lifecycle/SavedStateHandleAttacher;-><init>(Landroidx/lifecycle/SavedStateHandlesProvider;)V
+HSPLandroidx/lifecycle/SavedStateHandleAttacher;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLandroidx/lifecycle/SavedStateHandlesProvider;-><init>(Landroidx/savedstate/SavedStateRegistry;Landroidx/lifecycle/ViewModelStoreOwner;)V
+HSPLandroidx/lifecycle/SavedStateHandlesVM;-><init>()V
+HSPLandroidx/lifecycle/ViewModelStore;-><init>()V
+HSPLandroidx/lifecycle/viewmodel/CreationExtras$Empty;-><clinit>()V
+HSPLandroidx/lifecycle/viewmodel/CreationExtras;-><init>()V
+HSPLandroidx/lifecycle/viewmodel/MutableCreationExtras;-><init>(Landroidx/lifecycle/viewmodel/CreationExtras;)V
+HSPLandroidx/lifecycle/viewmodel/ViewModelInitializer;-><init>(Ljava/lang/Class;)V
+HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener;-><init>(Ljava/util/ArrayList;)V
+HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener;->onFrameMetricsAvailable(Landroid/view/Window;Landroid/view/FrameMetrics;I)V
+HSPLandroidx/metrics/performance/FrameData;-><init>(JJZLjava/util/ArrayList;)V
+HSPLandroidx/metrics/performance/FrameDataApi24;-><init>(JJJZLjava/util/ArrayList;)V
+HSPLandroidx/metrics/performance/FrameDataApi31;-><init>(JJJJZLjava/util/ArrayList;)V
+HSPLandroidx/metrics/performance/FrameDataApi31;->copy()Landroidx/metrics/performance/FrameData;
+HSPLandroidx/metrics/performance/JankStats;-><init>(Landroid/view/Window;Lcom/example/tvcomposebasedtests/JankStatsAggregator$listener$1;)V
+HSPLandroidx/metrics/performance/JankStats;->logFrameData$metrics_performance_release(Landroidx/metrics/performance/FrameData;)V
+HSPLandroidx/metrics/performance/JankStatsApi16Impl;-><init>(Landroidx/metrics/performance/JankStats;Landroid/view/View;)V
+HSPLandroidx/metrics/performance/JankStatsApi22Impl;-><init>(Landroidx/metrics/performance/JankStats;Landroid/view/View;)V
+HSPLandroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda0;-><init>(Landroidx/metrics/performance/JankStatsApi24Impl;Landroidx/metrics/performance/JankStats;)V
+HSPLandroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda0;->onFrameMetricsAvailable(Landroid/view/Window;Landroid/view/FrameMetrics;I)V
+HSPLandroidx/metrics/performance/JankStatsApi24Impl;-><init>(Landroidx/metrics/performance/JankStats;Landroid/view/View;Landroid/view/Window;)V
+HSPLandroidx/metrics/performance/JankStatsApi24Impl;->getOrCreateFrameMetricsListenerDelegator(Landroid/view/Window;)Landroidx/metrics/performance/DelegatingFrameMetricsListener;
+HSPLandroidx/metrics/performance/JankStatsApi24Impl;->setupFrameTimer(Z)V
+HSPLandroidx/metrics/performance/JankStatsApi26Impl;-><init>(Landroidx/metrics/performance/JankStats;Landroid/view/View;Landroid/view/Window;)V
+HSPLandroidx/metrics/performance/JankStatsApi26Impl;->getFrameStartTime$metrics_performance_release(Landroid/view/FrameMetrics;)J
+HSPLandroidx/metrics/performance/JankStatsApi31Impl;-><init>(Landroidx/metrics/performance/JankStats;Landroid/view/View;Landroid/view/Window;)V
+HSPLandroidx/metrics/performance/JankStatsApi31Impl;->getExpectedFrameDuration(Landroid/view/FrameMetrics;)J
+HSPLandroidx/metrics/performance/JankStatsApi31Impl;->getFrameData$metrics_performance_release(JJLandroid/view/FrameMetrics;)Landroidx/metrics/performance/FrameDataApi24;
+HSPLandroidx/metrics/performance/PerformanceMetricsState;-><init>()V
+HSPLandroidx/metrics/performance/PerformanceMetricsState;->addFrameState(JJLjava/util/ArrayList;Ljava/util/ArrayList;)V
+HSPLandroidx/metrics/performance/PerformanceMetricsState;->cleanupSingleFrameStates$metrics_performance_release()V
+HSPLandroidx/metrics/performance/PerformanceMetricsState;->getIntervalStates$metrics_performance_release(JJLjava/util/ArrayList;)V
+HSPLandroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda0;-><init>(Ljava/lang/Object;ILjava/lang/Object;)V
+HSPLandroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda0;->run()V
+HSPLandroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda1;-><init>(Landroid/content/Context;I)V
+HSPLandroidx/profileinstaller/ProfileInstallerInitializer$Choreographer16Impl;->postFrameCallback(Ljava/lang/Runnable;)V
+HSPLandroidx/profileinstaller/ProfileInstallerInitializer$Handler28Impl;->createAsync(Landroid/os/Looper;)Landroid/os/Handler;
+HSPLandroidx/profileinstaller/ProfileInstallerInitializer;-><init>()V
+HSPLandroidx/profileinstaller/ProfileInstallerInitializer;->create(Landroid/content/Context;)Ljava/lang/Object;
+HSPLandroidx/profileinstaller/ProfileInstallerInitializer;->dependencies()Ljava/util/List;
+HSPLandroidx/savedstate/Recreator;-><init>(Landroidx/savedstate/SavedStateRegistryOwner;)V
+HSPLandroidx/savedstate/Recreator;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLandroidx/savedstate/SavedStateRegistry$$ExternalSyntheticLambda0;-><init>(Landroidx/savedstate/SavedStateRegistry;)V
+HSPLandroidx/savedstate/SavedStateRegistry$$ExternalSyntheticLambda0;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLandroidx/savedstate/SavedStateRegistry;-><init>()V
+HSPLandroidx/savedstate/SavedStateRegistry;->consumeRestoredStateForKey(Ljava/lang/String;)Landroid/os/Bundle;
+HSPLandroidx/savedstate/SavedStateRegistry;->registerSavedStateProvider(Ljava/lang/String;Landroidx/savedstate/SavedStateRegistry$SavedStateProvider;)V
+HSPLandroidx/savedstate/SavedStateRegistryController;-><init>(Landroidx/savedstate/SavedStateRegistryOwner;)V
+HSPLandroidx/savedstate/SavedStateRegistryController;->performAttach()V
+HSPLandroidx/startup/AppInitializer;-><clinit>()V
+HSPLandroidx/startup/AppInitializer;-><init>(Landroid/content/Context;)V
+HSPLandroidx/startup/AppInitializer;->discoverAndInitialize(Landroid/os/Bundle;)V
+HSPLandroidx/startup/AppInitializer;->doInitialize(Ljava/lang/Class;Ljava/util/HashSet;)Ljava/lang/Object;
+HSPLandroidx/startup/AppInitializer;->getInstance(Landroid/content/Context;)Landroidx/startup/AppInitializer;
+HSPLandroidx/startup/InitializationProvider;-><init>()V
+HSPLandroidx/startup/InitializationProvider;->onCreate()Z
+HSPLandroidx/tracing/Trace$$ExternalSyntheticApiModelOutline0;->m()Z
+HSPLandroidx/tracing/Trace$$ExternalSyntheticApiModelOutline0;->m(Landroid/app/Activity;Landroidx/lifecycle/ReportFragment$LifecycleCallbacks;)V
+HSPLandroidx/tv/foundation/PivotOffsets;-><init>()V
+HSPLandroidx/tv/foundation/TvBringIntoViewSpec;-><init>(Landroidx/tv/foundation/PivotOffsets;Z)V
+HSPLandroidx/tv/foundation/TvBringIntoViewSpec;->calculateScrollDistance(FFF)F
+HSPLandroidx/tv/foundation/TvBringIntoViewSpec;->getScrollAnimationSpec()Landroidx/compose/animation/core/AnimationSpec;
+HSPLandroidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator;-><init>(I)V
+HSPLandroidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator;->reset()V
+HSPLandroidx/tv/foundation/lazy/grid/LazyGridKt$rememberLazyGridMeasurePolicy$1$1$3;-><init>(Landroidx/compose/foundation/lazy/layout/LazyLayoutMeasureScopeImpl;JIII)V
+HSPLandroidx/tv/foundation/lazy/grid/LazyGridKt$rememberLazyGridMeasurePolicy$1$1$3;->invoke(IILkotlin/jvm/functions/Function1;)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/tv/foundation/lazy/grid/LazyGridKt$rememberLazyGridMeasurePolicy$1$1$3;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/foundation/lazy/grid/LazyGridScrollPosition;-><init>(III)V
+HSPLandroidx/tv/foundation/lazy/grid/LazyGridScrollPosition;->getIndex()I
+HSPLandroidx/tv/foundation/lazy/grid/LazyGridScrollPosition;->setIndex(I)V
+HSPLandroidx/tv/foundation/lazy/grid/LazyGridScrollPosition;->update(II)V
+HSPLandroidx/tv/foundation/lazy/grid/TvLazyGridState$remeasurementModifier$1;-><init>(Landroidx/compose/foundation/gestures/ScrollableState;I)V
+HSPLandroidx/tv/foundation/lazy/layout/AwaitFirstLayoutModifier$waitForFirstLayout$1;-><init>(Landroidx/tv/foundation/lazy/layout/AwaitFirstLayoutModifier;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/tv/foundation/lazy/layout/AwaitFirstLayoutModifier;->waitForFirstLayout(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/tv/foundation/lazy/layout/LazyLayoutNearestRangeState;-><clinit>()V
+HSPLandroidx/tv/foundation/lazy/layout/LazyLayoutNearestRangeState;-><init>(III)V
+HSPLandroidx/tv/foundation/lazy/layout/LazyLayoutNearestRangeState;->getValue()Ljava/lang/Object;
+HSPLandroidx/tv/foundation/lazy/layout/LazyLayoutNearestRangeState;->update(I)V
+HSPLandroidx/tv/foundation/lazy/layout/LazyLayoutSemanticsKt$LazyLayoutSemanticState$1;-><init>(Landroidx/tv/foundation/lazy/list/TvLazyListState;Z)V
+HSPLandroidx/tv/foundation/lazy/layout/LazyLayoutSemanticsKt$LazyLayoutSemanticState$1;->collectionInfo()Landroidx/compose/ui/semantics/CollectionInfo;
+HSPLandroidx/tv/foundation/lazy/layout/LazyLayoutSemanticsKt$lazyLayoutSemantics$1$1;-><init>(Landroidx/tv/material3/TabKt$Tab$3$1;ZLandroidx/compose/ui/semantics/ScrollAxisRange;Landroidx/compose/foundation/ScrollKt$scroll$2$semantics$1$1;Landroidx/compose/foundation/layout/OffsetNode$measure$1;Landroidx/compose/ui/semantics/CollectionInfo;)V
+HSPLandroidx/tv/foundation/lazy/layout/NearestRangeKeyIndexMap$2$1;-><init>(IILjava/util/HashMap;Landroidx/tv/foundation/lazy/layout/NearestRangeKeyIndexMap;)V
+HSPLandroidx/tv/foundation/lazy/layout/NearestRangeKeyIndexMap$2$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/foundation/lazy/layout/NearestRangeKeyIndexMap;-><init>(Lkotlin/ranges/IntRange;Landroidx/tv/material3/TabKt;)V
+HSPLandroidx/tv/foundation/lazy/layout/NearestRangeKeyIndexMap;->getKey(I)Ljava/lang/Object;
+HSPLandroidx/tv/foundation/lazy/list/EmptyLazyListLayoutInfo;-><clinit>()V
+HSPLandroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsModifierLocal;-><clinit>()V
+HSPLandroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsModifierLocal;-><init>(Landroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsState;Landroidx/compose/runtime/Stack;ZLandroidx/compose/ui/unit/LayoutDirection;Landroidx/compose/foundation/gestures/Orientation;)V
+HSPLandroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsModifierLocal;->getKey()Landroidx/compose/ui/modifier/ProvidableModifierLocal;
+HSPLandroidx/tv/foundation/lazy/list/LazyListBeyondBoundsState;-><init>(Landroidx/tv/foundation/lazy/list/TvLazyListState;I)V
+HSPLandroidx/tv/foundation/lazy/list/LazyListItemProviderImpl;-><init>(Landroidx/tv/foundation/lazy/list/TvLazyListState;Landroidx/tv/foundation/lazy/list/TvLazyListIntervalContent;Landroidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl;Landroidx/tv/foundation/lazy/layout/NearestRangeKeyIndexMap;)V
+HSPLandroidx/tv/foundation/lazy/list/LazyListItemProviderImpl;->Item(ILjava/lang/Object;Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/tv/foundation/lazy/list/LazyListItemProviderImpl;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/tv/foundation/lazy/list/LazyListItemProviderImpl;->getContentType(I)Ljava/lang/Object;
+HSPLandroidx/tv/foundation/lazy/list/LazyListItemProviderImpl;->getItemCount()I
+HSPLandroidx/tv/foundation/lazy/list/LazyListItemProviderImpl;->getKey(I)Ljava/lang/Object;
+HSPLandroidx/tv/foundation/lazy/list/LazyListKt$rememberLazyListMeasurePolicy$1$1$measuredItemProvider$1;-><init>(JZLandroidx/tv/foundation/lazy/list/LazyListItemProviderImpl;Landroidx/compose/foundation/lazy/layout/LazyLayoutMeasureScopeImpl;IILandroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/Alignment$Vertical;ZIIJ)V
+HSPLandroidx/tv/foundation/lazy/list/LazyListKt$rememberLazyListMeasurePolicy$1$1$measuredItemProvider$1;->getAndMeasure(I)Landroidx/tv/foundation/lazy/list/LazyListMeasuredItem;
+HSPLandroidx/tv/foundation/lazy/list/LazyListKt$rememberLazyListMeasurePolicy$1$1;-><init>(Landroidx/tv/foundation/lazy/list/TvLazyListState;ZLandroidx/compose/foundation/layout/PaddingValuesImpl;ZLkotlin/reflect/KProperty0;Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/foundation/layout/Arrangement$Horizontal;ILandroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/Alignment$Vertical;)V
+HSPLandroidx/tv/foundation/lazy/list/LazyListKt$rememberLazyListMeasurePolicy$1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/foundation/lazy/list/LazyListMeasureResult;-><init>(Landroidx/tv/foundation/lazy/list/LazyListMeasuredItem;IZFLandroidx/compose/ui/layout/MeasureResult;FLjava/util/List;II)V
+HSPLandroidx/tv/foundation/lazy/list/LazyListMeasureResult;->getAlignmentLines()Ljava/util/Map;
+HSPLandroidx/tv/foundation/lazy/list/LazyListMeasureResult;->getHeight()I
+HSPLandroidx/tv/foundation/lazy/list/LazyListMeasureResult;->getTotalItemsCount()I
+HSPLandroidx/tv/foundation/lazy/list/LazyListMeasureResult;->getVisibleItemsInfo()Ljava/util/List;
+HSPLandroidx/tv/foundation/lazy/list/LazyListMeasureResult;->getWidth()I
+HSPLandroidx/tv/foundation/lazy/list/LazyListMeasureResult;->placeChildren()V
+HSPLandroidx/tv/foundation/lazy/list/LazyListMeasuredItem;-><init>(ILjava/util/List;ZLandroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/Alignment$Vertical;Landroidx/compose/ui/unit/LayoutDirection;ZIIIJLjava/lang/Object;Ljava/lang/Object;)V
+HSPLandroidx/tv/foundation/lazy/list/LazyListMeasuredItem;->getParentData(I)V
+HSPLandroidx/tv/foundation/lazy/list/LazyListMeasuredItem;->place(Landroidx/compose/ui/layout/Placeable$PlacementScope;)V
+HSPLandroidx/tv/foundation/lazy/list/LazyListMeasuredItem;->position(III)V
+HSPLandroidx/tv/foundation/lazy/list/LazyListStateKt$rememberTvLazyListState$1$1;-><init>(III)V
+HSPLandroidx/tv/foundation/lazy/list/LazyListStateKt$rememberTvLazyListState$1$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/tv/foundation/lazy/list/LazyListStateKt;-><clinit>()V
+HSPLandroidx/tv/foundation/lazy/list/LazyListStateKt;->rememberTvLazyListState(Landroidx/compose/runtime/Composer;)Landroidx/tv/foundation/lazy/list/TvLazyListState;
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListInterval;-><init>(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/internal/ComposableLambdaImpl;)V
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListInterval;->getKey()Lkotlin/jvm/functions/Function1;
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListInterval;->getType()Lkotlin/jvm/functions/Function1;
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListIntervalContent;-><init>(Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListIntervalContent;->getIntervals()Landroidx/compose/foundation/lazy/layout/MutableIntervalList;
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl;-><init>()V
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListScope;->items$default(Landroidx/tv/foundation/lazy/list/TvLazyListScope;ILandroidx/compose/runtime/internal/ComposableLambdaImpl;)V
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListState$scroll$1;-><init>(Landroidx/tv/foundation/lazy/list/TvLazyListState;Lkotlin/coroutines/Continuation;)V
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListState$scroll$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListState;-><clinit>()V
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListState;-><init>(II)V
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListState;->applyMeasureResult$tv_foundation_release(Landroidx/tv/foundation/lazy/list/LazyListMeasureResult;Z)V
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListState;->getCanScrollBackward()Z
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListState;->getCanScrollForward()Z
+HSPLandroidx/tv/foundation/lazy/list/TvLazyListState;->scroll(Landroidx/compose/foundation/MutatePriority;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/Border;-><clinit>()V
+HSPLandroidx/tv/material3/Border;-><init>(Landroidx/compose/foundation/BorderStroke;FLandroidx/compose/ui/graphics/Shape;)V
+HSPLandroidx/tv/material3/Border;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/tv/material3/ColorScheme;-><init>(JJJJJJJJJJJJJJJJJJJJJJJJJJJJJ)V
+HSPLandroidx/tv/material3/ColorScheme;->getInverseSurface-0d7_KjU()J
+HSPLandroidx/tv/material3/ColorScheme;->getOnSurface-0d7_KjU()J
+HSPLandroidx/tv/material3/ColorScheme;->getSurface-0d7_KjU()J
+HSPLandroidx/tv/material3/ColorSchemeKt$LocalColorScheme$1;-><clinit>()V
+HSPLandroidx/tv/material3/ColorSchemeKt$LocalColorScheme$1;-><init>(I)V
+HSPLandroidx/tv/material3/ColorSchemeKt$LocalColorScheme$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/tv/material3/ColorSchemeKt;-><clinit>()V
+HSPLandroidx/tv/material3/ColorSchemeKt;->contentColorFor-ek8zF_U(JLandroidx/compose/runtime/Composer;)J
+HSPLandroidx/tv/material3/ContentColorKt;-><clinit>()V
+HSPLandroidx/tv/material3/Glow;-><clinit>()V
+HSPLandroidx/tv/material3/Glow;-><init>(JF)V
+HSPLandroidx/tv/material3/Glow;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/tv/material3/GlowIndication;-><init>(JLandroidx/compose/ui/graphics/Shape;FFF)V
+HSPLandroidx/tv/material3/GlowIndication;->rememberUpdatedInstance(Landroidx/compose/foundation/interaction/InteractionSource;Landroidx/compose/runtime/Composer;)Landroidx/compose/foundation/IndicationInstance;
+HSPLandroidx/tv/material3/GlowIndicationInstance;-><init>(JLandroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/unit/Density;FFF)V
+HSPLandroidx/tv/material3/GlowIndicationInstance;->drawIndication(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V
+HSPLandroidx/tv/material3/ScaleIndication;-><init>(F)V
+HSPLandroidx/tv/material3/ScaleIndicationInstance;-><init>(F)V
+HSPLandroidx/tv/material3/ScaleIndicationInstance;->drawIndication(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V
+HSPLandroidx/tv/material3/ScaleIndicationTokens;-><clinit>()V
+HSPLandroidx/tv/material3/ShapesKt$LocalShapes$1;-><clinit>()V
+HSPLandroidx/tv/material3/ShapesKt$LocalShapes$1;-><init>(I)V
+HSPLandroidx/tv/material3/ShapesKt$LocalShapes$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/tv/material3/SurfaceKt$Surface$4;-><init>(ZLkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function0;FLandroidx/tv/material3/ToggleableSurfaceShape;Landroidx/tv/material3/ToggleableSurfaceColors;Landroidx/tv/material3/ToggleableSurfaceScale;Landroidx/tv/material3/ToggleableSurfaceBorder;Landroidx/tv/material3/ToggleableSurfaceGlow;Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Lkotlin/jvm/functions/Function3;III)V
+HSPLandroidx/tv/material3/SurfaceKt$SurfaceImpl$2$2$1;-><init>(ILjava/lang/Object;)V
+HSPLandroidx/tv/material3/SurfaceKt$SurfaceImpl$2$2$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/SurfaceKt$SurfaceImpl$2$4$1;-><init>(Landroidx/compose/ui/graphics/Shape;JI)V
+HSPLandroidx/tv/material3/SurfaceKt$SurfaceImpl$2$4$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/SurfaceKt$SurfaceImpl$2$5$1;-><init>(FLandroidx/compose/ui/graphics/Shape;)V
+HSPLandroidx/tv/material3/SurfaceKt$SurfaceImpl$2$5$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/SurfaceKt$SurfaceImpl$2;-><init>(JILandroidx/compose/ui/Modifier;Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;FLandroidx/tv/material3/Glow;Landroidx/compose/ui/graphics/Shape;Landroidx/tv/material3/Border;FLandroidx/compose/runtime/MutableState;ZLkotlin/jvm/functions/Function3;I)V
+HSPLandroidx/tv/material3/SurfaceKt$SurfaceImpl$3;-><init>(Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/graphics/Shape;JJFLandroidx/tv/material3/Border;Landroidx/tv/material3/Glow;FLandroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Lkotlin/jvm/functions/Function3;III)V
+HSPLandroidx/tv/material3/SurfaceKt$SurfaceImpl$3;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/SurfaceKt$handleDPadEnter$2$1;-><init>(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;I)V
+HSPLandroidx/tv/material3/SurfaceKt$handleDPadEnter$2$1;->invoke(Landroidx/compose/animation/core/AnimationScope;)V
+HSPLandroidx/tv/material3/SurfaceKt$handleDPadEnter$2$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/SurfaceKt$handleDPadEnter$2$2;-><init>(Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ZLandroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Landroidx/compose/foundation/interaction/PressInteraction$Press;Landroidx/compose/runtime/MutableState;)V
+HSPLandroidx/tv/material3/SurfaceKt$handleDPadEnter$2$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/SurfaceKt$handleDPadEnter$2;-><init>(Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ZZ)V
+HSPLandroidx/tv/material3/SurfaceKt$handleDPadEnter$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/SurfaceKt$tvToggleable$1$1;-><init>(Landroidx/compose/ui/platform/AndroidComposeView;Z)V
+HSPLandroidx/tv/material3/SurfaceKt$tvToggleable$1$2;-><init>(Lkotlin/jvm/functions/Function0;I)V
+HSPLandroidx/tv/material3/SurfaceKt$tvToggleable$1$2;->invoke()Ljava/lang/Object;
+HSPLandroidx/tv/material3/SurfaceKt$tvToggleable$1;-><init>(ZLkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function0;)V
+HSPLandroidx/tv/material3/SurfaceKt;-><clinit>()V
+HSPLandroidx/tv/material3/SurfaceKt;->Surface-xYaah8o(ZLkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function0;FLandroidx/tv/material3/ToggleableSurfaceShape;Landroidx/tv/material3/ToggleableSurfaceColors;Landroidx/tv/material3/ToggleableSurfaceScale;Landroidx/tv/material3/ToggleableSurfaceBorder;Landroidx/tv/material3/ToggleableSurfaceGlow;Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
+HSPLandroidx/tv/material3/SurfaceKt;->Surface_xYaah8o$lambda$4(Landroidx/compose/runtime/MutableState;)Z
+HSPLandroidx/tv/material3/SurfaceKt;->Surface_xYaah8o$lambda$5(Landroidx/compose/runtime/MutableState;)Z
+HSPLandroidx/tv/material3/SurfaceKt;->access$surfaceColorAtElevation-CLU3JFs(JFLandroidx/compose/runtime/Composer;)J
+HSPLandroidx/tv/material3/TabColors;-><init>(JJJJJJJJ)V
+HSPLandroidx/tv/material3/TabKt$Tab$1;-><clinit>()V
+HSPLandroidx/tv/material3/TabKt$Tab$1;-><init>()V
+HSPLandroidx/tv/material3/TabKt$Tab$3$1;-><init>(Lkotlin/jvm/functions/Function0;I)V
+HSPLandroidx/tv/material3/TabKt$Tab$3$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabKt$Tab$4$1;-><init>(IZ)V
+HSPLandroidx/tv/material3/TabKt$Tab$4$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabKt$Tab$6;-><init>(Lkotlin/jvm/functions/Function3;I)V
+HSPLandroidx/tv/material3/TabKt$Tab$6;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabKt$Tab$7;-><init>(Landroidx/tv/material3/TabRowScopeImpl;ZLkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;ZLandroidx/tv/material3/TabColors;Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Lkotlin/jvm/functions/Function3;II)V
+HSPLandroidx/tv/material3/TabKt;-><clinit>()V
+HSPLandroidx/tv/material3/TabKt;->BasicText-BpD7jsM(Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function1;IZILandroidx/compose/runtime/Composer;II)V
+HSPLandroidx/tv/material3/TabKt;->BasicText-VhcvRP8(Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function1;IZIILandroidx/compose/runtime/Composer;II)V
+HSPLandroidx/tv/material3/TabKt;->DisposableEffect(Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;)V
+HSPLandroidx/tv/material3/TabKt;->LaunchedEffect(Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;)V
+HSPLandroidx/tv/material3/TabKt;->LaunchedEffect(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;)V
+HSPLandroidx/tv/material3/TabKt;->LazyLayout(Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
+HSPLandroidx/tv/material3/TabKt;->LazyLayoutPrefetcher(Landroidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState;Landroidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory;Landroidx/compose/ui/layout/SubcomposeLayoutState;Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/tv/material3/TabKt;->SideEffect(Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;)V
+HSPLandroidx/tv/material3/TabKt;->Tab(Landroidx/tv/material3/TabRowScopeImpl;ZLkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;ZLandroidx/tv/material3/TabColors;Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
+HSPLandroidx/tv/material3/TabKt;->TabRow-pAZo6Ak(ILandroidx/compose/ui/Modifier;JJLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
+HSPLandroidx/tv/material3/TabKt;->access$binarySearch(ILandroidx/compose/runtime/collection/MutableVector;)I
+HSPLandroidx/tv/material3/TabKt;->animate(Landroidx/compose/animation/core/AnimationState;Landroidx/compose/animation/core/Animation;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabKt;->callWithFrameNanos(Landroidx/compose/animation/core/Animation;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/core/SuspendAnimationKt$animate$4;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabKt;->collectIsFocusedAsState(Landroidx/compose/foundation/interaction/InteractionSource;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/MutableState;
+HSPLandroidx/tv/material3/TabKt;->complexSqrt(D)Landroidx/compose/animation/core/ComplexDouble;
+HSPLandroidx/tv/material3/TabKt;->copy(Landroidx/compose/animation/core/AnimationVector;)Landroidx/compose/animation/core/AnimationVector;
+HSPLandroidx/tv/material3/TabKt;->createCompositionCoroutineScope(Landroidx/compose/runtime/Composer;)Lkotlinx/coroutines/internal/ContextScope;
+HSPLandroidx/tv/material3/TabKt;->derivedStateObservers()Landroidx/compose/runtime/collection/MutableVector;
+HSPLandroidx/tv/material3/TabKt;->doAnimationFrameWithScale(Landroidx/compose/animation/core/AnimationScope;JFLandroidx/compose/animation/core/Animation;Landroidx/compose/animation/core/AnimationState;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/tv/material3/TabKt;->finalConstraints-tfFHcEY(JZIF)J
+HSPLandroidx/tv/material3/TabKt;->getContentType(I)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabKt;->getDurationScale(Lkotlin/coroutines/CoroutineContext;)F
+HSPLandroidx/tv/material3/TabKt;->getPoolingContainerListenerHolder(Landroid/view/View;)Landroidx/customview/poolingcontainer/PoolingContainerListenerHolder;
+HSPLandroidx/tv/material3/TabKt;->mutableStateOf$default(Ljava/lang/Object;)Landroidx/compose/runtime/ParcelableSnapshotMutableState;
+HSPLandroidx/tv/material3/TabKt;->read(Landroidx/compose/runtime/PersistentCompositionLocalMap;Landroidx/compose/runtime/ProvidableCompositionLocal;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabKt;->rememberSaveable([Ljava/lang/Object;Landroidx/compose/runtime/saveable/SaverKt$Saver$1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabKt;->rememberUpdatedState(Ljava/lang/Object;Landroidx/compose/runtime/Composer;)Landroidx/compose/runtime/MutableState;
+HSPLandroidx/tv/material3/TabKt;->resumeCancellableWith(Lkotlin/coroutines/Continuation;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)V
+HSPLandroidx/tv/material3/TabKt;->spring$default(FLjava/lang/Comparable;I)Landroidx/compose/animation/core/SpringSpec;
+HSPLandroidx/tv/material3/TabKt;->tween$default(ILandroidx/compose/animation/core/Easing;)Landroidx/compose/animation/core/TweenSpec;
+HSPLandroidx/tv/material3/TabKt;->updateCompositionMap([Landroidx/compose/runtime/ProvidedValue;Landroidx/compose/runtime/PersistentCompositionLocalMap;Landroidx/compose/runtime/PersistentCompositionLocalMap;)Landroidx/compose/runtime/internal/PersistentCompositionLocalHashMap;
+HSPLandroidx/tv/material3/TabKt;->updateState(Landroidx/compose/animation/core/AnimationScope;Landroidx/compose/animation/core/AnimationState;)V
+HSPLandroidx/tv/material3/TabKt;->withFrameNanos(Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabRowDefaults$PillIndicator$1;-><init>(Landroidx/tv/material3/TabRowDefaults;Landroidx/compose/ui/unit/DpRect;ZLandroidx/compose/ui/Modifier;JJII)V
+HSPLandroidx/tv/material3/TabRowDefaults$PillIndicator$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabRowDefaults;-><clinit>()V
+HSPLandroidx/tv/material3/TabRowDefaults;->PillIndicator-jA1GFJw(Landroidx/compose/ui/unit/DpRect;ZLandroidx/compose/ui/Modifier;JJLandroidx/compose/runtime/Composer;II)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$1;-><init>(I)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$1$1;-><init>(Landroidx/compose/runtime/MutableState;I)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$1$1;->invoke(Landroidx/compose/ui/focus/FocusState;)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$2$1$1$2;-><init>(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;II)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$2$1$1$2;-><init>(Lkotlin/jvm/functions/Function4;Ljava/util/ArrayList;ILandroidx/compose/runtime/MutableState;)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$2$1$1$2;->invoke(Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$2$1$1$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$2$1$1;-><init>(Ljava/util/ArrayList;Landroidx/compose/ui/layout/SubcomposeMeasureScope;Ljava/util/ArrayList;ILkotlin/jvm/functions/Function4;ILandroidx/compose/runtime/MutableState;II)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$2$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$2$1$separators$1;-><init>(IILkotlin/jvm/functions/Function1;)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$2$1$separators$1;-><init>(ILkotlin/jvm/functions/Function2;I)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$2$1$separators$1;->invoke(Landroidx/compose/runtime/Composer;I)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$2$1$separators$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$2$1;-><init>(Landroidx/compose/runtime/MutableState;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function4;)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2$2$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2;-><init>(Landroidx/compose/ui/Modifier;JLandroidx/compose/foundation/ScrollState;Landroidx/compose/runtime/MutableState;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function4;I)V
+HSPLandroidx/tv/material3/TabRowKt$TabRow$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TabRowKt$TabRow$3;-><init>(ILandroidx/compose/ui/Modifier;JJLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;II)V
+HSPLandroidx/tv/material3/TabRowScopeImpl;-><init>(Z)V
+HSPLandroidx/tv/material3/TabRowSlots;-><clinit>()V
+HSPLandroidx/tv/material3/TabRowSlots;-><init>(ILjava/lang/String;)V
+HSPLandroidx/tv/material3/TextKt$Text$1;-><clinit>()V
+HSPLandroidx/tv/material3/TextKt$Text$1;-><init>()V
+HSPLandroidx/tv/material3/TextKt$Text$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/TextKt$Text$2;-><init>(Ljava/lang/String;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontFamily;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/text/style/TextAlign;JIZILkotlin/jvm/functions/Function1;Landroidx/compose/ui/text/TextStyle;IIII)V
+HSPLandroidx/tv/material3/TextKt;-><clinit>()V
+HSPLandroidx/tv/material3/TextKt;->Text-fLXpl1I(Ljava/lang/String;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontFamily;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/text/style/TextAlign;JIZILkotlin/jvm/functions/Function1;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/runtime/Composer;III)V
+HSPLandroidx/tv/material3/ToggleableSurfaceBorder;-><init>(Landroidx/tv/material3/Border;Landroidx/tv/material3/Border;Landroidx/tv/material3/Border;Landroidx/tv/material3/Border;Landroidx/tv/material3/Border;Landroidx/tv/material3/Border;Landroidx/tv/material3/Border;Landroidx/tv/material3/Border;Landroidx/tv/material3/Border;Landroidx/tv/material3/Border;)V
+HSPLandroidx/tv/material3/ToggleableSurfaceColors;-><init>(JJJJJJJJJJJJJJ)V
+HSPLandroidx/tv/material3/ToggleableSurfaceColors;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/tv/material3/ToggleableSurfaceGlow;-><init>(Landroidx/tv/material3/Glow;Landroidx/tv/material3/Glow;Landroidx/tv/material3/Glow;Landroidx/tv/material3/Glow;Landroidx/tv/material3/Glow;Landroidx/tv/material3/Glow;)V
+HSPLandroidx/tv/material3/ToggleableSurfaceScale;-><clinit>()V
+HSPLandroidx/tv/material3/ToggleableSurfaceScale;-><init>(FFFFFFFFFF)V
+HSPLandroidx/tv/material3/ToggleableSurfaceShape;-><init>(Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)V
+HSPLandroidx/tv/material3/ToggleableSurfaceShape;->equals(Ljava/lang/Object;)Z
+HSPLandroidx/tv/material3/tokens/ColorLightTokens;-><clinit>()V
+HSPLandroidx/tv/material3/tokens/Elevation;-><clinit>()V
+HSPLandroidx/tv/material3/tokens/PaletteTokens;-><clinit>()V
+HSPLandroidx/tv/material3/tokens/ShapeTokens$BorderDefaultShape$1;-><clinit>()V
+HSPLandroidx/tv/material3/tokens/ShapeTokens$BorderDefaultShape$1;-><init>(I)V
+HSPLandroidx/tv/material3/tokens/ShapeTokens$BorderDefaultShape$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/tv/material3/tokens/ShapeTokens$BorderDefaultShape$1;->invoke(Ljava/util/List;II)Ljava/lang/Integer;
+HSPLandroidx/tv/material3/tokens/ShapeTokens$BorderDefaultShape$1;->invoke-3p2s80s(Landroidx/compose/ui/layout/MeasureScope;Landroidx/compose/ui/layout/Measurable;J)Landroidx/compose/ui/layout/MeasureResult;
+HSPLandroidx/tv/material3/tokens/TypographyTokensKt;-><clinit>()V
+HSPLcom/example/tvcomposebasedtests/ComposableSingletons$MainActivityKt;-><clinit>()V
+HSPLcom/example/tvcomposebasedtests/ComposableSingletons$UtilsKt$lambda-1$1;-><clinit>()V
+HSPLcom/example/tvcomposebasedtests/ComposableSingletons$UtilsKt$lambda-1$1;-><init>(I)V
+HSPLcom/example/tvcomposebasedtests/ComposableSingletons$UtilsKt$lambda-1$1;->invoke(Landroidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl;ILandroidx/compose/runtime/Composer;I)V
+HSPLcom/example/tvcomposebasedtests/ComposableSingletons$UtilsKt$lambda-1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLcom/example/tvcomposebasedtests/Config;-><init>(Landroid/content/Context;Landroidx/activity/ComponentActivity;II)V
+HSPLcom/example/tvcomposebasedtests/Config;->toString()Ljava/lang/String;
+HSPLcom/example/tvcomposebasedtests/JankStatsAggregator$listener$1;-><init>(Lcom/example/tvcomposebasedtests/JankStatsAggregator;)V
+HSPLcom/example/tvcomposebasedtests/JankStatsAggregator;-><init>(Landroid/view/Window;Lcom/example/tvcomposebasedtests/MainActivity$jankReportListener$1;)V
+HSPLcom/example/tvcomposebasedtests/MainActivity$jankReportListener$1;-><init>(Lcom/example/tvcomposebasedtests/MainActivity;)V
+HSPLcom/example/tvcomposebasedtests/MainActivity$startFrameMetrics$listener$1;-><init>(Lcom/example/tvcomposebasedtests/MainActivity;)V
+HSPLcom/example/tvcomposebasedtests/MainActivity$startFrameMetrics$listener$1;->onFrameMetricsAvailable(Landroid/view/Window;Landroid/view/FrameMetrics;I)V
+HSPLcom/example/tvcomposebasedtests/MainActivity;-><init>()V
+HSPLcom/example/tvcomposebasedtests/MainActivity;->onCreate(Landroid/os/Bundle;)V
+HSPLcom/example/tvcomposebasedtests/MainActivity;->onResume()V
+HSPLcom/example/tvcomposebasedtests/UtilsKt$AddJankMetrics$1$2;-><init>(ILjava/lang/Object;)V
+HSPLcom/example/tvcomposebasedtests/UtilsKt$AddJankMetrics$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLcom/example/tvcomposebasedtests/UtilsKt$ScrollingRow$1$1$1;-><init>(II)V
+HSPLcom/example/tvcomposebasedtests/UtilsKt$ScrollingRow$1$1$1;->invoke(Landroidx/tv/foundation/lazy/list/TvLazyListScope;)V
+HSPLcom/example/tvcomposebasedtests/UtilsKt$ScrollingRow$1$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLcom/example/tvcomposebasedtests/UtilsKt$ScrollingRow$2;-><init>(III)V
+HSPLcom/example/tvcomposebasedtests/UtilsKt$ScrollingRow$2;->invoke(Landroidx/compose/runtime/Composer;I)V
+HSPLcom/example/tvcomposebasedtests/UtilsKt$ScrollingRow$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLcom/example/tvcomposebasedtests/UtilsKt;-><clinit>()V
+HSPLcom/example/tvcomposebasedtests/tvComponents/AppKt$App$1;-><init>(ILjava/lang/Object;)V
+HSPLcom/example/tvcomposebasedtests/tvComponents/AppKt$App$1;->invoke(Landroidx/compose/runtime/Composer;I)V
+HSPLcom/example/tvcomposebasedtests/tvComponents/AppKt$App$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLcom/example/tvcomposebasedtests/tvComponents/AppKt$App$1;->invoke-5SAbXVA(JLandroidx/compose/ui/unit/LayoutDirection;)J
+HSPLcom/example/tvcomposebasedtests/tvComponents/ComposableSingletons$LazyContainersKt;-><clinit>()V
+HSPLcom/example/tvcomposebasedtests/tvComponents/ComposableSingletons$TopNavigationKt;-><clinit>()V
+HSPLcom/example/tvcomposebasedtests/tvComponents/Navigation;-><clinit>()V
+HSPLcom/example/tvcomposebasedtests/tvComponents/Navigation;-><init>(Ljava/lang/String;ILjava/lang/String;Landroidx/compose/runtime/internal/ComposableLambdaImpl;)V
+HSPLcom/example/tvcomposebasedtests/tvComponents/Navigation;->values()[Lcom/example/tvcomposebasedtests/tvComponents/Navigation;
+HSPLcom/example/tvcomposebasedtests/tvComponents/TopNavigationKt$PillIndicatorTabRow$1$1$1$1;-><init>(ILkotlin/jvm/functions/Function1;)V
+HSPLcom/example/tvcomposebasedtests/tvComponents/TopNavigationKt$PillIndicatorTabRow$1$1$1$1;->invoke()Ljava/lang/Object;
+HSPLcom/example/tvcomposebasedtests/tvComponents/TopNavigationKt$PillIndicatorTabRow$1;-><init>(IILjava/util/List;Lkotlin/jvm/functions/Function1;)V
+HSPLcom/example/tvcomposebasedtests/tvComponents/TopNavigationKt$PillIndicatorTabRow$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLcom/example/tvcomposebasedtests/tvComponents/TopNavigationKt$TopNavigation$3$1;-><init>(Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/MutableState;Lkotlin/coroutines/Continuation;)V
+HSPLcom/example/tvcomposebasedtests/tvComponents/TopNavigationKt$TopNavigation$3$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLcom/example/tvcomposebasedtests/tvComponents/TopNavigationKt$TopNavigation$3$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLcom/google/gson/internal/ConstructorConstructor;-><init>()V
+HSPLcom/google/gson/internal/ConstructorConstructor;->access$commitTransaction(Lcom/google/gson/internal/ConstructorConstructor;)V
+HSPLcom/google/gson/internal/LinkedTreeMap$1;-><init>(I)V
+HSPLcom/google/gson/internal/LinkedTreeMap$1;->compare(Ljava/lang/Object;Ljava/lang/Object;)I
+HSPLkotlin/Pair;-><init>(Ljava/lang/Object;Ljava/lang/Object;)V
+HSPLkotlin/Result$Failure;-><init>(Ljava/lang/Throwable;)V
+HSPLkotlin/Result;->exceptionOrNull-impl(Ljava/lang/Object;)Ljava/lang/Throwable;
+HSPLkotlin/ResultKt$$ExternalSyntheticCheckNotZero0;->m$1()Ljava/util/Iterator;
+HSPLkotlin/ResultKt$$ExternalSyntheticCheckNotZero0;->m(III)I
+HSPLkotlin/ResultKt$$ExternalSyntheticCheckNotZero0;->m(ILjava/lang/String;)V
+HSPLkotlin/ResultKt$$ExternalSyntheticCheckNotZero0;->m(Ljava/lang/Object;)V
+HSPLkotlin/ResultKt$$ExternalSyntheticCheckNotZero0;->stringValueOf$1(I)Ljava/lang/String;
+HSPLkotlin/ResultKt$$ExternalSyntheticCheckNotZero0;->stringValueOf$2(I)Ljava/lang/String;
+HSPLkotlin/ResultKt$$ExternalSyntheticCheckNotZero0;->valueOf$1(Ljava/lang/String;)I
+HSPLkotlin/ResultKt$$ExternalSyntheticCheckNotZero0;->valueOf(Ljava/lang/String;)I
+HSPLkotlin/ResultKt;-><clinit>()V
+HSPLkotlin/ResultKt;-><init>(Landroidx/metrics/performance/JankStats;)V
+HSPLkotlin/ResultKt;->App(Landroidx/compose/runtime/Composer;I)V
+HSPLkotlin/ResultKt;->Constraints$default(III)J
+HSPLkotlin/ResultKt;->Constraints(IIII)J
+HSPLkotlin/ResultKt;->Density(Landroid/content/Context;)Landroidx/compose/ui/unit/DensityImpl;
+HSPLkotlin/ResultKt;->DpOffset-YgX7TsA(FF)J
+HSPLkotlin/ResultKt;->IntOffset(II)J
+HSPLkotlin/ResultKt;->IntSize(II)J
+HSPLkotlin/ResultKt;->MaterialTheme(Landroidx/compose/material3/ColorScheme;Landroidx/compose/material3/Shapes;Landroidx/compose/material3/Typography;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
+HSPLkotlin/ResultKt;->SampleCardItem(ILandroidx/compose/runtime/Composer;I)V
+HSPLkotlin/ResultKt;->SampleTvLazyRow(ILandroidx/compose/runtime/Composer;I)V
+HSPLkotlin/ResultKt;->TvLazyRow(Landroidx/compose/ui/Modifier;Landroidx/tv/foundation/lazy/list/TvLazyListState;Landroidx/compose/foundation/layout/PaddingValuesImpl;ZLandroidx/compose/foundation/layout/Arrangement$Horizontal;Landroidx/compose/ui/Alignment$Vertical;ZLandroidx/tv/foundation/PivotOffsets;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
+HSPLkotlin/ResultKt;->access$getHasEmojiCompat(Landroidx/compose/ui/text/TextStyle;)Z
+HSPLkotlin/ResultKt;->areEqual(Ljava/lang/Object;Ljava/lang/Object;)Z
+HSPLkotlin/ResultKt;->canBeSavedToBundle(Ljava/lang/Object;)Z
+HSPLkotlin/ResultKt;->checkArgument(Ljava/lang/String;Z)V
+HSPLkotlin/ResultKt;->checkElementIndex$runtime_release(II)V
+HSPLkotlin/ResultKt;->checkNotNull$1(Ljava/lang/Object;Ljava/lang/String;)V
+HSPLkotlin/ResultKt;->checkNotNull(Ljava/lang/Object;)V
+HSPLkotlin/ResultKt;->checkNotNull(Ljava/lang/Object;Ljava/lang/String;)V
+HSPLkotlin/ResultKt;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V
+HSPLkotlin/ResultKt;->checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V
+HSPLkotlin/ResultKt;->compare(II)I
+HSPLkotlin/ResultKt;->constrain-4WqzIAM(JJ)J
+HSPLkotlin/ResultKt;->constrainHeight-K40F9xA(JI)I
+HSPLkotlin/ResultKt;->constrainWidth-K40F9xA(JI)I
+HSPLkotlin/ResultKt;->create(Landroid/content/Context;)Landroidx/emoji2/text/FontRequestEmojiCompatConfig;
+HSPLkotlin/ResultKt;->createCoroutineUnintercepted(Ljava/lang/Object;Lkotlin/coroutines/Continuation;Lkotlin/jvm/functions/Function2;)Lkotlin/coroutines/Continuation;
+HSPLkotlin/ResultKt;->createFailure(Ljava/lang/Throwable;)Lkotlin/Result$Failure;
+HSPLkotlin/ResultKt;->distinctUntilChanged(Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;
+HSPLkotlin/ResultKt;->emitAllImpl$FlowKt__ChannelsKt(Lkotlinx/coroutines/flow/FlowCollector;Lkotlinx/coroutines/channels/ProducerCoroutine;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlin/ResultKt;->ensureActive(Lkotlin/coroutines/CoroutineContext;)V
+HSPLkotlin/ResultKt;->findIndexByKey$1(Landroidx/compose/foundation/lazy/layout/LazyLayoutItemProvider;Ljava/lang/Object;I)I
+HSPLkotlin/ResultKt;->first(Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlin/ResultKt;->get(Landroid/view/View;)Landroidx/lifecycle/LifecycleOwner;
+HSPLkotlin/ResultKt;->getExclusions()Ljava/util/Set;
+HSPLkotlin/ResultKt;->getJob(Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/Job;
+HSPLkotlin/ResultKt;->getOrCreateCancellableContinuation(Lkotlin/coroutines/Continuation;)Lkotlinx/coroutines/CancellableContinuationImpl;
+HSPLkotlin/ResultKt;->getProgressionLastElement(III)I
+HSPLkotlin/ResultKt;->getSp(D)J
+HSPLkotlin/ResultKt;->getSp(I)J
+HSPLkotlin/ResultKt;->hasFontAttributes(Landroidx/compose/ui/text/SpanStyle;)Z
+HSPLkotlin/ResultKt;->intercepted(Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLkotlin/ResultKt;->isEnabled()Z
+HSPLkotlin/ResultKt;->isUnspecified--R2X_6o(J)Z
+HSPLkotlin/ResultKt;->launch$default(Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/android/HandlerContext;ILkotlin/jvm/functions/Function2;I)Lkotlinx/coroutines/StandaloneCoroutine;
+HSPLkotlin/ResultKt;->launch(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/StandaloneCoroutine;
+HSPLkotlin/ResultKt;->lazyLayoutSemantics(Landroidx/compose/ui/Modifier;Lkotlin/reflect/KProperty0;Landroidx/tv/foundation/lazy/layout/LazyLayoutSemanticState;Landroidx/compose/foundation/gestures/Orientation;ZZLandroidx/compose/runtime/Composer;)Landroidx/compose/ui/Modifier;
+HSPLkotlin/ResultKt;->mapCapacity(I)I
+HSPLkotlin/ResultKt;->materializeModifier(Landroidx/compose/runtime/Composer;Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
+HSPLkotlin/ResultKt;->mmap(Landroid/content/Context;Landroid/net/Uri;)Ljava/nio/MappedByteBuffer;
+HSPLkotlin/ResultKt;->offset-NN6Ew-U(IIJ)J
+HSPLkotlin/ResultKt;->overscrollEffect(Landroidx/compose/runtime/Composer;)Landroidx/compose/foundation/OverscrollEffect;
+HSPLkotlin/ResultKt;->pack(FJ)J
+HSPLkotlin/ResultKt;->plus(Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext;
+HSPLkotlin/ResultKt;->read(Ljava/nio/MappedByteBuffer;)Landroidx/emoji2/text/flatbuffer/MetadataList;
+HSPLkotlin/ResultKt;->recoverResult(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlin/ResultKt;->requireOwner(Landroidx/compose/ui/node/LayoutNode;)Landroidx/compose/ui/node/Owner;
+HSPLkotlin/ResultKt;->resolveLineHeightInPx-o2QH7mI(JFLandroidx/compose/ui/unit/Density;)F
+HSPLkotlin/ResultKt;->restoreThreadContext(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Object;)V
+HSPLkotlin/ResultKt;->resume(Lkotlinx/coroutines/DispatchedTask;Lkotlin/coroutines/Continuation;Z)V
+HSPLkotlin/ResultKt;->roundToInt(F)I
+HSPLkotlin/ResultKt;->scrollableWithPivot(Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/gestures/ScrollableState;Landroidx/compose/foundation/gestures/Orientation;Landroidx/tv/foundation/PivotOffsets;ZZ)Landroidx/compose/ui/Modifier;
+HSPLkotlin/ResultKt;->startUndispatchedOrReturn(Lkotlinx/coroutines/internal/ScopeCoroutine;Lkotlinx/coroutines/internal/ScopeCoroutine;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLkotlin/ResultKt;->stateIn(Lkotlinx/coroutines/flow/SafeFlow;Lkotlinx/coroutines/internal/ContextScope;Lkotlinx/coroutines/flow/StartedWhileSubscribed;Ljava/lang/Float;)Lkotlinx/coroutines/flow/ReadonlyStateFlow;
+HSPLkotlin/ResultKt;->systemProp$default(Ljava/lang/String;IIII)I
+HSPLkotlin/ResultKt;->systemProp(Ljava/lang/String;JJJ)J
+HSPLkotlin/ResultKt;->threadContextElements(Lkotlin/coroutines/CoroutineContext;)Ljava/lang/Object;
+HSPLkotlin/ResultKt;->throwOnFailure(Ljava/lang/Object;)V
+HSPLkotlin/ResultKt;->toSize-ozmzZPI(J)J
+HSPLkotlin/ResultKt;->ulongToDouble(J)D
+HSPLkotlin/ResultKt;->updateThreadContext(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlin/ResultKt;->withContext(Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlin/SynchronizedLazyImpl;-><init>(Lkotlin/jvm/functions/Function0;)V
+HSPLkotlin/SynchronizedLazyImpl;->getValue()Ljava/lang/Object;
+HSPLkotlin/TuplesKt;-><clinit>()V
+HSPLkotlin/TuplesKt;->CornerRadius(FF)J
+HSPLkotlin/TuplesKt;->LazyList(Landroidx/compose/ui/Modifier;Landroidx/tv/foundation/lazy/list/TvLazyListState;Landroidx/compose/foundation/layout/PaddingValuesImpl;ZZZILandroidx/tv/foundation/PivotOffsets;Landroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/ui/Alignment$Vertical;Landroidx/compose/foundation/layout/Arrangement$Horizontal;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V
+HSPLkotlin/TuplesKt;->PillIndicatorTabRow(Ljava/util/List;ILkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)V
+HSPLkotlin/TuplesKt;->Rect-tz77jQw(JJ)Landroidx/compose/ui/geometry/Rect;
+HSPLkotlin/TuplesKt;->ScrollPositionUpdater(Lkotlin/jvm/functions/Function0;Landroidx/tv/foundation/lazy/list/TvLazyListState;Landroidx/compose/runtime/Composer;I)V
+HSPLkotlin/TuplesKt;->Size(FF)J
+HSPLkotlin/TuplesKt;->TopNavigation(Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
+HSPLkotlin/TuplesKt;->access$addLayoutNodeChildren(Landroidx/compose/runtime/collection/MutableVector;Landroidx/compose/ui/Modifier$Node;)V
+HSPLkotlin/TuplesKt;->access$insertEntryAtIndex([Ljava/lang/Object;ILjava/lang/Object;Ljava/lang/Object;)[Ljava/lang/Object;
+HSPLkotlin/TuplesKt;->access$pop(Landroidx/compose/runtime/collection/MutableVector;)Landroidx/compose/ui/Modifier$Node;
+HSPLkotlin/TuplesKt;->access$removeRange(Ljava/util/ArrayList;II)V
+HSPLkotlin/TuplesKt;->asLayoutModifierNode(Landroidx/compose/ui/Modifier$Node;)Landroidx/compose/ui/node/LayoutModifierNode;
+HSPLkotlin/TuplesKt;->autoInvalidateInsertedNode(Landroidx/compose/ui/Modifier$Node;)V
+HSPLkotlin/TuplesKt;->autoInvalidateNodeIncludingDelegates(Landroidx/compose/ui/Modifier$Node;II)V
+HSPLkotlin/TuplesKt;->autoInvalidateNodeSelf(Landroidx/compose/ui/Modifier$Node;II)V
+HSPLkotlin/TuplesKt;->autoInvalidateUpdatedNode(Landroidx/compose/ui/Modifier$Node;)V
+HSPLkotlin/TuplesKt;->beforeCheckcastToFunctionOfArity(ILjava/lang/Object;)V
+HSPLkotlin/TuplesKt;->binarySearch([II)I
+HSPLkotlin/TuplesKt;->bitsForSlot(II)I
+HSPLkotlin/TuplesKt;->calculateLazyLayoutPinnedIndices(Landroidx/compose/foundation/lazy/layout/LazyLayoutItemProvider;Landroidx/compose/foundation/lazy/layout/LazyLayoutPinnedItemList;Landroidx/compose/runtime/Stack;)Ljava/util/List;
+HSPLkotlin/TuplesKt;->calculateNodeKindSetFrom(Landroidx/compose/ui/Modifier$Element;)I
+HSPLkotlin/TuplesKt;->calculateNodeKindSetFrom(Landroidx/compose/ui/Modifier$Node;)I
+HSPLkotlin/TuplesKt;->calculateNodeKindSetFromIncludingDelegates(Landroidx/compose/ui/Modifier$Node;)I
+HSPLkotlin/TuplesKt;->checkRadix(I)V
+HSPLkotlin/TuplesKt;->coerceIn(DDD)D
+HSPLkotlin/TuplesKt;->coerceIn(FFF)F
+HSPLkotlin/TuplesKt;->coerceIn(III)I
+HSPLkotlin/TuplesKt;->compareValues(Ljava/lang/Comparable;Ljava/lang/Comparable;)I
+HSPLkotlin/TuplesKt;->composableLambda(Landroidx/compose/runtime/Composer;ILkotlin/jvm/internal/Lambda;)Landroidx/compose/runtime/internal/ComposableLambdaImpl;
+HSPLkotlin/TuplesKt;->composableLambdaInstance(ILkotlin/jvm/internal/Lambda;Z)Landroidx/compose/runtime/internal/ComposableLambdaImpl;
+HSPLkotlin/TuplesKt;->currentValueOf(Landroidx/compose/ui/node/CompositionLocalConsumerModifierNode;Landroidx/compose/runtime/ProvidableCompositionLocal;)Ljava/lang/Object;
+HSPLkotlin/TuplesKt;->findLocation(ILjava/util/List;)I
+HSPLkotlin/TuplesKt;->findSegmentInternal(Lkotlinx/coroutines/internal/Segment;JLkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLkotlin/TuplesKt;->get(Lkotlin/coroutines/CoroutineContext$Element;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
+HSPLkotlin/TuplesKt;->getCenter-uvyYCjk(J)J
+HSPLkotlin/TuplesKt;->getEllipsizedLeftPadding(Landroid/text/Layout;ILandroid/graphics/Paint;)F
+HSPLkotlin/TuplesKt;->getEllipsizedRightPadding(Landroid/text/Layout;ILandroid/graphics/Paint;)F
+HSPLkotlin/TuplesKt;->getIncludeSelfInTraversal-H91voCI(I)Z
+HSPLkotlin/TuplesKt;->getLastIndex(Ljava/util/List;)I
+HSPLkotlin/TuplesKt;->invalidateDraw(Landroidx/compose/ui/node/DrawModifierNode;)V
+HSPLkotlin/TuplesKt;->invalidateMeasurement(Landroidx/compose/ui/node/LayoutModifierNode;)V
+HSPLkotlin/TuplesKt;->invalidateSemantics(Landroidx/compose/ui/node/SemanticsModifierNode;)V
+HSPLkotlin/TuplesKt;->isWhitespace(C)Z
+HSPLkotlin/TuplesKt;->lazy(Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
+HSPLkotlin/TuplesKt;->listOf(Ljava/lang/Object;)Ljava/util/List;
+HSPLkotlin/TuplesKt;->listOf([Ljava/lang/Object;)Ljava/util/List;
+HSPLkotlin/TuplesKt;->minusKey(Lkotlin/coroutines/CoroutineContext$Element;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext;
+HSPLkotlin/TuplesKt;->observeReads(Landroidx/compose/ui/Modifier$Node;Lkotlin/jvm/functions/Function0;)V
+HSPLkotlin/TuplesKt;->painterResource(ILandroidx/compose/runtime/Composer;)Landroidx/compose/ui/graphics/painter/Painter;
+HSPLkotlin/TuplesKt;->replacableWith(Landroidx/compose/runtime/RecomposeScope;Landroidx/compose/runtime/RecomposeScopeImpl;)Z
+HSPLkotlin/TuplesKt;->requireCoordinator-64DMado(Landroidx/compose/ui/node/DelegatableNode;I)Landroidx/compose/ui/node/NodeCoordinator;
+HSPLkotlin/TuplesKt;->requireLayoutNode(Landroidx/compose/ui/node/DelegatableNode;)Landroidx/compose/ui/node/LayoutNode;
+HSPLkotlin/TuplesKt;->requireOwner(Landroidx/compose/ui/node/DelegatableNode;)Landroidx/compose/ui/node/Owner;
+HSPLkotlin/TuplesKt;->resolveDefaults(Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/unit/LayoutDirection;)Landroidx/compose/ui/text/TextStyle;
+HSPLkotlin/TuplesKt;->runtimeCheck(Z)V
+HSPLkotlin/TuplesKt;->until(II)Lkotlin/ranges/IntRange;
+HSPLkotlin/ULong$Companion;-><init>()V
+HSPLkotlin/ULong$Companion;-><init>(I)V
+HSPLkotlin/ULong$Companion;-><init>(II)V
+HSPLkotlin/ULong$Companion;->checkElementIndex$kotlin_stdlib(II)V
+HSPLkotlin/ULong$Companion;->computeScaleFactor-H7hwNQA(JJ)J
+HSPLkotlin/ULong$Companion;->dispatch$lifecycle_runtime_release(Landroid/app/Activity;Landroidx/lifecycle/Lifecycle$Event;)V
+HSPLkotlin/ULong$Companion;->getHolderForHierarchy(Landroid/view/View;)Landroidx/metrics/performance/PerformanceMetricsState$Holder;
+HSPLkotlin/ULong$Companion;->injectIfNeededIn(Landroid/app/Activity;)V
+HSPLkotlin/UNINITIALIZED_VALUE;-><clinit>()V
+HSPLkotlin/Unit;-><clinit>()V
+HSPLkotlin/UnsafeLazyImpl;-><init>(Lkotlin/jvm/functions/Function0;)V
+HSPLkotlin/UnsafeLazyImpl;->getValue()Ljava/lang/Object;
+HSPLkotlin/collections/AbstractCollection;->isEmpty()Z
+HSPLkotlin/collections/AbstractCollection;->size()I
+HSPLkotlin/collections/AbstractList;->equals(Ljava/lang/Object;)Z
+HSPLkotlin/collections/AbstractMap$toString$1;-><init>(ILjava/lang/Object;)V
+HSPLkotlin/collections/AbstractMap$toString$1;->invoke()Landroidx/compose/runtime/DisposableEffectResult;
+HSPLkotlin/collections/AbstractMap$toString$1;->invoke(F)Ljava/lang/Float;
+HSPLkotlin/collections/AbstractMap$toString$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlin/collections/AbstractMap$toString$1;->invoke(Ljava/lang/Object;)V
+HSPLkotlin/collections/AbstractMap;->entrySet()Ljava/util/Set;
+HSPLkotlin/collections/AbstractMap;->equals(Ljava/lang/Object;)Z
+HSPLkotlin/collections/AbstractMap;->size()I
+HSPLkotlin/collections/AbstractMutableList;-><init>()V
+HSPLkotlin/collections/AbstractMutableList;->size()I
+HSPLkotlin/collections/AbstractSet;->equals(Ljava/lang/Object;)Z
+HSPLkotlin/collections/ArrayDeque;-><clinit>()V
+HSPLkotlin/collections/ArrayDeque;-><init>()V
+HSPLkotlin/collections/ArrayDeque;->addLast(Ljava/lang/Object;)V
+HSPLkotlin/collections/ArrayDeque;->ensureCapacity(I)V
+HSPLkotlin/collections/ArrayDeque;->first()Ljava/lang/Object;
+HSPLkotlin/collections/ArrayDeque;->get(I)Ljava/lang/Object;
+HSPLkotlin/collections/ArrayDeque;->getSize()I
+HSPLkotlin/collections/ArrayDeque;->incremented(I)I
+HSPLkotlin/collections/ArrayDeque;->isEmpty()Z
+HSPLkotlin/collections/ArrayDeque;->positiveMod(I)I
+HSPLkotlin/collections/ArrayDeque;->removeFirst()Ljava/lang/Object;
+HSPLkotlin/collections/ArraysKt___ArraysKt;->asList([Ljava/lang/Object;)Ljava/util/List;
+HSPLkotlin/collections/ArraysKt___ArraysKt;->collectionSizeOrDefault(Ljava/lang/Iterable;)I
+HSPLkotlin/collections/ArraysKt___ArraysKt;->copyInto$default([I[III)V
+HSPLkotlin/collections/ArraysKt___ArraysKt;->copyInto$default([Ljava/lang/Object;[Ljava/lang/Object;III)V
+HSPLkotlin/collections/ArraysKt___ArraysKt;->copyInto([I[IIII)V
+HSPLkotlin/collections/ArraysKt___ArraysKt;->copyInto([Ljava/lang/Object;[Ljava/lang/Object;III)V
+HSPLkotlin/collections/ArraysKt___ArraysKt;->fill$default([Ljava/lang/Object;)V
+HSPLkotlin/collections/ArraysKt___ArraysKt;->fill(II[Ljava/lang/Object;)V
+HSPLkotlin/collections/ArraysKt___ArraysKt;->indexOf([Ljava/lang/Object;Ljava/lang/Object;)I
+HSPLkotlin/collections/CollectionsKt__MutableCollectionsJVMKt;->sortWith(Ljava/util/List;Ljava/util/Comparator;)V
+HSPLkotlin/collections/CollectionsKt__ReversedViewsKt;->addAll(Ljava/lang/Iterable;Ljava/util/Collection;)V
+HSPLkotlin/collections/CollectionsKt___CollectionsKt;->firstOrNull(Ljava/util/List;)Ljava/lang/Object;
+HSPLkotlin/collections/CollectionsKt___CollectionsKt;->last(Ljava/util/List;)Ljava/lang/Object;
+HSPLkotlin/collections/CollectionsKt___CollectionsKt;->plus(Ljava/util/List;Ljava/io/Serializable;)Ljava/util/ArrayList;
+HSPLkotlin/collections/CollectionsKt___CollectionsKt;->toIntArray(Ljava/util/ArrayList;)[I
+HSPLkotlin/collections/EmptyList;-><clinit>()V
+HSPLkotlin/collections/EmptyList;->contains(Ljava/lang/Object;)Z
+HSPLkotlin/collections/EmptyList;->isEmpty()Z
+HSPLkotlin/collections/EmptyList;->size()I
+HSPLkotlin/collections/EmptyList;->toArray()[Ljava/lang/Object;
+HSPLkotlin/collections/EmptyMap;-><clinit>()V
+HSPLkotlin/collections/EmptyMap;->isEmpty()Z
+HSPLkotlin/collections/EmptyMap;->size()I
+HSPLkotlin/coroutines/AbstractCoroutineContextElement;-><init>(Lkotlin/coroutines/CoroutineContext$Key;)V
+HSPLkotlin/coroutines/AbstractCoroutineContextElement;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLkotlin/coroutines/AbstractCoroutineContextElement;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
+HSPLkotlin/coroutines/AbstractCoroutineContextElement;->getKey()Lkotlin/coroutines/CoroutineContext$Key;
+HSPLkotlin/coroutines/AbstractCoroutineContextElement;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext;
+HSPLkotlin/coroutines/AbstractCoroutineContextElement;->plus(Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext;
+HSPLkotlin/coroutines/AbstractCoroutineContextKey;-><init>(Lkotlin/coroutines/CoroutineContext$Key;Lkotlin/jvm/functions/Function1;)V
+HSPLkotlin/coroutines/CombinedContext;-><init>(Lkotlin/coroutines/CoroutineContext$Element;Lkotlin/coroutines/CoroutineContext;)V
+HSPLkotlin/coroutines/CombinedContext;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLkotlin/coroutines/CombinedContext;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext;
+HSPLkotlin/coroutines/CombinedContext;->plus(Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext;
+HSPLkotlin/coroutines/CoroutineContext$plus$1;-><clinit>()V
+HSPLkotlin/coroutines/CoroutineContext$plus$1;-><init>(I)V
+HSPLkotlin/coroutines/CoroutineContext$plus$1;->invoke(Landroidx/compose/runtime/Composer;I)V
+HSPLkotlin/coroutines/CoroutineContext$plus$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlin/coroutines/EmptyCoroutineContext;-><clinit>()V
+HSPLkotlin/coroutines/EmptyCoroutineContext;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLkotlin/coroutines/EmptyCoroutineContext;->plus(Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext;
+HSPLkotlin/coroutines/intrinsics/CoroutineSingletons;-><clinit>()V
+HSPLkotlin/coroutines/intrinsics/CoroutineSingletons;-><init>(ILjava/lang/String;)V
+HSPLkotlin/coroutines/jvm/internal/BaseContinuationImpl;-><init>(Lkotlin/coroutines/Continuation;)V
+HSPLkotlin/coroutines/jvm/internal/BaseContinuationImpl;->resumeWith(Ljava/lang/Object;)V
+HSPLkotlin/coroutines/jvm/internal/CompletedContinuation;-><clinit>()V
+HSPLkotlin/coroutines/jvm/internal/ContinuationImpl;-><init>(Lkotlin/coroutines/Continuation;)V
+HSPLkotlin/coroutines/jvm/internal/ContinuationImpl;-><init>(Lkotlin/coroutines/Continuation;Lkotlin/coroutines/CoroutineContext;)V
+HSPLkotlin/coroutines/jvm/internal/ContinuationImpl;->getContext()Lkotlin/coroutines/CoroutineContext;
+HSPLkotlin/coroutines/jvm/internal/ContinuationImpl;->releaseIntercepted()V
+HSPLkotlin/coroutines/jvm/internal/SuspendLambda;-><init>(ILkotlin/coroutines/Continuation;)V
+HSPLkotlin/coroutines/jvm/internal/SuspendLambda;->getArity()I
+HSPLkotlin/jvm/internal/ArrayIterator;-><init>(ILjava/lang/Object;)V
+HSPLkotlin/jvm/internal/ArrayIterator;->hasNext()Z
+HSPLkotlin/jvm/internal/ArrayIterator;->next()Ljava/lang/Object;
+HSPLkotlin/jvm/internal/CallableReference$NoReceiver;-><clinit>()V
+HSPLkotlin/jvm/internal/CallableReference;-><init>(Ljava/lang/Object;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Z)V
+HSPLkotlin/jvm/internal/ClassReference;-><clinit>()V
+HSPLkotlin/jvm/internal/ClassReference;-><init>(Ljava/lang/Class;)V
+HSPLkotlin/jvm/internal/ClassReference;->getJClass()Ljava/lang/Class;
+HSPLkotlin/jvm/internal/FunctionReferenceImpl;-><init>(ILjava/lang/Class;Ljava/lang/String;Ljava/lang/String;I)V
+HSPLkotlin/jvm/internal/Lambda;-><init>(I)V
+HSPLkotlin/jvm/internal/Lambda;->getArity()I
+HSPLkotlin/jvm/internal/PropertyReference0Impl;->invoke()Ljava/lang/Object;
+HSPLkotlin/jvm/internal/PropertyReference;-><init>(Ljava/lang/Object;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;I)V
+HSPLkotlin/jvm/internal/PropertyReference;->equals(Ljava/lang/Object;)Z
+HSPLkotlin/jvm/internal/Reflection;-><clinit>()V
+HSPLkotlin/jvm/internal/Reflection;->getOrCreateKotlinClass(Ljava/lang/Class;)Lkotlin/jvm/internal/ClassReference;
+HSPLkotlin/random/FallbackThreadLocalRandom$implStorage$1;-><init>(I)V
+HSPLkotlin/ranges/IntProgression;-><init>(III)V
+HSPLkotlin/ranges/IntProgression;->iterator()Ljava/util/Iterator;
+HSPLkotlin/ranges/IntProgressionIterator;-><init>(III)V
+HSPLkotlin/ranges/IntProgressionIterator;->hasNext()Z
+HSPLkotlin/ranges/IntProgressionIterator;->nextInt()I
+HSPLkotlin/ranges/IntRange;-><clinit>()V
+HSPLkotlin/ranges/IntRange;-><init>(II)V
+HSPLkotlin/ranges/IntRange;->equals(Ljava/lang/Object;)Z
+HSPLkotlin/ranges/IntRange;->isEmpty()Z
+HSPLkotlin/sequences/ConstrainedOnceSequence;-><init>(Lkotlin/sequences/SequencesKt__SequencesKt$asSequence$$inlined$Sequence$1;)V
+HSPLkotlin/sequences/ConstrainedOnceSequence;->iterator()Ljava/util/Iterator;
+HSPLkotlin/sequences/FilteringSequence$iterator$1;-><init>(Lkotlin/sequences/FilteringSequence;)V
+HSPLkotlin/sequences/FilteringSequence$iterator$1;->calcNext()V
+HSPLkotlin/sequences/FilteringSequence$iterator$1;->hasNext()Z
+HSPLkotlin/sequences/FilteringSequence$iterator$1;->next()Ljava/lang/Object;
+HSPLkotlin/sequences/FilteringSequence;-><init>(Lkotlin/sequences/GeneratorSequence;)V
+HSPLkotlin/sequences/GeneratorSequence$iterator$1;-><init>(Lkotlin/sequences/GeneratorSequence;)V
+HSPLkotlin/sequences/GeneratorSequence$iterator$1;->calcNext()V
+HSPLkotlin/sequences/GeneratorSequence$iterator$1;->hasNext()Z
+HSPLkotlin/sequences/GeneratorSequence$iterator$1;->next()Ljava/lang/Object;
+HSPLkotlin/sequences/GeneratorSequence;-><init>(Landroidx/compose/ui/node/LayoutNode$_foldedChildren$1;Lkotlin/jvm/functions/Function1;)V
+HSPLkotlin/sequences/GeneratorSequence;-><init>(Lkotlin/sequences/Sequence;Lkotlin/jvm/functions/Function1;)V
+HSPLkotlin/sequences/GeneratorSequence;->iterator()Ljava/util/Iterator;
+HSPLkotlin/sequences/SequencesKt;->firstOrNull(Lkotlin/sequences/FilteringSequence;)Ljava/lang/Object;
+HSPLkotlin/sequences/SequencesKt;->mapNotNull(Lkotlin/sequences/Sequence;Lkotlinx/coroutines/CoroutineDispatcher$Key$1;)Lkotlin/sequences/FilteringSequence;
+HSPLkotlin/sequences/SequencesKt;->toList(Lkotlin/sequences/Sequence;)Ljava/util/List;
+HSPLkotlin/sequences/SequencesKt__SequencesKt$asSequence$$inlined$Sequence$1;-><init>(ILjava/lang/Object;)V
+HSPLkotlin/sequences/SequencesKt__SequencesKt$asSequence$$inlined$Sequence$1;->iterator()Ljava/util/Iterator;
+HSPLkotlin/sequences/TransformingSequence$iterator$1;-><init>(Lkotlin/sequences/GeneratorSequence;)V
+HSPLkotlin/sequences/TransformingSequence$iterator$1;->hasNext()Z
+HSPLkotlin/sequences/TransformingSequence$iterator$1;->next()Ljava/lang/Object;
+HSPLkotlin/text/StringsKt__IndentKt$getIndentFunction$2;-><init>(ILjava/lang/String;)V
+HSPLkotlin/text/StringsKt__RegexExtensionsKt;->generateSequence(Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lkotlin/sequences/Sequence;
+HSPLkotlin/text/StringsKt__StringsKt;->endsWith$default(Ljava/lang/CharSequence;Ljava/lang/String;)Z
+HSPLkotlin/text/StringsKt__StringsKt;->getLastIndex(Ljava/lang/CharSequence;)I
+HSPLkotlin/text/StringsKt__StringsKt;->indexOf(Ljava/lang/CharSequence;Ljava/lang/String;IZ)I
+HSPLkotlin/text/StringsKt__StringsKt;->isBlank(Ljava/lang/CharSequence;)Z
+HSPLkotlin/text/StringsKt__StringsKt;->replace$default(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
+HSPLkotlin/text/StringsKt__StringsKt;->substringAfterLast$default(Ljava/lang/String;)Ljava/lang/String;
+HSPLkotlin/text/StringsKt___StringsKt;->last(Ljava/lang/CharSequence;)C
+HSPLkotlinx/coroutines/AbstractCoroutine;-><init>(Lkotlin/coroutines/CoroutineContext;Z)V
+HSPLkotlinx/coroutines/AbstractCoroutine;->getContext()Lkotlin/coroutines/CoroutineContext;
+HSPLkotlinx/coroutines/AbstractCoroutine;->getCoroutineContext()Lkotlin/coroutines/CoroutineContext;
+HSPLkotlinx/coroutines/AbstractCoroutine;->isActive()Z
+HSPLkotlinx/coroutines/AbstractCoroutine;->onCancelled(Ljava/lang/Throwable;Z)V
+HSPLkotlinx/coroutines/AbstractCoroutine;->onCompleted(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/AbstractCoroutine;->onCompletionInternal(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/AbstractCoroutine;->resumeWith(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/AbstractCoroutine;->start$enumunboxing$(ILkotlinx/coroutines/AbstractCoroutine;Lkotlin/jvm/functions/Function2;)V
+HSPLkotlinx/coroutines/Active;-><clinit>()V
+HSPLkotlinx/coroutines/BlockingEventLoop;-><init>(Ljava/lang/Thread;)V
+HSPLkotlinx/coroutines/CancelHandler;-><init>()V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;-><clinit>()V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;-><init>(ILkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->callCancelHandler(Lkotlinx/coroutines/CancelHandler;Ljava/lang/Throwable;)V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->callSegmentOnCancellation(Lkotlinx/coroutines/internal/Segment;Ljava/lang/Throwable;)V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->cancel(Ljava/lang/Throwable;)Z
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->completeResume(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->detachChild$kotlinx_coroutines_core()V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->dispatchResume(I)V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->getContinuationCancellationCause(Lkotlinx/coroutines/JobSupport;)Ljava/lang/Throwable;
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->getDelegate$kotlinx_coroutines_core()Lkotlin/coroutines/Continuation;
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->getExceptionalResult$kotlinx_coroutines_core(Ljava/lang/Object;)Ljava/lang/Throwable;
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->getResult()Ljava/lang/Object;
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->getSuccessfulResult$kotlinx_coroutines_core(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->initCancellability()V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->installParentHandle()Lkotlinx/coroutines/DisposableHandle;
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->invokeOnCancellation(Lkotlin/jvm/functions/Function1;)V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->invokeOnCancellation(Lkotlinx/coroutines/internal/Segment;I)V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->invokeOnCancellationImpl(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->isReusable()Z
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->releaseClaimedReusableContinuation$kotlinx_coroutines_core()V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->resumeImpl(Ljava/lang/Object;ILkotlin/jvm/functions/Function1;)V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->resumeWith(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->resumedState(Lkotlinx/coroutines/NotCompleted;Ljava/lang/Object;ILkotlin/jvm/functions/Function1;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->takeState$kotlinx_coroutines_core()Ljava/lang/Object;
+HSPLkotlinx/coroutines/CancellableContinuationImpl;->tryResume(Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/internal/Symbol;
+HSPLkotlinx/coroutines/CancelledContinuation;-><clinit>()V
+HSPLkotlinx/coroutines/CancelledContinuation;-><init>(Lkotlin/coroutines/Continuation;Ljava/lang/Throwable;Z)V
+HSPLkotlinx/coroutines/ChildContinuation;-><init>(Lkotlinx/coroutines/CancellableContinuationImpl;)V
+HSPLkotlinx/coroutines/ChildContinuation;->invoke(Ljava/lang/Throwable;)V
+HSPLkotlinx/coroutines/ChildHandleNode;-><init>(Lkotlinx/coroutines/JobSupport;)V
+HSPLkotlinx/coroutines/ChildHandleNode;->childCancelled(Ljava/lang/Throwable;)Z
+HSPLkotlinx/coroutines/ChildHandleNode;->invoke(Ljava/lang/Throwable;)V
+HSPLkotlinx/coroutines/CompletedContinuation;-><init>(Ljava/lang/Object;Lkotlinx/coroutines/CancelHandler;Lkotlin/jvm/functions/Function1;Ljava/lang/Object;Ljava/lang/Throwable;)V
+HSPLkotlinx/coroutines/CompletedContinuation;-><init>(Ljava/lang/Object;Lkotlinx/coroutines/CancelHandler;Lkotlin/jvm/functions/Function1;Ljava/util/concurrent/CancellationException;I)V
+HSPLkotlinx/coroutines/CompletedExceptionally;-><clinit>()V
+HSPLkotlinx/coroutines/CompletedExceptionally;-><init>(Ljava/lang/Throwable;Z)V
+HSPLkotlinx/coroutines/CoroutineContextKt$foldCopies$1;-><clinit>()V
+HSPLkotlinx/coroutines/CoroutineContextKt$foldCopies$1;-><init>(I)V
+HSPLkotlinx/coroutines/CoroutineContextKt$foldCopies$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/CoroutineDispatcher$Key$1;-><clinit>()V
+HSPLkotlinx/coroutines/CoroutineDispatcher$Key$1;-><init>(I)V
+HSPLkotlinx/coroutines/CoroutineDispatcher$Key$1;->invoke(Landroid/view/View;)Landroid/view/View;
+HSPLkotlinx/coroutines/CoroutineDispatcher$Key$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/CoroutineDispatcher$Key;-><init>(I)V
+HSPLkotlinx/coroutines/CoroutineDispatcher;-><clinit>()V
+HSPLkotlinx/coroutines/CoroutineDispatcher;-><init>()V
+HSPLkotlinx/coroutines/CoroutineDispatcher;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
+HSPLkotlinx/coroutines/CoroutineDispatcher;->isDispatchNeeded()Z
+HSPLkotlinx/coroutines/CoroutineDispatcher;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext;
+HSPLkotlinx/coroutines/DefaultExecutor;-><clinit>()V
+HSPLkotlinx/coroutines/DefaultExecutorKt;-><clinit>()V
+HSPLkotlinx/coroutines/DispatchedTask;-><init>(I)V
+HSPLkotlinx/coroutines/DispatchedTask;->getExceptionalResult$kotlinx_coroutines_core(Ljava/lang/Object;)Ljava/lang/Throwable;
+HSPLkotlinx/coroutines/DispatchedTask;->getSuccessfulResult$kotlinx_coroutines_core(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/DispatchedTask;->handleFatalException(Ljava/lang/Throwable;Ljava/lang/Throwable;)V
+HSPLkotlinx/coroutines/DispatchedTask;->run()V
+HSPLkotlinx/coroutines/Dispatchers;-><clinit>()V
+HSPLkotlinx/coroutines/Empty;-><init>(Z)V
+HSPLkotlinx/coroutines/Empty;->getList()Lkotlinx/coroutines/NodeList;
+HSPLkotlinx/coroutines/Empty;->isActive()Z
+HSPLkotlinx/coroutines/EventLoopImplBase;-><clinit>()V
+HSPLkotlinx/coroutines/EventLoopImplBase;-><init>()V
+HSPLkotlinx/coroutines/EventLoopImplPlatform;-><init>()V
+HSPLkotlinx/coroutines/EventLoopImplPlatform;->decrementUseCount(Z)V
+HSPLkotlinx/coroutines/EventLoopImplPlatform;->incrementUseCount(Z)V
+HSPLkotlinx/coroutines/EventLoopImplPlatform;->isUnconfinedLoopActive()Z
+HSPLkotlinx/coroutines/EventLoopImplPlatform;->processUnconfinedEvent()Z
+HSPLkotlinx/coroutines/ExecutorCoroutineDispatcher$Key$1;-><clinit>()V
+HSPLkotlinx/coroutines/ExecutorCoroutineDispatcher$Key$1;-><init>(I)V
+HSPLkotlinx/coroutines/ExecutorCoroutineDispatcher$Key$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/ExecutorCoroutineDispatcher;-><clinit>()V
+HSPLkotlinx/coroutines/GlobalScope;-><clinit>()V
+HSPLkotlinx/coroutines/GlobalScope;->getCoroutineContext()Lkotlin/coroutines/CoroutineContext;
+HSPLkotlinx/coroutines/InvokeOnCancel;-><init>(ILjava/lang/Object;)V
+HSPLkotlinx/coroutines/InvokeOnCancel;->invoke(Ljava/lang/Throwable;)V
+HSPLkotlinx/coroutines/InvokeOnCompletion;-><init>(ILjava/lang/Object;)V
+HSPLkotlinx/coroutines/JobImpl;-><init>(Lkotlinx/coroutines/Job;)V
+HSPLkotlinx/coroutines/JobImpl;->getHandlesException$kotlinx_coroutines_core()Z
+HSPLkotlinx/coroutines/JobImpl;->getOnCancelComplete$kotlinx_coroutines_core()Z
+HSPLkotlinx/coroutines/JobNode;-><init>()V
+HSPLkotlinx/coroutines/JobNode;->dispose()V
+HSPLkotlinx/coroutines/JobNode;->getJob()Lkotlinx/coroutines/JobSupport;
+HSPLkotlinx/coroutines/JobNode;->getList()Lkotlinx/coroutines/NodeList;
+HSPLkotlinx/coroutines/JobNode;->isActive()Z
+HSPLkotlinx/coroutines/JobSupport$ChildCompletion;-><init>(Lkotlinx/coroutines/JobSupport;Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/JobSupport$ChildCompletion;->invoke(Ljava/lang/Throwable;)V
+HSPLkotlinx/coroutines/JobSupport$Finishing;-><clinit>()V
+HSPLkotlinx/coroutines/JobSupport$Finishing;-><init>(Lkotlinx/coroutines/NodeList;Ljava/lang/Throwable;)V
+HSPLkotlinx/coroutines/JobSupport$Finishing;->addExceptionLocked(Ljava/lang/Throwable;)V
+HSPLkotlinx/coroutines/JobSupport$Finishing;->getList()Lkotlinx/coroutines/NodeList;
+HSPLkotlinx/coroutines/JobSupport$Finishing;->getRootCause()Ljava/lang/Throwable;
+HSPLkotlinx/coroutines/JobSupport$Finishing;->isCancelling()Z
+HSPLkotlinx/coroutines/JobSupport$Finishing;->isCompleting()Z
+HSPLkotlinx/coroutines/JobSupport$Finishing;->sealLocked(Ljava/lang/Throwable;)Ljava/util/ArrayList;
+HSPLkotlinx/coroutines/JobSupport$addLastAtomic$$inlined$addLastIf$1;-><init>(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;Lkotlinx/coroutines/JobSupport;Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/JobSupport$addLastAtomic$$inlined$addLastIf$1;->complete(Ljava/lang/Object;Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/JobSupport$addLastAtomic$$inlined$addLastIf$1;->prepare(Ljava/lang/Object;)Lkotlinx/coroutines/internal/Symbol;
+HSPLkotlinx/coroutines/JobSupport;-><clinit>()V
+HSPLkotlinx/coroutines/JobSupport;-><init>(Z)V
+HSPLkotlinx/coroutines/JobSupport;->addLastAtomic(Ljava/lang/Object;Lkotlinx/coroutines/NodeList;Lkotlinx/coroutines/JobNode;)Z
+HSPLkotlinx/coroutines/JobSupport;->afterCompletion(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/JobSupport;->afterResume(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/JobSupport;->cancel(Ljava/util/concurrent/CancellationException;)V
+HSPLkotlinx/coroutines/JobSupport;->cancelImpl$kotlinx_coroutines_core(Ljava/lang/Object;)Z
+HSPLkotlinx/coroutines/JobSupport;->cancelInternal(Ljava/util/concurrent/CancellationException;)V
+HSPLkotlinx/coroutines/JobSupport;->cancelParent(Ljava/lang/Throwable;)Z
+HSPLkotlinx/coroutines/JobSupport;->childCancelled(Ljava/lang/Throwable;)Z
+HSPLkotlinx/coroutines/JobSupport;->completeStateFinalization(Lkotlinx/coroutines/Incomplete;Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/JobSupport;->createCauseException(Ljava/lang/Object;)Ljava/lang/Throwable;
+HSPLkotlinx/coroutines/JobSupport;->finalizeFinishingState(Lkotlinx/coroutines/JobSupport$Finishing;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/JobSupport;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/JobSupport;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
+HSPLkotlinx/coroutines/JobSupport;->getCancellationException()Ljava/util/concurrent/CancellationException;
+HSPLkotlinx/coroutines/JobSupport;->getFinalRootCause(Lkotlinx/coroutines/JobSupport$Finishing;Ljava/util/ArrayList;)Ljava/lang/Throwable;
+HSPLkotlinx/coroutines/JobSupport;->getKey()Lkotlin/coroutines/CoroutineContext$Key;
+HSPLkotlinx/coroutines/JobSupport;->getOnCancelComplete$kotlinx_coroutines_core()Z
+HSPLkotlinx/coroutines/JobSupport;->getOrPromoteCancellingList(Lkotlinx/coroutines/Incomplete;)Lkotlinx/coroutines/NodeList;
+HSPLkotlinx/coroutines/JobSupport;->getState$kotlinx_coroutines_core()Ljava/lang/Object;
+HSPLkotlinx/coroutines/JobSupport;->initParentJob(Lkotlinx/coroutines/Job;)V
+HSPLkotlinx/coroutines/JobSupport;->invokeOnCompletion(ZZLkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/DisposableHandle;
+HSPLkotlinx/coroutines/JobSupport;->isActive()Z
+HSPLkotlinx/coroutines/JobSupport;->isScopedCoroutine()Z
+HSPLkotlinx/coroutines/JobSupport;->makeCompletingOnce$kotlinx_coroutines_core(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/JobSupport;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext;
+HSPLkotlinx/coroutines/JobSupport;->nextChild(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)Lkotlinx/coroutines/ChildHandleNode;
+HSPLkotlinx/coroutines/JobSupport;->notifyCancelling(Lkotlinx/coroutines/NodeList;Ljava/lang/Throwable;)V
+HSPLkotlinx/coroutines/JobSupport;->onCompletionInternal(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/JobSupport;->plus(Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext;
+HSPLkotlinx/coroutines/JobSupport;->promoteSingleToNodeList(Lkotlinx/coroutines/JobNode;)V
+HSPLkotlinx/coroutines/JobSupport;->startInternal(Ljava/lang/Object;)I
+HSPLkotlinx/coroutines/JobSupport;->tryMakeCompleting(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/JobSupport;->tryWaitForChild(Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)Z
+HSPLkotlinx/coroutines/NodeList;-><init>()V
+HSPLkotlinx/coroutines/NodeList;->getList()Lkotlinx/coroutines/NodeList;
+HSPLkotlinx/coroutines/NodeList;->isActive()Z
+HSPLkotlinx/coroutines/NodeList;->isRemoved()Z
+HSPLkotlinx/coroutines/NonDisposableHandle;-><clinit>()V
+HSPLkotlinx/coroutines/NonDisposableHandle;->dispose()V
+HSPLkotlinx/coroutines/ThreadLocalEventLoop;-><clinit>()V
+HSPLkotlinx/coroutines/ThreadLocalEventLoop;->getEventLoop$kotlinx_coroutines_core()Lkotlinx/coroutines/EventLoopImplPlatform;
+HSPLkotlinx/coroutines/Unconfined;-><clinit>()V
+HSPLkotlinx/coroutines/UndispatchedCoroutine;-><init>(Lkotlin/coroutines/Continuation;Lkotlin/coroutines/CoroutineContext;)V
+HSPLkotlinx/coroutines/UndispatchedMarker;-><clinit>()V
+HSPLkotlinx/coroutines/UndispatchedMarker;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/UndispatchedMarker;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
+HSPLkotlinx/coroutines/UndispatchedMarker;->getKey()Lkotlin/coroutines/CoroutineContext$Key;
+HSPLkotlinx/coroutines/android/AndroidDispatcherFactory;-><init>()V
+HSPLkotlinx/coroutines/android/AndroidDispatcherFactory;->createDispatcher(Ljava/util/List;)Lkotlinx/coroutines/MainCoroutineDispatcher;
+HSPLkotlinx/coroutines/android/HandlerContext;-><init>(Landroid/os/Handler;)V
+HSPLkotlinx/coroutines/android/HandlerContext;-><init>(Landroid/os/Handler;Ljava/lang/String;Z)V
+HSPLkotlinx/coroutines/android/HandlerContext;->dispatch(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
+HSPLkotlinx/coroutines/android/HandlerContext;->isDispatchNeeded()Z
+HSPLkotlinx/coroutines/android/HandlerDispatcherKt;-><clinit>()V
+HSPLkotlinx/coroutines/android/HandlerDispatcherKt;->asHandler(Landroid/os/Looper;)Landroid/os/Handler;
+HSPLkotlinx/coroutines/channels/BufferOverflow;-><clinit>()V
+HSPLkotlinx/coroutines/channels/BufferOverflow;-><init>(ILjava/lang/String;)V
+HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;-><init>(Lkotlinx/coroutines/channels/BufferedChannel;)V
+HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->hasNext(Lkotlin/coroutines/jvm/internal/ContinuationImpl;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->invokeOnCancellation(Lkotlinx/coroutines/internal/Segment;I)V
+HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->next()Ljava/lang/Object;
+HSPLkotlinx/coroutines/channels/BufferedChannel;-><clinit>()V
+HSPLkotlinx/coroutines/channels/BufferedChannel;-><init>(ILkotlin/jvm/functions/Function1;)V
+HSPLkotlinx/coroutines/channels/BufferedChannel;->access$findSegmentSend(Lkotlinx/coroutines/channels/BufferedChannel;JLkotlinx/coroutines/channels/ChannelSegment;)Lkotlinx/coroutines/channels/ChannelSegment;
+HSPLkotlinx/coroutines/channels/BufferedChannel;->access$updateCellSend(Lkotlinx/coroutines/channels/BufferedChannel;Lkotlinx/coroutines/channels/ChannelSegment;ILjava/lang/Object;JLjava/lang/Object;Z)I
+HSPLkotlinx/coroutines/channels/BufferedChannel;->bufferOrRendezvousSend(J)Z
+HSPLkotlinx/coroutines/channels/BufferedChannel;->dropFirstElementUntilTheSpecifiedCellIsInTheBuffer(J)V
+HSPLkotlinx/coroutines/channels/BufferedChannel;->expandBuffer()V
+HSPLkotlinx/coroutines/channels/BufferedChannel;->findSegmentReceive(JLkotlinx/coroutines/channels/ChannelSegment;)Lkotlinx/coroutines/channels/ChannelSegment;
+HSPLkotlinx/coroutines/channels/BufferedChannel;->getBufferEndCounter()J
+HSPLkotlinx/coroutines/channels/BufferedChannel;->getReceiversCounter$kotlinx_coroutines_core()J
+HSPLkotlinx/coroutines/channels/BufferedChannel;->getSendersCounter$kotlinx_coroutines_core()J
+HSPLkotlinx/coroutines/channels/BufferedChannel;->incCompletedExpandBufferAttempts(J)V
+HSPLkotlinx/coroutines/channels/BufferedChannel;->isClosed(JZ)Z
+HSPLkotlinx/coroutines/channels/BufferedChannel;->isClosedForReceive()Z
+HSPLkotlinx/coroutines/channels/BufferedChannel;->isRendezvousOrUnlimited()Z
+HSPLkotlinx/coroutines/channels/BufferedChannel;->iterator()Lkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;
+HSPLkotlinx/coroutines/channels/BufferedChannel;->receive(Lkotlin/coroutines/jvm/internal/SuspendLambda;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/channels/BufferedChannel;->send(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/channels/BufferedChannel;->tryReceive-PtdJZtk()Ljava/lang/Object;
+HSPLkotlinx/coroutines/channels/BufferedChannel;->tryResumeReceiver(Ljava/lang/Object;Ljava/lang/Object;)Z
+HSPLkotlinx/coroutines/channels/BufferedChannel;->updateCellReceive(Lkotlinx/coroutines/channels/ChannelSegment;IJLjava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/channels/BufferedChannel;->waitExpandBufferCompletion$kotlinx_coroutines_core(J)V
+HSPLkotlinx/coroutines/channels/BufferedChannelKt$createSegmentFunction$1;-><clinit>()V
+HSPLkotlinx/coroutines/channels/BufferedChannelKt$createSegmentFunction$1;-><init>()V
+HSPLkotlinx/coroutines/channels/BufferedChannelKt$createSegmentFunction$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/channels/BufferedChannelKt;-><clinit>()V
+HSPLkotlinx/coroutines/channels/BufferedChannelKt;->tryResume0(Lkotlinx/coroutines/CancellableContinuation;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Z
+HSPLkotlinx/coroutines/channels/Channel$Factory;-><clinit>()V
+HSPLkotlinx/coroutines/channels/Channel;-><clinit>()V
+HSPLkotlinx/coroutines/channels/ChannelSegment;-><init>(JLkotlinx/coroutines/channels/ChannelSegment;Lkotlinx/coroutines/channels/BufferedChannel;I)V
+HSPLkotlinx/coroutines/channels/ChannelSegment;->casState$kotlinx_coroutines_core(Ljava/lang/Object;ILjava/lang/Object;)Z
+HSPLkotlinx/coroutines/channels/ChannelSegment;->getNumberOfSlots()I
+HSPLkotlinx/coroutines/channels/ChannelSegment;->getState$kotlinx_coroutines_core(I)Ljava/lang/Object;
+HSPLkotlinx/coroutines/channels/ChannelSegment;->onCancellation(ILkotlin/coroutines/CoroutineContext;)V
+HSPLkotlinx/coroutines/channels/ChannelSegment;->onCancelledRequest(IZ)V
+HSPLkotlinx/coroutines/channels/ChannelSegment;->retrieveElement$kotlinx_coroutines_core(I)Ljava/lang/Object;
+HSPLkotlinx/coroutines/channels/ChannelSegment;->setElementLazy(ILjava/lang/Object;)V
+HSPLkotlinx/coroutines/channels/ChannelSegment;->setState$kotlinx_coroutines_core(ILkotlinx/coroutines/internal/Symbol;)V
+HSPLkotlinx/coroutines/channels/ConflatedBufferedChannel;-><init>(ILkotlinx/coroutines/channels/BufferOverflow;Lkotlin/jvm/functions/Function1;)V
+HSPLkotlinx/coroutines/channels/ConflatedBufferedChannel;->trySend-JP2dKIU(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/channels/ConflatedBufferedChannel;->trySendImpl-Mj0NB7M(Ljava/lang/Object;Z)Ljava/lang/Object;
+HSPLkotlinx/coroutines/channels/ProducerCoroutine;-><init>(Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/channels/BufferedChannel;)V
+HSPLkotlinx/coroutines/channels/ProducerCoroutine;->iterator()Lkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;
+HSPLkotlinx/coroutines/channels/ProducerCoroutine;->send(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/AbstractFlow$collect$1;-><init>(Lkotlinx/coroutines/flow/SafeFlow;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/DistinctFlowImpl$collect$2$emit$1;-><init>(Lkotlinx/coroutines/flow/DistinctFlowImpl$collect$2;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/DistinctFlowImpl$collect$2;-><init>(Lkotlinx/coroutines/flow/DistinctFlowImpl;Lkotlin/jvm/internal/Ref$ObjectRef;Lkotlinx/coroutines/flow/FlowCollector;)V
+HSPLkotlinx/coroutines/flow/DistinctFlowImpl$collect$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/DistinctFlowImpl;-><init>(Lkotlinx/coroutines/flow/Flow;)V
+HSPLkotlinx/coroutines/flow/DistinctFlowImpl;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/FlowKt__ChannelsKt$emitAllImpl$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/FlowKt__LimitKt$dropWhile$$inlined$unsafeFlow$1;-><init>(Lkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest;Lkotlinx/coroutines/flow/StartedWhileSubscribed$command$2;)V
+HSPLkotlinx/coroutines/flow/FlowKt__LimitKt$dropWhile$$inlined$unsafeFlow$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/FlowKt__LimitKt$dropWhile$1$1$emit$1;-><init>(Lkotlinx/coroutines/flow/FlowKt__LimitKt$dropWhile$1$1;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/FlowKt__LimitKt$dropWhile$1$1;-><init>(Lkotlin/jvm/internal/Ref$BooleanRef;Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/jvm/functions/Function2;)V
+HSPLkotlinx/coroutines/flow/FlowKt__LimitKt$dropWhile$1$1;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/FlowKt__MergeKt$mapLatest$1;-><init>(Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/FlowKt__MergeKt$mapLatest$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/FlowKt__MergeKt$mapLatest$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/FlowKt__MergeKt;-><clinit>()V
+HSPLkotlinx/coroutines/flow/FlowKt__ReduceKt$first$$inlined$collectWhile$2$1;-><init>(Lkotlinx/coroutines/flow/FlowKt__ReduceKt$first$$inlined$collectWhile$2;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/FlowKt__ReduceKt$first$$inlined$collectWhile$2;-><init>(Lkotlin/jvm/functions/Function2;Lkotlin/jvm/internal/Ref$ObjectRef;)V
+HSPLkotlinx/coroutines/flow/FlowKt__ReduceKt$first$$inlined$collectWhile$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/FlowKt__ShareKt$launchSharing$1$2;-><init>(Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/MutableSharedFlow;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/FlowKt__ShareKt$launchSharing$1$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLkotlinx/coroutines/flow/FlowKt__ShareKt$launchSharing$1$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/FlowKt__ShareKt$launchSharing$1$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/FlowKt__ShareKt$launchSharing$1;-><init>(Lkotlinx/coroutines/flow/SharingStarted;Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/MutableSharedFlow;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/FlowKt__ShareKt$launchSharing$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLkotlinx/coroutines/flow/FlowKt__ShareKt$launchSharing$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/FlowKt__ShareKt$launchSharing$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/ReadonlyStateFlow;-><init>(Lkotlinx/coroutines/flow/StateFlowImpl;)V
+HSPLkotlinx/coroutines/flow/ReadonlyStateFlow;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/ReadonlyStateFlow;->getValue()Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/SafeFlow;-><init>(Lkotlin/jvm/functions/Function2;)V
+HSPLkotlinx/coroutines/flow/SafeFlow;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/SharedFlowImpl$collect$1;-><init>(Lkotlinx/coroutines/flow/SharedFlowImpl;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/SharedFlowImpl$collect$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;-><init>(IILkotlinx/coroutines/channels/BufferOverflow;)V
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->awaitValue(Lkotlinx/coroutines/flow/SharedFlowSlot;Lkotlinx/coroutines/flow/SharedFlowImpl$collect$1;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->cleanupTailLocked()V
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->collect$suspendImpl(Lkotlinx/coroutines/flow/SharedFlowImpl;Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/intrinsics/CoroutineSingletons;
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->createSlot()Lkotlinx/coroutines/flow/internal/AbstractSharedFlowSlot;
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->createSlotArray()[Lkotlinx/coroutines/flow/internal/AbstractSharedFlowSlot;
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->dropOldestLocked()V
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->enqueueLocked(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->findSlotsToResumeLocked([Lkotlin/coroutines/Continuation;)[Lkotlin/coroutines/Continuation;
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getHead()J
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->growBuffer(II[Ljava/lang/Object;)[Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->tryEmit(Ljava/lang/Object;)Z
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->tryEmitLocked(Ljava/lang/Object;)Z
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->tryPeekLocked(Lkotlinx/coroutines/flow/SharedFlowSlot;)J
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->tryTakeValue(Lkotlinx/coroutines/flow/SharedFlowSlot;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->updateBufferLocked(JJJJ)V
+HSPLkotlinx/coroutines/flow/SharedFlowImpl;->updateCollectorIndexLocked$kotlinx_coroutines_core(J)[Lkotlin/coroutines/Continuation;
+HSPLkotlinx/coroutines/flow/SharedFlowSlot;-><init>()V
+HSPLkotlinx/coroutines/flow/SharedFlowSlot;->allocateLocked(Lkotlinx/coroutines/flow/internal/AbstractSharedFlow;)Z
+HSPLkotlinx/coroutines/flow/SharingCommand;-><clinit>()V
+HSPLkotlinx/coroutines/flow/SharingCommand;-><init>(ILjava/lang/String;)V
+HSPLkotlinx/coroutines/flow/SharingConfig;-><init>()V
+HSPLkotlinx/coroutines/flow/SharingConfig;-><init>(ILkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/channels/BufferOverflow;Lkotlinx/coroutines/flow/Flow;)V
+HSPLkotlinx/coroutines/flow/SharingConfig;->add(Ljava/lang/Object;Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/flow/SharingConfig;->contains(Ljava/lang/Object;)Z
+HSPLkotlinx/coroutines/flow/SharingConfig;->find(Ljava/lang/Object;)I
+HSPLkotlinx/coroutines/flow/SharingConfig;->remove(Ljava/lang/Object;Ljava/lang/Object;)Z
+HSPLkotlinx/coroutines/flow/SharingConfig;->removeScope(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/flow/SharingConfig;->scopeSetAt(I)Landroidx/compose/runtime/collection/IdentityArraySet;
+HSPLkotlinx/coroutines/flow/StartedLazily;-><init>(I)V
+HSPLkotlinx/coroutines/flow/StartedWhileSubscribed$command$1;-><init>(Lkotlinx/coroutines/flow/StartedWhileSubscribed;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/StartedWhileSubscribed$command$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/StartedWhileSubscribed$command$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/StartedWhileSubscribed$command$2;-><init>(Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/StartedWhileSubscribed$command$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLkotlinx/coroutines/flow/StartedWhileSubscribed$command$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/StartedWhileSubscribed$command$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/StartedWhileSubscribed;-><init>(JJ)V
+HSPLkotlinx/coroutines/flow/StartedWhileSubscribed;->command(Lkotlinx/coroutines/flow/internal/SubscriptionCountStateFlow;)Lkotlinx/coroutines/flow/Flow;
+HSPLkotlinx/coroutines/flow/StartedWhileSubscribed;->equals(Ljava/lang/Object;)Z
+HSPLkotlinx/coroutines/flow/StateFlowImpl$collect$1;-><init>(Lkotlinx/coroutines/flow/StateFlowImpl;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/StateFlowImpl$collect$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/StateFlowImpl;-><clinit>()V
+HSPLkotlinx/coroutines/flow/StateFlowImpl;-><init>(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/flow/StateFlowImpl;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/StateFlowImpl;->createSlot()Lkotlinx/coroutines/flow/internal/AbstractSharedFlowSlot;
+HSPLkotlinx/coroutines/flow/StateFlowImpl;->createSlotArray()[Lkotlinx/coroutines/flow/internal/AbstractSharedFlowSlot;
+HSPLkotlinx/coroutines/flow/StateFlowImpl;->getValue()Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/StateFlowImpl;->setValue(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/flow/StateFlowImpl;->updateState(Ljava/lang/Object;Ljava/lang/Object;)Z
+HSPLkotlinx/coroutines/flow/StateFlowSlot;-><clinit>()V
+HSPLkotlinx/coroutines/flow/StateFlowSlot;->allocateLocked(Lkotlinx/coroutines/flow/internal/AbstractSharedFlow;)Z
+HSPLkotlinx/coroutines/flow/internal/AbstractSharedFlow;->allocateSlot()Lkotlinx/coroutines/flow/internal/AbstractSharedFlowSlot;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlow$collect$2;-><init>(Lkotlin/coroutines/Continuation;Lkotlinx/coroutines/flow/FlowCollector;Lkotlinx/coroutines/flow/internal/ChannelFlow;)V
+HSPLkotlinx/coroutines/flow/internal/ChannelFlow$collect$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlow$collect$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlow$collect$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlow$collectToFun$1;-><init>(Lkotlinx/coroutines/flow/internal/ChannelFlow;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/internal/ChannelFlow$collectToFun$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlow$collectToFun$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlow;-><init>(Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)V
+HSPLkotlinx/coroutines/flow/internal/ChannelFlow;->fuse(Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/Flow;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowOperator;-><init>(ILkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/channels/BufferOverflow;Lkotlinx/coroutines/flow/Flow;)V
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowOperator;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3$1$2;-><init>(Lkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest;Lkotlinx/coroutines/flow/FlowCollector;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3$1$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3$1$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3$1$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3$1$emit$1;-><init>(Lkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3$1;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3$1;-><init>(Lkotlin/jvm/internal/Ref$ObjectRef;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest;Lkotlinx/coroutines/flow/FlowCollector;)V
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3$1;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3;-><init>(Lkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest;Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)V
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest;-><init>(Lkotlin/jvm/functions/Function3;Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)V
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest;->create(Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/internal/ChannelFlow;
+HSPLkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest;->flowCollect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/internal/NoOpContinuation;-><clinit>()V
+HSPLkotlinx/coroutines/flow/internal/NopCollector;-><clinit>()V
+HSPLkotlinx/coroutines/flow/internal/SafeCollector;-><init>(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/CoroutineContext;)V
+HSPLkotlinx/coroutines/flow/internal/SendingCollector;-><init>(Lkotlinx/coroutines/channels/ProducerScope;)V
+HSPLkotlinx/coroutines/flow/internal/SendingCollector;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/flow/internal/SubscriptionCountStateFlow;-><init>(I)V
+HSPLkotlinx/coroutines/internal/AtomicOp;-><clinit>()V
+HSPLkotlinx/coroutines/internal/AtomicOp;-><init>()V
+HSPLkotlinx/coroutines/internal/AtomicOp;->perform(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/internal/ConcurrentLinkedListNode;-><clinit>()V
+HSPLkotlinx/coroutines/internal/ConcurrentLinkedListNode;-><init>(Lkotlinx/coroutines/internal/ConcurrentLinkedListNode;)V
+HSPLkotlinx/coroutines/internal/ConcurrentLinkedListNode;->cleanPrev()V
+HSPLkotlinx/coroutines/internal/ConcurrentLinkedListNode;->getNext()Lkotlinx/coroutines/internal/ConcurrentLinkedListNode;
+HSPLkotlinx/coroutines/internal/ContextScope;-><init>(Lkotlin/coroutines/CoroutineContext;)V
+HSPLkotlinx/coroutines/internal/ContextScope;->getCoroutineContext()Lkotlin/coroutines/CoroutineContext;
+HSPLkotlinx/coroutines/internal/DispatchedContinuation;-><clinit>()V
+HSPLkotlinx/coroutines/internal/DispatchedContinuation;-><init>(Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/coroutines/jvm/internal/ContinuationImpl;)V
+HSPLkotlinx/coroutines/internal/DispatchedContinuation;->getContext()Lkotlin/coroutines/CoroutineContext;
+HSPLkotlinx/coroutines/internal/DispatchedContinuation;->getDelegate$kotlinx_coroutines_core()Lkotlin/coroutines/Continuation;
+HSPLkotlinx/coroutines/internal/DispatchedContinuation;->resumeWith(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/internal/DispatchedContinuation;->takeState$kotlinx_coroutines_core()Ljava/lang/Object;
+HSPLkotlinx/coroutines/internal/LimitedDispatcher;-><clinit>()V
+HSPLkotlinx/coroutines/internal/LimitedDispatcher;-><init>(Lkotlinx/coroutines/scheduling/UnlimitedIoScheduler;I)V
+HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode$toString$1;-><init>(ILjava/lang/Object;)V
+HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;-><clinit>()V
+HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;-><init>()V
+HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->correctPrev()Lkotlinx/coroutines/internal/LockFreeLinkedListNode;
+HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->finishAdd(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)V
+HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->getNext()Ljava/lang/Object;
+HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->getNextNode()Lkotlinx/coroutines/internal/LockFreeLinkedListNode;
+HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->getPrevNode()Lkotlinx/coroutines/internal/LockFreeLinkedListNode;
+HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->isRemoved()Z
+HSPLkotlinx/coroutines/internal/LockFreeTaskQueue;-><clinit>()V
+HSPLkotlinx/coroutines/internal/LockFreeTaskQueue;-><init>()V
+HSPLkotlinx/coroutines/internal/LockFreeTaskQueueCore;-><clinit>()V
+HSPLkotlinx/coroutines/internal/LockFreeTaskQueueCore;-><init>(IZ)V
+HSPLkotlinx/coroutines/internal/MainDispatcherLoader;-><clinit>()V
+HSPLkotlinx/coroutines/internal/Removed;-><init>(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)V
+HSPLkotlinx/coroutines/internal/ResizableAtomicArray;-><init>(I)V
+HSPLkotlinx/coroutines/internal/ScopeCoroutine;-><init>(Lkotlin/coroutines/Continuation;Lkotlin/coroutines/CoroutineContext;)V
+HSPLkotlinx/coroutines/internal/ScopeCoroutine;->afterCompletion(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/internal/ScopeCoroutine;->afterResume(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/internal/ScopeCoroutine;->isScopedCoroutine()Z
+HSPLkotlinx/coroutines/internal/Segment;-><clinit>()V
+HSPLkotlinx/coroutines/internal/Segment;-><init>(JLkotlinx/coroutines/internal/Segment;I)V
+HSPLkotlinx/coroutines/internal/Segment;->decPointers$kotlinx_coroutines_core()Z
+HSPLkotlinx/coroutines/internal/Segment;->isRemoved()Z
+HSPLkotlinx/coroutines/internal/Segment;->onSlotCleaned()V
+HSPLkotlinx/coroutines/internal/Segment;->tryIncPointers$kotlinx_coroutines_core()Z
+HSPLkotlinx/coroutines/internal/Symbol;-><init>(ILjava/lang/String;)V
+HSPLkotlinx/coroutines/internal/SystemPropsKt__SystemPropsKt;-><clinit>()V
+HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;-><clinit>()V
+HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;-><init>(IIJLjava/lang/String;)V
+HSPLkotlinx/coroutines/scheduling/DefaultIoScheduler;-><clinit>()V
+HSPLkotlinx/coroutines/scheduling/DefaultScheduler;-><clinit>()V
+HSPLkotlinx/coroutines/scheduling/DefaultScheduler;-><init>()V
+HSPLkotlinx/coroutines/scheduling/NanoTimeSource;-><clinit>()V
+HSPLkotlinx/coroutines/scheduling/SchedulerCoroutineDispatcher;-><init>(IIJLjava/lang/String;)V
+HSPLkotlinx/coroutines/scheduling/Task;-><init>(JLkotlin/ULong$Companion;)V
+HSPLkotlinx/coroutines/scheduling/TasksKt;-><clinit>()V
+HSPLkotlinx/coroutines/scheduling/UnlimitedIoScheduler;-><clinit>()V
+HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner$resume$2;-><init>(Lkotlinx/coroutines/sync/MutexImpl;Lkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;I)V
+HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;-><init>(Lkotlinx/coroutines/sync/MutexImpl;Lkotlinx/coroutines/CancellableContinuationImpl;)V
+HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->completeResume(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->invokeOnCancellation(Lkotlinx/coroutines/internal/Segment;I)V
+HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->tryResume(Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/internal/Symbol;
+HSPLkotlinx/coroutines/sync/MutexImpl;-><clinit>()V
+HSPLkotlinx/coroutines/sync/MutexImpl;-><init>(Z)V
+HSPLkotlinx/coroutines/sync/MutexImpl;->isLocked()Z
+HSPLkotlinx/coroutines/sync/MutexImpl;->lock(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLkotlinx/coroutines/sync/MutexImpl;->unlock(Ljava/lang/Object;)V
+HSPLkotlinx/coroutines/sync/SemaphoreImpl$addAcquireToQueue$createNewSegment$1;-><clinit>()V
+HSPLkotlinx/coroutines/sync/SemaphoreImpl$addAcquireToQueue$createNewSegment$1;-><init>()V
+HSPLkotlinx/coroutines/sync/SemaphoreImpl$tryResumeNextFromQueue$createNewSegment$1;-><clinit>()V
+HSPLkotlinx/coroutines/sync/SemaphoreImpl$tryResumeNextFromQueue$createNewSegment$1;-><init>()V
+HSPLkotlinx/coroutines/sync/SemaphoreImpl;-><clinit>()V
+HSPLkotlinx/coroutines/sync/SemaphoreImpl;-><init>(I)V
+HSPLkotlinx/coroutines/sync/SemaphoreImpl;->acquire(Lkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;)V
+HSPLkotlinx/coroutines/sync/SemaphoreImpl;->release()V
+HSPLkotlinx/coroutines/sync/SemaphoreKt;-><clinit>()V
+HSPLkotlinx/coroutines/sync/SemaphoreSegment;-><init>(JLkotlinx/coroutines/sync/SemaphoreSegment;I)V
+HSPLkotlinx/coroutines/sync/SemaphoreSegment;->getNumberOfSlots()I
+HSPLokhttp3/Headers$Builder;-><init>()V
+HSPLokhttp3/Headers$Builder;->add(I)V
+HSPLokhttp3/Headers$Builder;->takeMax()I
+HSPLokhttp3/MediaType;-><clinit>()V
+HSPLokhttp3/MediaType;->Channel$default(ILkotlinx/coroutines/channels/BufferOverflow;I)Lkotlinx/coroutines/channels/BufferedChannel;
+HSPLokhttp3/MediaType;->CompositionLocalProvider(Landroidx/compose/runtime/ProvidedValue;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
+HSPLokhttp3/MediaType;->CoroutineScope(Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/internal/ContextScope;
+HSPLokhttp3/MediaType;->LazyLayoutPinnableItem(Ljava/lang/Object;ILandroidx/compose/foundation/lazy/layout/LazyLayoutPinnedItemList;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
+HSPLokhttp3/MediaType;->LazySaveableStateHolderProvider(Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)V
+HSPLokhttp3/MediaType;->Offset(FF)J
+HSPLokhttp3/MediaType;->ParagraphIntrinsics(Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/font/FontFamily$Resolver;Landroidx/compose/ui/unit/Density;Ljava/lang/String;Ljava/util/List;Ljava/util/List;)Landroidx/compose/ui/text/platform/AndroidParagraphIntrinsics;
+HSPLokhttp3/MediaType;->RoundRect-gG7oq9Y(FFFFJ)Landroidx/compose/ui/geometry/RoundRect;
+HSPLokhttp3/MediaType;->TextRange(II)J
+HSPLokhttp3/MediaType;->access$SkippableItem-JVlU9Rs(Landroidx/compose/foundation/lazy/layout/LazyLayoutItemProvider;Ljava/lang/Object;ILjava/lang/Object;Landroidx/compose/runtime/Composer;I)V
+HSPLokhttp3/MediaType;->access$checkIndex(ILjava/util/List;)V
+HSPLokhttp3/MediaType;->access$containsMark([II)Z
+HSPLokhttp3/MediaType;->access$groupSize([II)I
+HSPLokhttp3/MediaType;->access$hasAux([II)Z
+HSPLokhttp3/MediaType;->access$isChainUpdate(Landroidx/compose/ui/node/BackwardsCompatNode;)Z
+HSPLokhttp3/MediaType;->access$isNode([II)Z
+HSPLokhttp3/MediaType;->access$nodeCount([II)I
+HSPLokhttp3/MediaType;->access$slotAnchor([II)I
+HSPLokhttp3/MediaType;->access$updateGroupSize([III)V
+HSPLokhttp3/MediaType;->access$updateNodeCount([III)V
+HSPLokhttp3/MediaType;->adapt$default(Landroidx/compose/ui/graphics/colorspace/ColorSpace;)Landroidx/compose/ui/graphics/colorspace/ColorSpace;
+HSPLokhttp3/MediaType;->cancel(Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/LifecycleDestroyedException;)V
+HSPLokhttp3/MediaType;->ceilToIntPx(F)I
+HSPLokhttp3/MediaType;->checkParallelism(I)V
+HSPLokhttp3/MediaType;->chromaticAdaptation([F[F[F)[F
+HSPLokhttp3/MediaType;->coerceIn-8ffj60Q(IJ)J
+HSPLokhttp3/MediaType;->collectIsPressedAsState(Landroidx/compose/foundation/interaction/InteractionSource;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/MutableState;
+HSPLokhttp3/MediaType;->colors-u3YEpmA(JJJJJJJJJJJJLandroidx/compose/runtime/Composer;II)Landroidx/tv/material3/ToggleableSurfaceColors;
+HSPLokhttp3/MediaType;->compare(Landroidx/compose/ui/graphics/colorspace/WhitePoint;Landroidx/compose/ui/graphics/colorspace/WhitePoint;)Z
+HSPLokhttp3/MediaType;->coroutineScope(Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+HSPLokhttp3/MediaType;->countOneBits(I)I
+HSPLokhttp3/MediaType;->createFontFamilyResolver(Landroid/content/Context;)Landroidx/compose/ui/text/font/FontFamilyResolverImpl;
+HSPLokhttp3/MediaType;->foldCopies(Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext;Z)Lkotlin/coroutines/CoroutineContext;
+HSPLokhttp3/MediaType;->getCharSequenceBounds(Landroid/text/TextPaint;Ljava/lang/CharSequence;II)Landroid/graphics/Rect;
+HSPLokhttp3/MediaType;->getFontFamilyResult(Landroid/content/Context;Landroidx/core/provider/FontRequest;)Landroidx/compose/ui/input/pointer/util/PointerIdArray;
+HSPLokhttp3/MediaType;->getOrNull(Landroidx/compose/ui/semantics/SemanticsConfiguration;Landroidx/compose/ui/semantics/SemanticsPropertyKey;)Ljava/lang/Object;
+HSPLokhttp3/MediaType;->getSegment-impl(Ljava/lang/Object;)Lkotlinx/coroutines/internal/Segment;
+HSPLokhttp3/MediaType;->inverse3x3([F)[F
+HSPLokhttp3/MediaType;->invokeComposable(Landroidx/compose/runtime/Composer;Lkotlin/jvm/functions/Function2;)V
+HSPLokhttp3/MediaType;->invokeOnCompletion$default(Lkotlinx/coroutines/Job;ZLkotlinx/coroutines/JobNode;I)Lkotlinx/coroutines/DisposableHandle;
+HSPLokhttp3/MediaType;->isActive(Lkotlinx/coroutines/CoroutineScope;)Z
+HSPLokhttp3/MediaType;->isClosed-impl(Ljava/lang/Object;)Z
+HSPLokhttp3/MediaType;->mul3x3([F[F)[F
+HSPLokhttp3/MediaType;->mul3x3Diag([F[F)[F
+HSPLokhttp3/MediaType;->mul3x3Float3([F[F)V
+HSPLokhttp3/MediaType;->mul3x3Float3_0([FFFF)F
+HSPLokhttp3/MediaType;->mul3x3Float3_1([FFFF)F
+HSPLokhttp3/MediaType;->mul3x3Float3_2([FFFF)F
+HSPLokhttp3/MediaType;->search(Ljava/util/ArrayList;II)I
+HSPLokhttp3/MediaType;->set-impl(Landroidx/compose/runtime/Composer;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V
+HSPLokhttp3/MediaType;->setInt-A6tL2VI(Landroidx/compose/runtime/changelist/Operations;II)V
+HSPLokhttp3/MediaType;->setObject-DKhxnng(Landroidx/compose/runtime/changelist/Operations;ILjava/lang/Object;)V
+HSPLokhttp3/MediaType;->shape(Landroidx/compose/ui/graphics/RectangleShapeKt$RectangleShape$1;Landroidx/compose/runtime/Composer;I)Landroidx/tv/material3/ToggleableSurfaceShape;
+HSPLokhttp3/MediaType;->toArray(Ljava/util/Collection;)[Ljava/lang/Object;
+HSPLokhttp3/MediaType;->updateChangedFlags(I)I
+L_COROUTINE/ArtificialStackFrames;
+Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda0;
+Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda1;
+Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda2;
+Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda3;
+Landroidx/activity/ComponentActivity$1;
+Landroidx/activity/ComponentActivity$2;
+Landroidx/activity/ComponentActivity$3;
+Landroidx/activity/ComponentActivity$4;
+Landroidx/activity/ComponentActivity$5;
+Landroidx/activity/ComponentActivity$NonConfigurationInstances;
+Landroidx/activity/ComponentActivity$ReportFullyDrawnExecutorApi16Impl;
+Landroidx/activity/ComponentActivity;
+Landroidx/activity/FullyDrawnReporter;
+Landroidx/activity/OnBackPressedDispatcher;
+Landroidx/activity/compose/ComponentActivityKt;
+Landroidx/activity/contextaware/ContextAwareHelper;
+Landroidx/activity/result/ActivityResult$1;
+Landroidx/arch/core/executor/ArchTaskExecutor;
+Landroidx/arch/core/executor/DefaultTaskExecutor$1;
+Landroidx/arch/core/executor/DefaultTaskExecutor;
+Landroidx/arch/core/internal/FastSafeIterableMap;
+Landroidx/arch/core/internal/SafeIterableMap$AscendingIterator;
+Landroidx/arch/core/internal/SafeIterableMap$Entry;
+Landroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;
+Landroidx/arch/core/internal/SafeIterableMap$ListIterator;
+Landroidx/arch/core/internal/SafeIterableMap$SupportRemove;
+Landroidx/arch/core/internal/SafeIterableMap;
+Landroidx/collection/ArrayMap;
+Landroidx/collection/ArraySet;
+Landroidx/collection/LongSparseArray;
+Landroidx/collection/SimpleArrayMap;
+Landroidx/collection/SparseArrayCompat;
+Landroidx/compose/animation/FlingCalculator;
+Landroidx/compose/animation/FlingCalculatorKt;
+Landroidx/compose/animation/SingleValueAnimationKt;
+Landroidx/compose/animation/SplineBasedFloatDecayAnimationSpec;
+Landroidx/compose/animation/SplineBasedFloatDecayAnimationSpec_androidKt;
+Landroidx/compose/animation/core/Animatable$runAnimation$2;
+Landroidx/compose/animation/core/Animatable;
+Landroidx/compose/animation/core/AnimateAsStateKt$animateValueAsState$2;
+Landroidx/compose/animation/core/AnimateAsStateKt$animateValueAsState$3$1;
+Landroidx/compose/animation/core/AnimateAsStateKt$animateValueAsState$3;
+Landroidx/compose/animation/core/AnimateAsStateKt;
+Landroidx/compose/animation/core/Animation;
+Landroidx/compose/animation/core/AnimationEndReason$EnumUnboxingLocalUtility;
+Landroidx/compose/animation/core/AnimationResult;
+Landroidx/compose/animation/core/AnimationScope;
+Landroidx/compose/animation/core/AnimationSpec;
+Landroidx/compose/animation/core/AnimationState;
+Landroidx/compose/animation/core/AnimationVector1D;
+Landroidx/compose/animation/core/AnimationVector2D;
+Landroidx/compose/animation/core/AnimationVector3D;
+Landroidx/compose/animation/core/AnimationVector4D;
+Landroidx/compose/animation/core/AnimationVector;
+Landroidx/compose/animation/core/Animations;
+Landroidx/compose/animation/core/ComplexDouble;
+Landroidx/compose/animation/core/CubicBezierEasing;
+Landroidx/compose/animation/core/DecayAnimationSpecImpl;
+Landroidx/compose/animation/core/Easing;
+Landroidx/compose/animation/core/EasingKt$$ExternalSyntheticLambda0;
+Landroidx/compose/animation/core/EasingKt;
+Landroidx/compose/animation/core/FloatAnimationSpec;
+Landroidx/compose/animation/core/FloatDecayAnimationSpec;
+Landroidx/compose/animation/core/FloatSpringSpec;
+Landroidx/compose/animation/core/FloatTweenSpec;
+Landroidx/compose/animation/core/MutatorMutex$Mutator;
+Landroidx/compose/animation/core/MutatorMutex$mutate$2;
+Landroidx/compose/animation/core/MutatorMutex;
+Landroidx/compose/animation/core/SpringSimulation;
+Landroidx/compose/animation/core/SpringSpec;
+Landroidx/compose/animation/core/SuspendAnimationKt$animate$4;
+Landroidx/compose/animation/core/SuspendAnimationKt$animate$6;
+Landroidx/compose/animation/core/SuspendAnimationKt$animate$7;
+Landroidx/compose/animation/core/SuspendAnimationKt$animate$9;
+Landroidx/compose/animation/core/TargetBasedAnimation;
+Landroidx/compose/animation/core/TweenSpec;
+Landroidx/compose/animation/core/TwoWayConverterImpl;
+Landroidx/compose/animation/core/VectorConvertersKt;
+Landroidx/compose/animation/core/VectorizedDurationBasedAnimationSpec;
+Landroidx/compose/animation/core/VectorizedFiniteAnimationSpec;
+Landroidx/compose/animation/core/VectorizedFloatAnimationSpec;
+Landroidx/compose/animation/core/VectorizedSpringSpec;
+Landroidx/compose/animation/core/VectorizedTweenSpec;
+Landroidx/compose/animation/core/VisibilityThresholdsKt;
+Landroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect$effectModifier$1;
+Landroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect$onNewSize$1;
+Landroidx/compose/foundation/AndroidEdgeEffectOverscrollEffect;
+Landroidx/compose/foundation/AndroidOverscrollKt;
+Landroidx/compose/foundation/Api31Impl;
+Landroidx/compose/foundation/BackgroundElement;
+Landroidx/compose/foundation/BackgroundNode;
+Landroidx/compose/foundation/BorderCache;
+Landroidx/compose/foundation/BorderKt$drawRectBorder$1;
+Landroidx/compose/foundation/BorderModifierNode$drawGenericBorder$3;
+Landroidx/compose/foundation/BorderModifierNode$drawRoundRectBorder$1;
+Landroidx/compose/foundation/BorderModifierNode;
+Landroidx/compose/foundation/BorderModifierNodeElement;
+Landroidx/compose/foundation/BorderStroke;
+Landroidx/compose/foundation/ClipScrollableContainerKt;
+Landroidx/compose/foundation/DrawOverscrollModifier;
+Landroidx/compose/foundation/FocusableElement;
+Landroidx/compose/foundation/FocusableInteractionNode$emitWithFallback$1;
+Landroidx/compose/foundation/FocusableInteractionNode;
+Landroidx/compose/foundation/FocusableKt$FocusableInNonTouchModeElement$1;
+Landroidx/compose/foundation/FocusableKt;
+Landroidx/compose/foundation/FocusableNode$onFocusEvent$1;
+Landroidx/compose/foundation/FocusableNode;
+Landroidx/compose/foundation/FocusablePinnableContainerNode;
+Landroidx/compose/foundation/FocusableSemanticsNode;
+Landroidx/compose/foundation/FocusedBoundsKt;
+Landroidx/compose/foundation/FocusedBoundsNode;
+Landroidx/compose/foundation/FocusedBoundsObserverNode;
+Landroidx/compose/foundation/ImageKt$Image$1$1;
+Landroidx/compose/foundation/ImageKt$Image$1;
+Landroidx/compose/foundation/ImageKt$Image$2;
+Landroidx/compose/foundation/ImageKt;
+Landroidx/compose/foundation/Indication;
+Landroidx/compose/foundation/IndicationInstance;
+Landroidx/compose/foundation/IndicationKt$indication$2;
+Landroidx/compose/foundation/IndicationKt;
+Landroidx/compose/foundation/IndicationModifier;
+Landroidx/compose/foundation/MutatePriority;
+Landroidx/compose/foundation/MutatorMutex$Mutator;
+Landroidx/compose/foundation/MutatorMutex$mutateWith$2;
+Landroidx/compose/foundation/MutatorMutex;
+Landroidx/compose/foundation/OverscrollConfiguration;
+Landroidx/compose/foundation/OverscrollConfigurationKt;
+Landroidx/compose/foundation/OverscrollEffect;
+Landroidx/compose/foundation/ScrollKt$rememberScrollState$1$1;
+Landroidx/compose/foundation/ScrollKt$scroll$2$semantics$1$1;
+Landroidx/compose/foundation/ScrollKt$scroll$2$semantics$1;
+Landroidx/compose/foundation/ScrollKt$scroll$2;
+Landroidx/compose/foundation/ScrollState$canScrollForward$2;
+Landroidx/compose/foundation/ScrollState;
+Landroidx/compose/foundation/ScrollingLayoutElement;
+Landroidx/compose/foundation/ScrollingLayoutNode;
+Landroidx/compose/foundation/gestures/BringIntoViewRequestPriorityQueue;
+Landroidx/compose/foundation/gestures/BringIntoViewSpec$Companion$DefaultBringIntoViewSpec$1;
+Landroidx/compose/foundation/gestures/BringIntoViewSpec$Companion;
+Landroidx/compose/foundation/gestures/BringIntoViewSpec;
+Landroidx/compose/foundation/gestures/ContentInViewNode$Request;
+Landroidx/compose/foundation/gestures/ContentInViewNode$launchAnimation$2$1;
+Landroidx/compose/foundation/gestures/ContentInViewNode$launchAnimation$2;
+Landroidx/compose/foundation/gestures/ContentInViewNode;
+Landroidx/compose/foundation/gestures/DefaultFlingBehavior;
+Landroidx/compose/foundation/gestures/DefaultScrollableState$scroll$2$1;
+Landroidx/compose/foundation/gestures/DefaultScrollableState$scroll$2;
+Landroidx/compose/foundation/gestures/DefaultScrollableState$scrollScope$1;
+Landroidx/compose/foundation/gestures/DefaultScrollableState;
+Landroidx/compose/foundation/gestures/DraggableKt$awaitDrag$2;
+Landroidx/compose/foundation/gestures/DraggableNode$onAttach$1;
+Landroidx/compose/foundation/gestures/DraggableNode$pointerInputNode$1;
+Landroidx/compose/foundation/gestures/DraggableNode;
+Landroidx/compose/foundation/gestures/FlingBehavior;
+Landroidx/compose/foundation/gestures/ModifierLocalScrollableContainerProvider;
+Landroidx/compose/foundation/gestures/MouseWheelScrollNode$1;
+Landroidx/compose/foundation/gestures/MouseWheelScrollNode;
+Landroidx/compose/foundation/gestures/Orientation;
+Landroidx/compose/foundation/gestures/ScrollDraggableState;
+Landroidx/compose/foundation/gestures/ScrollScope;
+Landroidx/compose/foundation/gestures/ScrollableElement;
+Landroidx/compose/foundation/gestures/ScrollableGesturesNode$onDragStopped$1;
+Landroidx/compose/foundation/gestures/ScrollableGesturesNode;
+Landroidx/compose/foundation/gestures/ScrollableKt$DefaultScrollMotionDurationScale$1;
+Landroidx/compose/foundation/gestures/ScrollableKt$NoOpOnDragStarted$1;
+Landroidx/compose/foundation/gestures/ScrollableKt$NoOpScrollScope$1;
+Landroidx/compose/foundation/gestures/ScrollableKt$UnityDensity$1;
+Landroidx/compose/foundation/gestures/ScrollableKt;
+Landroidx/compose/foundation/gestures/ScrollableNestedScrollConnection;
+Landroidx/compose/foundation/gestures/ScrollableNode;
+Landroidx/compose/foundation/gestures/ScrollableState;
+Landroidx/compose/foundation/gestures/ScrollingLogic;
+Landroidx/compose/foundation/gestures/UpdatableAnimationState$animateToZero$1;
+Landroidx/compose/foundation/gestures/UpdatableAnimationState$animateToZero$4;
+Landroidx/compose/foundation/gestures/UpdatableAnimationState;
+Landroidx/compose/foundation/interaction/FocusInteraction$Focus;
+Landroidx/compose/foundation/interaction/FocusInteraction$Unfocus;
+Landroidx/compose/foundation/interaction/FocusInteractionKt$collectIsFocusedAsState$1$1$1;
+Landroidx/compose/foundation/interaction/FocusInteractionKt$collectIsFocusedAsState$1$1;
+Landroidx/compose/foundation/interaction/Interaction;
+Landroidx/compose/foundation/interaction/InteractionSource;
+Landroidx/compose/foundation/interaction/MutableInteractionSourceImpl;
+Landroidx/compose/foundation/interaction/PressInteraction$Press;
+Landroidx/compose/foundation/interaction/PressInteraction$Release;
+Landroidx/compose/foundation/interaction/PressInteractionKt$collectIsPressedAsState$1$1;
+Landroidx/compose/foundation/layout/Arrangement$Center$1;
+Landroidx/compose/foundation/layout/Arrangement$End$1;
+Landroidx/compose/foundation/layout/Arrangement$Horizontal;
+Landroidx/compose/foundation/layout/Arrangement$SpacedAligned;
+Landroidx/compose/foundation/layout/Arrangement$Top$1;
+Landroidx/compose/foundation/layout/Arrangement$Vertical;
+Landroidx/compose/foundation/layout/Arrangement;
+Landroidx/compose/foundation/layout/BoxKt$Box$2;
+Landroidx/compose/foundation/layout/BoxKt$EmptyBoxMeasurePolicy$1;
+Landroidx/compose/foundation/layout/BoxKt$boxMeasurePolicy$1$2;
+Landroidx/compose/foundation/layout/BoxKt$boxMeasurePolicy$1;
+Landroidx/compose/foundation/layout/BoxKt;
+Landroidx/compose/foundation/layout/BoxScope;
+Landroidx/compose/foundation/layout/BoxScopeInstance;
+Landroidx/compose/foundation/layout/ColumnKt;
+Landroidx/compose/foundation/layout/CrossAxisAlignment$VerticalCrossAxisAlignment;
+Landroidx/compose/foundation/layout/FillElement;
+Landroidx/compose/foundation/layout/FillNode;
+Landroidx/compose/foundation/layout/HorizontalAlignElement;
+Landroidx/compose/foundation/layout/HorizontalAlignNode;
+Landroidx/compose/foundation/layout/OffsetElement;
+Landroidx/compose/foundation/layout/OffsetKt;
+Landroidx/compose/foundation/layout/OffsetNode$measure$1;
+Landroidx/compose/foundation/layout/OffsetNode;
+Landroidx/compose/foundation/layout/PaddingElement;
+Landroidx/compose/foundation/layout/PaddingNode;
+Landroidx/compose/foundation/layout/PaddingValuesImpl;
+Landroidx/compose/foundation/layout/RowColumnImplKt$rowColumnMeasurePolicy$1;
+Landroidx/compose/foundation/layout/RowColumnMeasureHelperResult;
+Landroidx/compose/foundation/layout/RowColumnMeasurementHelper;
+Landroidx/compose/foundation/layout/RowColumnParentData;
+Landroidx/compose/foundation/layout/RowKt$DefaultRowMeasurePolicy$1;
+Landroidx/compose/foundation/layout/RowKt$rowMeasurePolicy$1$1;
+Landroidx/compose/foundation/layout/RowKt;
+Landroidx/compose/foundation/layout/RowScope;
+Landroidx/compose/foundation/layout/RowScopeInstance;
+Landroidx/compose/foundation/layout/SizeElement;
+Landroidx/compose/foundation/layout/SizeKt;
+Landroidx/compose/foundation/layout/SizeNode;
+Landroidx/compose/foundation/layout/SpacerMeasurePolicy;
+Landroidx/compose/foundation/layout/WrapContentElement;
+Landroidx/compose/foundation/layout/WrapContentNode$measure$1;
+Landroidx/compose/foundation/layout/WrapContentNode;
+Landroidx/compose/foundation/lazy/layout/DefaultLazyKey;
+Landroidx/compose/foundation/lazy/layout/IntervalList$Interval;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutIntervalContent$Interval;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory$CachedItemContent;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutItemProvider;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutItemReusePolicy;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutKt$LazyLayout$3$2$1;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutKt$LazyLayout$3$itemContentFactory$1$1;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutKt$LazyLayout$3;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutMeasureScopeImpl;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutPinnableItem;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutPinnedItemList;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState$PrefetchHandle;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState$Prefetcher;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher$PrefetchRequest;
+Landroidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher;
+Landroidx/compose/foundation/lazy/layout/LazySaveableStateHolder$1;
+Landroidx/compose/foundation/lazy/layout/LazySaveableStateHolder$SaveableStateProvider$2$invoke$$inlined$onDispose$1;
+Landroidx/compose/foundation/lazy/layout/LazySaveableStateHolder;
+Landroidx/compose/foundation/lazy/layout/MutableIntervalList;
+Landroidx/compose/foundation/relocation/BringIntoViewChildNode;
+Landroidx/compose/foundation/relocation/BringIntoViewKt;
+Landroidx/compose/foundation/relocation/BringIntoViewParent;
+Landroidx/compose/foundation/relocation/BringIntoViewRequesterImpl$bringIntoView$1;
+Landroidx/compose/foundation/relocation/BringIntoViewRequesterImpl;
+Landroidx/compose/foundation/relocation/BringIntoViewRequesterNode;
+Landroidx/compose/foundation/relocation/BringIntoViewResponder;
+Landroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2$1$1;
+Landroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2$1;
+Landroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2$2;
+Landroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$2;
+Landroidx/compose/foundation/relocation/BringIntoViewResponderNode$bringChildIntoView$parentRect$1;
+Landroidx/compose/foundation/relocation/BringIntoViewResponderNode;
+Landroidx/compose/foundation/relocation/BringIntoViewResponder_androidKt$defaultBringIntoViewParent$1;
+Landroidx/compose/foundation/shape/CornerBasedShape;
+Landroidx/compose/foundation/shape/CornerSize;
+Landroidx/compose/foundation/shape/DpCornerSize;
+Landroidx/compose/foundation/shape/GenericShape;
+Landroidx/compose/foundation/shape/PercentCornerSize;
+Landroidx/compose/foundation/shape/RoundedCornerShape;
+Landroidx/compose/foundation/shape/RoundedCornerShapeKt;
+Landroidx/compose/foundation/text/EmptyMeasurePolicy;
+Landroidx/compose/foundation/text/modifiers/InlineDensity;
+Landroidx/compose/foundation/text/modifiers/MinLinesConstrainer;
+Landroidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache;
+Landroidx/compose/foundation/text/modifiers/TextAnnotatedStringElement;
+Landroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode$TextSubstitutionValue;
+Landroidx/compose/foundation/text/modifiers/TextAnnotatedStringNode;
+Landroidx/compose/foundation/text/selection/SelectionRegistrarKt;
+Landroidx/compose/foundation/text/selection/TextSelectionColors;
+Landroidx/compose/foundation/text/selection/TextSelectionColorsKt;
+Landroidx/compose/material/ripple/AndroidRippleIndicationInstance;
+Landroidx/compose/material/ripple/PlatformRipple;
+Landroidx/compose/material/ripple/RippleAlpha;
+Landroidx/compose/material/ripple/RippleIndicationInstance;
+Landroidx/compose/material/ripple/RippleKt;
+Landroidx/compose/material/ripple/RippleTheme;
+Landroidx/compose/material/ripple/RippleThemeKt;
+Landroidx/compose/material3/ColorScheme;
+Landroidx/compose/material3/ColorSchemeKt;
+Landroidx/compose/material3/MaterialRippleTheme;
+Landroidx/compose/material3/MaterialThemeKt$MaterialTheme$2;
+Landroidx/compose/material3/ShapeDefaults;
+Landroidx/compose/material3/Shapes;
+Landroidx/compose/material3/ShapesKt$LocalShapes$1;
+Landroidx/compose/material3/ShapesKt;
+Landroidx/compose/material3/TextKt$ProvideTextStyle$1;
+Landroidx/compose/material3/TextKt$Text$1;
+Landroidx/compose/material3/TextKt;
+Landroidx/compose/material3/Typography;
+Landroidx/compose/material3/TypographyKt;
+Landroidx/compose/material3/tokens/ColorDarkTokens;
+Landroidx/compose/material3/tokens/PaletteTokens;
+Landroidx/compose/material3/tokens/ShapeTokens;
+Landroidx/compose/material3/tokens/TypeScaleTokens;
+Landroidx/compose/material3/tokens/TypefaceTokens;
+Landroidx/compose/material3/tokens/TypographyTokens;
+Landroidx/compose/runtime/Anchor;
+Landroidx/compose/runtime/Applier;
+Landroidx/compose/runtime/BroadcastFrameClock$FrameAwaiter;
+Landroidx/compose/runtime/BroadcastFrameClock;
+Landroidx/compose/runtime/ComposableSingletons$CompositionKt;
+Landroidx/compose/runtime/ComposeNodeLifecycleCallback;
+Landroidx/compose/runtime/Composer;
+Landroidx/compose/runtime/ComposerImpl$CompositionContextHolder;
+Landroidx/compose/runtime/ComposerImpl$CompositionContextImpl;
+Landroidx/compose/runtime/ComposerImpl$derivedStateObserver$1;
+Landroidx/compose/runtime/ComposerImpl;
+Landroidx/compose/runtime/Composition;
+Landroidx/compose/runtime/CompositionContext;
+Landroidx/compose/runtime/CompositionContextKt;
+Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;
+Landroidx/compose/runtime/CompositionImpl;
+Landroidx/compose/runtime/CompositionKt;
+Landroidx/compose/runtime/CompositionLocal;
+Landroidx/compose/runtime/CompositionLocalMap$Companion;
+Landroidx/compose/runtime/CompositionLocalMap;
+Landroidx/compose/runtime/CompositionObserverHolder;
+Landroidx/compose/runtime/CompositionScopedCoroutineScopeCanceller;
+Landroidx/compose/runtime/DerivedSnapshotState$ResultRecord;
+Landroidx/compose/runtime/DerivedSnapshotState;
+Landroidx/compose/runtime/DerivedStateObserver;
+Landroidx/compose/runtime/DisposableEffectImpl;
+Landroidx/compose/runtime/DisposableEffectResult;
+Landroidx/compose/runtime/DisposableEffectScope;
+Landroidx/compose/runtime/DynamicProvidableCompositionLocal;
+Landroidx/compose/runtime/GroupInfo;
+Landroidx/compose/runtime/IntStack;
+Landroidx/compose/runtime/Invalidation;
+Landroidx/compose/runtime/JoinedKey;
+Landroidx/compose/runtime/KeyInfo;
+Landroidx/compose/runtime/Latch$await$2$2;
+Landroidx/compose/runtime/Latch;
+Landroidx/compose/runtime/LaunchedEffectImpl;
+Landroidx/compose/runtime/LazyValueHolder;
+Landroidx/compose/runtime/MonotonicFrameClock;
+Landroidx/compose/runtime/MovableContentStateReference;
+Landroidx/compose/runtime/MutableFloatState;
+Landroidx/compose/runtime/MutableIntState;
+Landroidx/compose/runtime/MutableState;
+Landroidx/compose/runtime/OpaqueKey;
+Landroidx/compose/runtime/ParcelableSnapshotMutableFloatState;
+Landroidx/compose/runtime/ParcelableSnapshotMutableIntState;
+Landroidx/compose/runtime/ParcelableSnapshotMutableState$Companion$CREATOR$1;
+Landroidx/compose/runtime/ParcelableSnapshotMutableState;
+Landroidx/compose/runtime/PausableMonotonicFrameClock$withFrameNanos$1;
+Landroidx/compose/runtime/PausableMonotonicFrameClock;
+Landroidx/compose/runtime/Pending$keyMap$2;
+Landroidx/compose/runtime/Pending;
+Landroidx/compose/runtime/PersistentCompositionLocalMap;
+Landroidx/compose/runtime/ProduceStateScopeImpl;
+Landroidx/compose/runtime/ProvidableCompositionLocal;
+Landroidx/compose/runtime/ProvidedValue;
+Landroidx/compose/runtime/RecomposeScope;
+Landroidx/compose/runtime/RecomposeScopeImpl$end$1$2;
+Landroidx/compose/runtime/RecomposeScopeImpl;
+Landroidx/compose/runtime/RecomposeScopeOwner;
+Landroidx/compose/runtime/Recomposer$State;
+Landroidx/compose/runtime/Recomposer$effectJob$1$1;
+Landroidx/compose/runtime/Recomposer$join$2;
+Landroidx/compose/runtime/Recomposer$performRecompose$1$1;
+Landroidx/compose/runtime/Recomposer$recompositionRunner$2$3;
+Landroidx/compose/runtime/Recomposer$recompositionRunner$2$unregisterApplyObserver$1;
+Landroidx/compose/runtime/Recomposer$recompositionRunner$2;
+Landroidx/compose/runtime/Recomposer$runRecomposeAndApplyChanges$2$1;
+Landroidx/compose/runtime/Recomposer$runRecomposeAndApplyChanges$2;
+Landroidx/compose/runtime/Recomposer;
+Landroidx/compose/runtime/ReferentialEqualityPolicy;
+Landroidx/compose/runtime/RememberObserver;
+Landroidx/compose/runtime/SkippableUpdater;
+Landroidx/compose/runtime/SlotReader;
+Landroidx/compose/runtime/SlotTable;
+Landroidx/compose/runtime/SlotWriter;
+Landroidx/compose/runtime/SnapshotMutableFloatStateImpl$FloatStateStateRecord;
+Landroidx/compose/runtime/SnapshotMutableFloatStateImpl;
+Landroidx/compose/runtime/SnapshotMutableIntStateImpl$IntStateStateRecord;
+Landroidx/compose/runtime/SnapshotMutableIntStateImpl;
+Landroidx/compose/runtime/SnapshotMutableStateImpl$StateStateRecord;
+Landroidx/compose/runtime/SnapshotMutableStateImpl;
+Landroidx/compose/runtime/SnapshotMutationPolicy;
+Landroidx/compose/runtime/SnapshotStateKt__DerivedStateKt;
+Landroidx/compose/runtime/SnapshotStateKt__ProduceStateKt$produceState$3;
+Landroidx/compose/runtime/SnapshotStateKt__SnapshotFlowKt$collectAsState$1$1;
+Landroidx/compose/runtime/SnapshotStateKt__SnapshotFlowKt$collectAsState$1;
+Landroidx/compose/runtime/Stack;
+Landroidx/compose/runtime/State;
+Landroidx/compose/runtime/StaticProvidableCompositionLocal;
+Landroidx/compose/runtime/StaticValueHolder;
+Landroidx/compose/runtime/StructuralEqualityPolicy;
+Landroidx/compose/runtime/WeakReference;
+Landroidx/compose/runtime/changelist/ChangeList;
+Landroidx/compose/runtime/changelist/ComposerChangeListWriter;
+Landroidx/compose/runtime/changelist/FixupList;
+Landroidx/compose/runtime/changelist/Operation$AdvanceSlotsBy;
+Landroidx/compose/runtime/changelist/Operation$DeactivateCurrentGroup;
+Landroidx/compose/runtime/changelist/Operation$Downs;
+Landroidx/compose/runtime/changelist/Operation$EndCompositionScope;
+Landroidx/compose/runtime/changelist/Operation$EndCurrentGroup;
+Landroidx/compose/runtime/changelist/Operation$EnsureGroupStarted;
+Landroidx/compose/runtime/changelist/Operation$EnsureRootGroupStarted;
+Landroidx/compose/runtime/changelist/Operation$InsertNodeFixup;
+Landroidx/compose/runtime/changelist/Operation$InsertSlots;
+Landroidx/compose/runtime/changelist/Operation$InsertSlotsWithFixups;
+Landroidx/compose/runtime/changelist/Operation$MoveCurrentGroup;
+Landroidx/compose/runtime/changelist/Operation$PostInsertNodeFixup;
+Landroidx/compose/runtime/changelist/Operation$Remember;
+Landroidx/compose/runtime/changelist/Operation$SideEffect;
+Landroidx/compose/runtime/changelist/Operation$UpdateAuxData;
+Landroidx/compose/runtime/changelist/Operation$UpdateNode;
+Landroidx/compose/runtime/changelist/Operation$UpdateValue;
+Landroidx/compose/runtime/changelist/Operation$Ups;
+Landroidx/compose/runtime/changelist/Operation$UseCurrentNode;
+Landroidx/compose/runtime/changelist/Operation;
+Landroidx/compose/runtime/changelist/Operations$OpIterator;
+Landroidx/compose/runtime/changelist/Operations;
+Landroidx/compose/runtime/collection/IdentityArrayIntMap;
+Landroidx/compose/runtime/collection/IdentityArrayMap$asMap$1$entries$1$iterator$1$1;
+Landroidx/compose/runtime/collection/IdentityArraySet;
+Landroidx/compose/runtime/collection/MutableVector$MutableVectorList;
+Landroidx/compose/runtime/collection/MutableVector$VectorListIterator;
+Landroidx/compose/runtime/collection/MutableVector;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/ImmutableList;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/ImmutableSet;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/PersistentList;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/PersistentMap$Builder;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/PersistentMap;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/PersistentSet;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/AbstractPersistentList;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/SmallPersistentVector;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBaseIterator;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapKeys;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapKeysIterator;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNodeBaseIterator;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNodeKeysIterator;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/persistentOrderedSet/Links;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/persistentOrderedSet/PersistentOrderedSet;
+Landroidx/compose/runtime/external/kotlinx/collections/immutable/internal/DeltaCounter;
+Landroidx/compose/runtime/internal/ComposableLambda;
+Landroidx/compose/runtime/internal/ComposableLambdaImpl;
+Landroidx/compose/runtime/internal/PersistentCompositionLocalHashMap$Builder;
+Landroidx/compose/runtime/internal/PersistentCompositionLocalHashMap;
+Landroidx/compose/runtime/internal/ThreadMap;
+Landroidx/compose/runtime/saveable/ListSaverKt$listSaver$1;
+Landroidx/compose/runtime/saveable/RememberSaveableKt$rememberSaveable$1;
+Landroidx/compose/runtime/saveable/SaveableHolder;
+Landroidx/compose/runtime/saveable/SaveableStateHolder;
+Landroidx/compose/runtime/saveable/SaveableStateHolderImpl$RegistryHolder$registry$1;
+Landroidx/compose/runtime/saveable/SaveableStateHolderImpl$RegistryHolder;
+Landroidx/compose/runtime/saveable/SaveableStateHolderImpl$SaveableStateProvider$1$1$invoke$$inlined$onDispose$1;
+Landroidx/compose/runtime/saveable/SaveableStateHolderImpl;
+Landroidx/compose/runtime/saveable/SaveableStateRegistry;
+Landroidx/compose/runtime/saveable/SaveableStateRegistryImpl$registerProvider$3;
+Landroidx/compose/runtime/saveable/SaveableStateRegistryImpl;
+Landroidx/compose/runtime/saveable/SaveableStateRegistryKt;
+Landroidx/compose/runtime/saveable/SaverKt$Saver$1;
+Landroidx/compose/runtime/saveable/SaverKt;
+Landroidx/compose/runtime/snapshots/GlobalSnapshot$1$1$1;
+Landroidx/compose/runtime/snapshots/GlobalSnapshot;
+Landroidx/compose/runtime/snapshots/MutableSnapshot;
+Landroidx/compose/runtime/snapshots/ObserverHandle;
+Landroidx/compose/runtime/snapshots/ReadonlySnapshot;
+Landroidx/compose/runtime/snapshots/Snapshot$Companion$$ExternalSyntheticLambda0;
+Landroidx/compose/runtime/snapshots/Snapshot;
+Landroidx/compose/runtime/snapshots/SnapshotApplyResult$Failure;
+Landroidx/compose/runtime/snapshots/SnapshotApplyResult$Success;
+Landroidx/compose/runtime/snapshots/SnapshotDoubleIndexHeap;
+Landroidx/compose/runtime/snapshots/SnapshotIdSet;
+Landroidx/compose/runtime/snapshots/SnapshotKt$mergedReadObserver$1;
+Landroidx/compose/runtime/snapshots/SnapshotKt;
+Landroidx/compose/runtime/snapshots/SnapshotMutableState;
+Landroidx/compose/runtime/snapshots/SnapshotStateList$StateListStateRecord;
+Landroidx/compose/runtime/snapshots/SnapshotStateList$addAll$1;
+Landroidx/compose/runtime/snapshots/SnapshotStateList;
+Landroidx/compose/runtime/snapshots/SnapshotStateListKt;
+Landroidx/compose/runtime/snapshots/SnapshotStateObserver$ObservedScopeMap;
+Landroidx/compose/runtime/snapshots/SnapshotStateObserver;
+Landroidx/compose/runtime/snapshots/StateObject;
+Landroidx/compose/runtime/snapshots/StateRecord;
+Landroidx/compose/runtime/snapshots/TransparentObserverMutableSnapshot;
+Landroidx/compose/runtime/tooling/InspectionTablesKt;
+Landroidx/compose/ui/Alignment$Horizontal;
+Landroidx/compose/ui/Alignment$Vertical;
+Landroidx/compose/ui/Alignment;
+Landroidx/compose/ui/BiasAlignment$Horizontal;
+Landroidx/compose/ui/BiasAlignment$Vertical;
+Landroidx/compose/ui/BiasAlignment;
+Landroidx/compose/ui/CombinedModifier$toString$1;
+Landroidx/compose/ui/CombinedModifier;
+Landroidx/compose/ui/ComposedModifier;
+Landroidx/compose/ui/CompositionLocalMapInjectionElement;
+Landroidx/compose/ui/Modifier$Companion;
+Landroidx/compose/ui/Modifier$Element;
+Landroidx/compose/ui/Modifier$Node;
+Landroidx/compose/ui/Modifier;
+Landroidx/compose/ui/MotionDurationScale;
+Landroidx/compose/ui/ZIndexElement;
+Landroidx/compose/ui/ZIndexNode$measure$1;
+Landroidx/compose/ui/ZIndexNode;
+Landroidx/compose/ui/autofill/AndroidAutofill;
+Landroidx/compose/ui/autofill/Autofill;
+Landroidx/compose/ui/autofill/AutofillCallback;
+Landroidx/compose/ui/autofill/AutofillTree;
+Landroidx/compose/ui/draw/BuildDrawCacheParams;
+Landroidx/compose/ui/draw/CacheDrawModifierNode;
+Landroidx/compose/ui/draw/CacheDrawModifierNodeImpl;
+Landroidx/compose/ui/draw/CacheDrawScope$onDrawBehind$1;
+Landroidx/compose/ui/draw/CacheDrawScope;
+Landroidx/compose/ui/draw/ClipKt;
+Landroidx/compose/ui/draw/DrawModifier;
+Landroidx/compose/ui/draw/DrawResult;
+Landroidx/compose/ui/draw/DrawWithCacheElement;
+Landroidx/compose/ui/draw/EmptyBuildDrawCacheParams;
+Landroidx/compose/ui/draw/PainterElement;
+Landroidx/compose/ui/draw/PainterNode$measure$1;
+Landroidx/compose/ui/draw/PainterNode;
+Landroidx/compose/ui/focus/FocusChangedElement;
+Landroidx/compose/ui/focus/FocusChangedNode;
+Landroidx/compose/ui/focus/FocusDirection;
+Landroidx/compose/ui/focus/FocusEventModifierNode;
+Landroidx/compose/ui/focus/FocusInvalidationManager;
+Landroidx/compose/ui/focus/FocusModifierKt;
+Landroidx/compose/ui/focus/FocusOwner;
+Landroidx/compose/ui/focus/FocusOwnerImpl$modifier$1;
+Landroidx/compose/ui/focus/FocusOwnerImpl$moveFocus$foundNextItem$1;
+Landroidx/compose/ui/focus/FocusOwnerImpl;
+Landroidx/compose/ui/focus/FocusProperties$exit$1;
+Landroidx/compose/ui/focus/FocusProperties;
+Landroidx/compose/ui/focus/FocusPropertiesImpl;
+Landroidx/compose/ui/focus/FocusPropertiesModifierNode;
+Landroidx/compose/ui/focus/FocusRequester;
+Landroidx/compose/ui/focus/FocusRequesterModifierNode;
+Landroidx/compose/ui/focus/FocusState;
+Landroidx/compose/ui/focus/FocusStateImpl;
+Landroidx/compose/ui/focus/FocusTargetNode$FocusTargetElement;
+Landroidx/compose/ui/focus/FocusTargetNode;
+Landroidx/compose/ui/geometry/CornerRadius;
+Landroidx/compose/ui/geometry/MutableRect;
+Landroidx/compose/ui/geometry/Offset;
+Landroidx/compose/ui/geometry/Rect;
+Landroidx/compose/ui/geometry/RoundRect;
+Landroidx/compose/ui/geometry/Size;
+Landroidx/compose/ui/graphics/AndroidCanvas;
+Landroidx/compose/ui/graphics/AndroidCanvas_androidKt;
+Landroidx/compose/ui/graphics/AndroidImageBitmap;
+Landroidx/compose/ui/graphics/AndroidPaint;
+Landroidx/compose/ui/graphics/AndroidPaint_androidKt$WhenMappings;
+Landroidx/compose/ui/graphics/AndroidPath;
+Landroidx/compose/ui/graphics/BlendModeColorFilter;
+Landroidx/compose/ui/graphics/BlendModeColorFilterHelper;
+Landroidx/compose/ui/graphics/BlockGraphicsLayerElement;
+Landroidx/compose/ui/graphics/BlockGraphicsLayerModifier;
+Landroidx/compose/ui/graphics/Brush;
+Landroidx/compose/ui/graphics/BrushKt$ShaderBrush$1;
+Landroidx/compose/ui/graphics/BrushKt;
+Landroidx/compose/ui/graphics/Canvas;
+Landroidx/compose/ui/graphics/CanvasZHelper$$ExternalSyntheticApiModelOutline0;
+Landroidx/compose/ui/graphics/Color;
+Landroidx/compose/ui/graphics/ColorSpaceVerificationHelper$$ExternalSyntheticLambda1;
+Landroidx/compose/ui/graphics/Float16;
+Landroidx/compose/ui/graphics/GraphicsLayerElement;
+Landroidx/compose/ui/graphics/GraphicsLayerScopeKt;
+Landroidx/compose/ui/graphics/ImageBitmap;
+Landroidx/compose/ui/graphics/ImageBitmapConfig;
+Landroidx/compose/ui/graphics/Matrix;
+Landroidx/compose/ui/graphics/Outline$Generic;
+Landroidx/compose/ui/graphics/Outline$Rectangle;
+Landroidx/compose/ui/graphics/Outline$Rounded;
+Landroidx/compose/ui/graphics/Path;
+Landroidx/compose/ui/graphics/RectangleShapeKt$RectangleShape$1;
+Landroidx/compose/ui/graphics/ReusableGraphicsLayerScope;
+Landroidx/compose/ui/graphics/Shadow;
+Landroidx/compose/ui/graphics/Shape;
+Landroidx/compose/ui/graphics/SimpleGraphicsLayerModifier$layerBlock$1;
+Landroidx/compose/ui/graphics/SimpleGraphicsLayerModifier;
+Landroidx/compose/ui/graphics/SolidColor;
+Landroidx/compose/ui/graphics/TransformOrigin;
+Landroidx/compose/ui/graphics/colorspace/Adaptation$Companion$Bradford$1;
+Landroidx/compose/ui/graphics/colorspace/Adaptation;
+Landroidx/compose/ui/graphics/colorspace/ColorModel;
+Landroidx/compose/ui/graphics/colorspace/ColorSpace;
+Landroidx/compose/ui/graphics/colorspace/ColorSpaces;
+Landroidx/compose/ui/graphics/colorspace/Connector$Companion$identity$1;
+Landroidx/compose/ui/graphics/colorspace/Connector;
+Landroidx/compose/ui/graphics/colorspace/DoubleFunction;
+Landroidx/compose/ui/graphics/colorspace/Lab;
+Landroidx/compose/ui/graphics/colorspace/Oklab;
+Landroidx/compose/ui/graphics/colorspace/Rgb$$ExternalSyntheticLambda0;
+Landroidx/compose/ui/graphics/colorspace/Rgb$$ExternalSyntheticLambda1;
+Landroidx/compose/ui/graphics/colorspace/Rgb$$ExternalSyntheticLambda2;
+Landroidx/compose/ui/graphics/colorspace/Rgb$eotf$1;
+Landroidx/compose/ui/graphics/colorspace/Rgb;
+Landroidx/compose/ui/graphics/colorspace/TransferParameters;
+Landroidx/compose/ui/graphics/colorspace/WhitePoint;
+Landroidx/compose/ui/graphics/colorspace/Xyz;
+Landroidx/compose/ui/graphics/drawscope/CanvasDrawScope$DrawParams;
+Landroidx/compose/ui/graphics/drawscope/CanvasDrawScope$drawContext$1;
+Landroidx/compose/ui/graphics/drawscope/CanvasDrawScope;
+Landroidx/compose/ui/graphics/drawscope/CanvasDrawScopeKt$asDrawTransform$1;
+Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;
+Landroidx/compose/ui/graphics/drawscope/DrawScope;
+Landroidx/compose/ui/graphics/drawscope/EmptyCanvas;
+Landroidx/compose/ui/graphics/drawscope/Fill;
+Landroidx/compose/ui/graphics/drawscope/Stroke;
+Landroidx/compose/ui/graphics/painter/BitmapPainter;
+Landroidx/compose/ui/graphics/painter/Painter;
+Landroidx/compose/ui/graphics/vector/GroupComponent;
+Landroidx/compose/ui/graphics/vector/ImageVector$Builder$GroupParams;
+Landroidx/compose/ui/graphics/vector/ImageVector$Builder;
+Landroidx/compose/ui/graphics/vector/ImageVector;
+Landroidx/compose/ui/graphics/vector/VNode;
+Landroidx/compose/ui/graphics/vector/VectorComponent;
+Landroidx/compose/ui/graphics/vector/VectorGroup;
+Landroidx/compose/ui/graphics/vector/VectorKt;
+Landroidx/compose/ui/graphics/vector/VectorNode;
+Landroidx/compose/ui/graphics/vector/VectorPainter;
+Landroidx/compose/ui/graphics/vector/VectorPath;
+Landroidx/compose/ui/graphics/vector/compat/AndroidVectorParser;
+Landroidx/compose/ui/hapticfeedback/HapticFeedback;
+Landroidx/compose/ui/input/InputMode;
+Landroidx/compose/ui/input/InputModeManager;
+Landroidx/compose/ui/input/InputModeManagerImpl;
+Landroidx/compose/ui/input/key/Key;
+Landroidx/compose/ui/input/key/KeyEvent;
+Landroidx/compose/ui/input/key/KeyInputElement;
+Landroidx/compose/ui/input/key/KeyInputModifierNode;
+Landroidx/compose/ui/input/key/KeyInputNode;
+Landroidx/compose/ui/input/key/Key_androidKt;
+Landroidx/compose/ui/input/nestedscroll/NestedScrollConnection;
+Landroidx/compose/ui/input/nestedscroll/NestedScrollDispatcher;
+Landroidx/compose/ui/input/nestedscroll/NestedScrollNode;
+Landroidx/compose/ui/input/nestedscroll/NestedScrollNodeKt;
+Landroidx/compose/ui/input/pointer/AndroidPointerIconType;
+Landroidx/compose/ui/input/pointer/MotionEventAdapter;
+Landroidx/compose/ui/input/pointer/Node;
+Landroidx/compose/ui/input/pointer/NodeParent;
+Landroidx/compose/ui/input/pointer/PointerEvent;
+Landroidx/compose/ui/input/pointer/PointerIcon;
+Landroidx/compose/ui/input/pointer/PointerIconService;
+Landroidx/compose/ui/input/pointer/PointerInputChange;
+Landroidx/compose/ui/input/pointer/PointerInputScope;
+Landroidx/compose/ui/input/pointer/PointerKeyboardModifiers;
+Landroidx/compose/ui/input/pointer/PositionCalculator;
+Landroidx/compose/ui/input/pointer/SuspendPointerInputElement;
+Landroidx/compose/ui/input/pointer/SuspendingPointerInputFilterKt;
+Landroidx/compose/ui/input/pointer/SuspendingPointerInputModifierNode;
+Landroidx/compose/ui/input/pointer/SuspendingPointerInputModifierNodeImpl$PointerEventHandlerCoroutine;
+Landroidx/compose/ui/input/pointer/SuspendingPointerInputModifierNodeImpl;
+Landroidx/compose/ui/input/pointer/util/DataPointAtTime;
+Landroidx/compose/ui/input/pointer/util/PointerIdArray;
+Landroidx/compose/ui/input/pointer/util/VelocityTracker1D;
+Landroidx/compose/ui/input/pointer/util/VelocityTracker;
+Landroidx/compose/ui/input/rotary/RotaryInputElement;
+Landroidx/compose/ui/input/rotary/RotaryInputModifierKt;
+Landroidx/compose/ui/input/rotary/RotaryInputModifierNode;
+Landroidx/compose/ui/input/rotary/RotaryInputNode;
+Landroidx/compose/ui/layout/AlignmentLine;
+Landroidx/compose/ui/layout/AlignmentLineKt$FirstBaseline$1;
+Landroidx/compose/ui/layout/AlignmentLineKt$LastBaseline$1;
+Landroidx/compose/ui/layout/AlignmentLineKt;
+Landroidx/compose/ui/layout/BeyondBoundsLayout$BeyondBoundsScope;
+Landroidx/compose/ui/layout/BeyondBoundsLayout;
+Landroidx/compose/ui/layout/BeyondBoundsLayoutKt;
+Landroidx/compose/ui/layout/ComposableSingletons$SubcomposeLayoutKt;
+Landroidx/compose/ui/layout/ContentScale;
+Landroidx/compose/ui/layout/DefaultIntrinsicMeasurable;
+Landroidx/compose/ui/layout/FixedSizeIntrinsicsPlaceable;
+Landroidx/compose/ui/layout/HorizontalAlignmentLine;
+Landroidx/compose/ui/layout/IntrinsicMeasureScope;
+Landroidx/compose/ui/layout/IntrinsicMinMax;
+Landroidx/compose/ui/layout/IntrinsicWidthHeight;
+Landroidx/compose/ui/layout/IntrinsicsMeasureScope;
+Landroidx/compose/ui/layout/LayoutCoordinates;
+Landroidx/compose/ui/layout/LayoutElement;
+Landroidx/compose/ui/layout/LayoutKt$materializerOf$1;
+Landroidx/compose/ui/layout/LayoutKt;
+Landroidx/compose/ui/layout/LayoutModifierImpl;
+Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState$NodeState;
+Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState$PostLookaheadMeasureScopeImpl;
+Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState$Scope;
+Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState$createMeasurePolicy$1$measure-3p2s80s$$inlined$createMeasureResult$1;
+Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState$createMeasurePolicy$1;
+Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState$precompose$1;
+Landroidx/compose/ui/layout/LayoutNodeSubcompositionsState;
+Landroidx/compose/ui/layout/LookaheadLayoutCoordinates;
+Landroidx/compose/ui/layout/Measurable;
+Landroidx/compose/ui/layout/MeasurePolicy;
+Landroidx/compose/ui/layout/MeasureResult;
+Landroidx/compose/ui/layout/MeasureScope$layout$1;
+Landroidx/compose/ui/layout/MeasureScope;
+Landroidx/compose/ui/layout/Measured;
+Landroidx/compose/ui/layout/OnRemeasuredModifier;
+Landroidx/compose/ui/layout/OnSizeChangedModifier;
+Landroidx/compose/ui/layout/PinnableContainerKt;
+Landroidx/compose/ui/layout/Placeable$PlacementScope$Companion;
+Landroidx/compose/ui/layout/Placeable$PlacementScope;
+Landroidx/compose/ui/layout/Placeable;
+Landroidx/compose/ui/layout/PlaceableKt;
+Landroidx/compose/ui/layout/Remeasurement;
+Landroidx/compose/ui/layout/RemeasurementModifier;
+Landroidx/compose/ui/layout/RootMeasurePolicy$measure$2;
+Landroidx/compose/ui/layout/RootMeasurePolicy;
+Landroidx/compose/ui/layout/ScaleFactor;
+Landroidx/compose/ui/layout/SubcomposeLayoutKt$SubcomposeLayout$2;
+Landroidx/compose/ui/layout/SubcomposeLayoutKt$SubcomposeLayout$4;
+Landroidx/compose/ui/layout/SubcomposeLayoutKt$SubcomposeLayout$5$1$invoke$$inlined$onDispose$1;
+Landroidx/compose/ui/layout/SubcomposeLayoutState$setRoot$1;
+Landroidx/compose/ui/layout/SubcomposeLayoutState;
+Landroidx/compose/ui/layout/SubcomposeMeasureScope;
+Landroidx/compose/ui/layout/SubcomposeSlotReusePolicy$SlotIdsSet;
+Landroidx/compose/ui/layout/SubcomposeSlotReusePolicy;
+Landroidx/compose/ui/modifier/BackwardsCompatLocalMap;
+Landroidx/compose/ui/modifier/EmptyMap;
+Landroidx/compose/ui/modifier/ModifierLocal;
+Landroidx/compose/ui/modifier/ModifierLocalConsumer;
+Landroidx/compose/ui/modifier/ModifierLocalManager;
+Landroidx/compose/ui/modifier/ModifierLocalModifierNode;
+Landroidx/compose/ui/modifier/ModifierLocalProvider;
+Landroidx/compose/ui/modifier/ModifierLocalReadScope;
+Landroidx/compose/ui/modifier/ProvidableModifierLocal;
+Landroidx/compose/ui/modifier/SingleLocalMap;
+Landroidx/compose/ui/node/AlignmentLines;
+Landroidx/compose/ui/node/AlignmentLinesOwner;
+Landroidx/compose/ui/node/BackwardsCompatNode;
+Landroidx/compose/ui/node/CanFocusChecker;
+Landroidx/compose/ui/node/ComposeUiNode$Companion;
+Landroidx/compose/ui/node/ComposeUiNode;
+Landroidx/compose/ui/node/CompositionLocalConsumerModifierNode;
+Landroidx/compose/ui/node/DelegatableNode;
+Landroidx/compose/ui/node/DelegatingNode;
+Landroidx/compose/ui/node/DrawModifierNode;
+Landroidx/compose/ui/node/GlobalPositionAwareModifierNode;
+Landroidx/compose/ui/node/HitTestResult;
+Landroidx/compose/ui/node/InnerNodeCoordinator;
+Landroidx/compose/ui/node/IntrinsicsPolicy;
+Landroidx/compose/ui/node/LayerPositionalProperties;
+Landroidx/compose/ui/node/LayoutAwareModifierNode;
+Landroidx/compose/ui/node/LayoutModifierNode;
+Landroidx/compose/ui/node/LayoutModifierNodeCoordinator;
+Landroidx/compose/ui/node/LayoutNode$$ExternalSyntheticLambda0;
+Landroidx/compose/ui/node/LayoutNode$Companion$DummyViewConfiguration$1;
+Landroidx/compose/ui/node/LayoutNode$Companion$ErrorMeasurePolicy$1;
+Landroidx/compose/ui/node/LayoutNode$NoIntrinsicsMeasurePolicy;
+Landroidx/compose/ui/node/LayoutNode$WhenMappings;
+Landroidx/compose/ui/node/LayoutNode$_foldedChildren$1;
+Landroidx/compose/ui/node/LayoutNode;
+Landroidx/compose/ui/node/LayoutNodeDrawScope;
+Landroidx/compose/ui/node/LayoutNodeLayoutDelegate$LookaheadPassDelegate;
+Landroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate$placeOuterCoordinator$1;
+Landroidx/compose/ui/node/LayoutNodeLayoutDelegate$MeasurePassDelegate;
+Landroidx/compose/ui/node/LayoutNodeLayoutDelegate$performMeasure$2;
+Landroidx/compose/ui/node/LayoutNodeLayoutDelegate;
+Landroidx/compose/ui/node/LookaheadAlignmentLines;
+Landroidx/compose/ui/node/LookaheadCapablePlaceable;
+Landroidx/compose/ui/node/LookaheadDelegate;
+Landroidx/compose/ui/node/MeasureAndLayoutDelegate$PostponedRequest;
+Landroidx/compose/ui/node/MeasureAndLayoutDelegate;
+Landroidx/compose/ui/node/ModifierNodeElement;
+Landroidx/compose/ui/node/NodeChain$Differ;
+Landroidx/compose/ui/node/NodeChain;
+Landroidx/compose/ui/node/NodeChainKt$SentinelHead$1;
+Landroidx/compose/ui/node/NodeChainKt;
+Landroidx/compose/ui/node/NodeCoordinator$HitTestSource;
+Landroidx/compose/ui/node/NodeCoordinator$invoke$1;
+Landroidx/compose/ui/node/NodeCoordinator;
+Landroidx/compose/ui/node/NodeMeasuringIntrinsics$IntrinsicMinMax;
+Landroidx/compose/ui/node/NodeMeasuringIntrinsics$IntrinsicWidthHeight;
+Landroidx/compose/ui/node/ObserverModifierNode;
+Landroidx/compose/ui/node/ObserverNodeOwnerScope;
+Landroidx/compose/ui/node/OnPositionedDispatcher$Companion$DepthComparator;
+Landroidx/compose/ui/node/OnPositionedDispatcher;
+Landroidx/compose/ui/node/OwnedLayer;
+Landroidx/compose/ui/node/Owner$OnLayoutCompletedListener;
+Landroidx/compose/ui/node/Owner;
+Landroidx/compose/ui/node/OwnerScope;
+Landroidx/compose/ui/node/OwnerSnapshotObserver;
+Landroidx/compose/ui/node/ParentDataModifierNode;
+Landroidx/compose/ui/node/PointerInputModifierNode;
+Landroidx/compose/ui/node/RootForTest;
+Landroidx/compose/ui/node/SemanticsModifierNode;
+Landroidx/compose/ui/node/TailModifierNode;
+Landroidx/compose/ui/node/TreeSet;
+Landroidx/compose/ui/node/UiApplier;
+Landroidx/compose/ui/platform/AbstractComposeView;
+Landroidx/compose/ui/platform/AccessibilityManager;
+Landroidx/compose/ui/platform/AndroidAccessibilityManager;
+Landroidx/compose/ui/platform/AndroidClipboardManager;
+Landroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticApiModelOutline0;
+Landroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda1;
+Landroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda2;
+Landroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda3;
+Landroidx/compose/ui/platform/AndroidComposeView$AndroidComposeViewTranslationCallback;
+Landroidx/compose/ui/platform/AndroidComposeView$ViewTreeOwners;
+Landroidx/compose/ui/platform/AndroidComposeView$focusOwner$1;
+Landroidx/compose/ui/platform/AndroidComposeView$pointerIconService$1;
+Landroidx/compose/ui/platform/AndroidComposeView$viewTreeOwners$2;
+Landroidx/compose/ui/platform/AndroidComposeView;
+Landroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$$ExternalSyntheticLambda1;
+Landroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$$ExternalSyntheticLambda2;
+Landroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$1;
+Landroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$MyNodeProvider;
+Landroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$SemanticsNodeCopy;
+Landroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$boundsUpdatesEventLoop$1;
+Landroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$sendScrollEventIfNeeded$1;
+Landroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat;
+Landroidx/compose/ui/platform/AndroidComposeViewForceDarkModeQ;
+Landroidx/compose/ui/platform/AndroidComposeViewTranslationCallbackS;
+Landroidx/compose/ui/platform/AndroidComposeViewVerificationHelperMethodsN;
+Landroidx/compose/ui/platform/AndroidComposeViewVerificationHelperMethodsO;
+Landroidx/compose/ui/platform/AndroidCompositionLocals_androidKt$obtainImageVectorCache$callbacks$1$1;
+Landroidx/compose/ui/platform/AndroidCompositionLocals_androidKt;
+Landroidx/compose/ui/platform/AndroidUiDispatcher$dispatchCallback$1;
+Landroidx/compose/ui/platform/AndroidUiDispatcher;
+Landroidx/compose/ui/platform/AndroidUiFrameClock$withFrameNanos$2$callback$1;
+Landroidx/compose/ui/platform/AndroidUiFrameClock;
+Landroidx/compose/ui/platform/AndroidUriHandler;
+Landroidx/compose/ui/platform/AndroidViewConfiguration;
+Landroidx/compose/ui/platform/CalculateMatrixToWindow;
+Landroidx/compose/ui/platform/CalculateMatrixToWindowApi29;
+Landroidx/compose/ui/platform/ClipboardManager;
+Landroidx/compose/ui/platform/ComposableSingletons$Wrapper_androidKt;
+Landroidx/compose/ui/platform/ComposeView;
+Landroidx/compose/ui/platform/CompositionLocalsKt;
+Landroidx/compose/ui/platform/DeviceRenderNode;
+Landroidx/compose/ui/platform/DisposableSaveableStateRegistry;
+Landroidx/compose/ui/platform/DisposableSaveableStateRegistry_androidKt$DisposableSaveableStateRegistry$1;
+Landroidx/compose/ui/platform/DrawChildContainer;
+Landroidx/compose/ui/platform/GlobalSnapshotManager$ensureStarted$1;
+Landroidx/compose/ui/platform/GlobalSnapshotManager;
+Landroidx/compose/ui/platform/InspectableModifier$End;
+Landroidx/compose/ui/platform/InspectableModifier;
+Landroidx/compose/ui/platform/LayerMatrixCache;
+Landroidx/compose/ui/platform/MotionDurationScaleImpl;
+Landroidx/compose/ui/platform/OutlineResolver;
+Landroidx/compose/ui/platform/RenderNodeApi29;
+Landroidx/compose/ui/platform/RenderNodeApi29VerificationHelper;
+Landroidx/compose/ui/platform/RenderNodeLayer;
+Landroidx/compose/ui/platform/ScrollObservationScope;
+Landroidx/compose/ui/platform/SoftwareKeyboardController;
+Landroidx/compose/ui/platform/TextToolbar;
+Landroidx/compose/ui/platform/UriHandler;
+Landroidx/compose/ui/platform/ViewCompositionStrategy;
+Landroidx/compose/ui/platform/ViewConfiguration;
+Landroidx/compose/ui/platform/ViewLayer$Companion$OutlineProvider$1;
+Landroidx/compose/ui/platform/ViewLayer;
+Landroidx/compose/ui/platform/ViewLayerContainer;
+Landroidx/compose/ui/platform/WeakCache;
+Landroidx/compose/ui/platform/WindowInfo;
+Landroidx/compose/ui/platform/WindowInfoImpl;
+Landroidx/compose/ui/platform/WindowRecomposerFactory$Companion$$ExternalSyntheticLambda0;
+Landroidx/compose/ui/platform/WindowRecomposerFactory;
+Landroidx/compose/ui/platform/WindowRecomposerPolicy$createAndInstallWindowRecomposer$unsetJob$1;
+Landroidx/compose/ui/platform/WindowRecomposerPolicy;
+Landroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$1;
+Landroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2$WhenMappings;
+Landroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2$onStateChanged$1$1$1;
+Landroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2$onStateChanged$1;
+Landroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$2;
+Landroidx/compose/ui/platform/WindowRecomposer_androidKt$getAnimationScaleFlowFor$1$1$1;
+Landroidx/compose/ui/platform/WindowRecomposer_androidKt;
+Landroidx/compose/ui/platform/WrappedComposition$setContent$1$1$1;
+Landroidx/compose/ui/platform/WrappedComposition$setContent$1$1;
+Landroidx/compose/ui/platform/WrappedComposition$setContent$1;
+Landroidx/compose/ui/platform/WrappedComposition;
+Landroidx/compose/ui/platform/WrapperRenderNodeLayerHelperMethods;
+Landroidx/compose/ui/platform/WrapperVerificationHelperMethods;
+Landroidx/compose/ui/platform/Wrapper_androidKt;
+Landroidx/compose/ui/res/ImageVectorCache$ImageVectorEntry;
+Landroidx/compose/ui/res/ImageVectorCache$Key;
+Landroidx/compose/ui/res/ImageVectorCache;
+Landroidx/compose/ui/semantics/AppendedSemanticsElement;
+Landroidx/compose/ui/semantics/CollectionInfo;
+Landroidx/compose/ui/semantics/CoreSemanticsModifierNode;
+Landroidx/compose/ui/semantics/EmptySemanticsElement;
+Landroidx/compose/ui/semantics/EmptySemanticsModifier;
+Landroidx/compose/ui/semantics/Role;
+Landroidx/compose/ui/semantics/ScrollAxisRange;
+Landroidx/compose/ui/semantics/SemanticsConfiguration;
+Landroidx/compose/ui/semantics/SemanticsModifier;
+Landroidx/compose/ui/semantics/SemanticsModifierKt;
+Landroidx/compose/ui/semantics/SemanticsNode;
+Landroidx/compose/ui/semantics/SemanticsOwner;
+Landroidx/compose/ui/semantics/SemanticsProperties;
+Landroidx/compose/ui/semantics/SemanticsPropertiesKt;
+Landroidx/compose/ui/semantics/SemanticsPropertyKey;
+Landroidx/compose/ui/semantics/SemanticsPropertyReceiver;
+Landroidx/compose/ui/text/AndroidParagraph;
+Landroidx/compose/ui/text/AnnotatedString$Range;
+Landroidx/compose/ui/text/AnnotatedString;
+Landroidx/compose/ui/text/AnnotatedStringKt;
+Landroidx/compose/ui/text/EmojiSupportMatch;
+Landroidx/compose/ui/text/MultiParagraph;
+Landroidx/compose/ui/text/MultiParagraphIntrinsics$maxIntrinsicWidth$2;
+Landroidx/compose/ui/text/MultiParagraphIntrinsics;
+Landroidx/compose/ui/text/ParagraphInfo;
+Landroidx/compose/ui/text/ParagraphIntrinsicInfo;
+Landroidx/compose/ui/text/ParagraphIntrinsics;
+Landroidx/compose/ui/text/ParagraphStyle;
+Landroidx/compose/ui/text/ParagraphStyleKt;
+Landroidx/compose/ui/text/PlatformParagraphStyle;
+Landroidx/compose/ui/text/PlatformTextStyle;
+Landroidx/compose/ui/text/SaversKt$ColorSaver$1;
+Landroidx/compose/ui/text/SaversKt$ColorSaver$2;
+Landroidx/compose/ui/text/SaversKt;
+Landroidx/compose/ui/text/SpanStyle;
+Landroidx/compose/ui/text/SpanStyleKt;
+Landroidx/compose/ui/text/TextLayoutInput;
+Landroidx/compose/ui/text/TextLayoutResult;
+Landroidx/compose/ui/text/TextRange;
+Landroidx/compose/ui/text/TextStyle;
+Landroidx/compose/ui/text/TtsAnnotation;
+Landroidx/compose/ui/text/UrlAnnotation;
+Landroidx/compose/ui/text/VerbatimTtsAnnotation;
+Landroidx/compose/ui/text/android/BoringLayoutFactoryDefault;
+Landroidx/compose/ui/text/android/LayoutIntrinsics;
+Landroidx/compose/ui/text/android/Paint29$$ExternalSyntheticApiModelOutline0;
+Landroidx/compose/ui/text/android/Paint29;
+Landroidx/compose/ui/text/android/StaticLayoutFactory23;
+Landroidx/compose/ui/text/android/StaticLayoutFactory26;
+Landroidx/compose/ui/text/android/StaticLayoutFactory28;
+Landroidx/compose/ui/text/android/StaticLayoutFactoryImpl;
+Landroidx/compose/ui/text/android/StaticLayoutParams;
+Landroidx/compose/ui/text/android/TextAlignmentAdapter;
+Landroidx/compose/ui/text/android/TextAndroidCanvas;
+Landroidx/compose/ui/text/android/TextLayout;
+Landroidx/compose/ui/text/android/TextLayoutKt;
+Landroidx/compose/ui/text/android/style/LetterSpacingSpanEm;
+Landroidx/compose/ui/text/android/style/LetterSpacingSpanPx;
+Landroidx/compose/ui/text/android/style/LineHeightSpan;
+Landroidx/compose/ui/text/android/style/LineHeightStyleSpan;
+Landroidx/compose/ui/text/android/style/PlaceholderSpan;
+Landroidx/compose/ui/text/android/style/ShadowSpan;
+Landroidx/compose/ui/text/android/style/SkewXSpan;
+Landroidx/compose/ui/text/android/style/TextDecorationSpan;
+Landroidx/compose/ui/text/android/style/TypefaceSpan;
+Landroidx/compose/ui/text/caches/LruCache;
+Landroidx/compose/ui/text/caches/SimpleArrayMap;
+Landroidx/compose/ui/text/font/AndroidFontResolveInterceptor;
+Landroidx/compose/ui/text/font/AsyncTypefaceCache;
+Landroidx/compose/ui/text/font/DefaultFontFamily;
+Landroidx/compose/ui/text/font/Font$ResourceLoader;
+Landroidx/compose/ui/text/font/FontFamily$Resolver;
+Landroidx/compose/ui/text/font/FontFamily;
+Landroidx/compose/ui/text/font/FontFamilyResolverImpl;
+Landroidx/compose/ui/text/font/FontFamilyResolverKt;
+Landroidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter$special$$inlined$CoroutineExceptionHandler$1;
+Landroidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter;
+Landroidx/compose/ui/text/font/FontStyle;
+Landroidx/compose/ui/text/font/FontSynthesis;
+Landroidx/compose/ui/text/font/FontWeight;
+Landroidx/compose/ui/text/font/GenericFontFamily;
+Landroidx/compose/ui/text/font/PlatformResolveInterceptor$Companion$Default$1;
+Landroidx/compose/ui/text/font/PlatformResolveInterceptor$Companion;
+Landroidx/compose/ui/text/font/PlatformResolveInterceptor;
+Landroidx/compose/ui/text/font/PlatformTypefaces;
+Landroidx/compose/ui/text/font/SystemFontFamily;
+Landroidx/compose/ui/text/font/TypefaceRequest;
+Landroidx/compose/ui/text/font/TypefaceResult$Immutable;
+Landroidx/compose/ui/text/font/TypefaceResult;
+Landroidx/compose/ui/text/input/InputMethodManagerImpl;
+Landroidx/compose/ui/text/input/PlatformTextInputService;
+Landroidx/compose/ui/text/input/TextFieldValue;
+Landroidx/compose/ui/text/input/TextInputService;
+Landroidx/compose/ui/text/input/TextInputServiceAndroid;
+Landroidx/compose/ui/text/input/TextInputServiceAndroid_androidKt$$ExternalSyntheticLambda0;
+Landroidx/compose/ui/text/intl/AndroidLocale;
+Landroidx/compose/ui/text/intl/AndroidLocaleDelegateAPI24;
+Landroidx/compose/ui/text/intl/Locale;
+Landroidx/compose/ui/text/intl/LocaleList;
+Landroidx/compose/ui/text/intl/PlatformLocaleKt;
+Landroidx/compose/ui/text/platform/AndroidParagraphHelper_androidKt$NoopSpan$1;
+Landroidx/compose/ui/text/platform/AndroidParagraphHelper_androidKt;
+Landroidx/compose/ui/text/platform/AndroidParagraphIntrinsics$resolveTypeface$1;
+Landroidx/compose/ui/text/platform/AndroidParagraphIntrinsics;
+Landroidx/compose/ui/text/platform/AndroidTextPaint;
+Landroidx/compose/ui/text/platform/DefaultImpl$getFontLoadState$initCallback$1;
+Landroidx/compose/ui/text/platform/DefaultImpl;
+Landroidx/compose/ui/text/platform/EmojiCompatStatus;
+Landroidx/compose/ui/text/platform/ImmutableBool;
+Landroidx/compose/ui/text/platform/URLSpanCache;
+Landroidx/compose/ui/text/platform/extensions/LocaleListHelperMethods;
+Landroidx/compose/ui/text/platform/style/DrawStyleSpan;
+Landroidx/compose/ui/text/platform/style/ShaderBrushSpan;
+Landroidx/compose/ui/text/style/BaselineShift;
+Landroidx/compose/ui/text/style/BrushStyle;
+Landroidx/compose/ui/text/style/ColorStyle;
+Landroidx/compose/ui/text/style/Hyphens;
+Landroidx/compose/ui/text/style/LineBreak$Strategy;
+Landroidx/compose/ui/text/style/LineBreak$Strictness;
+Landroidx/compose/ui/text/style/LineBreak$WordBreak;
+Landroidx/compose/ui/text/style/LineBreak;
+Landroidx/compose/ui/text/style/LineHeightStyle$Alignment;
+Landroidx/compose/ui/text/style/LineHeightStyle;
+Landroidx/compose/ui/text/style/TextAlign;
+Landroidx/compose/ui/text/style/TextDecoration;
+Landroidx/compose/ui/text/style/TextDirection;
+Landroidx/compose/ui/text/style/TextForegroundStyle$Unspecified;
+Landroidx/compose/ui/text/style/TextForegroundStyle;
+Landroidx/compose/ui/text/style/TextGeometricTransform;
+Landroidx/compose/ui/text/style/TextIndent;
+Landroidx/compose/ui/text/style/TextMotion;
+Landroidx/compose/ui/unit/Constraints;
+Landroidx/compose/ui/unit/Density;
+Landroidx/compose/ui/unit/DensityImpl;
+Landroidx/compose/ui/unit/Dp$Companion;
+Landroidx/compose/ui/unit/Dp;
+Landroidx/compose/ui/unit/DpOffset;
+Landroidx/compose/ui/unit/DpRect;
+Landroidx/compose/ui/unit/IntOffset;
+Landroidx/compose/ui/unit/IntSize;
+Landroidx/compose/ui/unit/LayoutDirection;
+Landroidx/compose/ui/unit/TextUnit;
+Landroidx/compose/ui/unit/TextUnitType;
+Landroidx/core/app/ComponentActivity;
+Landroidx/core/app/CoreComponentFactory;
+Landroidx/core/content/res/ColorStateListInflaterCompat;
+Landroidx/core/content/res/ComplexColorCompat;
+Landroidx/core/graphics/TypefaceCompat;
+Landroidx/core/graphics/TypefaceCompatApi29Impl;
+Landroidx/core/graphics/TypefaceCompatUtil$Api19Impl;
+Landroidx/core/os/BuildCompat$Extensions30Impl$$ExternalSyntheticApiModelOutline0;
+Landroidx/core/os/BuildCompat$Extensions30Impl;
+Landroidx/core/os/BuildCompat;
+Landroidx/core/os/TraceCompat$Api18Impl;
+Landroidx/core/os/TraceCompat;
+Landroidx/core/provider/CallbackWithHandler$2;
+Landroidx/core/provider/FontProvider$Api16Impl;
+Landroidx/core/provider/FontRequest;
+Landroidx/core/provider/FontsContractCompat$FontInfo;
+Landroidx/core/text/TextUtilsCompat$Api17Impl;
+Landroidx/core/text/TextUtilsCompat;
+Landroidx/core/view/AccessibilityDelegateCompat$AccessibilityDelegateAdapter;
+Landroidx/core/view/AccessibilityDelegateCompat;
+Landroidx/core/view/MenuHostHelper;
+Landroidx/core/view/ViewCompat$$ExternalSyntheticLambda0;
+Landroidx/core/view/ViewCompat$Api29Impl;
+Landroidx/core/view/ViewCompat$Api30Impl;
+Landroidx/core/view/ViewCompat;
+Landroidx/core/view/accessibility/AccessibilityNodeProviderCompat;
+Landroidx/customview/poolingcontainer/PoolingContainerListenerHolder;
+Landroidx/emoji2/text/ConcurrencyHelpers$$ExternalSyntheticLambda0;
+Landroidx/emoji2/text/ConcurrencyHelpers$Handler28Impl;
+Landroidx/emoji2/text/DefaultGlyphChecker;
+Landroidx/emoji2/text/EmojiCompat$CompatInternal19$1;
+Landroidx/emoji2/text/EmojiCompat$CompatInternal19;
+Landroidx/emoji2/text/EmojiCompat$Config;
+Landroidx/emoji2/text/EmojiCompat$GlyphChecker;
+Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoader;
+Landroidx/emoji2/text/EmojiCompat;
+Landroidx/emoji2/text/EmojiCompatInitializer$1;
+Landroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$$ExternalSyntheticLambda0;
+Landroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$1;
+Landroidx/emoji2/text/EmojiCompatInitializer$LoadEmojiCompatRunnable;
+Landroidx/emoji2/text/EmojiCompatInitializer;
+Landroidx/emoji2/text/EmojiProcessor$EmojiProcessAddSpanCallback;
+Landroidx/emoji2/text/EmojiProcessor$EmojiProcessCallback;
+Landroidx/emoji2/text/EmojiProcessor$ProcessorSm;
+Landroidx/emoji2/text/EmojiProcessor;
+Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0;
+Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$1;
+Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;
+Landroidx/emoji2/text/FontRequestEmojiCompatConfig;
+Landroidx/emoji2/text/MetadataRepo$Node;
+Landroidx/emoji2/text/MetadataRepo;
+Landroidx/emoji2/text/TypefaceEmojiRasterizer;
+Landroidx/emoji2/text/TypefaceEmojiSpan;
+Landroidx/emoji2/text/UnprecomputeTextOnModificationSpannable;
+Landroidx/emoji2/text/flatbuffer/MetadataItem;
+Landroidx/emoji2/text/flatbuffer/MetadataList;
+Landroidx/emoji2/text/flatbuffer/Table;
+Landroidx/lifecycle/DefaultLifecycleObserver;
+Landroidx/lifecycle/DefaultLifecycleObserverAdapter$WhenMappings;
+Landroidx/lifecycle/DefaultLifecycleObserverAdapter;
+Landroidx/lifecycle/EmptyActivityLifecycleCallbacks;
+Landroidx/lifecycle/HasDefaultViewModelProviderFactory;
+Landroidx/lifecycle/Lifecycle$Event$Companion;
+Landroidx/lifecycle/Lifecycle$Event$WhenMappings;
+Landroidx/lifecycle/Lifecycle$Event;
+Landroidx/lifecycle/Lifecycle$State;
+Landroidx/lifecycle/Lifecycle;
+Landroidx/lifecycle/LifecycleDestroyedException;
+Landroidx/lifecycle/LifecycleDispatcher$DispatcherActivityCallback;
+Landroidx/lifecycle/LifecycleDispatcher;
+Landroidx/lifecycle/LifecycleEventObserver;
+Landroidx/lifecycle/LifecycleObserver;
+Landroidx/lifecycle/LifecycleOwner;
+Landroidx/lifecycle/LifecycleRegistry$ObserverWithState;
+Landroidx/lifecycle/LifecycleRegistry;
+Landroidx/lifecycle/Lifecycling;
+Landroidx/lifecycle/ProcessLifecycleInitializer;
+Landroidx/lifecycle/ProcessLifecycleOwner$Api29Impl;
+Landroidx/lifecycle/ProcessLifecycleOwner$attach$1$onActivityPreCreated$1;
+Landroidx/lifecycle/ProcessLifecycleOwner$attach$1;
+Landroidx/lifecycle/ProcessLifecycleOwner$initializationListener$1;
+Landroidx/lifecycle/ProcessLifecycleOwner;
+Landroidx/lifecycle/ReportFragment$LifecycleCallbacks$Companion;
+Landroidx/lifecycle/ReportFragment$LifecycleCallbacks;
+Landroidx/lifecycle/ReportFragment;
+Landroidx/lifecycle/SavedStateHandleAttacher;
+Landroidx/lifecycle/SavedStateHandlesProvider;
+Landroidx/lifecycle/SavedStateHandlesVM;
+Landroidx/lifecycle/ViewModelStore;
+Landroidx/lifecycle/ViewModelStoreOwner;
+Landroidx/lifecycle/viewmodel/CreationExtras$Empty;
+Landroidx/lifecycle/viewmodel/CreationExtras;
+Landroidx/lifecycle/viewmodel/MutableCreationExtras;
+Landroidx/lifecycle/viewmodel/ViewModelInitializer;
+Landroidx/metrics/performance/DelegatingFrameMetricsListener;
+Landroidx/metrics/performance/FrameData;
+Landroidx/metrics/performance/FrameDataApi24;
+Landroidx/metrics/performance/FrameDataApi31;
+Landroidx/metrics/performance/JankStats;
+Landroidx/metrics/performance/JankStatsApi16Impl;
+Landroidx/metrics/performance/JankStatsApi22Impl;
+Landroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda0;
+Landroidx/metrics/performance/JankStatsApi24Impl;
+Landroidx/metrics/performance/JankStatsApi26Impl;
+Landroidx/metrics/performance/JankStatsApi31Impl;
+Landroidx/metrics/performance/PerformanceMetricsState$Holder;
+Landroidx/metrics/performance/PerformanceMetricsState;
+Landroidx/profileinstaller/ProfileInstaller$DiagnosticsCallback;
+Landroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda0;
+Landroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda1;
+Landroidx/profileinstaller/ProfileInstallerInitializer$Choreographer16Impl;
+Landroidx/profileinstaller/ProfileInstallerInitializer$Handler28Impl;
+Landroidx/profileinstaller/ProfileInstallerInitializer;
+Landroidx/savedstate/Recreator;
+Landroidx/savedstate/SavedStateRegistry$$ExternalSyntheticLambda0;
+Landroidx/savedstate/SavedStateRegistry$SavedStateProvider;
+Landroidx/savedstate/SavedStateRegistry;
+Landroidx/savedstate/SavedStateRegistryController;
+Landroidx/savedstate/SavedStateRegistryOwner;
+Landroidx/startup/AppInitializer;
+Landroidx/startup/InitializationProvider;
+Landroidx/startup/Initializer;
+Landroidx/tracing/Trace$$ExternalSyntheticApiModelOutline0;
+Landroidx/tv/foundation/PivotOffsets;
+Landroidx/tv/foundation/TvBringIntoViewSpec;
+Landroidx/tv/foundation/lazy/grid/LazyGridIntervalContent;
+Landroidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator$onMeasured$$inlined$sortBy$1;
+Landroidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator;
+Landroidx/tv/foundation/lazy/grid/LazyGridItemProviderImpl;
+Landroidx/tv/foundation/lazy/grid/LazyGridKt$rememberLazyGridMeasurePolicy$1$1$3;
+Landroidx/tv/foundation/lazy/grid/LazyGridScrollPosition;
+Landroidx/tv/foundation/lazy/grid/TvGridItemSpan;
+Landroidx/tv/foundation/lazy/grid/TvLazyGridItemSpanScope;
+Landroidx/tv/foundation/lazy/grid/TvLazyGridScope;
+Landroidx/tv/foundation/lazy/grid/TvLazyGridState$remeasurementModifier$1;
+Landroidx/tv/foundation/lazy/grid/TvLazyGridState;
+Landroidx/tv/foundation/lazy/layout/AwaitFirstLayoutModifier$waitForFirstLayout$1;
+Landroidx/tv/foundation/lazy/layout/AwaitFirstLayoutModifier;
+Landroidx/tv/foundation/lazy/layout/LazyLayoutBeyondBoundsInfo$Interval;
+Landroidx/tv/foundation/lazy/layout/LazyLayoutKeyIndexMap;
+Landroidx/tv/foundation/lazy/layout/LazyLayoutNearestRangeState;
+Landroidx/tv/foundation/lazy/layout/LazyLayoutSemanticState;
+Landroidx/tv/foundation/lazy/layout/LazyLayoutSemanticsKt$LazyLayoutSemanticState$1;
+Landroidx/tv/foundation/lazy/layout/LazyLayoutSemanticsKt$lazyLayoutSemantics$1$1;
+Landroidx/tv/foundation/lazy/layout/NearestRangeKeyIndexMap$2$1;
+Landroidx/tv/foundation/lazy/layout/NearestRangeKeyIndexMap;
+Landroidx/tv/foundation/lazy/list/EmptyLazyListLayoutInfo;
+Landroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsModifierLocal$Companion$emptyBeyondBoundsScope$1;
+Landroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsModifierLocal;
+Landroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsState;
+Landroidx/tv/foundation/lazy/list/LazyListBeyondBoundsState;
+Landroidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator$onMeasured$$inlined$sortBy$2;
+Landroidx/tv/foundation/lazy/list/LazyListItemProviderImpl;
+Landroidx/tv/foundation/lazy/list/LazyListKt$rememberLazyListMeasurePolicy$1$1$measuredItemProvider$1;
+Landroidx/tv/foundation/lazy/list/LazyListKt$rememberLazyListMeasurePolicy$1$1;
+Landroidx/tv/foundation/lazy/list/LazyListMeasureResult;
+Landroidx/tv/foundation/lazy/list/LazyListMeasuredItem;
+Landroidx/tv/foundation/lazy/list/LazyListStateKt$rememberTvLazyListState$1$1;
+Landroidx/tv/foundation/lazy/list/LazyListStateKt;
+Landroidx/tv/foundation/lazy/list/TvLazyListInterval;
+Landroidx/tv/foundation/lazy/list/TvLazyListIntervalContent;
+Landroidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl;
+Landroidx/tv/foundation/lazy/list/TvLazyListLayoutInfo;
+Landroidx/tv/foundation/lazy/list/TvLazyListScope;
+Landroidx/tv/foundation/lazy/list/TvLazyListState$scroll$1;
+Landroidx/tv/foundation/lazy/list/TvLazyListState;
+Landroidx/tv/material3/Border;
+Landroidx/tv/material3/BorderIndication;
+Landroidx/tv/material3/ColorScheme;
+Landroidx/tv/material3/ColorSchemeKt$LocalColorScheme$1;
+Landroidx/tv/material3/ColorSchemeKt;
+Landroidx/tv/material3/ContentColorKt;
+Landroidx/tv/material3/Glow;
+Landroidx/tv/material3/GlowIndication;
+Landroidx/tv/material3/GlowIndicationInstance;
+Landroidx/tv/material3/ScaleIndication;
+Landroidx/tv/material3/ScaleIndicationInstance;
+Landroidx/tv/material3/ScaleIndicationTokens;
+Landroidx/tv/material3/ShapesKt$LocalShapes$1;
+Landroidx/tv/material3/SurfaceKt$Surface$4;
+Landroidx/tv/material3/SurfaceKt$SurfaceImpl$2$2$1;
+Landroidx/tv/material3/SurfaceKt$SurfaceImpl$2$4$1;
+Landroidx/tv/material3/SurfaceKt$SurfaceImpl$2$5$1;
+Landroidx/tv/material3/SurfaceKt$SurfaceImpl$2;
+Landroidx/tv/material3/SurfaceKt$SurfaceImpl$3;
+Landroidx/tv/material3/SurfaceKt$handleDPadEnter$2$1;
+Landroidx/tv/material3/SurfaceKt$handleDPadEnter$2$2;
+Landroidx/tv/material3/SurfaceKt$handleDPadEnter$2;
+Landroidx/tv/material3/SurfaceKt$tvToggleable$1$1;
+Landroidx/tv/material3/SurfaceKt$tvToggleable$1$2;
+Landroidx/tv/material3/SurfaceKt$tvToggleable$1;
+Landroidx/tv/material3/SurfaceKt;
+Landroidx/tv/material3/TabColors;
+Landroidx/tv/material3/TabKt$Tab$1;
+Landroidx/tv/material3/TabKt$Tab$3$1;
+Landroidx/tv/material3/TabKt$Tab$4$1;
+Landroidx/tv/material3/TabKt$Tab$6;
+Landroidx/tv/material3/TabKt$Tab$7;
+Landroidx/tv/material3/TabKt;
+Landroidx/tv/material3/TabRowDefaults$PillIndicator$1;
+Landroidx/tv/material3/TabRowDefaults;
+Landroidx/tv/material3/TabRowKt$TabRow$1;
+Landroidx/tv/material3/TabRowKt$TabRow$2$1$1;
+Landroidx/tv/material3/TabRowKt$TabRow$2$2$1$1$2;
+Landroidx/tv/material3/TabRowKt$TabRow$2$2$1$1;
+Landroidx/tv/material3/TabRowKt$TabRow$2$2$1$separators$1;
+Landroidx/tv/material3/TabRowKt$TabRow$2$2$1;
+Landroidx/tv/material3/TabRowKt$TabRow$2;
+Landroidx/tv/material3/TabRowKt$TabRow$3;
+Landroidx/tv/material3/TabRowScopeImpl;
+Landroidx/tv/material3/TabRowSlots;
+Landroidx/tv/material3/TextKt$Text$1;
+Landroidx/tv/material3/TextKt$Text$2;
+Landroidx/tv/material3/TextKt;
+Landroidx/tv/material3/ToggleableSurfaceBorder;
+Landroidx/tv/material3/ToggleableSurfaceColors;
+Landroidx/tv/material3/ToggleableSurfaceGlow;
+Landroidx/tv/material3/ToggleableSurfaceScale;
+Landroidx/tv/material3/ToggleableSurfaceShape;
+Landroidx/tv/material3/tokens/ColorLightTokens;
+Landroidx/tv/material3/tokens/Elevation;
+Landroidx/tv/material3/tokens/PaletteTokens;
+Landroidx/tv/material3/tokens/ShapeTokens$BorderDefaultShape$1;
+Landroidx/tv/material3/tokens/ShapeTokens;
+Landroidx/tv/material3/tokens/TypographyTokensKt;
+Lcom/example/tvcomposebasedtests/ComposableSingletons$MainActivityKt;
+Lcom/example/tvcomposebasedtests/ComposableSingletons$UtilsKt$lambda-1$1;
+Lcom/example/tvcomposebasedtests/Config;
+Lcom/example/tvcomposebasedtests/JankStatsAggregator$listener$1;
+Lcom/example/tvcomposebasedtests/JankStatsAggregator;
+Lcom/example/tvcomposebasedtests/MainActivity$jankReportListener$1;
+Lcom/example/tvcomposebasedtests/MainActivity$startFrameMetrics$listener$1;
+Lcom/example/tvcomposebasedtests/MainActivity;
+Lcom/example/tvcomposebasedtests/UtilsKt$AddJankMetrics$1$2;
+Lcom/example/tvcomposebasedtests/UtilsKt$ScrollingRow$1$1$1;
+Lcom/example/tvcomposebasedtests/UtilsKt$ScrollingRow$2;
+Lcom/example/tvcomposebasedtests/UtilsKt;
+Lcom/example/tvcomposebasedtests/tvComponents/AppKt$App$1;
+Lcom/example/tvcomposebasedtests/tvComponents/ComposableSingletons$LazyContainersKt;
+Lcom/example/tvcomposebasedtests/tvComponents/ComposableSingletons$TopNavigationKt;
+Lcom/example/tvcomposebasedtests/tvComponents/Navigation;
+Lcom/example/tvcomposebasedtests/tvComponents/TopNavigationKt$PillIndicatorTabRow$1$1$1$1;
+Lcom/example/tvcomposebasedtests/tvComponents/TopNavigationKt$PillIndicatorTabRow$1;
+Lcom/example/tvcomposebasedtests/tvComponents/TopNavigationKt$TopNavigation$3$1;
+Lcom/google/gson/JsonIOException;
+Lcom/google/gson/internal/ConstructorConstructor;
+Lcom/google/gson/internal/LinkedTreeMap$1;
+Lcom/google/gson/internal/ObjectConstructor;
+Lkotlin/Function;
+Lkotlin/Lazy;
+Lkotlin/Pair;
+Lkotlin/Result$Failure;
+Lkotlin/Result;
+Lkotlin/ResultKt$$ExternalSyntheticCheckNotZero0;
+Lkotlin/ResultKt;
+Lkotlin/SynchronizedLazyImpl;
+Lkotlin/TuplesKt;
+Lkotlin/ULong$Companion;
+Lkotlin/UNINITIALIZED_VALUE;
+Lkotlin/Unit;
+Lkotlin/UnsafeLazyImpl;
+Lkotlin/collections/AbstractCollection;
+Lkotlin/collections/AbstractList;
+Lkotlin/collections/AbstractMap$toString$1;
+Lkotlin/collections/AbstractMap;
+Lkotlin/collections/AbstractMutableList;
+Lkotlin/collections/AbstractSet;
+Lkotlin/collections/ArrayDeque;
+Lkotlin/collections/ArraysKt___ArraysKt;
+Lkotlin/collections/CollectionsKt__MutableCollectionsJVMKt;
+Lkotlin/collections/CollectionsKt__ReversedViewsKt;
+Lkotlin/collections/CollectionsKt___CollectionsKt;
+Lkotlin/collections/EmptyList;
+Lkotlin/collections/EmptyMap;
+Lkotlin/coroutines/AbstractCoroutineContextElement;
+Lkotlin/coroutines/AbstractCoroutineContextKey;
+Lkotlin/coroutines/CombinedContext;
+Lkotlin/coroutines/Continuation;
+Lkotlin/coroutines/ContinuationInterceptor;
+Lkotlin/coroutines/CoroutineContext$Element;
+Lkotlin/coroutines/CoroutineContext$Key;
+Lkotlin/coroutines/CoroutineContext$plus$1;
+Lkotlin/coroutines/CoroutineContext;
+Lkotlin/coroutines/EmptyCoroutineContext;
+Lkotlin/coroutines/intrinsics/CoroutineSingletons;
+Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;
+Lkotlin/coroutines/jvm/internal/CompletedContinuation;
+Lkotlin/coroutines/jvm/internal/ContinuationImpl;
+Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;
+Lkotlin/coroutines/jvm/internal/SuspendLambda;
+Lkotlin/jvm/functions/Function0;
+Lkotlin/jvm/functions/Function12;
+Lkotlin/jvm/functions/Function1;
+Lkotlin/jvm/functions/Function22;
+Lkotlin/jvm/functions/Function2;
+Lkotlin/jvm/functions/Function3;
+Lkotlin/jvm/functions/Function4;
+Lkotlin/jvm/functions/Function5;
+Lkotlin/jvm/internal/ArrayIterator;
+Lkotlin/jvm/internal/CallableReference$NoReceiver;
+Lkotlin/jvm/internal/CallableReference;
+Lkotlin/jvm/internal/ClassBasedDeclarationContainer;
+Lkotlin/jvm/internal/ClassReference;
+Lkotlin/jvm/internal/FunctionBase;
+Lkotlin/jvm/internal/FunctionReferenceImpl;
+Lkotlin/jvm/internal/Lambda;
+Lkotlin/jvm/internal/PropertyReference0Impl;
+Lkotlin/jvm/internal/PropertyReference;
+Lkotlin/jvm/internal/Ref$BooleanRef;
+Lkotlin/jvm/internal/Ref$ObjectRef;
+Lkotlin/jvm/internal/Reflection;
+Lkotlin/jvm/internal/ReflectionFactory;
+Lkotlin/jvm/internal/markers/KMappedMarker;
+Lkotlin/jvm/internal/markers/KMutableCollection;
+Lkotlin/jvm/internal/markers/KMutableMap;
+Lkotlin/math/MathKt;
+Lkotlin/random/FallbackThreadLocalRandom$implStorage$1;
+Lkotlin/ranges/IntProgression;
+Lkotlin/ranges/IntProgressionIterator;
+Lkotlin/ranges/IntRange;
+Lkotlin/reflect/KCallable;
+Lkotlin/reflect/KClass;
+Lkotlin/reflect/KFunction;
+Lkotlin/reflect/KProperty0;
+Lkotlin/reflect/KProperty;
+Lkotlin/sequences/ConstrainedOnceSequence;
+Lkotlin/sequences/FilteringSequence$iterator$1;
+Lkotlin/sequences/FilteringSequence;
+Lkotlin/sequences/GeneratorSequence$iterator$1;
+Lkotlin/sequences/GeneratorSequence;
+Lkotlin/sequences/Sequence;
+Lkotlin/sequences/SequencesKt;
+Lkotlin/sequences/SequencesKt__SequencesKt$asSequence$$inlined$Sequence$1;
+Lkotlin/sequences/TransformingSequence$iterator$1;
+Lkotlin/text/StringsKt__IndentKt$getIndentFunction$2;
+Lkotlin/text/StringsKt__RegexExtensionsKt;
+Lkotlin/text/StringsKt__StringBuilderKt;
+Lkotlin/text/StringsKt__StringNumberConversionsKt;
+Lkotlin/text/StringsKt__StringsKt;
+Lkotlin/text/StringsKt___StringsKt;
+Lkotlinx/coroutines/AbstractCoroutine;
+Lkotlinx/coroutines/Active;
+Lkotlinx/coroutines/BlockingCoroutine;
+Lkotlinx/coroutines/BlockingEventLoop;
+Lkotlinx/coroutines/CancelHandler;
+Lkotlinx/coroutines/CancellableContinuation;
+Lkotlinx/coroutines/CancellableContinuationImpl;
+Lkotlinx/coroutines/CancelledContinuation;
+Lkotlinx/coroutines/ChildContinuation;
+Lkotlinx/coroutines/ChildHandle;
+Lkotlinx/coroutines/ChildHandleNode;
+Lkotlinx/coroutines/ChildJob;
+Lkotlinx/coroutines/CompletableDeferredImpl;
+Lkotlinx/coroutines/CompletedContinuation;
+Lkotlinx/coroutines/CompletedExceptionally;
+Lkotlinx/coroutines/CompletedWithCancellation;
+Lkotlinx/coroutines/CoroutineContextKt$foldCopies$1;
+Lkotlinx/coroutines/CoroutineDispatcher$Key$1;
+Lkotlinx/coroutines/CoroutineDispatcher$Key;
+Lkotlinx/coroutines/CoroutineDispatcher;
+Lkotlinx/coroutines/CoroutineExceptionHandler;
+Lkotlinx/coroutines/CoroutineScope;
+Lkotlinx/coroutines/DefaultExecutor;
+Lkotlinx/coroutines/DefaultExecutorKt;
+Lkotlinx/coroutines/Delay;
+Lkotlinx/coroutines/DispatchedTask;
+Lkotlinx/coroutines/Dispatchers;
+Lkotlinx/coroutines/DisposableHandle;
+Lkotlinx/coroutines/Empty;
+Lkotlinx/coroutines/EventLoopImplBase;
+Lkotlinx/coroutines/EventLoopImplPlatform;
+Lkotlinx/coroutines/ExecutorCoroutineDispatcher$Key$1;
+Lkotlinx/coroutines/ExecutorCoroutineDispatcher;
+Lkotlinx/coroutines/GlobalScope;
+Lkotlinx/coroutines/InactiveNodeList;
+Lkotlinx/coroutines/Incomplete;
+Lkotlinx/coroutines/IncompleteStateBox;
+Lkotlinx/coroutines/InvokeOnCancel;
+Lkotlinx/coroutines/InvokeOnCancelling;
+Lkotlinx/coroutines/InvokeOnCompletion;
+Lkotlinx/coroutines/Job;
+Lkotlinx/coroutines/JobCancellingNode;
+Lkotlinx/coroutines/JobImpl;
+Lkotlinx/coroutines/JobNode;
+Lkotlinx/coroutines/JobSupport$ChildCompletion;
+Lkotlinx/coroutines/JobSupport$Finishing;
+Lkotlinx/coroutines/JobSupport$addLastAtomic$$inlined$addLastIf$1;
+Lkotlinx/coroutines/JobSupport;
+Lkotlinx/coroutines/MainCoroutineDispatcher;
+Lkotlinx/coroutines/NodeList;
+Lkotlinx/coroutines/NonDisposableHandle;
+Lkotlinx/coroutines/NotCompleted;
+Lkotlinx/coroutines/ParentJob;
+Lkotlinx/coroutines/StandaloneCoroutine;
+Lkotlinx/coroutines/SupervisorJobImpl;
+Lkotlinx/coroutines/ThreadLocalEventLoop;
+Lkotlinx/coroutines/TimeoutCancellationException;
+Lkotlinx/coroutines/Unconfined;
+Lkotlinx/coroutines/UndispatchedCoroutine;
+Lkotlinx/coroutines/UndispatchedMarker;
+Lkotlinx/coroutines/Waiter;
+Lkotlinx/coroutines/android/AndroidDispatcherFactory;
+Lkotlinx/coroutines/android/HandlerContext;
+Lkotlinx/coroutines/android/HandlerDispatcher;
+Lkotlinx/coroutines/android/HandlerDispatcherKt;
+Lkotlinx/coroutines/channels/BufferOverflow;
+Lkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;
+Lkotlinx/coroutines/channels/BufferedChannel;
+Lkotlinx/coroutines/channels/BufferedChannelKt$createSegmentFunction$1;
+Lkotlinx/coroutines/channels/BufferedChannelKt;
+Lkotlinx/coroutines/channels/Channel$Factory;
+Lkotlinx/coroutines/channels/Channel;
+Lkotlinx/coroutines/channels/ChannelResult$Closed;
+Lkotlinx/coroutines/channels/ChannelResult$Failed;
+Lkotlinx/coroutines/channels/ChannelSegment;
+Lkotlinx/coroutines/channels/ConflatedBufferedChannel;
+Lkotlinx/coroutines/channels/ProducerCoroutine;
+Lkotlinx/coroutines/channels/ProducerScope;
+Lkotlinx/coroutines/channels/ReceiveChannel;
+Lkotlinx/coroutines/channels/SendChannel;
+Lkotlinx/coroutines/channels/WaiterEB;
+Lkotlinx/coroutines/flow/AbstractFlow$collect$1;
+Lkotlinx/coroutines/flow/DistinctFlowImpl$collect$2$emit$1;
+Lkotlinx/coroutines/flow/DistinctFlowImpl$collect$2;
+Lkotlinx/coroutines/flow/DistinctFlowImpl;
+Lkotlinx/coroutines/flow/Flow;
+Lkotlinx/coroutines/flow/FlowCollector;
+Lkotlinx/coroutines/flow/FlowKt__ChannelsKt$emitAllImpl$1;
+Lkotlinx/coroutines/flow/FlowKt__LimitKt$dropWhile$$inlined$unsafeFlow$1;
+Lkotlinx/coroutines/flow/FlowKt__LimitKt$dropWhile$1$1$emit$1;
+Lkotlinx/coroutines/flow/FlowKt__LimitKt$dropWhile$1$1;
+Lkotlinx/coroutines/flow/FlowKt__MergeKt$mapLatest$1;
+Lkotlinx/coroutines/flow/FlowKt__MergeKt;
+Lkotlinx/coroutines/flow/FlowKt__ReduceKt$first$$inlined$collectWhile$2$1;
+Lkotlinx/coroutines/flow/FlowKt__ReduceKt$first$$inlined$collectWhile$2;
+Lkotlinx/coroutines/flow/FlowKt__ReduceKt$first$3;
+Lkotlinx/coroutines/flow/FlowKt__ShareKt$launchSharing$1$2;
+Lkotlinx/coroutines/flow/FlowKt__ShareKt$launchSharing$1;
+Lkotlinx/coroutines/flow/MutableSharedFlow;
+Lkotlinx/coroutines/flow/ReadonlyStateFlow;
+Lkotlinx/coroutines/flow/SafeFlow;
+Lkotlinx/coroutines/flow/SharedFlowImpl$Emitter;
+Lkotlinx/coroutines/flow/SharedFlowImpl$collect$1;
+Lkotlinx/coroutines/flow/SharedFlowImpl;
+Lkotlinx/coroutines/flow/SharedFlowSlot;
+Lkotlinx/coroutines/flow/SharingCommand;
+Lkotlinx/coroutines/flow/SharingConfig;
+Lkotlinx/coroutines/flow/SharingStarted;
+Lkotlinx/coroutines/flow/StartedLazily;
+Lkotlinx/coroutines/flow/StartedWhileSubscribed$command$1;
+Lkotlinx/coroutines/flow/StartedWhileSubscribed$command$2;
+Lkotlinx/coroutines/flow/StartedWhileSubscribed;
+Lkotlinx/coroutines/flow/StateFlow;
+Lkotlinx/coroutines/flow/StateFlowImpl$collect$1;
+Lkotlinx/coroutines/flow/StateFlowImpl;
+Lkotlinx/coroutines/flow/StateFlowSlot;
+Lkotlinx/coroutines/flow/internal/AbortFlowException;
+Lkotlinx/coroutines/flow/internal/AbstractSharedFlow;
+Lkotlinx/coroutines/flow/internal/AbstractSharedFlowSlot;
+Lkotlinx/coroutines/flow/internal/ChannelFlow$collect$2;
+Lkotlinx/coroutines/flow/internal/ChannelFlow$collectToFun$1;
+Lkotlinx/coroutines/flow/internal/ChannelFlow;
+Lkotlinx/coroutines/flow/internal/ChannelFlowOperator;
+Lkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3$1$2;
+Lkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3$1$emit$1;
+Lkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3$1;
+Lkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest$flowCollect$3;
+Lkotlinx/coroutines/flow/internal/ChannelFlowTransformLatest;
+Lkotlinx/coroutines/flow/internal/FusibleFlow;
+Lkotlinx/coroutines/flow/internal/NoOpContinuation;
+Lkotlinx/coroutines/flow/internal/NopCollector;
+Lkotlinx/coroutines/flow/internal/SafeCollector;
+Lkotlinx/coroutines/flow/internal/SendingCollector;
+Lkotlinx/coroutines/flow/internal/SubscriptionCountStateFlow;
+Lkotlinx/coroutines/internal/AtomicOp;
+Lkotlinx/coroutines/internal/ConcurrentLinkedListNode;
+Lkotlinx/coroutines/internal/ContextScope;
+Lkotlinx/coroutines/internal/DispatchedContinuation;
+Lkotlinx/coroutines/internal/LimitedDispatcher;
+Lkotlinx/coroutines/internal/LockFreeLinkedListNode$toString$1;
+Lkotlinx/coroutines/internal/LockFreeLinkedListNode;
+Lkotlinx/coroutines/internal/LockFreeTaskQueue;
+Lkotlinx/coroutines/internal/LockFreeTaskQueueCore;
+Lkotlinx/coroutines/internal/MainDispatcherFactory;
+Lkotlinx/coroutines/internal/MainDispatcherLoader;
+Lkotlinx/coroutines/internal/OpDescriptor;
+Lkotlinx/coroutines/internal/Removed;
+Lkotlinx/coroutines/internal/ResizableAtomicArray;
+Lkotlinx/coroutines/internal/ScopeCoroutine;
+Lkotlinx/coroutines/internal/Segment;
+Lkotlinx/coroutines/internal/StackTraceRecoveryKt;
+Lkotlinx/coroutines/internal/Symbol;
+Lkotlinx/coroutines/internal/SystemPropsKt__SystemPropsKt;
+Lkotlinx/coroutines/internal/ThreadState;
+Lkotlinx/coroutines/scheduling/CoroutineScheduler;
+Lkotlinx/coroutines/scheduling/DefaultIoScheduler;
+Lkotlinx/coroutines/scheduling/DefaultScheduler;
+Lkotlinx/coroutines/scheduling/GlobalQueue;
+Lkotlinx/coroutines/scheduling/NanoTimeSource;
+Lkotlinx/coroutines/scheduling/SchedulerCoroutineDispatcher;
+Lkotlinx/coroutines/scheduling/Task;
+Lkotlinx/coroutines/scheduling/TasksKt;
+Lkotlinx/coroutines/scheduling/UnlimitedIoScheduler;
+Lkotlinx/coroutines/sync/Mutex;
+Lkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner$resume$2;
+Lkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;
+Lkotlinx/coroutines/sync/MutexImpl;
+Lkotlinx/coroutines/sync/SemaphoreImpl$addAcquireToQueue$createNewSegment$1;
+Lkotlinx/coroutines/sync/SemaphoreImpl$tryResumeNextFromQueue$createNewSegment$1;
+Lkotlinx/coroutines/sync/SemaphoreImpl;
+Lkotlinx/coroutines/sync/SemaphoreKt;
+Lkotlinx/coroutines/sync/SemaphoreSegment;
+Lokhttp3/Headers$Builder;
+Lokhttp3/MediaType;
+PL_COROUTINE/ArtificialStackFrames;->access$removeRunning(Landroidx/compose/runtime/Stack;)V
+PL_COROUTINE/ArtificialStackFrames;->moveGroup(Landroidx/compose/runtime/SlotWriter;ILandroidx/compose/runtime/SlotWriter;ZZZ)Ljava/util/List;
+PLandroidx/activity/ComponentActivity$ReportFullyDrawnExecutorApi16Impl;->run()V
+PLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->next()Ljava/lang/Object;
+PLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->nextNode()Landroidx/arch/core/internal/SafeIterableMap$Entry;
+PLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->supportRemove(Landroidx/arch/core/internal/SafeIterableMap$Entry;)V
+PLandroidx/collection/ArrayMap$EntrySet;->iterator()Ljava/util/Iterator;
+PLandroidx/compose/foundation/DrawOverscrollModifier;->equals(Ljava/lang/Object;)Z
+PLandroidx/compose/foundation/OverscrollConfiguration;->equals(Ljava/lang/Object;)Z
+PLandroidx/compose/foundation/ScrollingLayoutElement;->equals(Ljava/lang/Object;)Z
+PLandroidx/compose/foundation/gestures/BringIntoViewRequestPriorityQueue;->resumeAndRemoveAll()V
+PLandroidx/compose/foundation/gestures/DraggableNode;->disposeInteractionSource()V
+PLandroidx/compose/foundation/gestures/DraggableNode;->onDetach()V
+PLandroidx/compose/foundation/gestures/ScrollableElement;->equals(Ljava/lang/Object;)Z
+PLandroidx/compose/foundation/layout/OffsetElement;->update(Landroidx/compose/ui/Modifier$Node;)V
+PLandroidx/compose/foundation/layout/SizeElement;->update(Landroidx/compose/ui/Modifier$Node;)V
+PLandroidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher;->onForgotten()V
+PLandroidx/compose/runtime/ComposerImpl$CompositionContextHolder;->onForgotten()V
+PLandroidx/compose/runtime/ComposerImpl$CompositionContextImpl;->dispose()V
+PLandroidx/compose/runtime/ComposerImpl;->recordDelete()V
+PLandroidx/compose/runtime/ComposerImpl;->reportFreeMovableContent$reportGroup(Landroidx/compose/runtime/ComposerImpl;IZI)I
+PLandroidx/compose/runtime/ComposerImpl;->reportFreeMovableContent(I)V
+PLandroidx/compose/runtime/CompositionContext;->unregisterComposer$runtime_release(Landroidx/compose/runtime/Composer;)V
+PLandroidx/compose/runtime/CompositionScopedCoroutineScopeCanceller;->onForgotten()V
+PLandroidx/compose/runtime/Pending;->nodePositionOf(Landroidx/compose/runtime/KeyInfo;)I
+PLandroidx/compose/runtime/Pending;->updateNodeCount(II)Z
+PLandroidx/compose/runtime/Recomposer$effectJob$1$1;->invoke(Ljava/lang/Throwable;)V
+PLandroidx/compose/runtime/Recomposer;->cancel()V
+PLandroidx/compose/runtime/Recomposer;->reportRemovedComposition$runtime_release(Landroidx/compose/runtime/CompositionImpl;)V
+PLandroidx/compose/runtime/SlotWriter;->ensureStarted(I)V
+PLandroidx/compose/runtime/SlotWriter;->startGroup()V
+PLandroidx/compose/runtime/changelist/ComposerChangeListWriter;->removeNode(II)V
+PLandroidx/compose/runtime/changelist/Operation$EndCompositionScope;-><clinit>()V
+PLandroidx/compose/runtime/changelist/Operation$EndCompositionScope;-><init>()V
+PLandroidx/compose/runtime/changelist/Operation$EndCompositionScope;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+PLandroidx/compose/runtime/changelist/Operation$EndCurrentGroup;-><clinit>()V
+PLandroidx/compose/runtime/changelist/Operation$EndCurrentGroup;-><init>()V
+PLandroidx/compose/runtime/changelist/Operation$EndCurrentGroup;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+PLandroidx/compose/runtime/changelist/Operation$EnsureGroupStarted;-><clinit>()V
+PLandroidx/compose/runtime/changelist/Operation$EnsureGroupStarted;-><init>()V
+PLandroidx/compose/runtime/changelist/Operation$EnsureGroupStarted;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+PLandroidx/compose/runtime/changelist/Operation$EnsureRootGroupStarted;-><clinit>()V
+PLandroidx/compose/runtime/changelist/Operation$EnsureRootGroupStarted;-><init>()V
+PLandroidx/compose/runtime/changelist/Operation$EnsureRootGroupStarted;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+PLandroidx/compose/runtime/changelist/Operation$RemoveCurrentGroup;-><clinit>()V
+PLandroidx/compose/runtime/changelist/Operation$RemoveCurrentGroup;-><init>()V
+PLandroidx/compose/runtime/changelist/Operation$RemoveCurrentGroup;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+PLandroidx/compose/runtime/changelist/Operation$RemoveNode;-><clinit>()V
+PLandroidx/compose/runtime/changelist/Operation$RemoveNode;-><init>()V
+PLandroidx/compose/runtime/changelist/Operation$RemoveNode;->execute(Landroidx/compose/runtime/changelist/Operations$OpIterator;Landroidx/compose/runtime/Applier;Landroidx/compose/runtime/SlotWriter;Landroidx/compose/runtime/CompositionImpl$RememberEventDispatcher;)V
+PLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->remove(IILandroidx/compose/runtime/Stack;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;
+PLandroidx/compose/runtime/saveable/SaveableHolder;->onForgotten()V
+PLandroidx/compose/runtime/saveable/SaveableStateRegistryImpl$registerProvider$3;->unregister()V
+PLandroidx/compose/runtime/snapshots/Snapshot$Companion$$ExternalSyntheticLambda0;->dispose()V
+PLandroidx/compose/runtime/snapshots/SnapshotIdSet$iterator$1;-><init>(Landroidx/compose/runtime/snapshots/SnapshotIdSet;Lkotlin/coroutines/Continuation;)V
+PLandroidx/compose/runtime/snapshots/SnapshotIdSet$iterator$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
+PLandroidx/compose/runtime/snapshots/SnapshotIdSet$iterator$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+PLandroidx/compose/runtime/snapshots/SnapshotIdSet;->iterator()Ljava/util/Iterator;
+PLandroidx/compose/ui/autofill/AutofillCallback;->unregister(Landroidx/compose/ui/autofill/AndroidAutofill;)V
+PLandroidx/compose/ui/focus/FocusOwnerImpl;->clearFocus(ZZ)V
+PLandroidx/compose/ui/input/nestedscroll/NestedScrollNode;->onDetach()V
+PLandroidx/compose/ui/input/pointer/SuspendPointerInputElement;->equals(Ljava/lang/Object;)Z
+PLandroidx/compose/ui/input/pointer/SuspendingPointerInputModifierNodeImpl;->onDetach()V
+PLandroidx/compose/ui/input/pointer/SuspendingPointerInputModifierNodeImpl;->resetPointerInputHandler()V
+PLandroidx/compose/ui/layout/OnSizeChangedModifier;->equals(Ljava/lang/Object;)Z
+PLandroidx/compose/ui/modifier/ModifierLocalManager;->invalidate()V
+PLandroidx/compose/ui/node/BackwardsCompatNode;->onDetach()V
+PLandroidx/compose/ui/node/MeasureAndLayoutDelegate$PostponedRequest;-><init>(Landroidx/compose/ui/node/LayoutNode;ZZ)V
+PLandroidx/compose/ui/node/UiApplier;->remove(II)V
+PLandroidx/compose/ui/platform/AndroidComposeView;->getModifierLocalManager()Landroidx/compose/ui/modifier/ModifierLocalManager;
+PLandroidx/compose/ui/platform/AndroidComposeView;->onDetachedFromWindow()V
+PLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$1;->onViewDetachedFromWindow(Landroid/view/View;)V
+PLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat$boundsUpdatesEventLoop$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+PLandroidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat;->onStop(Landroidx/lifecycle/LifecycleOwner;)V
+PLandroidx/compose/ui/platform/DisposableSaveableStateRegistry_androidKt$DisposableSaveableStateRegistry$1;->invoke()Ljava/lang/Object;
+PLandroidx/compose/ui/platform/WeakCache;-><init>(Lcom/google/gson/internal/ConstructorConstructor;Ljava/lang/Class;)V
+PLandroidx/compose/ui/platform/WindowRecomposer_androidKt$createLifecycleAwareWindowRecomposer$1;->onViewDetachedFromWindow(Landroid/view/View;)V
+PLandroidx/compose/ui/platform/WrappedComposition;->dispose()V
+PLandroidx/compose/ui/text/PlatformTextStyle;->equals(Ljava/lang/Object;)Z
+PLandroidx/concurrent/futures/AbstractResolvableFuture$Listener;-><clinit>()V
+PLandroidx/concurrent/futures/AbstractResolvableFuture$SafeAtomicHelper;-><init>(Ljava/util/concurrent/atomic/AtomicReferenceFieldUpdater;Ljava/util/concurrent/atomic/AtomicReferenceFieldUpdater;Ljava/util/concurrent/atomic/AtomicReferenceFieldUpdater;Ljava/util/concurrent/atomic/AtomicReferenceFieldUpdater;Ljava/util/concurrent/atomic/AtomicReferenceFieldUpdater;)V
+PLandroidx/concurrent/futures/AbstractResolvableFuture$SafeAtomicHelper;->casListeners(Landroidx/concurrent/futures/AbstractResolvableFuture;Landroidx/concurrent/futures/AbstractResolvableFuture$Listener;)Z
+PLandroidx/concurrent/futures/AbstractResolvableFuture$SafeAtomicHelper;->casValue(Landroidx/concurrent/futures/AbstractResolvableFuture;Ljava/lang/Object;Ljava/lang/Object;)Z
+PLandroidx/concurrent/futures/AbstractResolvableFuture$SafeAtomicHelper;->casWaiters(Landroidx/concurrent/futures/AbstractResolvableFuture;Landroidx/concurrent/futures/AbstractResolvableFuture$Waiter;Landroidx/concurrent/futures/AbstractResolvableFuture$Waiter;)Z
+PLandroidx/concurrent/futures/AbstractResolvableFuture$Waiter;-><clinit>()V
+PLandroidx/concurrent/futures/AbstractResolvableFuture$Waiter;-><init>(I)V
+PLandroidx/concurrent/futures/AbstractResolvableFuture;-><clinit>()V
+PLandroidx/concurrent/futures/AbstractResolvableFuture;->complete(Landroidx/concurrent/futures/AbstractResolvableFuture;)V
+PLandroidx/core/view/ViewKt$ancestors$1;-><clinit>()V
+PLandroidx/core/view/ViewKt$ancestors$1;-><init>()V
+PLandroidx/core/view/ViewKt$ancestors$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+PLandroidx/lifecycle/DefaultLifecycleObserver;->onDestroy(Landroidx/lifecycle/LifecycleOwner;)V
+PLandroidx/lifecycle/DefaultLifecycleObserver;->onStop(Landroidx/lifecycle/LifecycleOwner;)V
+PLandroidx/lifecycle/EmptyActivityLifecycleCallbacks;->onActivityDestroyed(Landroid/app/Activity;)V
+PLandroidx/lifecycle/EmptyActivityLifecycleCallbacks;->onActivityPaused(Landroid/app/Activity;)V
+PLandroidx/lifecycle/EmptyActivityLifecycleCallbacks;->onActivityStopped(Landroid/app/Activity;)V
+PLandroidx/lifecycle/ProcessLifecycleOwner$attach$1;->onActivityPaused(Landroid/app/Activity;)V
+PLandroidx/lifecycle/ProcessLifecycleOwner$attach$1;->onActivityStopped(Landroid/app/Activity;)V
+PLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityDestroyed(Landroid/app/Activity;)V
+PLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPaused(Landroid/app/Activity;)V
+PLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPreDestroyed(Landroid/app/Activity;)V
+PLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPrePaused(Landroid/app/Activity;)V
+PLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPreStopped(Landroid/app/Activity;)V
+PLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityStopped(Landroid/app/Activity;)V
+PLandroidx/lifecycle/ReportFragment;->onDestroy()V
+PLandroidx/lifecycle/ReportFragment;->onPause()V
+PLandroidx/lifecycle/ReportFragment;->onStop()V
+PLandroidx/metrics/performance/JankStatsApi24Impl;->removeFrameMetricsListenerDelegate(Landroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda0;Landroid/view/Window;)V
+PLandroidx/profileinstaller/ProfileInstaller$$ExternalSyntheticLambda1;-><init>(I)V
+PLandroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda1;->run()V
+PLandroidx/profileinstaller/ProfileVerifier$Cache;-><init>(IIJJ)V
+PLandroidx/profileinstaller/ProfileVerifier$Cache;->writeOnFile(Ljava/io/File;)V
+PLandroidx/profileinstaller/ProfileVerifier;-><clinit>()V
+PLandroidx/profileinstaller/ProfileVerifier;->setCompilationStatus(IZZ)Landroidx/compose/ui/unit/Dp$Companion;
+PLandroidx/profileinstaller/ProfileVerifier;->writeProfileVerification(Landroid/content/Context;Z)V
+PLandroidx/tv/foundation/lazy/layout/LazyLayoutBeyondBoundsInfo$Interval;->equals(Ljava/lang/Object;)Z
+PLandroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsModifierLocal;->getValue()Ljava/lang/Object;
+PLcom/example/tvcomposebasedtests/MainActivity;->onPause()V
+PLcom/google/gson/FieldNamingPolicy$1;-><init>()V
+PLcom/google/gson/FieldNamingPolicy$1;->translateName(Ljava/lang/reflect/Field;)Ljava/lang/String;
+PLcom/google/gson/FieldNamingPolicy$2;-><init>()V
+PLcom/google/gson/FieldNamingPolicy$3;-><init>()V
+PLcom/google/gson/FieldNamingPolicy$4;-><init>()V
+PLcom/google/gson/FieldNamingPolicy$5;-><init>()V
+PLcom/google/gson/FieldNamingPolicy$6;-><init>()V
+PLcom/google/gson/FieldNamingPolicy$7;-><init>()V
+PLcom/google/gson/FieldNamingPolicy;-><clinit>()V
+PLcom/google/gson/FieldNamingPolicy;-><init>(Ljava/lang/String;I)V
+PLcom/google/gson/Gson$1;-><init>(I)V
+PLcom/google/gson/Gson$3;-><init>(I)V
+PLcom/google/gson/Gson$4;-><init>(Lcom/google/gson/TypeAdapter;I)V
+PLcom/google/gson/Gson$FutureTypeAdapter;-><init>()V
+PLcom/google/gson/Gson;-><init>()V
+PLcom/google/gson/Gson;->newJsonWriter(Ljava/io/Writer;)Lcom/google/gson/stream/JsonWriter;
+PLcom/google/gson/Gson;->toJson(Lcom/google/gson/JsonObject;Lcom/google/gson/stream/JsonWriter;)V
+PLcom/google/gson/JsonNull;-><clinit>()V
+PLcom/google/gson/JsonPrimitive;-><init>(Ljava/lang/String;)V
+PLcom/google/gson/ToNumberPolicy$1;-><init>()V
+PLcom/google/gson/ToNumberPolicy$2;-><init>()V
+PLcom/google/gson/ToNumberPolicy$3;-><init>()V
+PLcom/google/gson/ToNumberPolicy$4;-><init>()V
+PLcom/google/gson/ToNumberPolicy;-><clinit>()V
+PLcom/google/gson/ToNumberPolicy;-><init>(Ljava/lang/String;I)V
+PLcom/google/gson/TypeAdapter;->nullSafe()Lcom/google/gson/Gson$4;
+PLcom/google/gson/internal/$Gson$Types$ParameterizedTypeImpl;-><init>(Ljava/lang/reflect/Type;Ljava/lang/reflect/Type;[Ljava/lang/reflect/Type;)V
+PLcom/google/gson/internal/$Gson$Types$ParameterizedTypeImpl;->getActualTypeArguments()[Ljava/lang/reflect/Type;
+PLcom/google/gson/internal/ConstructorConstructor;-><init>(Ljava/util/Map;Ljava/util/List;)V
+PLcom/google/gson/internal/ConstructorConstructor;->checkInstantiable(Ljava/lang/Class;)Ljava/lang/String;
+PLcom/google/gson/internal/ConstructorConstructor;->get(Lcom/google/gson/reflect/TypeToken;)Lcom/google/gson/internal/ObjectConstructor;
+PLcom/google/gson/internal/Excluder;-><clinit>()V
+PLcom/google/gson/internal/Excluder;-><init>()V
+PLcom/google/gson/internal/Excluder;->create(Lcom/google/gson/Gson;Lcom/google/gson/reflect/TypeToken;)Lcom/google/gson/TypeAdapter;
+PLcom/google/gson/internal/Excluder;->excludeClassInStrategy(Z)V
+PLcom/google/gson/internal/Excluder;->isAnonymousOrNonStaticLocal(Ljava/lang/Class;)Z
+PLcom/google/gson/internal/LinkedTreeMap$KeySet$1;-><init>(Landroidx/collection/ArrayMap$EntrySet;)V
+PLcom/google/gson/internal/LinkedTreeMap$KeySet$1;->next()Ljava/lang/Object;
+PLcom/google/gson/internal/LinkedTreeMap$LinkedTreeMapIterator;->nextNode()Lcom/google/gson/internal/LinkedTreeMap$Node;
+PLcom/google/gson/internal/LinkedTreeMap$Node;->getKey()Ljava/lang/Object;
+PLcom/google/gson/internal/LinkedTreeMap;-><clinit>()V
+PLcom/google/gson/internal/bind/ArrayTypeAdapter;-><clinit>()V
+PLcom/google/gson/internal/bind/CollectionTypeAdapterFactory;-><init>(Lcom/google/gson/internal/ConstructorConstructor;I)V
+PLcom/google/gson/internal/bind/CollectionTypeAdapterFactory;->create(Lcom/google/gson/Gson;Lcom/google/gson/reflect/TypeToken;)Lcom/google/gson/TypeAdapter;
+PLcom/google/gson/internal/bind/DateTypeAdapter;-><clinit>()V
+PLcom/google/gson/internal/bind/JsonTreeWriter$1;-><init>()V
+PLcom/google/gson/internal/bind/JsonTreeWriter;-><clinit>()V
+PLcom/google/gson/internal/bind/JsonTreeWriter;->beginObject()V
+PLcom/google/gson/internal/bind/JsonTreeWriter;->value(Ljava/lang/Boolean;)V
+PLcom/google/gson/internal/bind/MapTypeAdapterFactory;-><init>(Lcom/google/gson/internal/ConstructorConstructor;)V
+PLcom/google/gson/internal/bind/MapTypeAdapterFactory;->create(Lcom/google/gson/Gson;Lcom/google/gson/reflect/TypeToken;)Lcom/google/gson/TypeAdapter;
+PLcom/google/gson/internal/bind/NumberTypeAdapter$1;-><init>(ILjava/lang/Object;)V
+PLcom/google/gson/internal/bind/NumberTypeAdapter$1;->create(Lcom/google/gson/Gson;Lcom/google/gson/reflect/TypeToken;)Lcom/google/gson/TypeAdapter;
+PLcom/google/gson/internal/bind/NumberTypeAdapter;-><clinit>()V
+PLcom/google/gson/internal/bind/ObjectTypeAdapter;-><clinit>()V
+PLcom/google/gson/internal/bind/ObjectTypeAdapter;-><init>(Lcom/google/gson/Gson;)V
+PLcom/google/gson/internal/bind/ReflectiveTypeAdapterFactory$1;-><init>(Ljava/lang/String;Ljava/lang/reflect/Field;ZZLjava/lang/reflect/Method;ZLcom/google/gson/TypeAdapter;Lcom/google/gson/Gson;Lcom/google/gson/reflect/TypeToken;)V
+PLcom/google/gson/internal/bind/ReflectiveTypeAdapterFactory$Adapter;-><init>(Ljava/util/LinkedHashMap;)V
+PLcom/google/gson/internal/bind/ReflectiveTypeAdapterFactory;-><init>(Lcom/google/gson/internal/ConstructorConstructor;Lcom/google/gson/internal/Excluder;Lcom/google/gson/internal/bind/CollectionTypeAdapterFactory;Ljava/util/List;)V
+PLcom/google/gson/internal/bind/ReflectiveTypeAdapterFactory;->create(Lcom/google/gson/Gson;Lcom/google/gson/reflect/TypeToken;)Lcom/google/gson/TypeAdapter;
+PLcom/google/gson/internal/bind/ReflectiveTypeAdapterFactory;->getBoundFields(Lcom/google/gson/Gson;Lcom/google/gson/reflect/TypeToken;Ljava/lang/Class;Z)Ljava/util/LinkedHashMap;
+PLcom/google/gson/internal/bind/ReflectiveTypeAdapterFactory;->includeField(Ljava/lang/reflect/Field;Z)Z
+PLcom/google/gson/internal/bind/TypeAdapters$29;-><init>(I)V
+PLcom/google/gson/internal/bind/TypeAdapters$29;->create(Lcom/google/gson/Gson;Lcom/google/gson/reflect/TypeToken;)Lcom/google/gson/TypeAdapter;
+PLcom/google/gson/internal/bind/TypeAdapters$31;-><init>(Ljava/lang/Class;Lcom/google/gson/TypeAdapter;I)V
+PLcom/google/gson/internal/bind/TypeAdapters$31;->create(Lcom/google/gson/Gson;Lcom/google/gson/reflect/TypeToken;)Lcom/google/gson/TypeAdapter;
+PLcom/google/gson/internal/bind/TypeAdapters$32;-><init>(Ljava/lang/Class;Ljava/lang/Class;Lcom/google/gson/TypeAdapter;I)V
+PLcom/google/gson/internal/bind/TypeAdapters$32;->create(Lcom/google/gson/Gson;Lcom/google/gson/reflect/TypeToken;)Lcom/google/gson/TypeAdapter;
+PLcom/google/gson/internal/bind/TypeAdapters$34$1;-><init>(Lcom/google/gson/Gson;Ljava/lang/reflect/Type;Lcom/google/gson/TypeAdapter;Lcom/google/gson/internal/ObjectConstructor;)V
+PLcom/google/gson/internal/bind/TypeAdapters;-><clinit>()V
+PLcom/google/gson/internal/bind/TypeAdapters;->newFactory(Ljava/lang/Class;Lcom/google/gson/TypeAdapter;)Lcom/google/gson/internal/bind/TypeAdapters$31;
+PLcom/google/gson/internal/bind/TypeAdapters;->newFactory(Ljava/lang/Class;Ljava/lang/Class;Lcom/google/gson/TypeAdapter;)Lcom/google/gson/internal/bind/TypeAdapters$32;
+PLcom/google/gson/internal/reflect/ReflectionHelper$RecordNotSupportedHelper;-><init>()V
+PLcom/google/gson/internal/reflect/ReflectionHelper$RecordNotSupportedHelper;->isRecord(Ljava/lang/Class;)Z
+PLcom/google/gson/internal/reflect/ReflectionHelper$RecordSupportedHelper;-><init>()V
+PLcom/google/gson/internal/reflect/ReflectionHelper;-><clinit>()V
+PLcom/google/gson/internal/reflect/ReflectionHelper;->makeAccessible(Ljava/lang/reflect/AccessibleObject;)V
+PLcom/google/gson/internal/sql/SqlDateTypeAdapter;-><clinit>()V
+PLcom/google/gson/internal/sql/SqlTimeTypeAdapter;-><clinit>()V
+PLcom/google/gson/internal/sql/SqlTimestampTypeAdapter;-><clinit>()V
+PLcom/google/gson/internal/sql/SqlTypesSupport;-><clinit>()V
+PLcom/google/gson/stream/JsonWriter;-><clinit>()V
+PLcom/google/gson/stream/JsonWriter;->endArray()V
+PLcom/google/gson/stream/JsonWriter;->name(Ljava/lang/String;)V
+PLcom/google/gson/stream/JsonWriter;->newline()V
+PLkotlin/ResultKt;->SampleTvLazyColumn(ILandroidx/compose/runtime/Composer;I)V
+PLkotlin/ResultKt;->TvLazyColumn(Landroidx/compose/ui/Modifier;Landroidx/tv/foundation/lazy/list/TvLazyListState;Landroidx/compose/foundation/layout/PaddingValuesImpl;ZLandroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/ui/Alignment$Horizontal;ZLandroidx/tv/foundation/PivotOffsets;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
+PLkotlin/ResultKt;->checkArgument(Z)V
+PLkotlin/ResultKt;->checkNotPrimitive(Ljava/lang/reflect/Type;)V
+PLkotlin/ResultKt;->equals(Ljava/lang/reflect/Type;Ljava/lang/reflect/Type;)Z
+PLkotlin/ResultKt;->getFilterResult(Ljava/util/List;)V
+PLkotlin/ResultKt;->getGenericSupertype(Ljava/lang/reflect/Type;Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/reflect/Type;
+PLkotlin/ResultKt;->getSupertype(Ljava/lang/reflect/Type;Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/reflect/Type;
+PLkotlin/ResultKt;->resolve(Ljava/lang/reflect/Type;Ljava/lang/Class;Ljava/lang/reflect/Type;Ljava/util/HashMap;)Ljava/lang/reflect/Type;
+PLkotlin/ResultKt;->write(Lcom/google/gson/JsonElement;Lcom/google/gson/stream/JsonWriter;)V
+PLkotlin/TuplesKt;-><init>(I)V
+PLkotlin/TuplesKt;-><init>(Ljava/lang/Object;)V
+PLkotlin/TuplesKt;->access$removeEntryAtIndex([Ljava/lang/Object;I)[Ljava/lang/Object;
+PLkotlin/TuplesKt;->asMutableCollection(Ljava/util/LinkedHashSet;)Ljava/util/Collection;
+PLkotlin/TuplesKt;->closeFinally(Ljava/io/Closeable;Ljava/lang/Throwable;)V
+PLkotlin/TuplesKt;->writeProfile(Landroid/content/Context;Landroidx/profileinstaller/ProfileInstaller$$ExternalSyntheticLambda1;Landroidx/profileinstaller/ProfileInstaller$DiagnosticsCallback;Z)V
+PLkotlin/ULong$Companion;->checkPositionIndex$kotlin_stdlib(II)V
+PLkotlin/ULong$Companion;->onResultReceived(ILjava/lang/Object;)V
+PLkotlin/collections/CollectionsKt___CollectionsKt;->minus(Ljava/util/List;Lkotlin/Function;)Ljava/util/ArrayList;
+PLkotlin/coroutines/jvm/internal/BaseContinuationImpl;->releaseIntercepted()V
+PLkotlin/coroutines/jvm/internal/RestrictedContinuationImpl;-><init>(Lkotlin/coroutines/Continuation;)V
+PLkotlin/coroutines/jvm/internal/RestrictedSuspendLambda;-><init>(Lkotlin/coroutines/Continuation;)V
+PLkotlin/sequences/SequenceBuilderIterator;-><init>()V
+PLkotlin/sequences/SequenceBuilderIterator;->getContext()Lkotlin/coroutines/CoroutineContext;
+PLkotlin/sequences/SequenceBuilderIterator;->hasNext()Z
+PLkotlin/sequences/SequenceBuilderIterator;->next()Ljava/lang/Object;
+PLkotlin/sequences/SequenceBuilderIterator;->resumeWith(Ljava/lang/Object;)V
+PLkotlin/sequences/SequenceBuilderIterator;->yield(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)V
+PLkotlin/text/Charsets;-><clinit>()V
+PLkotlinx/coroutines/AbstractCoroutine;->cancellationExceptionMessage()Ljava/lang/String;
+PLkotlinx/coroutines/InvokeOnCompletion;->invoke(Ljava/lang/Throwable;)V
+PLkotlinx/coroutines/JobCancellationException;-><init>(Ljava/lang/String;Ljava/lang/Throwable;Lkotlinx/coroutines/Job;)V
+PLkotlinx/coroutines/JobCancellationException;->equals(Ljava/lang/Object;)Z
+PLkotlinx/coroutines/JobCancellationException;->fillInStackTrace()Ljava/lang/Throwable;
+PLkotlinx/coroutines/JobSupport$Finishing;->isActive()Z
+PLkotlinx/coroutines/JobSupport;->cancellationExceptionMessage()Ljava/lang/String;
+PLkotlinx/coroutines/UndispatchedCoroutine;->afterResume(Ljava/lang/Object;)V
+PLkotlinx/coroutines/flow/FlowKt__ReduceKt$first$3;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
+PLkotlinx/coroutines/flow/SharedFlowSlot;->freeLocked(Lkotlinx/coroutines/flow/internal/AbstractSharedFlow;)[Lkotlin/coroutines/Continuation;
+PLkotlinx/coroutines/flow/SharingConfig;->clear()V
+PLkotlinx/coroutines/flow/StateFlowSlot;->freeLocked(Lkotlinx/coroutines/flow/internal/AbstractSharedFlow;)[Lkotlin/coroutines/Continuation;
+PLkotlinx/coroutines/flow/internal/AbstractSharedFlow;->freeSlot(Lkotlinx/coroutines/flow/internal/AbstractSharedFlowSlot;)V
+PLkotlinx/coroutines/internal/DispatchedContinuation;->cancelCompletedResult$kotlinx_coroutines_core(Ljava/lang/Object;Ljava/util/concurrent/CancellationException;)V
+PLokhttp3/MediaType;->access$locationOf(Ljava/util/ArrayList;II)I
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/FeaturedCarousel.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/FeaturedCarousel.kt
index ac7fe19..85c9d28 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/FeaturedCarousel.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/FeaturedCarousel.kt
@@ -28,6 +28,7 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusGroup
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -45,6 +46,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
@@ -54,6 +56,7 @@
 import androidx.compose.ui.focus.FocusDirection
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.focusRestorer
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.onPlaced
@@ -69,37 +72,48 @@
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.rememberCarouselState
 
+@OptIn(ExperimentalComposeUiApi::class)
 @Composable
 fun FeaturedCarouselContent() {
     LazyColumn(verticalArrangement = Arrangement.spacedBy(20.dp)) {
         items(3) { SampleLazyRow() }
         item {
             Row(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
-                Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+                Column(
+                    modifier = Modifier.focusRestorer().focusGroup(),
+                    verticalArrangement = Arrangement.spacedBy(20.dp)
+                ) {
                     repeat(3) {
-                        Box(
-                            modifier = Modifier
-                                .background(Color.Magenta.copy(alpha = 0.3f))
-                                .width(50.dp)
-                                .height(50.dp)
-                                .drawBorderOnFocus()
-                                .focusable()
-                        )
+                        key(it) {
+                            Box(
+                                modifier = Modifier
+                                    .background(Color.Magenta.copy(alpha = 0.3f))
+                                    .width(50.dp)
+                                    .height(50.dp)
+                                    .drawBorderOnFocus()
+                                    .focusable()
+                            )
+                        }
                     }
                 }
 
                 FeaturedCarousel(Modifier.weight(1f))
 
-                Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+                Column(
+                    modifier = Modifier.focusRestorer().focusGroup(),
+                    verticalArrangement = Arrangement.spacedBy(20.dp)
+                ) {
                     repeat(3) {
-                        Box(
-                            modifier = Modifier
-                                .background(Color.Magenta.copy(alpha = 0.3f))
-                                .width(50.dp)
-                                .height(50.dp)
-                                .drawBorderOnFocus()
-                                .focusable()
-                        )
+                        key(it) {
+                            Box(
+                                modifier = Modifier
+                                    .background(Color.Magenta.copy(alpha = 0.3f))
+                                    .width(50.dp)
+                                    .height(50.dp)
+                                    .drawBorderOnFocus()
+                                    .focusable()
+                            )
+                        }
                     }
                 }
             }
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt
index 22cb95a..a7c3da3 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt
@@ -16,9 +16,7 @@
 
 package androidx.tv.integration.playground
 
-import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -26,38 +24,32 @@
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.selection.selectableGroup
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.KeyboardArrowLeft
-import androidx.compose.material.icons.filled.KeyboardArrowRight
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
+import androidx.compose.material.icons.filled.Settings
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
-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.draw.clip
-import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.semantics.selected
-import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.zIndex
-import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.Icon
 import androidx.tv.material3.NavigationDrawer
+import androidx.tv.material3.NavigationDrawerItem
+import androidx.tv.material3.NavigationDrawerItemDefaults
+import androidx.tv.material3.NavigationDrawerScope
 import androidx.tv.material3.Text
 
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -68,14 +60,7 @@
     CompositionLocalProvider(LocalLayoutDirection provides direction.value) {
         Row(Modifier.fillMaxSize()) {
             Box(modifier = Modifier.height(400.dp)) {
-                NavigationDrawer(
-                    drawerContent = { drawerValue ->
-                        Sidebar(
-                            drawerValue = drawerValue,
-                            direction = direction,
-                        )
-                    }
-                ) {
+                NavigationDrawer(drawerContent = { Sidebar(direction = direction) }) {
                     CommonBackground()
                 }
             }
@@ -92,14 +77,15 @@
         Row(Modifier.fillMaxSize()) {
             Box(modifier = Modifier.height(400.dp)) {
                 androidx.tv.material3.ModalNavigationDrawer(
-                    drawerContent = { drawerValue ->
-                        Sidebar(
-                            drawerValue = drawerValue,
-                            direction = direction,
+                    drawerContent = { Sidebar(direction = direction) },
+                    scrimBrush = Brush.verticalGradient(
+                        listOf(
+                            Color.DarkGray.copy(alpha = 0.2f),
+                            Color.LightGray.copy(alpha = 0.2f)
                         )
-                    }
+                    )
                 ) {
-                    CommonBackground()
+                    CommonBackground(startPadding = 90.dp)
                 }
             }
         }
@@ -107,18 +93,19 @@
 }
 
 @Composable
-private fun CommonBackground() {
-    Row(modifier = Modifier.padding(start = 10.dp)) {
-        Card(backgroundColor = Color.Red)
+private fun CommonBackground(startPadding: Dp = 10.dp) {
+    Box(modifier = Modifier
+        .fillMaxSize()
+        .background(Color.Blue.copy(alpha = 0.3f))) {
+        Row(modifier = Modifier.padding(start = startPadding)) {
+            Card(backgroundColor = Color.Red)
+        }
     }
 }
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
-private fun Sidebar(
-    drawerValue: DrawerValue,
-    direction: MutableState<LayoutDirection>,
-) {
+private fun NavigationDrawerScope.Sidebar(direction: MutableState<LayoutDirection>) {
     val selectedIndex = remember { mutableStateOf(0) }
 
     LaunchedEffect(selectedIndex.value) {
@@ -131,84 +118,53 @@
     Column(
         modifier = Modifier
             .fillMaxHeight()
-            .background(pageColor)
+            .background(pageColor.copy(alpha = 0.5f))
+            .padding(12.dp)
             .selectableGroup(),
-        horizontalAlignment = Alignment.CenterHorizontally,
+        horizontalAlignment = Alignment.Start,
         verticalArrangement = Arrangement.spacedBy(10.dp)
     ) {
-        @Suppress("DEPRECATION")
-        NavigationItem(
-            imageVector = Icons.Default.KeyboardArrowRight,
-            text = "LTR",
-            drawerValue = drawerValue,
-            selectedIndex = selectedIndex,
-            index = 0
-        )
-        @Suppress("DEPRECATION")
-        NavigationItem(
-            imageVector = Icons.Default.KeyboardArrowLeft,
-            text = "RTL",
-            drawerValue = drawerValue,
-            selectedIndex = selectedIndex,
-            index = 1
-        )
-    }
-}
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-private fun NavigationItem(
-    imageVector: ImageVector,
-    text: String,
-    drawerValue: DrawerValue,
-    selectedIndex: MutableState<Int>,
-    index: Int,
-    modifier: Modifier = Modifier,
-) {
-    var isFocused by remember { mutableStateOf(false) }
-
-    Box(
-        modifier = modifier
-            .clip(RoundedCornerShape(10.dp))
-            .onFocusChanged { isFocused = it.isFocused }
-            .background(if (isFocused) Color.White else Color.Transparent)
-            .semantics(mergeDescendants = true) {
-                selected = selectedIndex.value == index
-            }
-            .clickable {
-                selectedIndex.value = index
-            }
-    ) {
-        Box(modifier = Modifier.padding(10.dp)) {
-            Row(
-                verticalAlignment = Alignment.CenterVertically,
-                horizontalArrangement = Arrangement.spacedBy(5.dp),
-            ) {
+        NavigationDrawerItem(
+            selected = true,
+            onClick = { },
+            leadingContent = {
                 Icon(
-                    imageVector = imageVector,
-                    tint = if (isFocused) pageColor else Color.White,
+                    imageVector = Icons.Default.Settings,
                     contentDescription = null,
                 )
-                AnimatedVisibility(visible = drawerValue == DrawerValue.Open) {
-                    Text(
-                        text = text,
-                        modifier = Modifier,
-                        softWrap = false,
-                        color = if (isFocused) pageColor else Color.White,
-                    )
-                }
+            },
+            supportingContent = {
+                Text("Switch account")
+            },
+            trailingContent = {
+                NavigationDrawerItemDefaults.TrailingBadge("NEW")
             }
-            if (selectedIndex.value == index) {
-                Box(
-                    modifier = Modifier
-                        .width(10.dp)
-                        .height(3.dp)
-                        .offset(y = 5.dp)
-                        .align(Alignment.BottomCenter)
-                        .background(Color.Red)
-                        .zIndex(10f)
+        ) {
+            Text(text = "Hi there")
+        }
+        NavigationDrawerItem(
+            selected = selectedIndex.value == 0,
+            onClick = { selectedIndex.value = 0 },
+            leadingContent = {
+                Icon(
+                    imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
+                    contentDescription = null,
                 )
-            }
+            },
+        ) {
+            Text(text = "Left to right")
+        }
+        NavigationDrawerItem(
+            selected = selectedIndex.value == 1,
+            onClick = { selectedIndex.value = 1 },
+            leadingContent = {
+                Icon(
+                    imageVector = Icons.AutoMirrored.Default.KeyboardArrowLeft,
+                    contentDescription = null,
+                )
+            },
+        ) {
+            Text(text = "Right to left")
         }
     }
 }
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/TopNavigation.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/TopNavigation.kt
index 9ea77ed..f0ed752 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/TopNavigation.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/TopNavigation.kt
@@ -134,10 +134,10 @@
     TabRow(
         selectedTabIndex = selectedTabIndex,
         separator = { Spacer(modifier = Modifier.width(12.dp)) },
-        indicator = { tabPositions, isActivated ->
+        indicator = { tabPositions, doesTabRowHaveFocus ->
             TabRowDefaults.UnderlinedIndicator(
                 currentTabPosition = tabPositions[selectedTabIndex],
-                isActivated = isActivated,
+                doesTabRowHaveFocus = doesTabRowHaveFocus,
             )
         },
         modifier = Modifier
diff --git a/tv/integration-tests/presentation/build.gradle b/tv/integration-tests/presentation/build.gradle
index 5a9de93..1b3e68c 100644
--- a/tv/integration-tests/presentation/build.gradle
+++ b/tv/integration-tests/presentation/build.gradle
@@ -32,7 +32,8 @@
     implementation(project(":compose:material3:material3"))
     implementation(project(":navigation:navigation-runtime"))
     implementation(project(":profileinstaller:profileinstaller"))
-    implementation "androidx.compose.material:material-icons-extended:1.3.1"
+    implementation(project(":compose:material:material-icons-core"))
+    implementation(project(":compose:material:material-icons-extended"))
 
 //    implementation 'io.coil-kt:coil-compose:2.2.2'
 //    implementation 'com.google.code.gson:gson:2.8.9'
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt
index 44d35f9..22f0edd 100644
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt
@@ -31,7 +31,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.ArrowRight
+import androidx.compose.material.icons.automirrored.outlined.ArrowForward
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -86,7 +86,7 @@
                 @Suppress("DEPRECATION")
                 AppButton(
                     text = "Watch on YouTube",
-                    icon = Icons.Outlined.ArrowRight,
+                    icon = Icons.AutoMirrored.Outlined.ArrowForward,
                 )
             },
         )
diff --git a/tv/onboarding.md b/tv/onboarding.md
index 32d68cf..70a6c9d 100644
--- a/tv/onboarding.md
+++ b/tv/onboarding.md
@@ -1,51 +1,64 @@
 # Onboarding to Compose for TV libraries
 
-## Getting Started
+## Learn about Jetpack Compose
+1. The [Compose landing page][compose-landing-page] provides an overview of Compose and its features.
+2. The [Compose Quick Tutorial][compose-quick-tutorial] walks you through the basics of Compose using code examples.
+3. The [Compose Course][compose-course] is a more comprehensive guide to Compose, covering topics such as layout, animations, and state management.
 
-1. See what Google suggests are [good design patterns][good-design-patterns]
-   and [offered components][tv-components]
-2. Consult the documentation for information on the packages and various components that are
-   offered.
-    * [tv-foundation][tv-foundation]
-    * [tv-foundation-lazy-list][tv-foundation-lazy-list]
-    * [tv-foundation-lazy-grid][tv-foundation-lazy-grid]
-    * [tv-material][tv-material]
-3. Read documentation and examples on [developer.android.com][dac]
-4. Read up on the [codelabs][codelabs]
-5. Ensure that you are on the latest version of [Compose for TV libraries][compose-for-tv-libraries]
+
+## Learn about Compose for TV
+1. Explore the [available components][tv-components] and the [design patterns][good-design-patterns] that Google recommends.
+2. Consult the documentation for information on the packages and various components available.
+   * [tv-foundation][tv-foundation]
+   * [tv-foundation-lazy-list][tv-foundation-lazy-list]
+   * [tv-foundation-lazy-grid][tv-foundation-lazy-grid]
+   * [tv-material][tv-material]
+3. Refer to the documentation and examples on [developer.android.com][dac].
+4. Get up to speed with the [codelabs][codelabs].
+5. Find the sample app on [GitHub][github-sample-app].
+
+## If you run into issues
+1. Make sure that you are using the most recent version of [Compose for TV libraries][compose-for-tv-libraries]
    and Jetpack compose.
-6. Read the FAQs below
-7. Check with the community
-    * [stack overflow][stackoverflow]
-    * slack (TBD)
-    * discord (TBD)
-8. Check if there is a bug already reported on [issue-tracker][issue-tracker]
-9. [File a bug on issue-tracker][issue-tracker-file-a-bug]
-10. Contact a Developer Relations partner from Google who can involve someone from the engineering
-    team as necessary
+2. Read the FAQs below.
+3. Check with the community over on  [stack overflow][stackoverflow].
+4. Check if there is a bug already reported on [issue-tracker][issue-tracker].
+5. File a bug on [issue-tracker][issue-tracker-file-a-bug].
+6. Reach out to a Google Developer Relations partner who can, if necessary, bring in someone from the engineering team.
 
 ## FAQs
 
-1. How can I improve the performance of my app written using tv-compose?
-    * Any [performance improvements][improve-performance] suggested for a Compose app would
-      generally apply to apps built with Compose for TV libraries too.
-    * Use [baseline profiles][baseline-profiles] as recommended
-      in [Jetpack Compose Performance guide][jetpack-compose-performance].
-      Watch [Making apps blazing fast with Baseline Profiles][making-apps-blazing-fast-with-baseline-profiles]
-    * Checkout [Interpreting Compose Compiler Metrics][interpreting-compose-compiler-metrics].
-2. My app is crashing!
-    * Ensure that you are on the latest alpha version
-      of [Compose for TV libraries][compose-for-tv-libraries] and Jetpack Compose
-    * Check if there is a bug already reported on [issue-tracker][issue-tracker]
-    * [File a bug on issue-tracker][issue-tracker-file-a-bug]
-3. The Navigation drawer is pushing my content aside. I don’t like it.
+1. ### How can I improve the performance of my app written using tv-compose?
+   * [Performance improvements][improve-performance] suggested for a Compose app would typically apply to apps built with Compose for TV libraries as well.
+   * Use [baseline profiles][baseline-profiles] as recommended
+     in [Jetpack Compose Performance guide][jetpack-compose-performance].
+     Watch [Making apps blazing fast with Baseline Profiles][making-apps-blazing-fast-with-baseline-profiles].
+   * Check out [Interpreting Compose Compiler Metrics][interpreting-compose-compiler-metrics].
+2. ### My app is crashing!
+   * Ensure that you are on the latest version.
+     of [Compose for TV libraries][compose-for-tv-libraries] and Jetpack Compose
+   * Check if there is a bug already reported on [issue-tracker][issue-tracker].
+   * [File a bug on issue-tracker][issue-tracker-file-a-bug].
+3. ### The Navigation drawer is pushing my content aside. I don’t like it.
    Consider using a [Modal Navigation Drawer][modal-navigation-drawer] provided
-   in [Compose for TV library][compose-for-tv-modal-navigation-drawer]
+   in [Compose for TV library][compose-for-tv-modal-navigation-drawer].
+4. ### Sideloading baseline profiles to test performance, without releasing the app.
+   Refer to the steps for [applying baseline profiles][tv-samples-baseline-profiles] in the
+   Jetstream sample app.
+
+
+[compose-landing-page]: https://developer.android.com/jetpack/compose
+
+[compose-quick-tutorial]: https://developer.android.com/jetpack/compose/tutorial
+
+[compose-course]: https://developer.android.com/courses/jetpack-compose/course
 
 [good-design-patterns]: https://developer.android.com/design/ui/tv
 
 [dac]: https://developer.android.com/training/tv/playback/compose
 
+[github-sample-app]: https://github.com/android/tv-samples/tree/main/JetStreamCompose
+
 [modal-navigation-drawer]: https://m3.material.io/components/navigation-drawer/overview#15a3aa10-1be4-4be4-8370-36a1779f65e5
 
 [compose-for-tv-modal-navigation-drawer]: https://developer.android.com/reference/kotlin/androidx/tv/material3/package-summary#ModalNavigationDrawer(kotlin.Function1,androidx.compose.ui.Modifier,androidx.tv.material3.DrawerState,androidx.compose.ui.graphics.Color,kotlin.Function0)
@@ -79,3 +92,5 @@
 [tv-foundation-lazy-grid]: https://developer.android.com/reference/kotlin/androidx/tv/foundation/lazy/grid/package-summary
 
 [tv-material]: https://developer.android.com/reference/kotlin/androidx/tv/material3/package-summary
+
+[tv-samples-baseline-profiles]: https://github.com/android/tv-samples/blob/main/JetStreamCompose/baseline-profiles.md
\ No newline at end of file
diff --git a/tv/samples/src/main/java/androidx/tv/samples/NavigationDrawerSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/NavigationDrawerSamples.kt
index dc061ec..c4cea35 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/NavigationDrawerSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/NavigationDrawerSamples.kt
@@ -17,62 +17,79 @@
 package androidx.tv.samples
 
 import androidx.annotation.Sampled
-import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.background
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Settings
 import androidx.compose.material3.Button
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
-import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Icon
 import androidx.tv.material3.ModalNavigationDrawer
 import androidx.tv.material3.NavigationDrawer
+import androidx.tv.material3.NavigationDrawerItem
 import androidx.tv.material3.Text
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Sampled
 @Composable
 fun SampleNavigationDrawer() {
-    val navigationRow: @Composable (drawerValue: DrawerValue, color: Color, text: String) -> Unit =
-        { drawerValue, color, text ->
-            Row(Modifier.padding(10.dp).focusable()) {
-                Box(Modifier.size(50.dp).background(color).padding(end = 20.dp))
-                AnimatedVisibility(visible = drawerValue == DrawerValue.Open) {
-                    Text(
-                        text = text,
-                        softWrap = false,
-                        modifier = Modifier.padding(15.dp).width(50.dp),
-                        textAlign = TextAlign.Center
-                    )
-                }
-            }
-        }
+    var selectedIndex by remember { mutableIntStateOf(0) }
+
+    val items = listOf(
+        "Home" to Icons.Default.Home,
+        "Settings" to Icons.Default.Settings,
+        "Favourites" to Icons.Default.Favorite,
+    )
 
     NavigationDrawer(
         drawerContent = {
-            Column(Modifier.background(Color.Gray).fillMaxHeight()) {
-                navigationRow(it, Color.Red, "Red")
-                navigationRow(it, Color.Blue, "Blue")
-                navigationRow(it, Color.Yellow, "Yellow")
+            Column(
+                Modifier
+                    .background(Color.Gray)
+                    .fillMaxHeight()
+                    .padding(12.dp)
+                    .selectableGroup(),
+                horizontalAlignment = Alignment.Start,
+                verticalArrangement = Arrangement.spacedBy(10.dp)
+            ) {
+                items.forEachIndexed { index, item ->
+                    val (text, icon) = item
+
+                    NavigationDrawerItem(
+                        selected = selectedIndex == index,
+                        onClick = { selectedIndex = index },
+                        leadingContent = {
+                            Icon(
+                                imageVector = icon,
+                                contentDescription = null,
+                            )
+                        }
+                    ) {
+                        Text(text)
+                    }
+                }
             }
         }
     ) {
-        Button(modifier = Modifier
-            .height(100.dp)
-            .fillMaxWidth(), onClick = {}) {
+        Button(modifier = Modifier.height(100.dp).fillMaxWidth(), onClick = {}) {
             Text("BUTTON")
         }
     }
@@ -82,33 +99,53 @@
 @Sampled
 @Composable
 fun SampleModalNavigationDrawerWithSolidScrim() {
-    val navigationRow: @Composable (drawerValue: DrawerValue, color: Color, text: String) -> Unit =
-        { drawerValue, color, text ->
-            Row(Modifier.padding(10.dp).focusable()) {
-                Box(Modifier.size(50.dp).background(color).padding(end = 20.dp))
-                AnimatedVisibility(visible = drawerValue == DrawerValue.Open) {
-                    Text(
-                        text = text,
-                        softWrap = false,
-                        modifier = Modifier.padding(15.dp).width(50.dp),
-                        textAlign = TextAlign.Center
-                    )
+    var selectedIndex by remember { mutableIntStateOf(0) }
+
+    val items = listOf(
+        "Home" to Icons.Default.Home,
+        "Settings" to Icons.Default.Settings,
+        "Favourites" to Icons.Default.Favorite,
+    )
+
+    val closeDrawerWidth = 80.dp
+    val backgroundContentPadding = 10.dp
+    ModalNavigationDrawer(
+        drawerContent = {
+            Column(
+                Modifier
+                    .background(Color.Gray)
+                    .fillMaxHeight()
+                    .padding(12.dp)
+                    .selectableGroup(),
+                horizontalAlignment = Alignment.Start,
+                verticalArrangement = Arrangement.spacedBy(10.dp)
+            ) {
+                items.forEachIndexed { index, item ->
+                    val (text, icon) = item
+
+                    NavigationDrawerItem(
+                        selected = selectedIndex == index,
+                        onClick = { selectedIndex = index },
+                        leadingContent = {
+                            Icon(
+                                imageVector = icon,
+                                contentDescription = null,
+                            )
+                        }
+                    ) {
+                        Text(text)
+                    }
                 }
             }
         }
-
-    ModalNavigationDrawer(
-        drawerContent = {
-            Column(Modifier.background(Color.Gray).fillMaxHeight()) {
-                navigationRow(it, Color.Red, "Red")
-                navigationRow(it, Color.Blue, "Blue")
-                navigationRow(it, Color.Yellow, "Yellow")
-            }
-        }
     ) {
-        Button(modifier = Modifier
-            .height(100.dp)
-            .fillMaxWidth(), onClick = {}) {
+        Button(
+            modifier = Modifier
+                .padding(closeDrawerWidth + backgroundContentPadding)
+                .height(100.dp)
+                .fillMaxWidth(),
+            onClick = {}
+        ) {
             Text("BUTTON")
         }
     }
@@ -118,34 +155,55 @@
 @Sampled
 @Composable
 fun SampleModalNavigationDrawerWithGradientScrim() {
-    val navigationRow: @Composable (drawerValue: DrawerValue, color: Color, text: String) -> Unit =
-        { drawerValue, color, text ->
-            Row(Modifier.padding(10.dp).focusable()) {
-                Box(Modifier.size(50.dp).background(color).padding(end = 20.dp))
-                AnimatedVisibility(visible = drawerValue == DrawerValue.Open) {
-                    Text(
-                        text = text,
-                        softWrap = false,
-                        modifier = Modifier.padding(15.dp).width(50.dp),
-                        textAlign = TextAlign.Center
-                    )
-                }
-            }
-        }
+    var selectedIndex by remember { mutableIntStateOf(0) }
 
-    androidx.tv.material3.ModalNavigationDrawer(
+    val items = listOf(
+        "Home" to Icons.Default.Home,
+        "Settings" to Icons.Default.Settings,
+        "Favourites" to Icons.Default.Favorite,
+    )
+
+    val closeDrawerWidth = 80.dp
+    val backgroundContentPadding = 10.dp
+
+    ModalNavigationDrawer(
         drawerContent = {
-            Column(Modifier.fillMaxHeight()) {
-                navigationRow(it, Color.Red, "Red")
-                navigationRow(it, Color.Blue, "Blue")
-                navigationRow(it, Color.Yellow, "Yellow")
+            Column(
+                Modifier
+                    .background(Color.Gray)
+                    .fillMaxHeight()
+                    .padding(12.dp)
+                    .selectableGroup(),
+                horizontalAlignment = Alignment.Start,
+                verticalArrangement = Arrangement.spacedBy(10.dp)
+            ) {
+                items.forEachIndexed { index, item ->
+                    val (text, icon) = item
+
+                    NavigationDrawerItem(
+                        selected = selectedIndex == index,
+                        onClick = { selectedIndex = index },
+                        leadingContent = {
+                            Icon(
+                                imageVector = icon,
+                                contentDescription = null,
+                            )
+                        }
+                    ) {
+                        Text(text)
+                    }
+                }
             }
         },
         scrimBrush = Brush.horizontalGradient(listOf(Color.DarkGray, Color.Transparent))
     ) {
-        Button(modifier = Modifier
-            .height(100.dp)
-            .fillMaxWidth(), onClick = {}) {
+        Button(
+            modifier = Modifier
+                .padding(closeDrawerWidth + backgroundContentPadding)
+                .height(100.dp)
+                .fillMaxWidth(),
+            onClick = {}
+        ) {
             Text("BUTTON")
         }
     }
diff --git a/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
index bf47504..d09d324 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
@@ -90,10 +90,10 @@
   TabRow(
     selectedTabIndex = selectedTabIndex,
     separator = { Spacer(modifier = Modifier.width(12.dp)) },
-    indicator = { tabPositions, isActivated ->
+    indicator = { tabPositions, doesTabRowHaveFocus ->
       TabRowDefaults.UnderlinedIndicator(
         currentTabPosition = tabPositions[selectedTabIndex],
-        isActivated = isActivated,
+        doesTabRowHaveFocus = doesTabRowHaveFocus,
       )
     },
     modifier = Modifier.focusRestorer()
@@ -181,19 +181,19 @@
   ) {
     TabRow(
       selectedTabIndex = focusedTabIndex,
-      indicator = { tabPositions, isActivated ->
+      indicator = { tabPositions, doesTabRowHaveFocus ->
         // FocusedTab's indicator
         TabRowDefaults.PillIndicator(
           currentTabPosition = tabPositions[focusedTabIndex],
           activeColor = Color.Blue.copy(alpha = 0.4f),
           inactiveColor = Color.Transparent,
-          isActivated = isActivated,
+          doesTabRowHaveFocus = doesTabRowHaveFocus,
         )
 
         // SelectedTab's indicator
         TabRowDefaults.PillIndicator(
           currentTabPosition = tabPositions[activeTabIndex],
-          isActivated = isActivated,
+          doesTabRowHaveFocus = doesTabRowHaveFocus,
         )
       },
       modifier = Modifier.focusRestorer()
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
new file mode 100644
index 0000000..0913946
--- /dev/null
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
@@ -0,0 +1,634 @@
+/*
+ * 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.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.BeyondBoundsLayout
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
+import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
+import androidx.compose.ui.modifier.modifierLocalConsumer
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.LayoutDirection.Ltr
+import androidx.compose.ui.unit.LayoutDirection.Rtl
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.lazy.list.PlacementComparator
+import androidx.tv.foundation.lazy.list.TrackPlacedElement
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalComposeUiApi::class)
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyGridBeyondBoundsTest(param: Param) {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    // We need to wrap the inline class parameter in another class because Java can't instantiate
+    // the inline class.
+    class Param(
+        val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
+        val reverseLayout: Boolean,
+        val layoutDirection: LayoutDirection,
+    ) {
+        override fun toString() = "beyondBoundsLayoutDirection=$beyondBoundsLayoutDirection " +
+            "reverseLayout=$reverseLayout " +
+            "layoutDirection=$layoutDirection"
+    }
+
+    private val beyondBoundsLayoutDirection = param.beyondBoundsLayoutDirection
+    private val reverseLayout = param.reverseLayout
+    private val layoutDirection = param.layoutDirection
+    private val placedItems = sortedMapOf<Int, Rect>()
+    private var beyondBoundsLayout: BeyondBoundsLayout? = null
+    private lateinit var lazyGridState: TvLazyGridState
+    private val placementComparator =
+        PlacementComparator(beyondBoundsLayoutDirection, layoutDirection, reverseLayout)
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters() = buildList {
+            for (beyondBoundsLayoutDirection in listOf(Left, Right, Above, Below, Before, After)) {
+                for (reverseLayout in listOf(false, true)) {
+                    for (layoutDirection in listOf(Ltr, Rtl)) {
+                        add(Param(beyondBoundsLayoutDirection, reverseLayout, layoutDirection))
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun onlyOneVisibleItemIsPlaced() {
+        // Arrange.
+        rule.setLazyContent(size = 10.toDp(), firstVisibleItem = 0) {
+            items(100) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(0)
+            assertThat(visibleItems).containsExactly(0)
+        }
+    }
+
+    @Test
+    fun onlyTwoVisibleItemsArePlaced() {
+        // Arrange.
+        rule.setLazyContent(size = 20.toDp(), firstVisibleItem = 0) {
+            items(100) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(0, 1)
+            assertThat(visibleItems).containsExactly(0, 1)
+        }
+    }
+
+    @Test
+    fun onlyThreeVisibleItemsArePlaced() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
+            items(100) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(0, 1, 2)
+            assertThat(visibleItems).containsExactly(0, 1, 2)
+        }
+    }
+
+    @Test
+    fun emptyLazyList_doesNotCrash() {
+        // Arrange.
+        var addItems by mutableStateOf(true)
+        lateinit var beyondBoundsLayoutRef: BeyondBoundsLayout
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
+            if (addItems) {
+                item {
+                    Box(
+                        Modifier.modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                    )
+                }
+            }
+        }
+        rule.runOnIdle {
+            beyondBoundsLayoutRef = beyondBoundsLayout!!
+            addItems = false
+        }
+
+        // Act.
+        val hasMoreContent = rule.runOnIdle {
+            beyondBoundsLayoutRef.layout(beyondBoundsLayoutDirection) {
+                hasMoreContent
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(hasMoreContent).isFalse()
+        }
+    }
+
+    @Test
+    fun oneExtraItemBeyondVisibleBounds() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(Modifier
+                    .size(10.toDp())
+                    .trackPlaced(5)
+                    .modifierLocalConsumer {
+                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                    }
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Assert that the beyond bounds items are present.
+                if (expectedExtraItemsBeforeVisibleBounds()) {
+                    assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
+                } else {
+                    assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
+                }
+                assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                assertThat(placedItems.values).isInOrder(placementComparator)
+
+                // Just return true so that we stop as soon as we run this once.
+                // This should result in one extra item being added.
+                true
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+    }
+
+    @Test
+    fun oneExtraItemBeyondVisibleBounds_multipleCells() {
+        val itemSize = 50
+        val itemSizeDp = itemSize.toDp()
+        // Arrange.
+        rule.setLazyContent(cells = 2, size = itemSizeDp * 3, firstVisibleItem = 10) {
+            // item | item  | x5
+            // item | local | x1
+            // item | item  | x5
+            items(11) { index ->
+                Box(
+                    Modifier
+                        .size(itemSizeDp)
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(Modifier
+                    .size(itemSizeDp)
+                    .trackPlaced(11)
+                    .modifierLocalConsumer {
+                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                    }
+                )
+            }
+            items(10) { index ->
+                Box(
+                    Modifier
+                        .size(itemSizeDp)
+                        .trackPlaced(index + 12)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Assert that the beyond bounds items are present.
+                if (expectedExtraItemsBeforeVisibleBounds()) {
+                    assertThat(placedItems.keys).containsExactly(9, 10, 11, 12, 13, 14, 15)
+                } else {
+                    assertThat(placedItems.keys).containsExactly(10, 11, 12, 13, 14, 15, 16)
+                }
+                assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
+
+                assertThat(placedItems.values).isInOrder(placementComparator)
+
+                // Just return true so that we stop as soon as we run this once.
+                // This should result in one extra item being added.
+                true
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(10, 11, 12, 13, 14, 15)
+            assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
+        }
+    }
+
+    @Test
+    fun twoExtraItemsBeyondVisibleBounds() {
+        // Arrange.
+        var extraItemCount = 2
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(5)
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                if (--extraItemCount > 0) {
+                    // Return null to continue the search.
+                    null
+                } else {
+                    // Assert that the beyond bounds items are present.
+                    if (expectedExtraItemsBeforeVisibleBounds()) {
+                        assertThat(placedItems.keys).containsExactly(3, 4, 5, 6, 7)
+                    } else {
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9)
+                    }
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                    assertThat(placedItems.values).isInOrder(placementComparator)
+
+                    // Return true to stop the search.
+                    true
+                }
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+    }
+
+    @Test
+    fun allBeyondBoundsItemsInSpecifiedDirection() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                        .trackPlaced(5)
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                if (hasMoreContent) {
+                    // Just return null so that we keep adding more items till we reach the end.
+                    null
+                } else {
+                    // Assert that the beyond bounds items are present.
+                    if (expectedExtraItemsBeforeVisibleBounds()) {
+                        assertThat(placedItems.keys).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
+                    } else {
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9, 10)
+                    }
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                    assertThat(placedItems.values).isInOrder(placementComparator)
+
+                    // Return true to end the search.
+                    true
+                }
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+        }
+    }
+
+    @Test
+    fun beyondBoundsLayoutRequest_inDirectionPerpendicularToLazyListOrientation() {
+        // Arrange.
+        var beyondBoundsLayoutCount = 0
+        rule.setLazyContentInPerpendicularDirection(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(5)
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+
+        // Act.
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                beyondBoundsLayoutCount++
+                when (beyondBoundsLayoutDirection) {
+                    Left, Right, Above, Below -> {
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
+                    }
+                    Before, After -> {
+                        if (expectedExtraItemsBeforeVisibleBounds()) {
+                            assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
+                            assertThat(visibleItems).containsExactly(5, 6, 7)
+                        } else {
+                            assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
+                            assertThat(visibleItems).containsExactly(5, 6, 7)
+                        }
+                    }
+                }
+                // Just return true so that we stop as soon as we run this once.
+                // This should result in one extra item being added.
+                true
+            }
+        }
+
+        rule.runOnIdle {
+            when (beyondBoundsLayoutDirection) {
+                Left, Right, Above, Below -> {
+                    assertThat(beyondBoundsLayoutCount).isEqualTo(0)
+                }
+                Before, After -> {
+                    assertThat(beyondBoundsLayoutCount).isEqualTo(1)
+
+                    // Assert that the beyond bounds items are removed.
+                    assertThat(placedItems.keys).containsExactly(5, 6, 7)
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+                }
+                else -> error("Unsupported BeyondBoundsLayoutDirection")
+            }
+        }
+    }
+
+    @Test
+    fun returningNullDoesNotCauseInfiniteLoop() {
+        // Arrange.
+        rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index)
+                )
+            }
+            item {
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
+                        .trackPlaced(5)
+                )
+            }
+            items(5) { index ->
+                Box(
+                    Modifier
+                        .size(10.toDp())
+                        .trackPlaced(index + 6)
+                )
+            }
+        }
+
+        // Act.
+        var count = 0
+        rule.runOnUiThread {
+            beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
+                // Assert that we don't keep iterating when there is no ending condition.
+                assertThat(count++).isLessThan(lazyGridState.layoutInfo.totalItemsCount)
+                // Always return null to continue the search.
+                null
+            }
+        }
+
+        // Assert that the beyond bounds items are removed.
+        rule.runOnIdle {
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+        }
+    }
+
+    private fun ComposeContentTestRule.setLazyContent(
+        size: Dp,
+        firstVisibleItem: Int,
+        cells: Int = 1,
+        content: TvLazyGridScope.() -> Unit
+    ) {
+        setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                lazyGridState = rememberTvLazyGridState(firstVisibleItem)
+                when (beyondBoundsLayoutDirection) {
+                    Left, Right, Before, After ->
+                        TvLazyHorizontalGrid(
+                            rows = TvGridCells.Fixed(cells),
+                            modifier = Modifier.size(size),
+                            state = lazyGridState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    Above, Below ->
+                        TvLazyVerticalGrid(
+                            columns = TvGridCells.Fixed(cells),
+                            modifier = Modifier.size(size),
+                            state = lazyGridState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    else -> unsupportedDirection()
+                }
+            }
+        }
+    }
+
+    private fun ComposeContentTestRule.setLazyContentInPerpendicularDirection(
+        size: Dp,
+        firstVisibleItem: Int,
+        content: TvLazyGridScope.() -> Unit
+    ) {
+        setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                lazyGridState = rememberTvLazyGridState(firstVisibleItem)
+                when (beyondBoundsLayoutDirection) {
+                    Left, Right, Before, After ->
+                        TvLazyVerticalGrid(
+                            columns = TvGridCells.Fixed(1),
+                            modifier = Modifier.size(size),
+                            state = lazyGridState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    Above, Below ->
+                        TvLazyHorizontalGrid(
+                            rows = TvGridCells.Fixed(1),
+                            modifier = Modifier.size(size),
+                            state = lazyGridState,
+                            reverseLayout = reverseLayout,
+                            content = content
+                        )
+                    else -> unsupportedDirection()
+                }
+            }
+        }
+    }
+
+    private fun Int.toDp(): Dp = with(rule.density) { toDp() }
+
+    private val visibleItems: List<Int>
+        get() = lazyGridState.layoutInfo.visibleItemsInfo.map { it.index }
+
+    private fun expectedExtraItemsBeforeVisibleBounds() = when (beyondBoundsLayoutDirection) {
+        Right -> if (layoutDirection == Ltr) reverseLayout else !reverseLayout
+        Left -> if (layoutDirection == Ltr) !reverseLayout else reverseLayout
+        Above -> !reverseLayout
+        Below -> reverseLayout
+        After -> false
+        Before -> true
+        else -> error("Unsupported BeyondBoundsDirection")
+    }
+
+    private fun unsupportedDirection(): Nothing = error(
+        "Lazy list does not support beyond bounds layout for the specified direction"
+    )
+
+    private fun Modifier.trackPlaced(index: Int): Modifier =
+        this then TrackPlacedElement(index, placedItems)
+}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
index 4544704..87f3c79 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.input.key.NativeKeyEvent
 import androidx.compose.ui.layout.BeyondBoundsLayout
 import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
@@ -41,6 +42,7 @@
 import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
+import androidx.compose.ui.layout.findRootCoordinates
 import androidx.compose.ui.modifier.modifierLocalConsumer
 import androidx.compose.ui.node.LayoutAwareModifierNode
 import androidx.compose.ui.node.ModifierNodeElement
@@ -84,9 +86,11 @@
     private val beyondBoundsLayoutDirection = param.beyondBoundsLayoutDirection
     private val reverseLayout = param.reverseLayout
     private val layoutDirection = param.layoutDirection
-    private val placedItems = mutableSetOf<Int>()
+    private val placedItems = sortedMapOf<Int, Rect>()
     private var beyondBoundsLayout: BeyondBoundsLayout? = null
     private lateinit var lazyListState: TvLazyListState
+    private val placementComparator =
+        PlacementComparator(beyondBoundsLayoutDirection, layoutDirection, reverseLayout)
 
     companion object {
         @JvmStatic
@@ -117,7 +121,7 @@
 
         // Assert.
         rule.runOnIdle {
-            assertThat(placedItems).containsExactly(0)
+            assertThat(placedItems.keys).containsExactly(0)
             assertThat(visibleItems).containsExactly(0)
         }
     }
@@ -137,7 +141,7 @@
 
         // Assert.
         rule.runOnIdle {
-            assertThat(placedItems).containsExactly(0, 1)
+            assertThat(placedItems.keys).containsExactly(0, 1)
             assertThat(visibleItems).containsExactly(0, 1)
         }
     }
@@ -157,7 +161,7 @@
 
         // Assert.
         rule.runOnIdle {
-            assertThat(placedItems).containsExactly(0, 1, 2)
+            assertThat(placedItems.keys).containsExactly(0, 1, 2)
             assertThat(visibleItems).containsExactly(0, 1, 2)
         }
     }
@@ -210,11 +214,11 @@
             item {
                 Box(
                     Modifier
-                    .size(10.toDp())
-                    .trackPlaced(5)
-                    .modifierLocalConsumer {
-                        beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
-                    }
+                        .size(10.toDp())
+                        .trackPlaced(5)
+                        .modifierLocalConsumer {
+                            beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+                        }
                 )
             }
             items(5) { index ->
@@ -231,12 +235,14 @@
             beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
                 // Assert that the beyond bounds items are present.
                 if (expectedExtraItemsBeforeVisibleBounds()) {
-                    assertThat(placedItems).containsExactly(4, 5, 6, 7)
-                    assertThat(visibleItems).containsExactly(5, 6, 7)
+                    assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
                 } else {
-                    assertThat(placedItems).containsExactly(5, 6, 7, 8)
-                    assertThat(visibleItems).containsExactly(5, 6, 7)
+                    assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
                 }
+                assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                assertThat(placedItems.values).isInOrder(placementComparator)
+
                 // Just return true so that we stop as soon as we run this once.
                 // This should result in one extra item being added.
                 true
@@ -245,7 +251,7 @@
 
         // Assert that the beyond bounds items are removed.
         rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
             assertThat(visibleItems).containsExactly(5, 6, 7)
         }
     }
@@ -290,12 +296,14 @@
                 } else {
                     // Assert that the beyond bounds items are present.
                     if (expectedExtraItemsBeforeVisibleBounds()) {
-                        assertThat(placedItems).containsExactly(3, 4, 5, 6, 7)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
+                        assertThat(placedItems.keys).containsExactly(3, 4, 5, 6, 7)
                     } else {
-                        assertThat(placedItems).containsExactly(5, 6, 7, 8, 9)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9)
                     }
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                    assertThat(placedItems.values).isInOrder(placementComparator)
+
                     // Return true to stop the search.
                     true
                 }
@@ -304,7 +312,7 @@
 
         // Assert that the beyond bounds items are removed.
         rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
             assertThat(visibleItems).containsExactly(5, 6, 7)
         }
     }
@@ -348,12 +356,14 @@
                 } else {
                     // Assert that the beyond bounds items are present.
                     if (expectedExtraItemsBeforeVisibleBounds()) {
-                        assertThat(placedItems).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
+                        assertThat(placedItems.keys).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
                     } else {
-                        assertThat(placedItems).containsExactly(5, 6, 7, 8, 9, 10)
-                        assertThat(visibleItems).containsExactly(5, 6, 7)
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9, 10)
                     }
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
+
+                    assertThat(placedItems.values).isInOrder(placementComparator)
+
                     // Return true to end the search.
                     true
                 }
@@ -362,7 +372,7 @@
 
         // Assert that the beyond bounds items are removed.
         rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
         }
     }
 
@@ -397,7 +407,7 @@
             }
         }
         rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
             assertThat(visibleItems).containsExactly(5, 6, 7)
         }
 
@@ -407,15 +417,15 @@
                 beyondBoundsLayoutCount++
                 when (beyondBoundsLayoutDirection) {
                     Left, Right, Above, Below -> {
-                        assertThat(placedItems).containsExactly(5, 6, 7)
+                        assertThat(placedItems.keys).containsExactly(5, 6, 7)
                         assertThat(visibleItems).containsExactly(5, 6, 7)
                     }
                     Before, After -> {
                         if (expectedExtraItemsBeforeVisibleBounds()) {
-                            assertThat(placedItems).containsExactly(4, 5, 6, 7)
+                            assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
                             assertThat(visibleItems).containsExactly(5, 6, 7)
                         } else {
-                            assertThat(placedItems).containsExactly(5, 6, 7, 8)
+                            assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
                             assertThat(visibleItems).containsExactly(5, 6, 7)
                         }
                     }
@@ -435,7 +445,7 @@
                     assertThat(beyondBoundsLayoutCount).isEqualTo(1)
 
                     // Assert that the beyond bounds items are removed.
-                    assertThat(placedItems).containsExactly(5, 6, 7)
+                    assertThat(placedItems.keys).containsExactly(5, 6, 7)
                     assertThat(visibleItems).containsExactly(5, 6, 7)
                 }
                 else -> error("Unsupported BeyondBoundsLayoutDirection")
@@ -486,7 +496,7 @@
 
         // Assert that the beyond bounds items are removed.
         rule.runOnIdle {
-            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(placedItems.keys).containsExactly(5, 6, 7)
             assertThat(visibleItems).containsExactly(5, 6, 7)
         }
     }
@@ -604,35 +614,60 @@
     )
 
     private fun Modifier.trackPlaced(index: Int): Modifier =
-        this then TrackPlacedElement(placedItems, index)
+        this then TrackPlacedElement(index, placedItems)
 }
 
 internal data class TrackPlacedElement(
-    var placedItems: MutableSet<Int>,
-    var index: Int
+    var index: Int,
+    var placedItems: MutableMap<Int, Rect>
 ) : ModifierNodeElement<TrackPlacedNode>() {
-    override fun create() = TrackPlacedNode(placedItems, index)
+    override fun create() = TrackPlacedNode(index, placedItems)
 
     override fun update(node: TrackPlacedNode) {
-        node.placedItems = placedItems
         node.index = index
+        node.placedItems = placedItems
     }
 
     override fun InspectorInfo.inspectableProperties() {
         name = "trackPlaced"
         properties["index"] = index
+        properties["placedItems"] = placedItems
     }
 }
 
 internal class TrackPlacedNode(
-    var placedItems: MutableSet<Int>,
-    var index: Int
+    var index: Int,
+    var placedItems: MutableMap<Int, Rect>
 ) : LayoutAwareModifierNode, Modifier.Node() {
     override fun onPlaced(coordinates: LayoutCoordinates) {
-        placedItems += index
+        placedItems[index] =
+            coordinates.findRootCoordinates().localBoundingBoxOf(coordinates, false)
     }
 
     override fun onDetach() {
-        placedItems -= index
+        placedItems.remove(index)
+    }
+}
+
+internal class PlacementComparator(
+    val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
+    val layoutDirection: LayoutDirection,
+    val reverseLayout: Boolean
+) : Comparator<Rect> {
+    private fun itemsInReverseOrder() = when (beyondBoundsLayoutDirection) {
+        Above, Below -> reverseLayout
+        else -> if (layoutDirection == Ltr) reverseLayout else !reverseLayout
+    }
+
+    private fun compareOffset(o1: Float, o2: Float): Int {
+        return if (itemsInReverseOrder()) o2.compareTo(o1) else o1.compareTo(o2)
+    }
+
+    override fun compare(o1: Rect?, o2: Rect?): Int {
+        if (o1 == null || o2 == null) return 0
+        return when (beyondBoundsLayoutDirection) {
+            Above, Below -> compareOffset(o1.top, o2.top)
+            else -> compareOffset(o1.left, o2.left)
+        }
     }
 }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
index f602de1..6431eaf 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
@@ -28,6 +28,7 @@
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachReversed
 import androidx.compose.ui.util.fastSumBy
 import androidx.tv.foundation.lazy.layout.LazyLayoutKeyIndexMap
 import androidx.tv.foundation.lazy.list.fastFilter
@@ -397,7 +398,7 @@
     } else {
         var currentMainAxis = firstLineScrollOffset
 
-        itemsBefore.fastForEach {
+        itemsBefore.fastForEachReversed {
             currentMainAxis -= it.mainAxisSizeWithSpacings
             it.position(currentMainAxis, 0, layoutWidth, layoutHeight)
             positionedItems.add(it)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
index e5396f5..5908cf4 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
@@ -29,6 +29,7 @@
 import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachReversed
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.contract
 import kotlin.math.abs
@@ -459,7 +460,7 @@
         list.add(measuredItemProvider.getAndMeasure(i))
     }
 
-    pinnedItems.fastForEach { index ->
+    pinnedItems.fastForEachReversed { index ->
         if (index < start) {
             if (list == null) list = mutableListOf()
             list?.add(measuredItemProvider.getAndMeasure(index))
diff --git a/tv/tv-material/api/api_lint.ignore b/tv/tv-material/api/api_lint.ignore
new file mode 100644
index 0000000..5954166
--- /dev/null
+++ b/tv/tv-material/api/api_lint.ignore
@@ -0,0 +1,4 @@
+// Baseline format: 1.0
+
+GetterSetterNames: field NavigationDrawerScope.doesNavigationDrawerHaveFocus:
+    Invalid name for boolean property `doesNavigationDrawerHaveFocus`. Should start with one of `has`, `can`, `should`, `is`.
\ No newline at end of file
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 6fc7ea7..2ccd66b 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -640,6 +640,40 @@
     property public final long selectedContentColor;
   }
 
+  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemDefaults {
+    method @androidx.compose.runtime.Composable public void TrailingBadge(String text, optional long containerColor, optional long contentColor);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.NavigationDrawerItemBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border selectedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedSelectedBorder, optional androidx.tv.material3.Border focusedDisabledBorder, optional androidx.tv.material3.Border pressedSelectedBorder);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.NavigationDrawerItemColors colors(optional long containerColor, optional long contentColor, optional long inactiveContentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long selectedContainerColor, optional long selectedContentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledInactiveContentColor, optional long focusedSelectedContainerColor, optional long focusedSelectedContentColor, optional long pressedSelectedContainerColor, optional long pressedSelectedContentColor);
+    method public float getCollapsedDrawerItemWidth();
+    method public float getContainerHeightOneLine();
+    method public float getContainerHeightTwoLine();
+    method public androidx.compose.animation.EnterTransition getContentAnimationEnter();
+    method public androidx.compose.animation.ExitTransition getContentAnimationExit();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Border getDefaultBorder();
+    method public float getExpandedDrawerItemWidth();
+    method public float getIconSize();
+    method public float getNavigationDrawerItemElevation();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getTrailingBadgeContainerColor();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getTrailingBadgeContentColor();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.TextStyle getTrailingBadgeTextStyle();
+    method public androidx.tv.material3.NavigationDrawerItemGlow glow(optional androidx.tv.material3.Glow glow, optional androidx.tv.material3.Glow focusedGlow, optional androidx.tv.material3.Glow pressedGlow, optional androidx.tv.material3.Glow selectedGlow, optional androidx.tv.material3.Glow focusedSelectedGlow, optional androidx.tv.material3.Glow pressedSelectedGlow);
+    method public androidx.tv.material3.NavigationDrawerItemScale scale(optional @FloatRange(from=0.0) float scale, optional @FloatRange(from=0.0) float focusedScale, optional @FloatRange(from=0.0) float pressedScale, optional @FloatRange(from=0.0) float selectedScale, optional @FloatRange(from=0.0) float disabledScale, optional @FloatRange(from=0.0) float focusedSelectedScale, optional @FloatRange(from=0.0) float focusedDisabledScale, optional @FloatRange(from=0.0) float pressedSelectedScale);
+    method public androidx.tv.material3.NavigationDrawerItemShape shape(optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.graphics.Shape focusedShape, optional androidx.compose.ui.graphics.Shape pressedShape, optional androidx.compose.ui.graphics.Shape selectedShape, optional androidx.compose.ui.graphics.Shape disabledShape, optional androidx.compose.ui.graphics.Shape focusedSelectedShape, optional androidx.compose.ui.graphics.Shape focusedDisabledShape, optional androidx.compose.ui.graphics.Shape pressedSelectedShape);
+    property public final float CollapsedDrawerItemWidth;
+    property public final float ContainerHeightOneLine;
+    property public final float ContainerHeightTwoLine;
+    property public final androidx.compose.animation.EnterTransition ContentAnimationEnter;
+    property public final androidx.compose.animation.ExitTransition ContentAnimationExit;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.tv.material3.Border DefaultBorder;
+    property public final float ExpandedDrawerItemWidth;
+    property public final float IconSize;
+    property public final float NavigationDrawerItemElevation;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long TrailingBadgeContainerColor;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long TrailingBadgeContentColor;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.ui.text.TextStyle TrailingBadgeTextStyle;
+    field public static final androidx.tv.material3.NavigationDrawerItemDefaults INSTANCE;
+  }
+
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemGlow {
     ctor public NavigationDrawerItemGlow(androidx.tv.material3.Glow glow, androidx.tv.material3.Glow focusedGlow, androidx.tv.material3.Glow pressedGlow, androidx.tv.material3.Glow selectedGlow, androidx.tv.material3.Glow focusedSelectedGlow, androidx.tv.material3.Glow pressedSelectedGlow);
     method public androidx.tv.material3.Glow getFocusedGlow();
@@ -656,6 +690,10 @@
     property public final androidx.tv.material3.Glow selectedGlow;
   }
 
+  public final class NavigationDrawerItemKt {
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawerItem(androidx.tv.material3.NavigationDrawerScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> leadingContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.NavigationDrawerItemShape shape, optional androidx.tv.material3.NavigationDrawerItemColors colors, optional androidx.tv.material3.NavigationDrawerItemScale scale, optional androidx.tv.material3.NavigationDrawerItemBorder border, optional androidx.tv.material3.NavigationDrawerItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemScale {
     ctor public NavigationDrawerItemScale(@FloatRange(from=0.0) float scale, @FloatRange(from=0.0) float focusedScale, @FloatRange(from=0.0) float pressedScale, @FloatRange(from=0.0) float selectedScale, @FloatRange(from=0.0) float disabledScale, @FloatRange(from=0.0) float focusedSelectedScale, @FloatRange(from=0.0) float focusedDisabledScale, @FloatRange(from=0.0) float pressedSelectedScale);
     method public float getDisabledScale();
@@ -703,14 +741,14 @@
   }
 
   public final class NavigationDrawerKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ModalNavigationDrawer(kotlin.jvm.functions.Function1<? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, optional androidx.compose.ui.graphics.Brush scrimBrush, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawer(kotlin.jvm.functions.Function1<? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ModalNavigationDrawer(kotlin.jvm.functions.Function2<? super androidx.tv.material3.NavigationDrawerScope,? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, optional androidx.compose.ui.graphics.Brush scrimBrush, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawer(kotlin.jvm.functions.Function2<? super androidx.tv.material3.NavigationDrawerScope,? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.DrawerState rememberDrawerState(androidx.tv.material3.DrawerValue initialValue);
   }
 
   @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public interface NavigationDrawerScope {
-    method public boolean isActivated();
-    property public abstract boolean isActivated;
+    method public boolean getDoesNavigationDrawerHaveFocus();
+    property public abstract boolean doesNavigationDrawerHaveFocus;
   }
 
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NonInteractiveSurfaceColors {
@@ -876,9 +914,9 @@
   }
 
   @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabRowDefaults {
-    method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, boolean isActivated, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, boolean doesTabRowHaveFocus, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
     method @androidx.compose.runtime.Composable public void TabSeparator();
-    method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, boolean isActivated, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, boolean doesTabRowHaveFocus, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
     method @androidx.compose.runtime.Composable public long contentColor();
     method public long getContainerColor();
     property public final long ContainerColor;
@@ -890,8 +928,8 @@
   }
 
   @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public interface TabRowScope {
-    method public boolean isActivated();
-    property public abstract boolean isActivated;
+    method public boolean getHasFocus();
+    property public abstract boolean hasFocus;
   }
 
   public final class TextKt {
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 6fc7ea7..2ccd66b 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -640,6 +640,40 @@
     property public final long selectedContentColor;
   }
 
+  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemDefaults {
+    method @androidx.compose.runtime.Composable public void TrailingBadge(String text, optional long containerColor, optional long contentColor);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.NavigationDrawerItemBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border selectedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedSelectedBorder, optional androidx.tv.material3.Border focusedDisabledBorder, optional androidx.tv.material3.Border pressedSelectedBorder);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.NavigationDrawerItemColors colors(optional long containerColor, optional long contentColor, optional long inactiveContentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long selectedContainerColor, optional long selectedContentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledInactiveContentColor, optional long focusedSelectedContainerColor, optional long focusedSelectedContentColor, optional long pressedSelectedContainerColor, optional long pressedSelectedContentColor);
+    method public float getCollapsedDrawerItemWidth();
+    method public float getContainerHeightOneLine();
+    method public float getContainerHeightTwoLine();
+    method public androidx.compose.animation.EnterTransition getContentAnimationEnter();
+    method public androidx.compose.animation.ExitTransition getContentAnimationExit();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Border getDefaultBorder();
+    method public float getExpandedDrawerItemWidth();
+    method public float getIconSize();
+    method public float getNavigationDrawerItemElevation();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getTrailingBadgeContainerColor();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getTrailingBadgeContentColor();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.TextStyle getTrailingBadgeTextStyle();
+    method public androidx.tv.material3.NavigationDrawerItemGlow glow(optional androidx.tv.material3.Glow glow, optional androidx.tv.material3.Glow focusedGlow, optional androidx.tv.material3.Glow pressedGlow, optional androidx.tv.material3.Glow selectedGlow, optional androidx.tv.material3.Glow focusedSelectedGlow, optional androidx.tv.material3.Glow pressedSelectedGlow);
+    method public androidx.tv.material3.NavigationDrawerItemScale scale(optional @FloatRange(from=0.0) float scale, optional @FloatRange(from=0.0) float focusedScale, optional @FloatRange(from=0.0) float pressedScale, optional @FloatRange(from=0.0) float selectedScale, optional @FloatRange(from=0.0) float disabledScale, optional @FloatRange(from=0.0) float focusedSelectedScale, optional @FloatRange(from=0.0) float focusedDisabledScale, optional @FloatRange(from=0.0) float pressedSelectedScale);
+    method public androidx.tv.material3.NavigationDrawerItemShape shape(optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.graphics.Shape focusedShape, optional androidx.compose.ui.graphics.Shape pressedShape, optional androidx.compose.ui.graphics.Shape selectedShape, optional androidx.compose.ui.graphics.Shape disabledShape, optional androidx.compose.ui.graphics.Shape focusedSelectedShape, optional androidx.compose.ui.graphics.Shape focusedDisabledShape, optional androidx.compose.ui.graphics.Shape pressedSelectedShape);
+    property public final float CollapsedDrawerItemWidth;
+    property public final float ContainerHeightOneLine;
+    property public final float ContainerHeightTwoLine;
+    property public final androidx.compose.animation.EnterTransition ContentAnimationEnter;
+    property public final androidx.compose.animation.ExitTransition ContentAnimationExit;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.tv.material3.Border DefaultBorder;
+    property public final float ExpandedDrawerItemWidth;
+    property public final float IconSize;
+    property public final float NavigationDrawerItemElevation;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long TrailingBadgeContainerColor;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long TrailingBadgeContentColor;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.ui.text.TextStyle TrailingBadgeTextStyle;
+    field public static final androidx.tv.material3.NavigationDrawerItemDefaults INSTANCE;
+  }
+
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemGlow {
     ctor public NavigationDrawerItemGlow(androidx.tv.material3.Glow glow, androidx.tv.material3.Glow focusedGlow, androidx.tv.material3.Glow pressedGlow, androidx.tv.material3.Glow selectedGlow, androidx.tv.material3.Glow focusedSelectedGlow, androidx.tv.material3.Glow pressedSelectedGlow);
     method public androidx.tv.material3.Glow getFocusedGlow();
@@ -656,6 +690,10 @@
     property public final androidx.tv.material3.Glow selectedGlow;
   }
 
+  public final class NavigationDrawerItemKt {
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawerItem(androidx.tv.material3.NavigationDrawerScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> leadingContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.NavigationDrawerItemShape shape, optional androidx.tv.material3.NavigationDrawerItemColors colors, optional androidx.tv.material3.NavigationDrawerItemScale scale, optional androidx.tv.material3.NavigationDrawerItemBorder border, optional androidx.tv.material3.NavigationDrawerItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemScale {
     ctor public NavigationDrawerItemScale(@FloatRange(from=0.0) float scale, @FloatRange(from=0.0) float focusedScale, @FloatRange(from=0.0) float pressedScale, @FloatRange(from=0.0) float selectedScale, @FloatRange(from=0.0) float disabledScale, @FloatRange(from=0.0) float focusedSelectedScale, @FloatRange(from=0.0) float focusedDisabledScale, @FloatRange(from=0.0) float pressedSelectedScale);
     method public float getDisabledScale();
@@ -703,14 +741,14 @@
   }
 
   public final class NavigationDrawerKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ModalNavigationDrawer(kotlin.jvm.functions.Function1<? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, optional androidx.compose.ui.graphics.Brush scrimBrush, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawer(kotlin.jvm.functions.Function1<? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ModalNavigationDrawer(kotlin.jvm.functions.Function2<? super androidx.tv.material3.NavigationDrawerScope,? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, optional androidx.compose.ui.graphics.Brush scrimBrush, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawer(kotlin.jvm.functions.Function2<? super androidx.tv.material3.NavigationDrawerScope,? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.DrawerState rememberDrawerState(androidx.tv.material3.DrawerValue initialValue);
   }
 
   @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public interface NavigationDrawerScope {
-    method public boolean isActivated();
-    property public abstract boolean isActivated;
+    method public boolean getDoesNavigationDrawerHaveFocus();
+    property public abstract boolean doesNavigationDrawerHaveFocus;
   }
 
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NonInteractiveSurfaceColors {
@@ -876,9 +914,9 @@
   }
 
   @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabRowDefaults {
-    method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, boolean isActivated, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, boolean doesTabRowHaveFocus, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
     method @androidx.compose.runtime.Composable public void TabSeparator();
-    method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, boolean isActivated, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, boolean doesTabRowHaveFocus, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
     method @androidx.compose.runtime.Composable public long contentColor();
     method public long getContainerColor();
     property public final long ContainerColor;
@@ -890,8 +928,8 @@
   }
 
   @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public interface TabRowScope {
-    method public boolean isActivated();
-    property public abstract boolean isActivated;
+    method public boolean getHasFocus();
+    property public abstract boolean hasFocus;
   }
 
   public final class TextKt {
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 2edb6b7..7526ff4 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -43,6 +43,7 @@
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
     androidTestImplementation(project(":compose:test-utils"))
     androidTestImplementation(project(":test:screenshot:screenshot"))
+    androidTestImplementation(project(":compose:material:material-icons-core"))
     androidTestImplementation(libs.testRunner)
 
     samples(project(":tv:tv-samples"))
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
index 5b302d8..c6569aa 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
@@ -24,6 +24,7 @@
 import androidx.compose.animation.slideOutHorizontally
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
+import androidx.compose.foundation.focusGroup
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -44,14 +45,17 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusProperties
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.key.NativeKeyEvent
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsFocused
 import androidx.compose.ui.test.assertIsNotFocused
@@ -63,6 +67,7 @@
 import androidx.compose.ui.test.onParent
 import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.test.performKeyPress
+import androidx.compose.ui.test.performSemanticsAction
 import androidx.compose.ui.test.requestFocus
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
@@ -828,6 +833,80 @@
         // slide should have changed.
         rule.onNodeWithText("Left Button 2").assertIsFocused()
     }
+
+    @Test
+    fun carousel_manualScrollingLtr_loopsAroundWhenNoAdjacentFocusableItemsArePresent() {
+        rule.setContent {
+            // No AutoScrolling
+            SampleCarousel(timeToDisplayItemMillis = Long.MAX_VALUE, itemCount = 3) {
+                Row {
+                    SampleButton("Button-$it")
+                }
+            }
+        }
+
+        rule.onNodeWithText("Button-0")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // Carousel should loop around the edges if there are no adjacent focusable items in the
+        // direction of dpad key press (left)
+        performKeyPress(KeyEvent.KEYCODE_DPAD_LEFT)
+        rule.onNodeWithText("Button-2").assertIsFocused()
+
+        // Carousel should loop around the edges if there are no adjacent focusable items in the
+        // direction of dpad key press (right)
+        performKeyPress(KeyEvent.KEYCODE_DPAD_RIGHT)
+        rule.onNodeWithText("Button-0").assertIsFocused()
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun carousel_manualScrollingLtr_focusMovesToAdjacentItemsOutsideCarousel() {
+        rule.setContent {
+            val focusRequester = remember { FocusRequester() }
+            Row {
+                Column(
+                    Modifier
+                        .focusProperties {
+                            enter = {
+                                focusRequester.requestFocus()
+                                FocusRequester.Cancel
+                            }
+                        }
+                        .focusGroup()
+                ) {
+                    repeat(3) {
+                        Box(
+                            modifier = Modifier
+                                .size(10.dp)
+                                .testTag("Item-$it")
+                                .then(
+                                    if (it == 0) Modifier.focusRequester(focusRequester)
+                                    else Modifier
+                                )
+                                .focusable()
+                        )
+                    }
+                }
+                // No AutoScrolling
+                Box(Modifier.weight(1f)) {
+                    SampleCarousel(timeToDisplayItemMillis = Long.MAX_VALUE, itemCount = 2) {
+                        Row {
+                            SampleButton("Button-$it")
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithText("Button-0")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // Focus should exit Carousel if there are any adjacent focusable items in the direction
+        // of dpad key press (left)
+        performKeyPress(KeyEvent.KEYCODE_DPAD_LEFT)
+        rule.onNodeWithTag("Item-0").assertIsFocused()
+    }
 }
 
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -861,7 +940,6 @@
     )
 }
 
-@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
 @Composable
 private fun AnimatedContentScope.SampleCarouselItem(
     index: Int,
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/DenseListItemScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/DenseListItemScreenshotTest.kt
index 5c64b89..245ee20 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/DenseListItemScreenshotTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/DenseListItemScreenshotTest.kt
@@ -24,8 +24,8 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
 import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material.icons.filled.KeyboardArrowRight
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -371,9 +371,8 @@
                         )
                     },
                     trailingContent = {
-                        @Suppress("DEPRECATION")
                         Icon(
-                            imageVector = Icons.Filled.KeyboardArrowRight,
+                            imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
                             contentDescription = null,
                             modifier = Modifier.size(ListItemDefaults.IconSizeDense)
                         )
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/ListItemScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/ListItemScreenshotTest.kt
index 7948021..97c5ecf 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/ListItemScreenshotTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/ListItemScreenshotTest.kt
@@ -24,8 +24,8 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
 import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material.icons.filled.KeyboardArrowRight
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -380,9 +380,8 @@
                         )
                     },
                     trailingContent = {
-                        @Suppress("DEPRECATION")
                         Icon(
-                            imageVector = Icons.Filled.KeyboardArrowRight,
+                            imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
                             contentDescription = null,
                             modifier = Modifier.size(ListItemDefaults.IconSize)
                         )
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/ModalNavigationDrawerTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/ModalNavigationDrawerTest.kt
index a522dc83..09a3c0f 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/ModalNavigationDrawerTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/ModalNavigationDrawerTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.tv.material3
 
+import android.os.Build
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.focusable
@@ -23,6 +24,7 @@
 import androidx.compose.foundation.layout.Row
 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.layout.width
 import androidx.compose.foundation.text.BasicText
@@ -31,6 +33,8 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertContainsColor
+import androidx.compose.testutils.assertDoesNotContainColor
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
@@ -38,6 +42,7 @@
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
@@ -49,7 +54,9 @@
 import androidx.compose.ui.test.assertIsEqualTo
 import androidx.compose.ui.test.assertIsFocused
 import androidx.compose.ui.test.assertIsNotFocused
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onAllNodesWithText
@@ -62,6 +69,7 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.toSize
+import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
 import org.junit.Rule
 import org.junit.Test
@@ -143,7 +151,7 @@
                             text = if (it == DrawerValue.Open) "Opened" else "Closed"
                         )
                     }) {
-                    Box(modifier = Modifier.focusable()) {
+                    Box(modifier = Modifier.padding(start = 100.dp).focusable()) {
                         BasicText("Button")
                     }
                 }
@@ -154,6 +162,7 @@
         }
         rule.onAllNodesWithText("Opened").assertAnyAreDisplayed()
         rule.onRoot().performKeyInput { pressKey(Key.DirectionRight) }
+
         rule.onAllNodesWithText("Closed").assertAnyAreDisplayed()
     }
 
@@ -182,6 +191,7 @@
                     }) {
                     Box(
                         modifier = Modifier
+                            .padding(start = 100.dp)
                             .focusRequester(buttonFocusRequester)
                             .focusable()
                     ) {
@@ -356,6 +366,79 @@
         rule.onNodeWithTag("box-container").assertIsFocused()
     }
 
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun modalNavigationDrawer_onOpen_scrimIsDrawnAboveContent() {
+        val backgroundContentColor = Color.Blue
+        val scrimColor = Color.Red
+        rule.setContent {
+            ModalNavigationDrawer(
+                drawerState = remember { DrawerState(DrawerValue.Open) },
+                drawerContent = {
+                    BasicText(text = if (it == DrawerValue.Open) "Opened" else "Closed")
+                },
+                scrimBrush = SolidColor(scrimColor)
+            ) { Box(Modifier.fillMaxSize().background(backgroundContentColor)) }
+        }
+
+        // the image should show only scrim color and no background content color
+        rule.onRoot().captureToImage().assertContainsColor(scrimColor)
+        rule.onRoot().captureToImage().assertDoesNotContainColor(backgroundContentColor)
+    }
+
+    @Test
+    fun modalNavigationDrawer_drawerOpen_backgroundContentShouldStartWithoutPadding() {
+        val backgroundContentColor = Color.Blue
+        rule.setContent {
+            ModalNavigationDrawer(
+                drawerState = remember { DrawerState(DrawerValue.Open) },
+                drawerContent = {
+                    BasicText(text = if (it == DrawerValue.Open) "Opened" else "Closed")
+                }
+            ) {
+                Box(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .background(backgroundContentColor)
+                        .testTag("background")
+                ) {
+                    Box(modifier = Modifier.padding(start = 100.dp).focusable()) {
+                        BasicText("Button")
+                    }
+                }
+            }
+        }
+
+        // the image should show only scrim color and no background content color
+        rule.onNodeWithTag("background").assertLeftPositionInRootIsEqualTo(0.dp)
+    }
+
+    fun modalNavigationDrawer_drawerClosed_backgroundContentShouldStartWithoutPadding() {
+        val backgroundContentColor = Color.Blue
+        rule.setContent {
+            ModalNavigationDrawer(
+                drawerState = remember { DrawerState(DrawerValue.Closed) },
+                drawerContent = {
+                    BasicText(text = if (it == DrawerValue.Open) "Opened" else "Closed")
+                }
+            ) {
+                Box(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .background(backgroundContentColor)
+                        .testTag("background")
+                ) {
+                    Box(modifier = Modifier.padding(start = 100.dp).focusable()) {
+                        BasicText("Button")
+                    }
+                }
+            }
+        }
+
+        // the image should show only scrim color and no background content color
+        rule.onNodeWithTag("background").assertLeftPositionInRootIsEqualTo(0.dp)
+    }
+
     private fun SemanticsNodeInteractionCollection.assertAnyAreDisplayed() {
         val result = (0 until fetchSemanticsNodes().size).map { get(it) }.any {
             try {
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemScreenshotTest.kt
new file mode 100644
index 0000000..e98dd07
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemScreenshotTest.kt
@@ -0,0 +1,379 @@
+/*
+ * 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.tv.material3
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalTvMaterial3Api::class)
+class NavigationDrawerItemScreenshotTest(private val scheme: ColorSchemeWrapper) {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(TV_GOLDEN_MATERIAL3)
+
+    val wrapperModifier = Modifier
+        .testTag(NavigationDrawerItemWrapperTag)
+        .background(scheme.colorScheme.surface)
+        .padding(20.dp)
+
+    @Test
+    fun navigationDrawerItem_customColor() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    colors = NavigationDrawerItemDefaults.colors(containerColor = Color.Red)
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_customColor")
+    }
+
+    @Test
+    fun navigationDrawerItem_oneLine() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_oneLine")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine_focused() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemWrapperTag)
+            .onChild()
+            .requestFocus()
+        rule.waitForIdle()
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_focused")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine_disabled() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    enabled = false,
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_disabled")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine_focusedDisabled() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    enabled = false,
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemWrapperTag)
+            .onChild()
+            .requestFocus()
+        rule.waitForIdle()
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_focusedDisabled")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine_selected() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = true,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_selected")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine_focusedSelected() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = true,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemWrapperTag)
+            .onChild()
+            .requestFocus()
+        rule.waitForIdle()
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_focusedSelected")
+    }
+
+    @Test
+    fun navigationDrawerItem_inactive() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope(false) {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_inactive")
+    }
+
+    @Test
+    fun navigationDrawerItem_inactive_selected() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope(false) {
+                NavigationDrawerItem(
+                    selected = true,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_inactive_selected")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine_withTrailingContent() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") },
+                    trailingContent = {
+                        NavigationDrawerItemDefaults.TrailingBadge("NEW")
+                    }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_withTrailingContent")
+    }
+
+    private fun assertAgainstGolden(goldenName: String) {
+        rule.onNodeWithTag(NavigationDrawerItemWrapperTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, goldenName)
+    }
+
+    // Provide the ColorScheme and their name parameter in a ColorSchemeWrapper.
+    // This makes sure that the default method name and the initial Scuba image generated
+    // name is as expected.
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun parameters() = arrayOf(
+            ColorSchemeWrapper("lightTheme", lightColorScheme()),
+            ColorSchemeWrapper("darkTheme", darkColorScheme()),
+        )
+    }
+
+    class ColorSchemeWrapper constructor(val name: String, val colorScheme: ColorScheme) {
+        override fun toString(): String {
+            return name
+        }
+    }
+
+    @Composable
+    private fun DrawerScope(
+        doesNavigationDrawerHaveFocus: Boolean = true,
+        content: @Composable NavigationDrawerScope.() -> Unit
+    ) {
+        Box(wrapperModifier) {
+            NavigationDrawerScopeImpl(doesNavigationDrawerHaveFocus).apply {
+                content()
+            }
+        }
+    }
+}
+
+private const val NavigationDrawerItemWrapperTag = "navigationDrawerItem_wrapper"
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemTest.kt
new file mode 100644
index 0000000..74d9ee4
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemTest.kt
@@ -0,0 +1,443 @@
+/*
+ * 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.tv.material3
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+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.input.key.Key
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsEqualTo
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.width
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(
+    ExperimentalTestApi::class,
+    ExperimentalTvMaterial3Api::class
+)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class NavigationDrawerItemTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun navigationDrawerItem_findByTagAndClick() {
+        var counter = 0
+        val onClick: () -> Unit = { ++counter }
+
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = onClick,
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .requestFocus()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        rule.runOnIdle {
+            Truth.assertThat(counter).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun navigationDrawerItem_clickIsIndependentBetweenItems() {
+        var openItemClickCounter = 0
+        val openItemOnClick: () -> Unit = { ++openItemClickCounter }
+        val openItemTag = "OpenItem"
+
+        var closeItemClickCounter = 0
+        val closeItemOnClick: () -> Unit = { ++closeItemClickCounter }
+        val closeItemTag = "CloseItem"
+
+        rule.setContent {
+            DrawerScope {
+                Column {
+                    NavigationDrawerItem(
+                        selected = false,
+                        onClick = openItemOnClick,
+                        leadingContent = { },
+                        modifier = Modifier.testTag(openItemTag),
+                    ) {
+                        Text(text = "Test Text")
+                    }
+                    NavigationDrawerItem(
+                        selected = false,
+                        onClick = closeItemOnClick,
+                        leadingContent = { },
+                        modifier = Modifier.testTag(closeItemTag),
+                    ) {
+                        Text(text = "Test Text")
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(openItemTag)
+            .requestFocus()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+
+        rule.runOnIdle {
+            Truth.assertThat(openItemClickCounter).isEqualTo(1)
+            Truth.assertThat(closeItemClickCounter).isEqualTo(0)
+        }
+
+        rule.onNodeWithTag(closeItemTag)
+            .requestFocus()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+
+        rule.runOnIdle {
+            Truth.assertThat(openItemClickCounter).isEqualTo(1)
+            Truth.assertThat(closeItemClickCounter).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun navigationDrawerItem_longClickAction() {
+        var counter = 0
+        val onLongClick: () -> Unit = { ++counter }
+
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = { },
+                    onLongClick = onLongClick,
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .requestFocus()
+            .performLongKeyPress(rule, Key.DirectionCenter)
+        rule.runOnIdle {
+            Truth.assertThat(counter).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .requestFocus()
+            .performLongKeyPress(rule, Key.DirectionCenter, count = 2)
+        rule.runOnIdle {
+            Truth.assertThat(counter).isEqualTo(3)
+        }
+    }
+
+    @Test
+    fun navigationDrawerItem_findByTagAndStateChangeCheck() {
+        var checkedState by mutableStateOf(true)
+        val onClick: () -> Unit = { checkedState = !checkedState }
+
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = checkedState,
+                    onClick = onClick,
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .requestFocus()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        rule.runOnIdle {
+            Truth.assertThat(!checkedState)
+        }
+    }
+
+    @Test
+    fun navigationDrawerItem_trailingContentPadding() {
+        val testTrailingContentTag = "TrailingIconTag"
+
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    trailingContent = {
+                        Box(
+                            modifier = Modifier
+                                .size(NavigationDrawerItemDefaults.IconSize)
+                                .background(Color.Red)
+                                .testTag(testTrailingContentTag)
+                        )
+                    },
+                    leadingContent = { },
+                    modifier = Modifier
+                        .testTag(NavigationDrawerItemTag)
+                        .border(1.dp, Color.Blue),
+                ) {
+                    Text(
+                        text = "Test Text",
+                        modifier = Modifier
+                            .testTag(NavigationDrawerItemTextTag)
+                            .fillMaxWidth()
+                    )
+                }
+            }
+        }
+
+        rule.waitForIdle()
+
+        val itemBounds = rule.onNodeWithTag(NavigationDrawerItemTag).getUnclippedBoundsInRoot()
+        val textBounds = rule.onNodeWithTag(
+            NavigationDrawerItemTextTag,
+            useUnmergedTree = true
+        ).getUnclippedBoundsInRoot()
+        val trailingContentBounds = rule
+            .onNodeWithTag(testTrailingContentTag, useUnmergedTree = true)
+            .getUnclippedBoundsInRoot()
+
+        (itemBounds.bottom - trailingContentBounds.bottom).assertIsEqualTo(
+            16.dp,
+            "padding between the bottom of the trailing content and the bottom of the nav " +
+                "drawer item"
+        )
+
+        (itemBounds.right - trailingContentBounds.right).assertIsEqualTo(
+            16.dp,
+            "padding between the end of the trailing content and the end of the nav drawer item"
+        )
+
+        (trailingContentBounds.left - textBounds.right).assertIsEqualTo(
+            8.dp,
+            "padding between the start of the trailing content and the end of the text."
+        )
+    }
+
+    @Test
+    fun navigationDrawerItem_semantics() {
+        var selected by mutableStateOf(false)
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = selected,
+                    onClick = { selected = !selected },
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Selected))
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Selected, false))
+            .requestFocus()
+            .assertIsEnabled()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Selected, true))
+        Truth.assertThat(selected).isEqualTo(true)
+    }
+
+    @Test
+    fun navigationDrawerItem_longClickSemantics() {
+        var selected by mutableStateOf(false)
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = selected,
+                    onClick = {},
+                    onLongClick = { selected = !selected },
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.OnLongClick))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Selected))
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Selected, false))
+            .requestFocus()
+            .assertIsEnabled()
+            .performLongKeyPress(rule, Key.DirectionCenter)
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Selected, true))
+        Truth.assertThat(selected).isEqualTo(true)
+    }
+
+    @Test
+    fun navigationDrawerItem_disabledSemantics() {
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    enabled = false,
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .assertIsNotEnabled()
+    }
+
+    @Test
+    fun navigationDrawerItem_canBeDisabled() {
+        rule.setContent {
+            var enabled by remember { mutableStateOf(true) }
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = { enabled = false },
+                    leadingContent = { },
+                    enabled = enabled,
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            // Confirm the button starts off enabled, with a click action
+            .assertHasClickAction()
+            .assertIsEnabled()
+            .requestFocus()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            // Then confirm it's disabled with click action after clicking it
+            .assertHasClickAction()
+            .assertIsNotEnabled()
+    }
+
+    @Test
+    fun navigationDrawerItem_oneLineHeight() {
+        val expectedHeightNoIcon = 56.dp
+
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag).assertHeightIsEqualTo(expectedHeightNoIcon)
+    }
+
+    @Test
+    fun navigationDrawerItem_width() {
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = { },
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .assertWidthIsEqualTo(rule.onRoot().getUnclippedBoundsInRoot().width)
+    }
+
+    @Test
+    fun navigationDrawerItem_widthInInactiveState() {
+        rule.setContent {
+            DrawerScope(false) {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = { },
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .assertWidthIsEqualTo(56.dp)
+    }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun DrawerScope(
+    isActivated: Boolean = true,
+    content: @Composable NavigationDrawerScope.() -> Unit
+) {
+    Box {
+        NavigationDrawerScopeImpl(isActivated).apply {
+            content()
+        }
+    }
+}
+
+private const val NavigationDrawerItemTag = "NavigationDrawerItem"
+private const val NavigationDrawerItemTextTag = "NavigationDrawerItemText"
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowScreenshotTest.kt
index ab61794..093e9b7 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowScreenshotTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowScreenshotTest.kt
@@ -203,10 +203,10 @@
                 TabRow(
                     selectedTabIndex = selectedTabIndex,
                     separator = { Spacer(modifier = Modifier.width(12.dp)) },
-                    indicator = { tabPositions, isisActivated ->
+                    indicator = { tabPositions, doesTabRowHaveFocus ->
                         TabRowDefaults.UnderlinedIndicator(
                             currentTabPosition = tabPositions[selectedTabIndex],
-                            isActivated = isisActivated,
+                            doesTabRowHaveFocus = doesTabRowHaveFocus,
                         )
                     }
                 ) {
@@ -247,10 +247,10 @@
                 TabRow(
                     selectedTabIndex = selectedTabIndex,
                     separator = { Spacer(modifier = Modifier.width(12.dp)) },
-                    indicator = { tabPositions, isActivated ->
+                    indicator = { tabPositions, doesTabRowHaveFocus ->
                         TabRowDefaults.UnderlinedIndicator(
                             currentTabPosition = tabPositions[selectedTabIndex],
-                            isActivated = isActivated,
+                            doesTabRowHaveFocus = doesTabRowHaveFocus,
                         )
                     }
                 ) {
@@ -297,10 +297,10 @@
                 TabRow(
                     selectedTabIndex = selectedTabIndex,
                     separator = { Spacer(modifier = Modifier.width(12.dp)) },
-                    indicator = { tabPositions, isActivated ->
+                    indicator = { tabPositions, doesTabRowHaveFocus ->
                         TabRowDefaults.UnderlinedIndicator(
                             currentTabPosition = tabPositions[selectedTabIndex],
-                            isActivated = isActivated,
+                            doesTabRowHaveFocus = doesTabRowHaveFocus,
                         )
                     },
                 ) {
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
index f1ca6d3..d71e93e 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
@@ -169,11 +169,11 @@
                     buildTabPanel = @Composable { index, _ ->
                         BasicText(text = "Panel ${index + 1}")
                     },
-                    indicator = @Composable { tabPositions, isActivated ->
+                    indicator = @Composable { tabPositions, doesTabRowHaveFocus ->
                         // FocusedTab's indicator
                         TabRowDefaults.PillIndicator(
                             currentTabPosition = tabPositions[focusedTabIndex],
-                            isActivated = isActivated,
+                            doesTabRowHaveFocus = doesTabRowHaveFocus,
                             activeColor = Color.Blue.copy(alpha = 0.4f),
                             inactiveColor = Color.Transparent,
                         )
@@ -181,7 +181,7 @@
                         // SelectedTab's indicator
                         TabRowDefaults.PillIndicator(
                             currentTabPosition = tabPositions[activeTabIndex],
-                            isActivated = isActivated,
+                            doesTabRowHaveFocus = doesTabRowHaveFocus,
                         )
                     }
                 )
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/API_28_OR_ABOVE.kt b/tv/tv-material/src/main/java/androidx/tv/material3/API_28_OR_ABOVE.kt
new file mode 100644
index 0000000..7de9b2c
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/API_28_OR_ABOVE.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.tv.material3
+
+import android.os.Build
+
+internal val API_28_OR_ABOVE = Build.VERSION.SDK_INT >= 28
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
index 23ffe4e..1f059dc 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
@@ -291,7 +291,8 @@
                     KeyEventPropagation.StopPropagation
                 }
 
-            !focusManager.moveFocus(direction) -> {
+            !focusManager.moveFocus(direction) &&
+                currentCarouselBoxFocusState()?.hasFocus == true -> {
                 // if focus search was unsuccessful, interpret as input for slide change
                 updateItemBasedOnLayout(direction, isLtr)
                 KeyEventPropagation.StopPropagation
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/IfElseModifier.kt b/tv/tv-material/src/main/java/androidx/tv/material3/IfElseModifier.kt
new file mode 100644
index 0000000..44e8dac
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/IfElseModifier.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.tv.material3
+
+import androidx.compose.ui.Modifier
+
+internal fun Modifier.ifElse(
+    condition: Boolean,
+    ifTrueModifier: Modifier,
+    ifFalseModifier: Modifier = Modifier
+): Modifier = then(if (condition) ifTrueModifier else ifFalseModifier)
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt
index 049ddf1..3172056 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt
@@ -23,7 +23,6 @@
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
@@ -45,7 +44,6 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
 
 /**
@@ -74,12 +72,14 @@
  * @param modifier the [Modifier] to be applied to this drawer
  * @param drawerState state of the drawer
  * @param scrimBrush brush to paint the scrim that obscures content when the drawer is open
- * @param content content of the rest of the UI
+ * @param content content of the rest of the UI. The content extends to the edge of the container
+ * under the modal navigation drawer. Focusable content that is not part of the background must have
+ * start-padding sufficient to prevent it from being drawn under the drawer in the Closed state.
  */
 @ExperimentalTvMaterial3Api
 @Composable
 fun ModalNavigationDrawer(
-    drawerContent: @Composable (DrawerValue) -> Unit,
+    drawerContent: @Composable NavigationDrawerScope.(DrawerValue) -> Unit,
     modifier: Modifier = Modifier,
     drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
     scrimBrush: Brush = SolidColor(LocalColorScheme.current.scrim.copy(alpha = 0.5f)),
@@ -114,15 +114,14 @@
             content = drawerContent
         )
 
+        content()
+
         if (drawerState.currentValue == DrawerValue.Open) {
             // Scrim
             Canvas(Modifier.fillMaxSize()) {
                 drawRect(scrimBrush)
             }
         }
-        Box(Modifier.padding(start = closedDrawerWidth.value ?: ClosedDrawerWidth.dp)) {
-            content()
-        }
     }
 }
 
@@ -155,7 +154,7 @@
 @ExperimentalTvMaterial3Api
 @Composable
 fun NavigationDrawer(
-    drawerContent: @Composable (DrawerValue) -> Unit,
+    drawerContent: @Composable NavigationDrawerScope.(DrawerValue) -> Unit,
     modifier: Modifier = Modifier,
     drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
     content: @Composable () -> Unit
@@ -230,14 +229,13 @@
     }
 }
 
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 private fun DrawerSheet(
     modifier: Modifier = Modifier,
     drawerState: DrawerState = remember { DrawerState() },
     sizeAnimationFinishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null,
-    content: @Composable (DrawerValue) -> Unit
+    content: @Composable NavigationDrawerScope.(DrawerValue) -> Unit
 ) {
     // indicates that the drawer has been set to its initial state and has grabbed focus if
     // necessary. Controls whether focus is used to decide the state of the drawer going forward.
@@ -269,7 +267,9 @@
             }
             .focusGroup()
 
-    Box(modifier = internalModifier) { content.invoke(drawerState.currentValue) }
+    Box(modifier = internalModifier) {
+        NavigationDrawerScopeImpl(drawerState.currentValue == DrawerValue.Open).apply {
+            content(drawerState.currentValue)
+        }
+    }
 }
-
-private const val ClosedDrawerWidth = 80
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt
new file mode 100644
index 0000000..cc97ac8
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt
@@ -0,0 +1,241 @@
+/*
+ * 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.tv.material3
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.unit.Dp
+
+/**
+ * TV Material Design navigation drawer item.
+ *
+ * A [NavigationDrawerItem] represents a destination within drawers, either [NavigationDrawer] or
+ * [ModalNavigationDrawer]
+ *
+ * @sample androidx.tv.samples.SampleNavigationDrawer
+ * @sample androidx.tv.samples.SampleModalNavigationDrawerWithSolidScrim
+ * @sample androidx.tv.samples.SampleModalNavigationDrawerWithGradientScrim
+ *
+ * @param selected defines whether this composable is selected or not
+ * @param onClick called when this composable is clicked
+ * @param leadingContent the leading content of the list item
+ * @param modifier to be applied to the list item
+ * @param enabled controls the enabled state of this composable. When `false`, this component will
+ * not respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services
+ * @param onLongClick called when this composable is long clicked (long-pressed)
+ * @param supportingContent the content displayed below the headline content
+ * @param trailingContent the trailing meta badge or icon
+ * @param tonalElevation the tonal elevation of this composable
+ * @param shape defines the shape of Composable's container in different interaction states
+ * @param colors defines the background and content colors used in the composable
+ * for different interaction states
+ * @param scale defines the size of the composable relative to its original size in
+ * different interaction states
+ * @param border defines a border around the composable in different interaction states
+ * @param glow defines a shadow to be shown behind the composable for different interaction states
+ * @param interactionSource the [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this component. You can create and pass in your own [remember]ed instance
+ * to observe [Interaction]s and customize the appearance / behavior of this composable in different
+ * states
+ * @param content main content of this composable
+ */
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
+@Composable
+fun NavigationDrawerScope.NavigationDrawerItem(
+    selected: Boolean,
+    onClick: () -> Unit,
+    leadingContent: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    onLongClick: (() -> Unit)? = null,
+    supportingContent: (@Composable () -> Unit)? = null,
+    trailingContent: (@Composable () -> Unit)? = null,
+    tonalElevation: Dp = NavigationDrawerItemDefaults.NavigationDrawerItemElevation,
+    shape: NavigationDrawerItemShape = NavigationDrawerItemDefaults.shape(),
+    colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(),
+    scale: NavigationDrawerItemScale = NavigationDrawerItemScale.None,
+    border: NavigationDrawerItemBorder = NavigationDrawerItemDefaults.border(),
+    glow: NavigationDrawerItemGlow = NavigationDrawerItemDefaults.glow(),
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    content: @Composable () -> Unit,
+) {
+    val animatedWidth by animateDpAsState(
+        targetValue = if (doesNavigationDrawerHaveFocus) {
+            NavigationDrawerItemDefaults.ExpandedDrawerItemWidth
+        } else {
+            NavigationDrawerItemDefaults.CollapsedDrawerItemWidth
+        },
+        label = "NavigationDrawerItem width open/closed state of the drawer item"
+    )
+    val navDrawerItemHeight = if (supportingContent == null) {
+        NavigationDrawerItemDefaults.ContainerHeightOneLine
+    } else {
+        NavigationDrawerItemDefaults.ContainerHeightTwoLine
+    }
+    ListItem(
+        selected = selected,
+        onClick = onClick,
+        headlineContent = {
+            AnimatedVisibility(
+                visible = doesNavigationDrawerHaveFocus,
+                enter = NavigationDrawerItemDefaults.ContentAnimationEnter,
+                exit = NavigationDrawerItemDefaults.ContentAnimationExit,
+            ) {
+                content()
+            }
+        },
+        leadingContent = {
+            Box(Modifier.size(NavigationDrawerItemDefaults.IconSize)) {
+                leadingContent()
+            }
+        },
+        trailingContent = trailingContent?.let {
+            {
+                AnimatedVisibility(
+                    visible = doesNavigationDrawerHaveFocus,
+                    enter = NavigationDrawerItemDefaults.ContentAnimationEnter,
+                    exit = NavigationDrawerItemDefaults.ContentAnimationExit,
+                ) {
+                    it()
+                }
+            }
+        },
+        supportingContent = supportingContent?.let {
+            {
+                AnimatedVisibility(
+                    visible = doesNavigationDrawerHaveFocus,
+                    enter = NavigationDrawerItemDefaults.ContentAnimationEnter,
+                    exit = NavigationDrawerItemDefaults.ContentAnimationExit,
+                ) {
+                    it()
+                }
+            }
+        },
+        modifier = modifier
+            .layout { measurable, constraints ->
+                val width = animatedWidth.roundToPx()
+                val height = navDrawerItemHeight.roundToPx()
+                val placeable = measurable.measure(
+                    constraints.copy(
+                        minWidth = width,
+                        maxWidth = width,
+                        minHeight = height,
+                        maxHeight = height,
+                    )
+                )
+                layout(placeable.width, placeable.height) {
+                    placeable.place(0, 0)
+                }
+            },
+        enabled = enabled,
+        onLongClick = onLongClick,
+        tonalElevation = tonalElevation,
+        shape = shape.toToggleableListItemShape(),
+        colors = colors.toToggleableListItemColors(doesNavigationDrawerHaveFocus),
+        scale = scale.toToggleableListItemScale(),
+        border = border.toToggleableListItemBorder(),
+        glow = glow.toToggleableListItemGlow(),
+        interactionSource = interactionSource,
+    )
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemShape.toToggleableListItemShape() =
+    ListItemDefaults.shape(
+        shape = shape,
+        focusedShape = focusedShape,
+        pressedShape = pressedShape,
+        selectedShape = selectedShape,
+        disabledShape = disabledShape,
+        focusedSelectedShape = focusedSelectedShape,
+        focusedDisabledShape = focusedDisabledShape,
+        pressedSelectedShape = pressedSelectedShape,
+    )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemColors.toToggleableListItemColors(
+    doesNavigationDrawerHaveFocus: Boolean
+) =
+    ListItemDefaults.colors(
+        containerColor = containerColor,
+        contentColor = if (doesNavigationDrawerHaveFocus) contentColor else inactiveContentColor,
+        focusedContainerColor = focusedContainerColor,
+        focusedContentColor = focusedContentColor,
+        pressedContainerColor = pressedContainerColor,
+        pressedContentColor = pressedContentColor,
+        selectedContainerColor = selectedContainerColor,
+        selectedContentColor = selectedContentColor,
+        disabledContainerColor = disabledContainerColor,
+        disabledContentColor =
+        if (doesNavigationDrawerHaveFocus) disabledContentColor else disabledInactiveContentColor,
+        focusedSelectedContainerColor = focusedSelectedContainerColor,
+        focusedSelectedContentColor = focusedSelectedContentColor,
+        pressedSelectedContainerColor = pressedSelectedContainerColor,
+        pressedSelectedContentColor = pressedSelectedContentColor,
+    )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemScale.toToggleableListItemScale() =
+    ListItemDefaults.scale(
+        scale = scale,
+        focusedScale = focusedScale,
+        pressedScale = pressedScale,
+        selectedScale = selectedScale,
+        disabledScale = disabledScale,
+        focusedSelectedScale = focusedSelectedScale,
+        focusedDisabledScale = focusedDisabledScale,
+        pressedSelectedScale = pressedSelectedScale,
+    )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemBorder.toToggleableListItemBorder() =
+    ListItemDefaults.border(
+        border = border,
+        focusedBorder = focusedBorder,
+        pressedBorder = pressedBorder,
+        selectedBorder = selectedBorder,
+        disabledBorder = disabledBorder,
+        focusedSelectedBorder = focusedSelectedBorder,
+        focusedDisabledBorder = focusedDisabledBorder,
+        pressedSelectedBorder = pressedSelectedBorder,
+    )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemGlow.toToggleableListItemGlow() =
+    ListItemDefaults.glow(
+        glow = glow,
+        focusedGlow = focusedGlow,
+        pressedGlow = pressedGlow,
+        selectedGlow = selectedGlow,
+        focusedSelectedGlow = focusedSelectedGlow,
+        pressedSelectedGlow = pressedSelectedGlow,
+    )
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemDefaults.kt
new file mode 100644
index 0000000..debfd6f
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemDefaults.kt
@@ -0,0 +1,358 @@
+/*
+ * 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.tv.material3
+
+import androidx.annotation.FloatRange
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideIn
+import androidx.compose.animation.slideOut
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.tokens.Elevation
+
+/**
+ * Contains the default values used by selectable [NavigationDrawerItem]
+ */
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
+object NavigationDrawerItemDefaults {
+    /**
+     * The default Icon size used by [NavigationDrawerItem]
+     */
+    val IconSize = 24.dp
+
+    /**
+     * The size of the [NavigationDrawerItem] when the drawer is collapsed
+     */
+    val CollapsedDrawerItemWidth = 56.dp
+
+    /**
+     * The size of the [NavigationDrawerItem] when the drawer is expanded
+     */
+    val ExpandedDrawerItemWidth = 256.dp
+
+    /**
+     * The default content padding [PaddingValues] used by [NavigationDrawerItem] when the drawer
+     * is expanded
+     */
+
+    val ContainerHeightOneLine = 56.dp
+    val ContainerHeightTwoLine = 64.dp
+
+    /**
+     * The default elevation used by [NavigationDrawerItem]
+     */
+    val NavigationDrawerItemElevation = Elevation.Level0
+
+    /**
+     * Animation enter default for inner content
+     */
+    val ContentAnimationEnter = fadeIn() + slideIn { IntOffset(-it.width, 0) }
+
+    /**
+     * Animation exit default for inner content
+     */
+    val ContentAnimationExit = fadeOut() + slideOut { IntOffset(0, 0) }
+
+    /**
+     * Default border used by [NavigationDrawerItem]
+     */
+    val DefaultBorder
+        @ReadOnlyComposable
+        @Composable get() = Border(
+            border = BorderStroke(
+                width = 2.dp,
+                color = MaterialTheme.colorScheme.border
+            ),
+        )
+
+    /**
+     * The default container color used by [NavigationDrawerItem]'s trailing badge
+     */
+    val TrailingBadgeContainerColor
+        @ReadOnlyComposable
+        @Composable get() = MaterialTheme.colorScheme.tertiary
+
+    /**
+     * The default text style used by [NavigationDrawerItem]'s trailing badge
+     */
+    val TrailingBadgeTextStyle
+        @ReadOnlyComposable
+        @Composable get() = MaterialTheme.typography.labelSmall
+
+    /**
+     * The default content color used by [NavigationDrawerItem]'s trailing badge
+     */
+    val TrailingBadgeContentColor
+        @ReadOnlyComposable
+        @Composable get() = MaterialTheme.colorScheme.onTertiary
+
+    /**
+     * Creates a trailing badge for [NavigationDrawerItem]
+     */
+    @Composable
+    fun TrailingBadge(
+        text: String,
+        containerColor: Color = TrailingBadgeContainerColor,
+        contentColor: Color = TrailingBadgeContentColor
+    ) {
+        Box(
+            modifier = Modifier
+                .background(containerColor, RoundedCornerShape(50))
+                .padding(10.dp, 2.dp)
+        ) {
+            ProvideTextStyle(value = TrailingBadgeTextStyle) {
+                Text(
+                    text = text,
+                    color = contentColor,
+                )
+            }
+        }
+    }
+
+    /**
+     * Creates a [NavigationDrawerItemShape] that represents the default container shapes
+     * used in a selectable [NavigationDrawerItem]
+     *
+     * @param shape the default shape used when the [NavigationDrawerItem] is enabled
+     * @param focusedShape the shape used when the [NavigationDrawerItem] is enabled and focused
+     * @param pressedShape the shape used when the [NavigationDrawerItem] is enabled and pressed
+     * @param selectedShape the shape used when the [NavigationDrawerItem] is enabled and selected
+     * @param disabledShape the shape used when the [NavigationDrawerItem] is not enabled
+     * @param focusedSelectedShape the shape used when the [NavigationDrawerItem] is enabled,
+     * focused and selected
+     * @param focusedDisabledShape the shape used when the [NavigationDrawerItem] is not enabled
+     * and focused
+     * @param pressedSelectedShape the shape used when the [NavigationDrawerItem] is enabled,
+     * pressed and selected
+     */
+    fun shape(
+        shape: Shape = RoundedCornerShape(50),
+        focusedShape: Shape = shape,
+        pressedShape: Shape = shape,
+        selectedShape: Shape = shape,
+        disabledShape: Shape = shape,
+        focusedSelectedShape: Shape = shape,
+        focusedDisabledShape: Shape = disabledShape,
+        pressedSelectedShape: Shape = shape
+    ) = NavigationDrawerItemShape(
+        shape = shape,
+        focusedShape = focusedShape,
+        pressedShape = pressedShape,
+        selectedShape = selectedShape,
+        disabledShape = disabledShape,
+        focusedSelectedShape = focusedSelectedShape,
+        focusedDisabledShape = focusedDisabledShape,
+        pressedSelectedShape = pressedSelectedShape
+    )
+
+    /**
+     * Creates a [NavigationDrawerItemColors] that represents the default container &
+     * content colors used in a selectable [NavigationDrawerItem]
+     *
+     * @param containerColor the default container color used when the [NavigationDrawerItem] is
+     * enabled
+     * @param contentColor the default content color used when the [NavigationDrawerItem] is enabled
+     * @param inactiveContentColor the content color used when none of the navigation items have
+     * focus
+     * @param focusedContainerColor the container color used when the [NavigationDrawerItem] is
+     * enabled and focused
+     * @param focusedContentColor the content color used when the [NavigationDrawerItem] is enabled
+     * and focused
+     * @param pressedContainerColor the container color used when the [NavigationDrawerItem] is
+     * enabled and pressed
+     * @param pressedContentColor the content color used when the [NavigationDrawerItem] is enabled
+     * and pressed
+     * @param selectedContainerColor the container color used when the [NavigationDrawerItem] is
+     * enabled and selected
+     * @param selectedContentColor the content color used when the [NavigationDrawerItem] is
+     * enabled and selected
+     * @param disabledContainerColor the container color used when the [NavigationDrawerItem] is
+     * not enabled
+     * @param disabledContentColor the content color used when the [NavigationDrawerItem] is not
+     * enabled
+     * @param disabledInactiveContentColor the content color used when none of the navigation items
+     * have focus and this item is disabled
+     * @param focusedSelectedContainerColor the container color used when the
+     * NavigationDrawerItem is enabled, focused and selected
+     * @param focusedSelectedContentColor the content color used when the [NavigationDrawerItem]
+     * is enabled, focused and selected
+     * @param pressedSelectedContainerColor the container color used when the
+     * [NavigationDrawerItem] is enabled, pressed and selected
+     * @param pressedSelectedContentColor the content color used when the [NavigationDrawerItem] is
+     * enabled, pressed and selected
+     */
+    @ReadOnlyComposable
+    @Composable
+    fun colors(
+        containerColor: Color = Color.Transparent,
+        contentColor: Color = MaterialTheme.colorScheme.onSurface,
+        inactiveContentColor: Color = contentColor.copy(alpha = 0.4f),
+        focusedContainerColor: Color = MaterialTheme.colorScheme.inverseSurface,
+        focusedContentColor: Color = contentColorFor(focusedContainerColor),
+        pressedContainerColor: Color = focusedContainerColor,
+        pressedContentColor: Color = contentColorFor(focusedContainerColor),
+        selectedContainerColor: Color =
+            MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.4f),
+        selectedContentColor: Color = MaterialTheme.colorScheme.onSecondaryContainer,
+        disabledContainerColor: Color = Color.Transparent,
+        disabledContentColor: Color = MaterialTheme.colorScheme.onSurface,
+        disabledInactiveContentColor: Color = disabledContentColor.copy(alpha = 0.4f),
+        focusedSelectedContainerColor: Color = focusedContainerColor,
+        focusedSelectedContentColor: Color = focusedContentColor,
+        pressedSelectedContainerColor: Color = pressedContainerColor,
+        pressedSelectedContentColor: Color = pressedContentColor
+    ) = NavigationDrawerItemColors(
+        containerColor = containerColor,
+        contentColor = contentColor,
+        inactiveContentColor = inactiveContentColor,
+        focusedContainerColor = focusedContainerColor,
+        focusedContentColor = focusedContentColor,
+        pressedContainerColor = pressedContainerColor,
+        pressedContentColor = pressedContentColor,
+        selectedContainerColor = selectedContainerColor,
+        selectedContentColor = selectedContentColor,
+        disabledContainerColor = disabledContainerColor,
+        disabledContentColor = disabledContentColor,
+        disabledInactiveContentColor = disabledInactiveContentColor,
+        focusedSelectedContainerColor = focusedSelectedContainerColor,
+        focusedSelectedContentColor = focusedSelectedContentColor,
+        pressedSelectedContainerColor = pressedSelectedContainerColor,
+        pressedSelectedContentColor = pressedSelectedContentColor
+    )
+
+    /**
+     * Creates a [NavigationDrawerItemScale] that represents the default scales used in a
+     * selectable [NavigationDrawerItem]
+     *
+     * scales are used to modify the size of a composable in different [Interaction] states
+     * e.g. `1f` (original) in default state, `1.2f` (scaled up) in focused state, `0.8f` (scaled
+     * down) in pressed state, etc.
+     *
+     * @param scale the scale used when the [NavigationDrawerItem] is enabled
+     * @param focusedScale the scale used when the [NavigationDrawerItem] is enabled and focused
+     * @param pressedScale the scale used when the [NavigationDrawerItem] is enabled and pressed
+     * @param selectedScale the scale used when the [NavigationDrawerItem] is enabled and selected
+     * @param disabledScale the scale used when the [NavigationDrawerItem] is not enabled
+     * @param focusedSelectedScale the scale used when the [NavigationDrawerItem] is enabled,
+     * focused and selected
+     * @param focusedDisabledScale the scale used when the [NavigationDrawerItem] is not enabled and
+     * focused
+     * @param pressedSelectedScale the scale used when the [NavigationDrawerItem] is enabled,
+     * pressed and selected
+     */
+    fun scale(
+        @FloatRange(from = 0.0) scale: Float = 1f,
+        @FloatRange(from = 0.0) focusedScale: Float = 1.05f,
+        @FloatRange(from = 0.0) pressedScale: Float = scale,
+        @FloatRange(from = 0.0) selectedScale: Float = scale,
+        @FloatRange(from = 0.0) disabledScale: Float = scale,
+        @FloatRange(from = 0.0) focusedSelectedScale: Float = focusedScale,
+        @FloatRange(from = 0.0) focusedDisabledScale: Float = disabledScale,
+        @FloatRange(from = 0.0) pressedSelectedScale: Float = scale
+    ) = NavigationDrawerItemScale(
+        scale = scale,
+        focusedScale = focusedScale,
+        pressedScale = pressedScale,
+        selectedScale = selectedScale,
+        disabledScale = disabledScale,
+        focusedSelectedScale = focusedSelectedScale,
+        focusedDisabledScale = focusedDisabledScale,
+        pressedSelectedScale = pressedSelectedScale
+    )
+
+    /**
+     * Creates a [NavigationDrawerItemBorder] that represents the default [Border]s
+     * applied on a selectable [NavigationDrawerItem] in different [Interaction] states
+     *
+     * @param border the default [Border] used when the [NavigationDrawerItem] is enabled
+     * @param focusedBorder the [Border] used when the [NavigationDrawerItem] is enabled and focused
+     * @param pressedBorder the [Border] used when the [NavigationDrawerItem] is enabled and pressed
+     * @param selectedBorder the [Border] used when the [NavigationDrawerItem] is enabled and
+     * selected
+     * @param disabledBorder the [Border] used when the [NavigationDrawerItem] is not enabled
+     * @param focusedSelectedBorder the [Border] used when the [NavigationDrawerItem] is enabled,
+     * focused and selected
+     * @param focusedDisabledBorder the [Border] used when the [NavigationDrawerItem] is not
+     * enabled and focused
+     * @param pressedSelectedBorder the [Border] used when the [NavigationDrawerItem] is enabled,
+     * pressed and selected
+     */
+    @ReadOnlyComposable
+    @Composable
+    fun border(
+        border: Border = Border.None,
+        focusedBorder: Border = border,
+        pressedBorder: Border = focusedBorder,
+        selectedBorder: Border = border,
+        disabledBorder: Border = border,
+        focusedSelectedBorder: Border = focusedBorder,
+        focusedDisabledBorder: Border = DefaultBorder,
+        pressedSelectedBorder: Border = border
+    ) = NavigationDrawerItemBorder(
+        border = border,
+        focusedBorder = focusedBorder,
+        pressedBorder = pressedBorder,
+        selectedBorder = selectedBorder,
+        disabledBorder = disabledBorder,
+        focusedSelectedBorder = focusedSelectedBorder,
+        focusedDisabledBorder = focusedDisabledBorder,
+        pressedSelectedBorder = pressedSelectedBorder
+    )
+
+    /**
+     * Creates a [NavigationDrawerItemGlow] that represents the default [Glow]s used in a
+     * selectable [NavigationDrawerItem]
+     *
+     * @param glow the [Glow] used when the [NavigationDrawerItem] is enabled, and has no other
+     * [Interaction]s
+     * @param focusedGlow the [Glow] used when the [NavigationDrawerItem] is enabled and focused
+     * @param pressedGlow the [Glow] used when the [NavigationDrawerItem] is enabled and pressed
+     * @param selectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled and selected
+     * @param focusedSelectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled,
+     * focused and selected
+     * @param pressedSelectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled,
+     * pressed and selected
+     */
+    fun glow(
+        glow: Glow = Glow.None,
+        focusedGlow: Glow = glow,
+        pressedGlow: Glow = glow,
+        selectedGlow: Glow = glow,
+        focusedSelectedGlow: Glow = focusedGlow,
+        pressedSelectedGlow: Glow = glow
+    ) = NavigationDrawerItemGlow(
+        glow = glow,
+        focusedGlow = focusedGlow,
+        pressedGlow = pressedGlow,
+        selectedGlow = selectedGlow,
+        focusedSelectedGlow = focusedSelectedGlow,
+        pressedSelectedGlow = pressedSelectedGlow
+    )
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemStyles.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemStyles.kt
index 027ac27..f08e865 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemStyles.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemStyles.kt
@@ -23,21 +23,21 @@
 import androidx.compose.ui.graphics.Shape
 
 /**
- * Defines [Shape] for all TV [Indication] states of a NavigationDrawerItem
+ * Defines [Shape] for all TV [Indication] states of a [NavigationDrawerItem]
  *
- * @constructor create an instance with arbitrary shape. See NavigationDrawerItemDefaults.shape
- * for the default shape used in a NavigationDrawerItem
+ * @constructor create an instance with arbitrary shape. See [NavigationDrawerItemDefaults.shape]
+ * for the default shape used in a [NavigationDrawerItem]
  *
- * @param shape the default shape used when the NavigationDrawerItem is enabled
- * @param focusedShape the shape used when the NavigationDrawerItem is enabled and focused
- * @param pressedShape the shape used when the NavigationDrawerItem is enabled and pressed
- * @param selectedShape the shape used when the NavigationDrawerItem is enabled and selected
- * @param disabledShape the shape used when the NavigationDrawerItem is not enabled
- * @param focusedSelectedShape the shape used when the NavigationDrawerItem is enabled,
+ * @param shape the default shape used when the [NavigationDrawerItem] is enabled
+ * @param focusedShape the shape used when the [NavigationDrawerItem] is enabled and focused
+ * @param pressedShape the shape used when the [NavigationDrawerItem] is enabled and pressed
+ * @param selectedShape the shape used when the [NavigationDrawerItem] is enabled and selected
+ * @param disabledShape the shape used when the [NavigationDrawerItem] is not enabled
+ * @param focusedSelectedShape the shape used when the [NavigationDrawerItem] is enabled,
  * focused and selected
- * @param focusedDisabledShape the shape used when the NavigationDrawerItem is not enabled
+ * @param focusedDisabledShape the shape used when the [NavigationDrawerItem] is not enabled
  * and focused
- * @param pressedSelectedShape the shape used when the NavigationDrawerItem is enabled,
+ * @param pressedSelectedShape the shape used when the [NavigationDrawerItem] is enabled,
  * pressed and selected
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@@ -95,41 +95,41 @@
 
 /**
  * Defines container & content color [Color] for all TV [Indication] states of a
- * NavigationDrawerItem
+ * [NavigationDrawerItem]
  *
- * @constructor create an instance with arbitrary colors. See NavigationDrawerItemDefaults.colors
- * for the default colors used in a NavigationDrawerItem
+ * @constructor create an instance with arbitrary colors. See [NavigationDrawerItemDefaults.colors]
+ * for the default colors used in a [NavigationDrawerItem]
  *
- * @param containerColor the default container color used when the NavigationDrawerItem is
+ * @param containerColor the default container color used when the [NavigationDrawerItem] is
  * enabled
- * @param contentColor the default content color used when the NavigationDrawerItem is enabled
+ * @param contentColor the default content color used when the [NavigationDrawerItem] is enabled
  * @param inactiveContentColor the content color used when none of the navigation items have
  * focus
- * @param focusedContainerColor the container color used when the NavigationDrawerItem is
+ * @param focusedContainerColor the container color used when the [NavigationDrawerItem] is
  * enabled and focused
- * @param focusedContentColor the content color used when the NavigationDrawerItem is enabled
+ * @param focusedContentColor the content color used when the [NavigationDrawerItem] is enabled
  * and focused
- * @param pressedContainerColor the container color used when the NavigationDrawerItem is
+ * @param pressedContainerColor the container color used when the [NavigationDrawerItem] is
  * enabled and pressed
- * @param pressedContentColor the content color used when the NavigationDrawerItem is enabled
+ * @param pressedContentColor the content color used when the [NavigationDrawerItem] is enabled
  * and pressed
- * @param selectedContainerColor the container color used when the NavigationDrawerItem is
+ * @param selectedContainerColor the container color used when the [NavigationDrawerItem] is
  * enabled and selected
- * @param selectedContentColor the content color used when the NavigationDrawerItem is
+ * @param selectedContentColor the content color used when the [NavigationDrawerItem] is
  * enabled and selected
- * @param disabledContainerColor the container color used when the NavigationDrawerItem is
+ * @param disabledContainerColor the container color used when the [NavigationDrawerItem] is
  * not enabled
- * @param disabledContentColor the content color used when the NavigationDrawerItem is not
+ * @param disabledContentColor the content color used when the [NavigationDrawerItem] is not
  * enabled
  * @param disabledInactiveContentColor the content color used when none of the navigation items
  * have focus and this item is disabled
  * @param focusedSelectedContainerColor the container color used when the
- * NavigationDrawerItem is enabled, focused and selected
- * @param focusedSelectedContentColor the content color used when the NavigationDrawerItem
+ * [NavigationDrawerItem] is enabled, focused and selected
+ * @param focusedSelectedContentColor the content color used when the [NavigationDrawerItem]
  * is enabled, focused and selected
  * @param pressedSelectedContainerColor the container color used when the
- * NavigationDrawerItem is enabled, pressed and selected
- * @param pressedSelectedContentColor the content color used when the NavigationDrawerItem is
+ * [NavigationDrawerItem] is enabled, pressed and selected
+ * @param pressedSelectedContentColor the content color used when the [NavigationDrawerItem] is
  * enabled, pressed and selected
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@@ -215,21 +215,21 @@
 }
 
 /**
- * Defines the scale for all TV [Indication] states of a NavigationDrawerItem
+ * Defines the scale for all TV [Indication] states of a [NavigationDrawerItem]
  *
- * @constructor create an instance with arbitrary scale. See NavigationDrawerItemDefaults.scale
- * for the default scale used in a NavigationDrawerItem
+ * @constructor create an instance with arbitrary scale. See [NavigationDrawerItemDefaults.scale]
+ * for the default scale used in a [NavigationDrawerItem]
  *
- * @param scale the scale used when the NavigationDrawerItem is enabled
- * @param focusedScale the scale used when the NavigationDrawerItem is enabled and focused
- * @param pressedScale the scale used when the NavigationDrawerItem is enabled and pressed
- * @param selectedScale the scale used when the NavigationDrawerItem is enabled and selected
- * @param disabledScale the scale used when the NavigationDrawerItem is not enabled
- * @param focusedSelectedScale the scale used when the NavigationDrawerItem is enabled,
+ * @param scale the scale used when the [NavigationDrawerItem] is enabled
+ * @param focusedScale the scale used when the [NavigationDrawerItem] is enabled and focused
+ * @param pressedScale the scale used when the [NavigationDrawerItem] is enabled and pressed
+ * @param selectedScale the scale used when the [NavigationDrawerItem] is enabled and selected
+ * @param disabledScale the scale used when the [NavigationDrawerItem] is not enabled
+ * @param focusedSelectedScale the scale used when the [NavigationDrawerItem] is enabled,
  * focused and selected
- * @param focusedDisabledScale the scale used when the NavigationDrawerItem is not enabled and
+ * @param focusedDisabledScale the scale used when the [NavigationDrawerItem] is not enabled and
  * focused
- * @param pressedSelectedScale the scale used when the NavigationDrawerItem is enabled,
+ * @param pressedSelectedScale the scale used when the [NavigationDrawerItem] is enabled,
  * pressed and selected
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@@ -286,7 +286,7 @@
 
     companion object {
         /**
-         * Signifies the absence of a [ScaleIndication] in NavigationDrawerItem
+         * Signifies the absence of a [ScaleIndication] in [NavigationDrawerItem]
          */
         val None = NavigationDrawerItemScale(
             scale = 1f,
@@ -302,22 +302,22 @@
 }
 
 /**
- * Defines [Border] for all TV [Indication] states of a NavigationDrawerItem
+ * Defines [Border] for all TV [Indication] states of a [NavigationDrawerItem]
  *
- * @constructor create an instance with arbitrary border. See NavigationDrawerItemDefaults.border
- * for the default border used in a NavigationDrawerItem
+ * @constructor create an instance with arbitrary border. See [NavigationDrawerItemDefaults.border]
+ * for the default border used in a [NavigationDrawerItem]
  *
- * @param border the default [Border] used when the NavigationDrawerItem is enabled
- * @param focusedBorder the [Border] used when the NavigationDrawerItem is enabled and focused
- * @param pressedBorder the [Border] used when the NavigationDrawerItem is enabled and pressed
- * @param selectedBorder the [Border] used when the NavigationDrawerItem is enabled and
+ * @param border the default [Border] used when the [NavigationDrawerItem] is enabled
+ * @param focusedBorder the [Border] used when the [NavigationDrawerItem] is enabled and focused
+ * @param pressedBorder the [Border] used when the [NavigationDrawerItem] is enabled and pressed
+ * @param selectedBorder the [Border] used when the [NavigationDrawerItem] is enabled and
  * selected
- * @param disabledBorder the [Border] used when the NavigationDrawerItem is not enabled
- * @param focusedSelectedBorder the [Border] used when the NavigationDrawerItem is enabled,
+ * @param disabledBorder the [Border] used when the [NavigationDrawerItem] is not enabled
+ * @param focusedSelectedBorder the [Border] used when the [NavigationDrawerItem] is enabled,
  * focused and selected
- * @param focusedDisabledBorder the [Border] used when the NavigationDrawerItem is not
+ * @param focusedDisabledBorder the [Border] used when the [NavigationDrawerItem] is not
  * enabled and focused
- * @param pressedSelectedBorder the [Border] used when the NavigationDrawerItem is enabled,
+ * @param pressedSelectedBorder the [Border] used when the [NavigationDrawerItem] is enabled,
  * pressed and selected
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@@ -374,18 +374,18 @@
 }
 
 /**
- * Defines [Glow] for all TV [Indication] states of a NavigationDrawerItem
+ * Defines [Glow] for all TV [Indication] states of a [NavigationDrawerItem]
  *
- * @constructor create an instance with arbitrary glow. See NavigationDrawerItemDefaults.glow
- * for the default glow used in a NavigationDrawerItem
+ * @constructor create an instance with arbitrary glow. See [NavigationDrawerItemDefaults.glow]
+ * for the default glow used in a [NavigationDrawerItem]
  *
- * @param glow the [Glow] used when the NavigationDrawerItem is enabled
- * @param focusedGlow the [Glow] used when the NavigationDrawerItem is enabled and focused
- * @param pressedGlow the [Glow] used when the NavigationDrawerItem is enabled and pressed
- * @param selectedGlow the [Glow] used when the NavigationDrawerItem is enabled and selected
- * @param focusedSelectedGlow the [Glow] used when the NavigationDrawerItem is enabled,
+ * @param glow the [Glow] used when the [NavigationDrawerItem] is enabled
+ * @param focusedGlow the [Glow] used when the [NavigationDrawerItem] is enabled and focused
+ * @param pressedGlow the [Glow] used when the [NavigationDrawerItem] is enabled and pressed
+ * @param selectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled and selected
+ * @param focusedSelectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled,
  * focused and selected
- * @param pressedSelectedGlow the [Glow] used when the NavigationDrawerItem is enabled,
+ * @param pressedSelectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled,
  * pressed and selected
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerScope.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerScope.kt
index ccdcec7..85434a3 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerScope.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerScope.kt
@@ -17,7 +17,7 @@
 package androidx.tv.material3
 
 /**
- * [NavigationDrawerScope] is used to provide the isActivated state to the NavigationDrawerItem
+ * [NavigationDrawerScope] is used to provide the isActivated state to the [NavigationDrawerItem]
  * composable
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@@ -25,10 +25,10 @@
     /**
      * Whether any item within the [NavigationDrawer] or [ModalNavigationDrawer] is focused
      */
-    val isActivated: Boolean
+    val doesNavigationDrawerHaveFocus: Boolean
 }
 
 @OptIn(ExperimentalTvMaterial3Api::class)
-internal class NavigationDrawerScopeImpl constructor(
-    override val isActivated: Boolean
+internal class NavigationDrawerScopeImpl(
+    override val doesNavigationDrawerHaveFocus: Boolean
 ) : NavigationDrawerScope
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
index 2546e1c..55da109 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
@@ -72,7 +72,8 @@
  * @param colors Defines the background & content color to be used in this Surface.
  * See [NonInteractiveSurfaceDefaults.colors].
  * @param border Defines a border around the Surface.
- * @param glow Diffused shadow to be shown behind the Surface.
+ * @param glow Diffused shadow to be shown behind the Surface. Note that glow is disabled for API
+ * levels below 28 as it is not supported by the underlying OS
  * @param content defines the [Composable] content inside the surface
  */
 @ExperimentalTvMaterial3Api
@@ -122,7 +123,8 @@
  * interaction states. See [ClickableSurfaceDefaults.colors].
  * @param scale Defines size of the Surface relative to its original size.
  * @param border Defines a border around the Surface.
- * @param glow Diffused shadow to be shown behind the Surface.
+ * @param glow Diffused shadow to be shown behind the Surface. Note that glow is disabled for API
+ * levels below 28 as it is not supported by the underlying OS
  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
  * for this Surface. You can create and pass in your own remembered [MutableInteractionSource] if
  * you want to observe [Interaction]s and customize the appearance / behavior of this Surface in
@@ -226,7 +228,8 @@
  * interaction states. See [ToggleableSurfaceDefaults.colors].
  * @param scale Defines size of the Surface relative to its original size.
  * @param border Defines a border around the Surface.
- * @param glow Diffused shadow to be shown behind the Surface.
+ * @param glow Diffused shadow to be shown behind the Surface. Note that glow is disabled for API
+ * levels below 28 as it is not supported by the underlying OS
  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
  * for this Surface. You can create and pass in your own remembered [MutableInteractionSource] if
  * you want to observe [Interaction]s and customize the appearance / behavior of this Surface in
@@ -357,23 +360,25 @@
             elevation = LocalAbsoluteTonalElevation.current
         )
 
+        val glowIndicationModifier = Modifier.indication(
+            interactionSource = interactionSource,
+            indication = rememberGlowIndication(
+                color = surfaceColorAtElevation(
+                    color = glow.elevationColor,
+                    elevation = glow.elevation
+                ),
+                shape = shape,
+                glowBlurRadius = glow.elevation
+            )
+        )
+
         Box(
             modifier = modifier
                 .indication(
                     interactionSource = interactionSource,
                     indication = remember(scale) { ScaleIndication(scale = scale) }
                 )
-                .indication(
-                    interactionSource = interactionSource,
-                    indication = rememberGlowIndication(
-                        color = surfaceColorAtElevation(
-                            color = glow.elevationColor,
-                            elevation = glow.elevation
-                        ),
-                        shape = shape,
-                        glowBlurRadius = glow.elevation
-                    )
-                )
+                .ifElse(API_28_OR_ABOVE, glowIndicationModifier)
                 // Increasing the zIndex of this Surface when it is in the focused state to
                 // avoid the glowIndication from being overlapped by subsequent items if
                 // this Surface is inside a list composable (like a Row/Column).
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
index c791d57..51a16c8 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
@@ -82,7 +82,7 @@
                 this.role = Role.Tab
             },
         colors = colors.toToggleableSurfaceColors(
-            isActivated = isActivated,
+            doesTabRowHaveFocus = hasFocus,
             enabled = enabled,
         ),
         enabled = enabled,
@@ -227,16 +227,16 @@
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 internal fun TabColors.toToggleableSurfaceColors(
-    isActivated: Boolean,
+    doesTabRowHaveFocus: Boolean,
     enabled: Boolean,
 ) =
     ToggleableSurfaceDefaults.colors(
-        contentColor = if (isActivated) contentColor else inactiveContentColor,
+        contentColor = if (doesTabRowHaveFocus) contentColor else inactiveContentColor,
         selectedContentColor = if (enabled) selectedContentColor else disabledSelectedContentColor,
         focusedContentColor = focusedContentColor,
         focusedSelectedContentColor = focusedSelectedContentColor,
         disabledContentColor =
-        if (isActivated) disabledContentColor else disabledInactiveContentColor,
+        if (doesTabRowHaveFocus) disabledContentColor else disabledInactiveContentColor,
         containerColor = Color.Transparent,
         focusedContainerColor = Color.Transparent,
         pressedContainerColor = Color.Transparent,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt b/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
index a76a111..f68edbc 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
@@ -53,6 +53,7 @@
 import androidx.compose.ui.util.fastForEachIndexed
 import androidx.compose.ui.util.fastMap
 import androidx.compose.ui.util.fastMaxOfOrNull
+import androidx.compose.ui.util.fastSumBy
 import androidx.compose.ui.zIndex
 
 /**
@@ -78,7 +79,10 @@
  * @param containerColor the color used for the background of this tab row
  * @param contentColor the primary color used in the tabs
  * @param separator use this composable to add a separator between the tabs
- * @param indicator used to indicate which tab is currently selected and/or focused
+ * @param indicator used to indicate which tab is currently selected and/or focused. This lambda
+ * provides 2 values:
+ * * tabPositions: list of [DpRect] which provides the position of each tab
+ * * doesTabRowHaveFocus: whether any [Tab] within [TabRow] is focused
  * @param tabs a composable which will render all the tabs
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@@ -89,19 +93,19 @@
     containerColor: Color = TabRowDefaults.ContainerColor,
     contentColor: Color = TabRowDefaults.contentColor(),
     separator: @Composable () -> Unit = { TabRowDefaults.TabSeparator() },
-    indicator: @Composable (tabPositions: List<DpRect>, isActivated: Boolean) -> Unit =
-        @Composable { tabPositions, isActivated ->
+    indicator: @Composable (tabPositions: List<DpRect>, doesTabRowHaveFocus: Boolean) -> Unit =
+        @Composable { tabPositions, doesTabRowHaveFocus ->
             tabPositions.getOrNull(selectedTabIndex)?.let { currentTabPosition ->
                 TabRowDefaults.PillIndicator(
                     currentTabPosition = currentTabPosition,
-                    isActivated = isActivated
+                    doesTabRowHaveFocus = doesTabRowHaveFocus
                 )
             }
         },
     tabs: @Composable TabRowScope.() -> Unit
 ) {
     val scrollState = rememberScrollState()
-    var isActivated by remember { mutableStateOf(false) }
+    var doesTabRowHaveFocus by remember { mutableStateOf(false) }
 
     CompositionLocalProvider(LocalContentColor provides contentColor) {
 
@@ -111,12 +115,12 @@
                 .background(containerColor)
                 .clipToBounds()
                 .horizontalScroll(scrollState)
-                .onFocusChanged { isActivated = it.hasFocus }
+                .onFocusChanged { doesTabRowHaveFocus = it.hasFocus }
                 .selectableGroup()
         ) { constraints ->
             // Tab measurables
             val tabMeasurables = subcompose(TabRowSlots.Tabs) {
-                TabRowScopeImpl(isActivated).apply {
+                TabRowScopeImpl(doesTabRowHaveFocus).apply {
                     tabs()
                 }
             }
@@ -141,7 +145,8 @@
                 }
             val separatorWidth = separatorPlaceables.firstOrNull()?.width ?: 0
 
-            val layoutWidth = tabPlaceables.sumOf { it.width } + separatorsCount * separatorWidth
+            val layoutWidth = tabPlaceables.fastSumBy { it.width } +
+                separatorsCount * separatorWidth
             val layoutHeight = (tabMeasurables.fastMaxOfOrNull {
                 it.maxIntrinsicHeight(Constraints.Infinity)
             } ?: 0).coerceAtLeast(0)
@@ -174,7 +179,7 @@
 
                 // Place the indicator
                 subcompose(TabRowSlots.Indicator) {
-                    indicator(tabPositions, isActivated)
+                    indicator(tabPositions, doesTabRowHaveFocus)
                 }
                     .fastForEach {
                         it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0)
@@ -203,7 +208,7 @@
      * Adds a pill indicator behind the tab
      *
      * @param currentTabPosition position of the current selected tab
-     * @param isActivated whether any tab in TabRow is focused
+     * @param doesTabRowHaveFocus whether any tab in TabRow is focused
      * @param modifier modifier to be applied to the indicator
      * @param activeColor color of indicator when [TabRow] is active
      * @param inactiveColor color of indicator when [TabRow] is inactive
@@ -211,7 +216,7 @@
     @Composable
     fun PillIndicator(
         currentTabPosition: DpRect,
-        isActivated: Boolean,
+        doesTabRowHaveFocus: Boolean,
         modifier: Modifier = Modifier,
         activeColor: Color = MaterialTheme.colorScheme.onSurface,
         inactiveColor: Color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.4f)
@@ -229,7 +234,7 @@
 
         val pillColor by
         animateColorAsState(
-            targetValue = if (isActivated) activeColor else inactiveColor,
+            targetValue = if (doesTabRowHaveFocus) activeColor else inactiveColor,
             label = "PillIndicator.pillColor"
         )
 
@@ -249,7 +254,7 @@
      * Adds an underlined indicator below the tab
      *
      * @param currentTabPosition position of the current selected tab
-     * @param isActivated whether any tab in TabRow is focused
+     * @param doesTabRowHaveFocus whether any tab in TabRow is focused
      * @param modifier modifier to be applied to the indicator
      * @param activeColor color of indicator when [TabRow] is active
      * @param inactiveColor color of indicator when [TabRow] is inactive
@@ -257,7 +262,7 @@
     @Composable
     fun UnderlinedIndicator(
         currentTabPosition: DpRect,
-        isActivated: Boolean,
+        doesTabRowHaveFocus: Boolean,
         modifier: Modifier = Modifier,
         activeColor: Color = MaterialTheme.colorScheme.primary,
         inactiveColor: Color = MaterialTheme.colorScheme.secondary,
@@ -267,7 +272,7 @@
         val width by
         animateDpAsState(
             targetValue =
-            if (isActivated)
+            if (doesTabRowHaveFocus)
                 currentTabPosition.width
             else
                 unfocusedUnderlineWidth,
@@ -276,7 +281,7 @@
         val leftOffset by
         animateDpAsState(
             targetValue =
-            if (isActivated) {
+            if (doesTabRowHaveFocus) {
                 currentTabPosition.left
             } else {
                 val tabCenter = currentTabPosition.left + currentTabPosition.width / 2
@@ -287,7 +292,7 @@
 
         val underlineColor by
         animateColorAsState(
-            targetValue = if (isActivated) activeColor else inactiveColor,
+            targetValue = if (doesTabRowHaveFocus) activeColor else inactiveColor,
             label = "UnderlinedIndicator.underlineColor",
         )
 
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/TabRowScope.kt b/tv/tv-material/src/main/java/androidx/tv/material3/TabRowScope.kt
index 9db88b3..27b9848 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/TabRowScope.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/TabRowScope.kt
@@ -17,17 +17,18 @@
 package androidx.tv.material3
 
 /**
- * [TabRowScope] is used to provide the isActivated state to the [Tab] composable
+ * [TabRowScope] is used to provide the doesTabRowHaveFocus state to the [Tab] composable
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 interface TabRowScope {
     /**
-     * Whether any tab within the [TabRow] is focused
+     * Whether any [Tab] within the [TabRow] is focused
      */
-    val isActivated: Boolean
+    @get:Suppress("GetterSetterNames")
+    val hasFocus: Boolean
 }
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 internal class TabRowScopeImpl internal constructor(
-    override val isActivated: Boolean
+    override val hasFocus: Boolean
 ) : TabRowScope
diff --git a/viewpager2/integration-tests/testapp/build.gradle b/viewpager2/integration-tests/testapp/build.gradle
index 28dd268..bc3fc1d 100644
--- a/viewpager2/integration-tests/testapp/build.gradle
+++ b/viewpager2/integration-tests/testapp/build.gradle
@@ -14,13 +14,6 @@
  * limitations under the License.
  */
 
-buildscript {
-    // TODO: Remove this when this test app no longer depends on 1.0.0 of vectordrawable-animated.
-    // vectordrawable and vectordrawable-animated were accidentally using the same package name
-    // which is no longer valid in namespaced resource world.
-    project.ext["android.uniquePackageNames"] = false
-}
-
 plugins {
     id("AndroidXPlugin")
     id("com.android.application")
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/BasicCurvedTextTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/BasicCurvedTextTest.kt
index 6ea9e3c..8611081 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/BasicCurvedTextTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/BasicCurvedTextTest.kt
@@ -49,7 +49,7 @@
 
         rule.runOnIdle {
             // TODO(b/219885899): Investigate why we need the extra passes.
-            assertEquals(CapturedInfo(2, 3, 2), capturedInfo)
+            assertEquals(CapturedInfo(2, 3, 1), capturedInfo)
         }
     }
 }
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
index c07beb5..9b8dbb5 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
@@ -18,11 +18,16 @@
 
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.TouchInjectionScope
 import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
@@ -138,6 +143,148 @@
     }
 
     @Test
+    fun onSwipe_whenNotAllowed_doesNotSwipe() {
+        lateinit var revealState: RevealState
+        rule.setContent {
+            revealState = rememberRevealState(
+                confirmValueChange = { revealValue ->
+                    revealValue != RevealValue.Revealing
+                }
+            )
+            swipeToRevealWithDefaults(state = revealState, modifier = Modifier.testTag(TEST_TAG))
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeLeft(startX = width / 2f, endX = 0f) }
+
+        rule.runOnIdle {
+            assertEquals(RevealValue.Covered, revealState.currentValue)
+        }
+    }
+
+    @Test
+    fun onMultiSwipe_whenNotAllowed_doesNotReset() {
+        lateinit var revealStateOne: RevealState
+        lateinit var revealStateTwo: RevealState
+        val testTagOne = "testTagOne"
+        val testTagTwo = "testTagTwo"
+        rule.setContent {
+            revealStateOne = rememberRevealState()
+            revealStateTwo = rememberRevealState(
+                confirmValueChange = { revealValue -> revealValue != RevealValue.Revealing }
+            )
+            Column {
+                swipeToRevealWithDefaults(
+                    state = revealStateOne,
+                    modifier = Modifier.testTag(testTagOne)
+                )
+                swipeToRevealWithDefaults(
+                    state = revealStateTwo,
+                    modifier = Modifier.testTag(testTagTwo)
+                )
+            }
+        }
+
+        // swipe the first S2R
+        rule.onNodeWithTag(testTagOne).performTouchInput {
+            swipeLeft(startX = width / 2f, endX = 0f)
+        }
+
+        // swipe the second S2R to a reveal value which is not allowed
+        rule.onNodeWithTag(testTagTwo).performTouchInput {
+            swipeLeft(startX = width / 2f, endX = 0f)
+        }
+
+        rule.runOnIdle {
+            assertEquals(RevealValue.Revealing, revealStateOne.currentValue)
+            assertEquals(RevealValue.Covered, revealStateTwo.currentValue)
+        }
+    }
+
+    @Test
+    fun onMultiSwipe_whenAllowed_resetsLastState() {
+        lateinit var revealStateOne: RevealState
+        lateinit var revealStateTwo: RevealState
+        val testTagOne = "testTagOne"
+        val testTagTwo = "testTagTwo"
+        rule.setContent {
+            revealStateOne = rememberRevealState()
+            revealStateTwo = rememberRevealState()
+            Column {
+                swipeToRevealWithDefaults(
+                    state = revealStateOne,
+                    modifier = Modifier.testTag(testTagOne)
+                )
+                swipeToRevealWithDefaults(
+                    state = revealStateTwo,
+                    modifier = Modifier.testTag(testTagTwo)
+                )
+            }
+        }
+
+        // swipe the first S2R
+        rule.onNodeWithTag(testTagOne).performTouchInput {
+            swipeLeft(startX = width / 2f, endX = 0f)
+        }
+
+        // swipe the second S2R to a reveal value
+        rule.onNodeWithTag(testTagTwo).performTouchInput {
+            swipeLeft(startX = width / 2f, endX = 0f)
+        }
+
+        rule.runOnIdle {
+            assertEquals(RevealValue.Covered, revealStateOne.currentValue)
+            assertEquals(RevealValue.Revealing, revealStateTwo.currentValue)
+        }
+    }
+
+    @Test
+    fun onSnapForDifferentStates_lastOneGetsReset() {
+        lateinit var revealStateOne: RevealState
+        lateinit var revealStateTwo: RevealState
+        rule.setContent {
+            revealStateOne = rememberRevealState()
+            revealStateTwo = rememberRevealState()
+            swipeToRevealWithDefaults(state = revealStateOne)
+            swipeToRevealWithDefaults(state = revealStateTwo)
+
+            val coroutineScope = rememberCoroutineScope()
+            coroutineScope.launch {
+                // First change
+                revealStateOne.snapTo(RevealValue.Revealing)
+                // Second change, in a different state
+                revealStateTwo.snapTo(RevealValue.Revealing)
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(RevealValue.Covered, revealStateOne.currentValue)
+        }
+    }
+
+    @Test
+    fun onMultiSnapOnSameState_doesNotReset() {
+        lateinit var revealStateOne: RevealState
+        lateinit var revealStateTwo: RevealState
+        val lastValue = RevealValue.Revealed
+        rule.setContent {
+            revealStateOne = rememberRevealState()
+            revealStateTwo = rememberRevealState()
+            swipeToRevealWithDefaults(state = revealStateOne)
+            swipeToRevealWithDefaults(state = revealStateTwo)
+
+            val coroutineScope = rememberCoroutineScope()
+            coroutineScope.launch {
+                revealStateOne.snapTo(RevealValue.Revealing) // First change
+                revealStateOne.snapTo(lastValue) // Second change, same state
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(lastValue, revealStateOne.currentValue)
+        }
+    }
+
+    @Test
     fun onSecondaryActionClick_setsLastClickAction() = verifyLastClickAction(
         expectedClickType = RevealActionType.SecondaryAction,
         initialRevealValue = RevealValue.Revealing,
@@ -158,6 +305,64 @@
         undoActionModifier = Modifier.testTag(TEST_TAG)
     )
 
+    @Test
+    fun onRightSwipe_dispatchEventsToParent() {
+        var onPreScrollDispatch = 0f
+        rule.setContent {
+            val nestedScrollConnection = remember {
+                object : NestedScrollConnection {
+                    override fun onPreScroll(
+                        available: Offset,
+                        source: NestedScrollSource
+                    ): Offset {
+                        onPreScrollDispatch = available.x
+                        return available
+                    }
+                }
+            }
+            Box(
+                modifier = Modifier.nestedScroll(nestedScrollConnection)
+            ) {
+                swipeToRevealWithDefaults(
+                    modifier = Modifier.testTag(TEST_TAG)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+
+        assert(onPreScrollDispatch > 0)
+    }
+
+    @Test
+    fun onLeftSwipe_dispatchEventsToParent() {
+        var onPreScrollDispatch = 0f
+        rule.setContent {
+            val nestedScrollConnection = remember {
+                object : NestedScrollConnection {
+                    override fun onPreScroll(
+                        available: Offset,
+                        source: NestedScrollSource
+                    ): Offset {
+                        onPreScrollDispatch = available.x
+                        return available
+                    }
+                }
+            }
+            Box(
+                modifier = Modifier.nestedScroll(nestedScrollConnection)
+            ) {
+                swipeToRevealWithDefaults(
+                    modifier = Modifier.testTag(TEST_TAG)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeLeft() }
+
+        assert(onPreScrollDispatch < 0) // Swiping left means the dispatch will be negative
+    }
+
     private fun verifyLastClickAction(
         expectedClickType: RevealActionType,
         initialRevealValue: RevealValue,
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeableV2Test.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeableV2Test.kt
index 9f96a9d..5c8e950 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeableV2Test.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeableV2Test.kt
@@ -17,13 +17,22 @@
 package androidx.wear.compose.foundation
 
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.ViewConfiguration
@@ -32,15 +41,23 @@
 import androidx.compose.ui.semantics.SemanticsProperties.VerticalScrollAxisRange
 import androidx.compose.ui.semantics.getOrNull
 import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.TouchInjectionScope
 import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.test.swipeUp
 import androidx.compose.ui.unit.dp
 import kotlin.math.absoluteValue
 import org.junit.Rule
 import org.junit.Test
 
+internal const val CHILD_TEST_TAG = "childTestTag"
+
 // TODO(b/201009199) Some of these tests may need specific values adjusted when swipeableV2
 // supports property nested scrolling, but the tests should all still be valid.
 @OptIn(ExperimentalWearFoundationApi::class)
@@ -221,6 +238,260 @@
             .assert(SemanticsMatcher.keyNotDefined(VerticalScrollAxisRange))
     }
 
+    @Test
+    fun onSwipeLeft_sendsPreScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeLeft() },
+            consumePreScrollDelta = { offset ->
+                delta = offset.x
+            }
+        )
+
+        assert(delta < 0) {
+            "Expected delta to be negative, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeRight_sendsPreScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeRight() },
+            consumePreScrollDelta = { offset ->
+                delta = offset.x
+            }
+        )
+
+        assert(delta > 0) {
+            "Expected delta to be positive, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeUp_sendsPreScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeUp() },
+            consumePreScrollDelta = { offset ->
+                delta = offset.y
+            },
+            orientation = Orientation.Vertical
+        )
+
+        assert(delta < 0) {
+            "Expected delta to be negative, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeDown_sendsPreScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeDown() },
+            consumePreScrollDelta = { offset ->
+                delta = offset.y
+            },
+            orientation = Orientation.Vertical
+        )
+
+        assert(delta > 0) {
+            "Expected delta to be positive, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeLeft_sendsPostScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeLeft() },
+            consumePostScrollDelta = { offset ->
+                delta = offset.x
+            }
+        )
+
+        assert(delta < 0) {
+            "Expected delta to be negative, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeRight_sendsPostScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeRight() },
+            consumePostScrollDelta = { offset ->
+                delta = offset.x
+            },
+            reverseAnchors = true //  reverse anchors or else swipeable consumes whole delta
+        )
+
+        assert(delta > 0) {
+            "Expected delta to be positive, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeUp_sendsPostScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeUp() },
+            consumePostScrollDelta = { offset ->
+                delta = offset.y
+            },
+            orientation = Orientation.Vertical
+        )
+
+        assert(delta < 0) {
+            "Expected delta to be negative, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeDown_sendsPostScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeDown() },
+            consumePostScrollDelta = { offset ->
+                delta = offset.y
+            },
+            orientation = Orientation.Vertical,
+            reverseAnchors = true //  reverse anchors or else swipeable consumes whole delta
+        )
+
+        assert(delta > 0) {
+            "Expected delta to be positive, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeLeftToChild_sendsPreScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeLeft() },
+            consumePreScrollDelta = { offset ->
+                delta = offset.x
+            },
+            testTag = CHILD_TEST_TAG
+        )
+
+        assert(delta < 0) {
+            "Expected delta to be negative, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeRightToChild_sendsPreScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeRight() },
+            consumePreScrollDelta = { offset ->
+                delta = offset.x
+            },
+            testTag = CHILD_TEST_TAG
+        )
+
+        assert(delta > 0) {
+            "Expected delta to be positive, was $delta"
+        }
+    }
+
+    private fun ComposeContentTestRule.testSwipe(
+        touchInput: TouchInjectionScope.() -> Unit,
+        consumePreScrollDelta: (Offset) -> Unit = {},
+        consumePostScrollDelta: (Offset) -> Unit = {},
+        orientation: Orientation = Orientation.Horizontal,
+        reverseAnchors: Boolean = false,
+        testTag: String = TEST_TAG
+    ) {
+        setContent {
+            val nestedScrollConnection = remember {
+                object : NestedScrollConnection {
+                    override fun onPreScroll(
+                        available: Offset,
+                        source: NestedScrollSource
+                    ): Offset {
+                        consumePreScrollDelta(available)
+                        return super.onPreScroll(available, source)
+                    }
+
+                    override fun onPostScroll(
+                        consumed: Offset,
+                        available: Offset,
+                        source: NestedScrollSource
+                    ): Offset {
+                        consumePostScrollDelta(available)
+                        return super.onPostScroll(consumed, available, source)
+                    }
+                }
+            }
+            Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
+                SwipeableContent(
+                    orientation = orientation,
+                    reverseAnchors = reverseAnchors,
+                    modifier = Modifier.testTag(TEST_TAG)
+                ) {
+                    Box(modifier = Modifier
+                        .fillMaxSize()
+                        .testTag(CHILD_TEST_TAG)
+                        .nestedScroll(remember { object : NestedScrollConnection {} })
+                        .scrollable(
+                            state = rememberScrollableState { _ ->
+                                0f // Do not consume any delta, just return it
+                            },
+                            orientation = orientation
+                        )
+                    )
+                }
+            }
+        }
+
+        onNodeWithTag(testTag).performTouchInput { touchInput() }
+    }
+
+    @Composable
+    private fun SwipeableContent(
+        modifier: Modifier = Modifier,
+        orientation: Orientation = Orientation.Horizontal,
+        reverseAnchors: Boolean = false,
+        content: @Composable BoxScope.() -> Unit = {}
+    ) {
+        // To participate as a producer of scroll events
+        val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
+        // To participate as a consumer of scroll events
+        val nestedScrollConnection = remember { object : NestedScrollConnection {} }
+        val swipeableV2State = remember {
+            SwipeableV2State(
+                initialValue = false,
+                nestedScrollDispatcher = nestedScrollDispatcher
+            )
+        }
+        val factor = if (reverseAnchors) -1 else 1
+        Box(
+            modifier = modifier
+                .fillMaxSize()
+                .nestedScroll(nestedScrollConnection)
+                .swipeableV2(
+                    swipeableV2State,
+                    orientation
+                )
+                .swipeAnchors(
+                    state = swipeableV2State,
+                    possibleValues = setOf(false, true)
+                ) { value, layoutSize ->
+                    when (value) {
+                        false -> 0f
+                        true -> factor * (
+                            if (orientation == Orientation.Horizontal) layoutSize.width.toFloat()
+                            else layoutSize.height.toFloat()
+                            )
+                    }
+                }
+                .nestedScroll(nestedScrollConnection, nestedScrollDispatcher),
+            content = content
+        )
+    }
+
     /**
      * A square [Box] has the [TEST_TAG] test tag. Touch slop is disabled to make swipe calculations
      * more exact.
diff --git a/wear/compose/compose-foundation/src/main/baseline-prof.txt b/wear/compose/compose-foundation/src/main/baseline-prof.txt
index 00329b5..edfde2d 100644
--- a/wear/compose/compose-foundation/src/main/baseline-prof.txt
+++ b/wear/compose/compose-foundation/src/main/baseline-prof.txt
@@ -30,21 +30,28 @@
 SPLandroidx/wear/compose/foundation/ExpandableItemsDefaults;->**(**)**
 HSPLandroidx/wear/compose/foundation/ExpandableKt**->**(**)**
 HSPLandroidx/wear/compose/foundation/ExpandableState;->**(**)**
-PLandroidx/wear/compose/foundation/FocusNode;->**(**)**
-HPLandroidx/wear/compose/foundation/HierarchicalFocusCoordinatorKt**->**(**)**
-PLandroidx/wear/compose/foundation/InternalMutatorMutex;->**(**)**
+SPLandroidx/wear/compose/foundation/FocusNode;->**(**)**
+HSPLandroidx/wear/compose/foundation/HierarchicalFocusCoordinatorKt**->**(**)**
+SPLandroidx/wear/compose/foundation/InternalMutatorMutex;->**(**)**
+HSPLandroidx/wear/compose/foundation/Modifiers;->**(**)**
 HSPLandroidx/wear/compose/foundation/PaddingWrapper;->**(**)**
 HSPLandroidx/wear/compose/foundation/PartialLayoutInfo;->**(**)**
 Landroidx/wear/compose/foundation/ReduceMotion;
+HSPLandroidx/wear/compose/foundation/ResourcesKt**->**(**)**
+PLandroidx/wear/compose/foundation/RevealActionType;->**(**)**
 HPLandroidx/wear/compose/foundation/RevealScopeImpl;->**(**)**
 HPLandroidx/wear/compose/foundation/RevealState;->**(**)**
 HPLandroidx/wear/compose/foundation/RevealValue;->**(**)**
-HPLandroidx/wear/compose/foundation/SwipeAnchorsModifier;->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeAnchorsModifier;->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeToDismissBoxKt**->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeToDismissBoxState;->**(**)**
+SPLandroidx/wear/compose/foundation/SwipeToDismissKeys;->**(**)**
+SPLandroidx/wear/compose/foundation/SwipeToDismissValue;->**(**)**
 PLandroidx/wear/compose/foundation/SwipeToRevealDefaults;->**(**)**
 HPLandroidx/wear/compose/foundation/SwipeToRevealKt**->**(**)**
-PLandroidx/wear/compose/foundation/SwipeableV2Defaults;->**(**)**
-HPLandroidx/wear/compose/foundation/SwipeableV2Kt**->**(**)**
-HPLandroidx/wear/compose/foundation/SwipeableV2State;->**(**)**
+SPLandroidx/wear/compose/foundation/SwipeableV2Defaults;->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeableV2Kt**->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeableV2State;->**(**)**
 SPLandroidx/wear/compose/foundation/lazy/AutoCenteringParams;->**(**)**
 HSPLandroidx/wear/compose/foundation/lazy/CombinedPaddingValues;->**(**)**
 HSPLandroidx/wear/compose/foundation/lazy/DefaultScalingLazyListItemInfo;->**(**)**
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
index 4a3185b..50f3c3b 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
@@ -45,17 +45,25 @@
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.AbsoluteAlignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
+import androidx.core.util.Predicate
+import java.util.concurrent.atomic.AtomicReference
 import kotlin.math.abs
 import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 
 /**
  * Short animation in milliseconds.
@@ -176,7 +184,9 @@
     animationSpec: AnimationSpec<Float>,
     confirmValueChange: (RevealValue) -> Boolean,
     positionalThreshold: Density.(totalDistance: Float) -> Float,
-    internal val anchors: Map<RevealValue, Float>
+    internal val anchors: Map<RevealValue, Float>,
+    internal val coroutineScope: CoroutineScope,
+    internal val nestedScrollDispatcher: NestedScrollDispatcher
 ) {
     /**
      * [SwipeableV2State] internal instance for the state.
@@ -184,8 +194,14 @@
     internal val swipeableState = SwipeableV2State(
         initialValue = initialValue,
         animationSpec = animationSpec,
-        confirmValueChange = confirmValueChange,
-        positionalThreshold = positionalThreshold
+        confirmValueChange = { revealValue ->
+            confirmValueChangeAndReset(
+                confirmValueChange,
+                revealValue
+            )
+        },
+        positionalThreshold = positionalThreshold,
+        nestedScrollDispatcher = nestedScrollDispatcher
     )
 
     public var lastActionType by mutableStateOf(RevealActionType.None)
@@ -242,7 +258,13 @@
      *
      * @see Modifier.swipeableV2
      */
-    public suspend fun snapTo(targetValue: RevealValue) = swipeableState.snapTo(targetValue)
+    public suspend fun snapTo(targetValue: RevealValue) {
+        // Cover the previously open component if revealing a different one
+        if (targetValue != RevealValue.Covered) {
+            resetLastState(this)
+        }
+        swipeableState.snapTo(targetValue)
+    }
 
     /**
      * Animates to the [targetValue] with the animation spec provided.
@@ -250,7 +272,13 @@
      * @param targetValue The target [RevealValue] where the [currentValue] will animate
      * to.
      */
-    public suspend fun animateTo(targetValue: RevealValue) = swipeableState.animateTo(targetValue)
+    public suspend fun animateTo(targetValue: RevealValue) {
+        // Cover the previously open component if revealing a different one
+        if (targetValue != RevealValue.Covered) {
+            resetLastState(this)
+        }
+        swipeableState.animateTo(targetValue)
+    }
 
     /**
      * Require the current offset.
@@ -258,6 +286,41 @@
      * @throws IllegalStateException If the offset has not been initialized yet
      */
     internal fun requireOffset(): Float = swipeableState.requireOffset()
+
+    private fun confirmValueChangeAndReset(
+        confirmValueChange: Predicate<RevealValue>,
+        revealValue: RevealValue,
+    ): Boolean {
+        val canChangeValue = confirmValueChange.test(revealValue)
+        val currentState = this
+        // Update the state if the reveal value is changing to a different value than Covered.
+        if (canChangeValue &&
+            revealValue != RevealValue.Covered) {
+            coroutineScope.launch {
+                resetLastState(currentState)
+            }
+        }
+        return canChangeValue
+    }
+
+    /**
+     * Resets last state if a different SwipeToReveal is being moved to new anchor.
+     */
+    private suspend fun resetLastState(
+        currentState: RevealState
+    ) {
+        val oldState = SingleSwipeCoordinator.lastUpdatedState.getAndSet(currentState)
+        if (currentState != oldState) {
+            oldState?.animateTo(RevealValue.Covered)
+        }
+    }
+
+    /**
+     * A singleton instance to keep track of the [RevealState] which was modified the last time.
+     */
+    private object SingleSwipeCoordinator {
+        var lastUpdatedState: AtomicReference<RevealState?> = AtomicReference(null)
+    }
 }
 
 /**
@@ -282,15 +345,19 @@
     confirmValueChange: (RevealValue) -> Boolean = { true },
     positionalThreshold: Density.(totalDistance: Float) -> Float =
         SwipeToRevealDefaults.defaultThreshold(),
-    anchors: Map<RevealValue, Float> = createAnchors()
+    anchors: Map<RevealValue, Float> = createAnchors(),
 ): RevealState {
+    val coroutineScope = rememberCoroutineScope()
+    val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
     return remember(initialValue, animationSpec) {
         RevealState(
             initialValue = initialValue,
             animationSpec = animationSpec,
             confirmValueChange = confirmValueChange,
             positionalThreshold = positionalThreshold,
-            anchors = anchors
+            anchors = anchors,
+            coroutineScope = coroutineScope,
+            nestedScrollDispatcher = nestedScrollDispatcher
         )
     }
 }
@@ -344,6 +411,8 @@
     content: @Composable () -> Unit
 ) {
     val revealScope = remember(state) { RevealScopeImpl(state) }
+    // A no-op NestedScrollConnection which does not consume scroll/fling events
+    val noOpNestedScrollConnection = remember { object : NestedScrollConnection {} }
     Box(
         modifier = modifier
             .swipeableV2(
@@ -361,6 +430,10 @@
                 // Multiply the anchor with -1f to get the actual swipeable anchor
                 -state.swipeAnchors[value]!! * swipeableWidth
             }
+            // NestedScrollDispatcher sends the scroll/fling events from the node to its parent
+            // and onwards including the modifier chain. Apply it in the end to let nested scroll
+            // connection applied before this modifier consume the scroll/fling events.
+            .nestedScroll(noOpNestedScrollConnection, state.nestedScrollDispatcher)
     ) {
         val swipeCompleted by remember {
             derivedStateOf { state.currentValue == RevealValue.Revealed }
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
index 784c92f..94200e9 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
@@ -39,6 +39,9 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.layout.LayoutModifier
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
@@ -55,6 +58,7 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import kotlin.math.abs
 import kotlinx.coroutines.CancellationException
@@ -126,6 +130,9 @@
         }
     }
 
+    // Update the orientation in the swipeable state
+    state.orientation = orientation
+
     return this.then(semantics).draggable(
         state = state.swipeDraggableState,
         orientation = orientation,
@@ -218,6 +225,7 @@
     internal val positionalThreshold: Density.(totalDistance: Float) -> Float =
         SwipeableV2Defaults.PositionalThreshold,
     internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
+    private val nestedScrollDispatcher: NestedScrollDispatcher? = null
 ) {
 
     private val swipeMutex = InternalMutatorMutex()
@@ -242,6 +250,11 @@
     }
 
     /**
+     * The orientation in which the swipeable can be swiped.
+     */
+    internal var orientation = Orientation.Horizontal
+
+    /**
      * The current value of the [SwipeableV2State].
      */
     var currentValue: T by mutableStateOf(initialValue)
@@ -427,17 +440,29 @@
      * Find the closest anchor taking into account the velocity and settle at it with an animation.
      */
     suspend fun settle(velocity: Float) {
+        var availableVelocity = velocity
+        // Dispatch the velocity to parent nodes for consuming
+        nestedScrollDispatcher?.let {
+            val consumedVelocity = nestedScrollDispatcher.dispatchPreFling(
+                if (orientation == Orientation.Horizontal) {
+                    Velocity(x = velocity, y = 0f)
+                } else {
+                    Velocity(x = 0f, y = velocity)
+                }
+            )
+            availableVelocity -= (consumedVelocity.x + consumedVelocity.y)
+        }
         val previousValue = this.currentValue
         val targetValue = computeTarget(
             offset = requireOffset(),
             currentValue = previousValue,
-            velocity = velocity
+            velocity = availableVelocity
         )
         if (confirmValueChange(targetValue)) {
-            animateTo(targetValue, velocity)
+            animateTo(targetValue, availableVelocity)
         } else {
             // If the user vetoed the state change, rollback to the previous state.
-            animateTo(previousValue, velocity)
+            animateTo(previousValue, availableVelocity)
         }
     }
 
@@ -447,14 +472,41 @@
      * @return The delta the consumed by the [SwipeableV2State]
      */
     fun dispatchRawDelta(delta: Float): Float {
+        var remainingDelta = delta
+
+        // Dispatch the delta as a scroll event to parent node for consuming it
+        nestedScrollDispatcher?.let {
+            val consumedByParent = nestedScrollDispatcher.dispatchPreScroll(
+                available = offsetWithOrientation(remainingDelta),
+                source = NestedScrollSource.Drag
+            )
+            remainingDelta -= (consumedByParent.x + consumedByParent.y)
+        }
         val currentDragPosition = offset ?: 0f
-        val potentiallyConsumed = currentDragPosition + delta
+        val potentiallyConsumed = currentDragPosition + remainingDelta
         val clamped = potentiallyConsumed.coerceIn(minOffset, maxOffset)
         val deltaToConsume = clamped - currentDragPosition
         if (abs(deltaToConsume) >= 0) {
             offset = ((offset ?: 0f) + deltaToConsume).coerceIn(minOffset, maxOffset)
         }
-        return deltaToConsume
+
+        nestedScrollDispatcher?.let {
+            val consumedDelta = nestedScrollDispatcher.dispatchPostScroll(
+                consumed = offsetWithOrientation(deltaToConsume),
+                available = offsetWithOrientation(delta - deltaToConsume),
+                source = NestedScrollSource.Drag
+            )
+            remainingDelta -= (deltaToConsume + consumedDelta.x + consumedDelta.y)
+        }
+        return remainingDelta
+    }
+
+    private fun offsetWithOrientation(delta: Float): Offset {
+        return if (orientation == Orientation.Horizontal) {
+            Offset(x = delta, y = 0f)
+        } else {
+            Offset(x = 0f, y = delta)
+        }
     }
 
     private fun computeTarget(
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/SwipeToRevealSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/SwipeToRevealSample.kt
new file mode 100644
index 0000000..e988234
--- /dev/null
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/SwipeToRevealSample.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.material.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
+import androidx.wear.compose.foundation.rememberRevealState
+import androidx.wear.compose.material.AppCard
+import androidx.wear.compose.material.CardDefaults
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.ExperimentalWearMaterialApi
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.SwipeToRevealCard
+import androidx.wear.compose.material.SwipeToRevealChip
+import androidx.wear.compose.material.SwipeToRevealDefaults
+import androidx.wear.compose.material.Text
+
+@OptIn(ExperimentalWearMaterialApi::class, ExperimentalWearFoundationApi::class)
+@Composable
+@Sampled
+fun SwipeToRevealChipSample() {
+    SwipeToRevealChip(
+        revealState = rememberRevealState(),
+        modifier = Modifier.fillMaxWidth(),
+        primaryAction = SwipeToRevealDefaults.primaryAction(
+            icon = { Icon(SwipeToRevealDefaults.Delete, "Delete") },
+            label = { Text("Delete") },
+            onClick = { /* Add the click handler here */ }
+        ),
+        secondaryAction = SwipeToRevealDefaults.secondaryAction(
+            icon = { Icon(SwipeToRevealDefaults.MoreOptions, "More Options") },
+            onClick = { /* Add the click handler here */ }
+        ),
+        undoPrimaryAction = SwipeToRevealDefaults.undoAction(
+            label = { Text("Undo") },
+            onClick = { /* Add the undo handler for primary action */ }
+        ),
+        undoSecondaryAction = SwipeToRevealDefaults.undoAction(
+            label = { Text("Undo") },
+            onClick = { /* Add the undo handler for secondary action */ }
+        )
+    ) {
+        Chip(
+            onClick = { /* Add the chip click handler here */ },
+            colors = ChipDefaults.primaryChipColors(),
+            border = ChipDefaults.outlinedChipBorder()
+        ) {
+            Text("SwipeToReveal Chip")
+        }
+    }
+}
+
+@OptIn(ExperimentalWearMaterialApi::class, ExperimentalWearFoundationApi::class)
+@Composable
+@Sampled
+fun SwipeToRevealCardSample() {
+    SwipeToRevealCard(
+        revealState = rememberRevealState(),
+        modifier = Modifier.fillMaxWidth(),
+        primaryAction = SwipeToRevealDefaults.primaryAction(
+            icon = { Icon(SwipeToRevealDefaults.Delete, "Delete") },
+            label = { Text("Delete") },
+            onClick = { /* Add the click handler here */ }
+        ),
+        secondaryAction = SwipeToRevealDefaults.secondaryAction(
+            icon = { Icon(SwipeToRevealDefaults.MoreOptions, "More Options") },
+            onClick = { /* Add the click handler here */ }
+        ),
+        undoPrimaryAction = SwipeToRevealDefaults.undoAction(
+            label = { Text("Undo") },
+            onClick = { /* Add the undo handler for primary action */ }
+        ),
+        undoSecondaryAction = SwipeToRevealDefaults.undoAction(
+            label = { Text("Undo") },
+            onClick = { /* Add the undo handler for secondary action */ }
+        )
+    ) {
+        AppCard(
+            onClick = { /* Add the Card click handler */ },
+            appName = { Text("AppName") },
+            appImage = {
+                Icon(
+                    painter = painterResource(id = R.drawable.ic_airplanemode_active_24px),
+                    contentDescription = "airplane",
+                    modifier = Modifier.size(CardDefaults.AppImageSize)
+                        .wrapContentSize(align = Alignment.Center),
+                )
+            },
+            title = { Text("AppCard") },
+            time = { Text("now") }
+        ) {
+            Text("Basic card with SwipeToReveal actions")
+        }
+    }
+}
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/MaterialTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/MaterialTest.kt
index 3cd42b0..8e2e2bc 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/MaterialTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/MaterialTest.kt
@@ -16,7 +16,9 @@
 package androidx.wear.compose.material
 
 import android.graphics.Bitmap
+import android.os.Build
 import android.util.Log
+import androidx.annotation.RequiresApi
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
@@ -25,6 +27,8 @@
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.Add
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Rect
@@ -33,10 +37,12 @@
 import androidx.compose.ui.graphics.asAndroidBitmap
 import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
@@ -45,11 +51,13 @@
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.DpRect
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.height
 import androidx.compose.ui.unit.isUnspecified
 import androidx.compose.ui.unit.toSize
 import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.screenshot.AndroidXScreenshotTestRule
 import java.io.File
 import java.io.FileOutputStream
 import java.io.IOException
@@ -209,6 +217,25 @@
     return this
 }
 
+@RequiresApi(Build.VERSION_CODES.O)
+internal fun ComposeContentTestRule.verifyScreenshot(
+    screenshotRule: AndroidXScreenshotTestRule,
+    methodName: String,
+    testTag: String = TEST_TAG,
+    layoutDirection: LayoutDirection = LayoutDirection.Ltr,
+    content: @Composable () -> Unit
+) {
+    setContentWithTheme {
+        CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+            content()
+        }
+    }
+
+    onNodeWithTag(testTag)
+        .captureToImage()
+        .assertAgainstGolden(screenshotRule, methodName)
+}
+
 /**
  * Asserts that the layout of this node has height equal to [expectedHeight].
  *
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToRevealScreenshotTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToRevealScreenshotTest.kt
new file mode 100644
index 0000000..9622c31
--- /dev/null
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToRevealScreenshotTest.kt
@@ -0,0 +1,234 @@
+/*
+ * 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.material
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
+import androidx.wear.compose.foundation.RevealActionType
+import androidx.wear.compose.foundation.RevealState
+import androidx.wear.compose.foundation.RevealValue
+import androidx.wear.compose.foundation.rememberRevealState
+import kotlinx.coroutines.launch
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@RequiresApi(Build.VERSION_CODES.O)
+@OptIn(ExperimentalWearMaterialApi::class, ExperimentalWearFoundationApi::class)
+class SwipeToRevealScreenshotTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
+
+    @get:Rule
+    val testName = TestName()
+
+    @Test
+    fun swipeToRevealCard_singleAction() {
+        rule.verifyScreenshot(
+            screenshotRule = screenshotRule,
+            methodName = testName.methodName
+        ) {
+            swipeToRevealCard(
+                revealState = rememberRevealState(initialValue = RevealValue.Revealing),
+                secondaryAction = null
+            )
+        }
+    }
+
+    @Test
+    fun swipeToRevealChip_singleAction() {
+        rule.verifyScreenshot(
+            screenshotRule = screenshotRule,
+            methodName = testName.methodName
+        ) {
+            swipeToRevealChip(
+                revealState = rememberRevealState(initialValue = RevealValue.Revealing),
+                secondaryAction = null
+            )
+        }
+    }
+
+    @Test
+    fun swipeToRevealCard_twoActions() {
+        rule.verifyScreenshot(
+            screenshotRule = screenshotRule,
+            methodName = testName.methodName
+        ) {
+            swipeToRevealCard(
+                revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+            )
+        }
+    }
+
+    @Test
+    fun swipeToRevealChip_twoActions() {
+        rule.verifyScreenshot(
+            screenshotRule = screenshotRule,
+            methodName = testName.methodName
+        ) {
+            swipeToRevealChip(
+                revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+            )
+        }
+    }
+
+    @Test
+    fun swipeToRevealChip_undoPrimaryAction() {
+        rule.verifyScreenshot(
+            screenshotRule = screenshotRule,
+            methodName = testName.methodName
+        ) {
+            swipeToRevealChip(
+                revealState = rememberRevealState(initialValue = RevealValue.Revealed)
+            )
+        }
+    }
+
+    @Test
+    fun swipeToRevealCard_undoPrimaryAction() {
+        rule.verifyScreenshot(
+            screenshotRule = screenshotRule,
+            methodName = testName.methodName
+        ) {
+            swipeToRevealCard(
+                revealState = rememberRevealState(initialValue = RevealValue.Revealed)
+            )
+        }
+    }
+
+    @Test
+    fun swipeToRevealChip_undoSecondaryAction() {
+        rule.verifyScreenshot(
+            screenshotRule = screenshotRule,
+            methodName = testName.methodName
+        ) {
+            val revealState = rememberRevealState()
+            val coroutineScope = rememberCoroutineScope()
+            coroutineScope.launch { revealState.animateTo(RevealValue.Revealed) }
+            revealState.lastActionType = RevealActionType.SecondaryAction
+            swipeToRevealChip(
+                revealState = revealState
+            )
+        }
+    }
+
+    @Test
+    fun swipeToRevealCard_undoSecondaryAction() {
+        rule.verifyScreenshot(
+            screenshotRule = screenshotRule,
+            methodName = testName.methodName
+        ) {
+            val revealState = rememberRevealState()
+            val coroutineScope = rememberCoroutineScope()
+            coroutineScope.launch { revealState.animateTo(RevealValue.Revealed) }
+            revealState.lastActionType = RevealActionType.SecondaryAction
+            swipeToRevealCard(
+                revealState = revealState
+            )
+        }
+    }
+
+    @Composable
+    private fun swipeToRevealCard(
+        revealState: RevealState = rememberRevealState(),
+        secondaryAction: SwipeToRevealAction? = SwipeToRevealDefaults.secondaryAction(
+            icon = { Icon(SwipeToRevealDefaults.MoreOptions, "More Options") }
+        ),
+        undoPrimaryAction: SwipeToRevealAction? = SwipeToRevealDefaults.undoAction(
+            label = { Text("Undo") }
+        ),
+        undoSecondaryAction: SwipeToRevealAction? = SwipeToRevealDefaults.undoAction(
+            label = { Text("Undo") }
+        )
+    ) {
+        Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background)) {
+            SwipeToRevealCard(
+                modifier = Modifier.testTag(TEST_TAG),
+                primaryAction = SwipeToRevealDefaults.primaryAction(
+                    icon = { Icon(SwipeToRevealDefaults.Delete, "Delete") },
+                    label = { Text("Delete") }
+                ),
+                secondaryAction = secondaryAction,
+                undoPrimaryAction = undoPrimaryAction,
+                undoSecondaryAction = undoSecondaryAction,
+                revealState = revealState
+            ) {
+                TitleCard(
+                    onClick = { /*TODO*/ },
+                    title = { Text("Title of card") },
+                    time = { Text("now") },
+                ) {
+                    Text("Swipe To Reveal - Card")
+                }
+            }
+        }
+    }
+
+    @Composable
+    private fun swipeToRevealChip(
+        revealState: RevealState = rememberRevealState(),
+        secondaryAction: SwipeToRevealAction? = SwipeToRevealDefaults.secondaryAction(
+            icon = { Icon(SwipeToRevealDefaults.MoreOptions, "More Options") }
+        ),
+        undoPrimaryAction: SwipeToRevealAction? = SwipeToRevealDefaults.undoAction(
+            label = { Text("Undo") }
+        ),
+        undoSecondaryAction: SwipeToRevealAction? = SwipeToRevealDefaults.undoAction(
+            label = { Text("Undo") }
+        )
+    ) {
+        Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background)) {
+            SwipeToRevealChip(
+                modifier = Modifier.testTag(TEST_TAG),
+                primaryAction = SwipeToRevealDefaults.primaryAction(
+                    icon = { Icon(SwipeToRevealDefaults.Delete, "Delete") },
+                    label = { Text("Delete") }
+                ),
+                secondaryAction = secondaryAction,
+                undoPrimaryAction = undoPrimaryAction,
+                undoSecondaryAction = undoSecondaryAction,
+                revealState = revealState
+            ) {
+                Chip(
+                    onClick = { /* onClick handler for chip */ },
+                    colors = ChipDefaults.primaryChipColors(),
+                    border = ChipDefaults.outlinedChipBorder()
+                ) {
+                    Text("Swipe To Reveal - Chip")
+                }
+            }
+        }
+    }
+}
diff --git a/wear/compose/compose-material/src/main/baseline-prof.txt b/wear/compose/compose-material/src/main/baseline-prof.txt
index de41148..8c7b94c 100644
--- a/wear/compose/compose-material/src/main/baseline-prof.txt
+++ b/wear/compose/compose-material/src/main/baseline-prof.txt
@@ -67,7 +67,7 @@
 HPLandroidx/wear/compose/material/PlaceholderKt**->**(**)**
 PLandroidx/wear/compose/material/PlaceholderModifier;->**(**)**
 HPLandroidx/wear/compose/material/PlaceholderShimmerModifier;->**(**)**
-PLandroidx/wear/compose/material/PlaceholderStage;->**(**)**
+HPLandroidx/wear/compose/material/PlaceholderStage;->**(**)**
 HPLandroidx/wear/compose/material/PlaceholderState;->**(**)**
 HSPLandroidx/wear/compose/material/PositionIndicatorAlignment;->**(**)**
 HSPLandroidx/wear/compose/material/PositionIndicatorKt**->**(**)**
@@ -146,7 +146,12 @@
 HSPLandroidx/wear/compose/materialcore/IconKt**->**(**)**
 HPLandroidx/wear/compose/materialcore/RangeDefaults;->**(**)**
 PLandroidx/wear/compose/materialcore/RangeIcons;->**(**)**
+HPLandroidx/wear/compose/materialcore/RepeatableClickableKt**->**(**)**
+HSPLandroidx/wear/compose/materialcore/ResourcesKt**->**(**)**
+HPLandroidx/wear/compose/materialcore/SelectionControlsKt**->**(**)**
+PLandroidx/wear/compose/materialcore/SelectionStage;->**(**)**
 HPLandroidx/wear/compose/materialcore/SliderKt**->**(**)**
 PLandroidx/wear/compose/materialcore/StepperDefaults;->**(**)**
 HPLandroidx/wear/compose/materialcore/StepperKt**->**(**)**
 HSPLandroidx/wear/compose/materialcore/TextKt**->**(**)**
+HSPLandroidx/wear/compose/materialcore/ToggleButtonKt**->**(**)**
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/SwipeToReveal.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/SwipeToReveal.kt
index 1d1e5ea..56ede21 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/SwipeToReveal.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/SwipeToReveal.kt
@@ -55,6 +55,9 @@
 /**
  * [SwipeToReveal] Material composable for Chips. This provides the default style for consistency.
  *
+ * Example of [SwipeToRevealChip] with primary and secondary actions
+ * @sample androidx.wear.compose.material.samples.SwipeToRevealChipSample
+ *
  * @param primaryAction A [SwipeToRevealAction] instance to describe the primary action when
  * swiping. See [SwipeToRevealDefaults.primaryAction]. The action will be triggered on click or a
  * full swipe.
@@ -105,6 +108,9 @@
 /**
  * [SwipeToReveal] Material composable for Cards. This provides the default style for consistency.
  *
+ * Example of [SwipeToRevealCard] with primary and secondary actions
+ * @sample androidx.wear.compose.material.samples.SwipeToRevealCardSample
+ *
  * @param primaryAction A [SwipeToRevealAction] instance to describe the primary action when
  * swiping. See [SwipeToRevealDefaults.primaryAction]. The action will be triggered on click or a
  * full swipe.
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index be867cc..9b379c2 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -91,6 +91,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class CheckboxColors {
+    ctor public CheckboxColors(long checkedBoxColor, long checkedCheckmarkColor, long uncheckedBoxColor, long uncheckedCheckmarkColor, long disabledCheckedBoxColor, long disabledCheckedCheckmarkColor, long disabledUncheckedBoxColor, long disabledUncheckedCheckmarkColor);
     method public long getCheckedBoxColor();
     method public long getCheckedCheckmarkColor();
     method public long getDisabledCheckedBoxColor();
@@ -222,20 +223,20 @@
   }
 
   public final class IconButtonDefaults {
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors filledIconButtonColors(optional long containerColor, optional long contentColor);
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors filledTonalIconButtonColors(optional long containerColor, optional long contentColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors filledIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors filledTonalIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public float getDefaultButtonSize();
     method public float getDefaultIconSize();
     method public float getExtraSmallButtonSize();
     method public float getLargeButtonSize();
     method public float getLargeIconSize();
-    method public androidx.compose.foundation.shape.RoundedCornerShape getShape();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
     method public float getSmallButtonSize();
     method public float getSmallIconSize();
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public float iconSizeFor(float size);
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors iconToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors outlinedIconButtonColors(optional long contentColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors outlinedIconButtonColors(optional long contentColor, optional long disabledContentColor);
     property public final float DefaultButtonSize;
     property public final float DefaultIconSize;
     property public final float ExtraSmallButtonSize;
@@ -243,7 +244,7 @@
     property public final float LargeIconSize;
     property public final float SmallButtonSize;
     property public final float SmallIconSize;
-    property public final androidx.compose.foundation.shape.RoundedCornerShape shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.wear.compose.material3.IconButtonDefaults INSTANCE;
   }
 
@@ -353,6 +354,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class RadioButtonColors {
+    ctor public RadioButtonColors(long selectedColor, long unselectedColor, long disabledSelectedColor, long disabledUnselectedColor);
     method public long getDisabledSelectedColor();
     method public long getDisabledUnselectedColor();
     method public long getSelectedColor();
@@ -431,6 +433,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class SwitchColors {
+    ctor public SwitchColors(long checkedThumbColor, long checkedThumbIconColor, long checkedTrackColor, long checkedTrackBorderColor, long uncheckedThumbColor, long uncheckedThumbIconColor, long uncheckedTrackColor, long uncheckedTrackBorderColor, long disabledCheckedThumbColor, long disabledCheckedThumbIconColor, long disabledCheckedTrackColor, long disabledCheckedTrackBorderColor, long disabledUncheckedThumbColor, long disabledUncheckedThumbIconColor, long disabledUncheckedTrackColor, long disabledUncheckedTrackBorderColor);
     method public long getCheckedThumbColor();
     method public long getCheckedThumbIconColor();
     method public long getCheckedTrackBorderColor();
@@ -466,7 +469,7 @@
   }
 
   public final class SwitchDefaults {
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.SwitchColors colors(optional long checkedThumbColor, optional long checkedThumbIconColor, optional long checkedTrackColor, optional long checkedTrackStrokeColor, optional long uncheckedThumbColor, optional long uncheckedThumbIconColor, optional long uncheckedTrackColor, optional long uncheckedTrackStrokeColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.SwitchColors colors(optional long checkedThumbColor, optional long checkedThumbIconColor, optional long checkedTrackColor, optional long checkedTrackBorderColor, optional long uncheckedThumbColor, optional long uncheckedThumbIconColor, optional long uncheckedTrackColor, optional long uncheckedTrackBorderColor);
     field public static final androidx.wear.compose.material3.SwitchDefaults INSTANCE;
   }
 
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index be867cc..9b379c2 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -91,6 +91,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class CheckboxColors {
+    ctor public CheckboxColors(long checkedBoxColor, long checkedCheckmarkColor, long uncheckedBoxColor, long uncheckedCheckmarkColor, long disabledCheckedBoxColor, long disabledCheckedCheckmarkColor, long disabledUncheckedBoxColor, long disabledUncheckedCheckmarkColor);
     method public long getCheckedBoxColor();
     method public long getCheckedCheckmarkColor();
     method public long getDisabledCheckedBoxColor();
@@ -222,20 +223,20 @@
   }
 
   public final class IconButtonDefaults {
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors filledIconButtonColors(optional long containerColor, optional long contentColor);
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors filledTonalIconButtonColors(optional long containerColor, optional long contentColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors filledIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors filledTonalIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public float getDefaultButtonSize();
     method public float getDefaultIconSize();
     method public float getExtraSmallButtonSize();
     method public float getLargeButtonSize();
     method public float getLargeIconSize();
-    method public androidx.compose.foundation.shape.RoundedCornerShape getShape();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
     method public float getSmallButtonSize();
     method public float getSmallIconSize();
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public float iconSizeFor(float size);
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors iconToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors outlinedIconButtonColors(optional long contentColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors outlinedIconButtonColors(optional long contentColor, optional long disabledContentColor);
     property public final float DefaultButtonSize;
     property public final float DefaultIconSize;
     property public final float ExtraSmallButtonSize;
@@ -243,7 +244,7 @@
     property public final float LargeIconSize;
     property public final float SmallButtonSize;
     property public final float SmallIconSize;
-    property public final androidx.compose.foundation.shape.RoundedCornerShape shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.wear.compose.material3.IconButtonDefaults INSTANCE;
   }
 
@@ -353,6 +354,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class RadioButtonColors {
+    ctor public RadioButtonColors(long selectedColor, long unselectedColor, long disabledSelectedColor, long disabledUnselectedColor);
     method public long getDisabledSelectedColor();
     method public long getDisabledUnselectedColor();
     method public long getSelectedColor();
@@ -431,6 +433,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class SwitchColors {
+    ctor public SwitchColors(long checkedThumbColor, long checkedThumbIconColor, long checkedTrackColor, long checkedTrackBorderColor, long uncheckedThumbColor, long uncheckedThumbIconColor, long uncheckedTrackColor, long uncheckedTrackBorderColor, long disabledCheckedThumbColor, long disabledCheckedThumbIconColor, long disabledCheckedTrackColor, long disabledCheckedTrackBorderColor, long disabledUncheckedThumbColor, long disabledUncheckedThumbIconColor, long disabledUncheckedTrackColor, long disabledUncheckedTrackBorderColor);
     method public long getCheckedThumbColor();
     method public long getCheckedThumbIconColor();
     method public long getCheckedTrackBorderColor();
@@ -466,7 +469,7 @@
   }
 
   public final class SwitchDefaults {
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.SwitchColors colors(optional long checkedThumbColor, optional long checkedThumbIconColor, optional long checkedTrackColor, optional long checkedTrackStrokeColor, optional long uncheckedThumbColor, optional long uncheckedThumbIconColor, optional long uncheckedTrackColor, optional long uncheckedTrackStrokeColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.SwitchColors colors(optional long checkedThumbColor, optional long checkedThumbIconColor, optional long checkedTrackColor, optional long checkedTrackBorderColor, optional long uncheckedThumbColor, optional long uncheckedThumbIconColor, optional long uncheckedTrackColor, optional long uncheckedTrackBorderColor);
     field public static final androidx.wear.compose.material3.SwitchDefaults INSTANCE;
   }
 
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonTest.kt
index 0c068d7..e8d1930 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonTest.kt
@@ -425,8 +425,8 @@
             status = Status.Disabled,
             checked = false,
             colors = { IconButtonDefaults.iconToggleButtonColors() },
-            containerColor = { MaterialTheme.colorScheme.surface },
-            contentColor = { MaterialTheme.colorScheme.onSurfaceVariant }
+            containerColor = { MaterialTheme.colorScheme.surface.toDisabledColor() },
+            contentColor = { MaterialTheme.colorScheme.onSurfaceVariant.toDisabledColor() }
         )
 
     @RequiresApi(Build.VERSION_CODES.O)
@@ -436,8 +436,8 @@
             status = Status.Disabled,
             checked = true,
             colors = { IconButtonDefaults.iconToggleButtonColors() },
-            containerColor = { MaterialTheme.colorScheme.primary },
-            contentColor = { MaterialTheme.colorScheme.onPrimary },
+            containerColor = { MaterialTheme.colorScheme.primary.toDisabledColor() },
+            contentColor = { MaterialTheme.colorScheme.onPrimary.toDisabledColor() },
         )
 
     @RequiresApi(Build.VERSION_CODES.O)
@@ -523,11 +523,11 @@
             colors = {
                 IconButtonDefaults.iconToggleButtonColors(
                     // Apply the content color override for the content alpha to be applied
-                    checkedContainerColor = overrideColor
+                    disabledCheckedContainerColor = overrideColor
                 )
             },
             containerColor = { overrideColor },
-            contentColor = { MaterialTheme.colorScheme.onPrimary }
+            contentColor = { MaterialTheme.colorScheme.onPrimary.toDisabledColor() }
         )
     }
 
@@ -542,10 +542,10 @@
             colors = {
                 IconButtonDefaults.iconToggleButtonColors(
                     // Apply the content color override for the content alpha to be applied
-                    checkedContentColor = overrideColor
+                    disabledCheckedContentColor = overrideColor
                 )
             },
-            containerColor = { MaterialTheme.colorScheme.primary },
+            containerColor = { MaterialTheme.colorScheme.primary.toDisabledColor() },
             contentColor = { overrideColor }
         )
     }
@@ -561,11 +561,11 @@
             colors = {
                 IconButtonDefaults.iconToggleButtonColors(
                     // Apply the content color override for the content alpha to be applied
-                    uncheckedContainerColor = overrideColor
+                    disabledUncheckedContainerColor = overrideColor
                 )
             },
             containerColor = { overrideColor },
-            contentColor = { MaterialTheme.colorScheme.onSurfaceVariant }
+            contentColor = { MaterialTheme.colorScheme.onSurfaceVariant.toDisabledColor() }
         )
     }
 
@@ -580,11 +580,11 @@
             colors = {
                 IconButtonDefaults.iconToggleButtonColors(
                     // Apply the content color override for the content alpha to be applied
-                    uncheckedContentColor = overrideColor
+                    disabledUncheckedContentColor = overrideColor
                 )
             },
             contentColor = { overrideColor },
-            containerColor = { MaterialTheme.colorScheme.surface }
+            containerColor = { MaterialTheme.colorScheme.surface.toDisabledColor() }
         )
     }
 
@@ -618,7 +618,9 @@
                 onCheckedChange = {},
                 enabled = false,
                 content = { TestImage() },
-                modifier = Modifier.testTag(TEST_TAG).semantics { role = overrideRole }
+                modifier = Modifier
+                    .testTag(TEST_TAG)
+                    .semantics { role = overrideRole }
             )
         }
 
@@ -639,7 +641,6 @@
         contentColor: @Composable () -> Color,
     ) {
         verifyColors(
-            status = status,
             expectedContainerColor = containerColor,
             expectedContentColor = contentColor,
             content = {
@@ -705,10 +706,8 @@
 
     @RequiresApi(Build.VERSION_CODES.O)
     private fun ComposeContentTestRule.verifyColors(
-        status: Status,
         expectedContainerColor: @Composable () -> Color,
         expectedContentColor: @Composable () -> Color,
-        applyAlphaForDisabled: Boolean = true,
         content: @Composable () -> Color
     ) {
         val testBackgroundColor = Color.White
@@ -717,17 +716,8 @@
         var actualContentColor = Color.Transparent
         setContentWithTheme {
             finalExpectedContainerColor =
-                if (status.enabled() || !applyAlphaForDisabled) {
-                    expectedContainerColor()
-                } else {
-                    expectedContainerColor().copy(ContentAlpha.disabled)
-                }.compositeOver(testBackgroundColor)
-            finalExpectedContent =
-                if (status.enabled() || !applyAlphaForDisabled) {
-                    expectedContentColor()
-                } else {
-                    expectedContentColor().copy(ContentAlpha.disabled)
-                }
+                expectedContainerColor().compositeOver(testBackgroundColor)
+            finalExpectedContent = expectedContentColor()
             Box(
                 Modifier
                     .fillMaxSize()
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
index f5728b5..9d77f8c 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
@@ -460,7 +460,7 @@
                     checkedThumbColor = thumbColor,
                     checkedThumbIconColor = thumbIconColor,
                     checkedTrackColor = trackColor,
-                    checkedTrackStrokeColor = trackStrokeColor
+                    checkedTrackBorderColor = trackStrokeColor
                 ),
                 modifier = Modifier.testTag(TEST_TAG)
             )
@@ -487,7 +487,7 @@
                     uncheckedThumbColor = thumbColor,
                     uncheckedThumbIconColor = thumbIconColor,
                     uncheckedTrackColor = trackColor,
-                    uncheckedTrackStrokeColor = trackStrokeColor
+                    uncheckedTrackBorderColor = trackStrokeColor
                 ),
                 modifier = Modifier.testTag(TEST_TAG)
             )
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
index 7a694e8..8755ede 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
@@ -20,7 +20,6 @@
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.State
@@ -30,8 +29,12 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.max
+import androidx.wear.compose.material3.tokens.FilledIconButtonTokens
+import androidx.wear.compose.material3.tokens.FilledTonalIconButtonTokens
+import androidx.wear.compose.material3.tokens.IconButtonTokens
+import androidx.wear.compose.material3.tokens.IconToggleButtonTokens
+import androidx.wear.compose.material3.tokens.OutlinedIconButtonTokens
 
 /**
  * Wear Material [IconButton] is a circular, icon-only button with transparent background and
@@ -297,7 +300,7 @@
     enabled: Boolean = true,
     colors: ToggleButtonColors = IconButtonDefaults.iconToggleButtonColors(),
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = CircleShape,
+    shape: Shape = IconButtonDefaults.shape,
     border: BorderStroke? = null,
     content: @Composable BoxScope.() -> Unit,
 ) {
@@ -315,7 +318,6 @@
         shape = shape,
         content = provideScopeContent(
             colors.contentColor(enabled = enabled, checked = checked),
-            MaterialTheme.typography.labelMedium,
             content
         )
     )
@@ -328,7 +330,8 @@
     /**
      * Recommended [Shape] for [IconButton].
      */
-    val shape = CircleShape
+    val shape: Shape
+        @Composable get() = IconButtonTokens.ContainerShape.value
 
     /**
      * Recommended icon size for a given icon button size.
@@ -348,20 +351,25 @@
      * If the icon button is disabled then the colors will default to
      * the MaterialTheme onSurface color with suitable alpha values applied.
      *
-     * @param containerColor The background color of this icon button when enabled
-     * @param contentColor The color of this icon button when enabled
+     * @param containerColor The background color of this icon button when enabled.
+     * @param contentColor The color of this icon when enabled.
+     * @param disabledContainerColor The background color of this icon button when not enabled.
+     * @param disabledContentColor The color of this icon when not enabled.
      */
     @Composable
     fun filledIconButtonColors(
-        containerColor: Color = MaterialTheme.colorScheme.primary,
-        contentColor: Color = MaterialTheme.colorScheme.onPrimary,
+        containerColor: Color = FilledIconButtonTokens.ContainerColor.value,
+        contentColor: Color = FilledIconButtonTokens.ContentColor.value,
+        disabledContainerColor: Color = FilledIconButtonTokens.DisabledContainerColor.value
+            .toDisabledColor(disabledAlpha = FilledIconButtonTokens.DisabledContainerOpacity),
+        disabledContentColor: Color = FilledIconButtonTokens.DisabledContentColor.value
+            .toDisabledColor(disabledAlpha = FilledIconButtonTokens.DisabledContentOpacity)
     ): IconButtonColors {
         return iconButtonColors(
             containerColor = containerColor,
             contentColor = contentColor,
-            disabledContainerColor = MaterialTheme.colorScheme.onSurface.toDisabledColor(
-                disabledAlpha = DisabledContainerAlpha
-            ),
+            disabledContainerColor = disabledContainerColor,
+            disabledContentColor = disabledContentColor
         )
     }
 
@@ -371,20 +379,25 @@
      * If the icon button is disabled then the colors will default to
      * the MaterialTheme onSurface color with suitable alpha values applied.
      *
-     * @param containerColor The background color of this icon button when enabled
-     * @param contentColor The color of this icon button when enabled
+     * @param containerColor The background color of this icon button when enabled.
+     * @param contentColor The color of this icon when enabled.
+     * @param disabledContainerColor The background color of this icon button when not enabled.
+     * @param disabledContentColor The color of this icon when not enabled.
      */
     @Composable
     fun filledTonalIconButtonColors(
-        containerColor: Color = MaterialTheme.colorScheme.surface,
-        contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
+        containerColor: Color = FilledTonalIconButtonTokens.ContainerColor.value,
+        contentColor: Color = FilledTonalIconButtonTokens.ContentColor.value,
+        disabledContainerColor: Color = FilledTonalIconButtonTokens.DisabledContainerColor.value
+            .toDisabledColor(disabledAlpha = FilledTonalIconButtonTokens.DisabledContainerOpacity),
+        disabledContentColor: Color = FilledTonalIconButtonTokens.DisabledContentColor.value
+            .toDisabledColor(disabledAlpha = FilledTonalIconButtonTokens.DisabledContentOpacity)
     ): IconButtonColors {
         return iconButtonColors(
             containerColor = containerColor,
             contentColor = contentColor,
-            disabledContainerColor = MaterialTheme.colorScheme.onSurface.toDisabledColor(
-                disabledAlpha = DisabledContainerAlpha
-            ),
+            disabledContainerColor = disabledContainerColor,
+            disabledContentColor = disabledContentColor
         )
     }
 
@@ -394,15 +407,20 @@
      * If the icon button is disabled then the colors will default to
      * the MaterialTheme onSurface color with suitable alpha values applied.
      *
-     * @param contentColor The color of this icon button when enabled
+     * @param contentColor The color of this icon button when enabled.
+     * @param disabledContentColor The color of this icon when not enabled.
      */
     @Composable
     fun outlinedIconButtonColors(
-        contentColor: Color = MaterialTheme.colorScheme.primary,
+        contentColor: Color = OutlinedIconButtonTokens.ContentColor.value,
+        disabledContentColor: Color = OutlinedIconButtonTokens.DisabledContentColor.value
+            .toDisabledColor(OutlinedIconButtonTokens.DisabledContentOpacity)
     ): IconButtonColors {
         return iconButtonColors(
             containerColor = Color.Transparent,
             contentColor = contentColor,
+            disabledContainerColor = Color.Transparent,
+            disabledContentColor = disabledContentColor
         )
     }
 
@@ -412,17 +430,19 @@
      * If the icon button is disabled then the colors will default to
      * the MaterialTheme onSurface color with suitable alpha values applied.
      *
-     * @param containerColor the background color of this icon button when enabled
-     * @param contentColor the color of this icon when enabled
-     * @param disabledContainerColor the background color of this icon button when not enabled
-     * @param disabledContentColor the color of this icon when not enabled
+     * @param containerColor The background color of this icon button when enabled.
+     * @param contentColor The color of this icon when enabled.
+     * @param disabledContainerColor The background color of this icon button when not enabled.
+     * @param disabledContentColor The color of this icon when not enabled.
      */
     @Composable
     fun iconButtonColors(
         containerColor: Color = Color.Transparent,
-        contentColor: Color = MaterialTheme.colorScheme.onBackground,
+        contentColor: Color = IconButtonTokens.ContentColor.value,
         disabledContainerColor: Color = Color.Transparent,
-        disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor()
+        disabledContentColor: Color = IconButtonTokens.DisabledContentColor.value.toDisabledColor(
+            disabledAlpha = IconButtonTokens.DisabledContentOpacity
+        )
     ): IconButtonColors = IconButtonColors(
         containerColor = containerColor,
         contentColor = contentColor,
@@ -455,14 +475,19 @@
      */
     @Composable
     fun iconToggleButtonColors(
-        checkedContainerColor: Color = MaterialTheme.colorScheme.primary,
-        checkedContentColor: Color = MaterialTheme.colorScheme.onPrimary,
-        uncheckedContainerColor: Color = MaterialTheme.colorScheme.surface,
-        uncheckedContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
-        disabledCheckedContainerColor: Color = checkedContainerColor.toDisabledColor(),
-        disabledCheckedContentColor: Color = checkedContentColor.toDisabledColor(),
-        disabledUncheckedContainerColor: Color = uncheckedContainerColor.toDisabledColor(),
-        disabledUncheckedContentColor: Color = uncheckedContentColor.toDisabledColor(),
+        checkedContainerColor: Color = IconToggleButtonTokens.CheckedContainerColor.value,
+        checkedContentColor: Color = IconToggleButtonTokens.CheckedContentColor.value,
+        uncheckedContainerColor: Color = IconToggleButtonTokens.UncheckedContainerColor.value,
+        uncheckedContentColor: Color = IconToggleButtonTokens.UncheckedContentColor.value,
+        disabledCheckedContainerColor: Color = IconToggleButtonTokens.DisabledCheckedContainerColor
+            .value.toDisabledColor(IconToggleButtonTokens.DisabledCheckedContainerOpacity),
+        disabledCheckedContentColor: Color = IconToggleButtonTokens.DisabledCheckedContentColor
+            .value.toDisabledColor(IconToggleButtonTokens.DisabledCheckedContentOpacity),
+        disabledUncheckedContainerColor: Color = IconToggleButtonTokens
+            .DisabledUncheckedContainerColor.value
+            .toDisabledColor(IconToggleButtonTokens.DisabledUncheckedContainerOpacity),
+        disabledUncheckedContentColor: Color = IconToggleButtonTokens.DisabledUncheckedContentColor
+            .value.toDisabledColor(IconToggleButtonTokens.DisabledUncheckedContentOpacity),
     ): ToggleButtonColors {
         return ToggleButtonColors(
             checkedContainerColor = checkedContainerColor,
@@ -481,43 +506,43 @@
      * [SmallButtonSize] or [ExtraSmallButtonSize].
      * Use [iconSizeFor] to easily determine the icon size.
      */
-    val SmallIconSize = 24.dp
+    val SmallIconSize = IconButtonTokens.IconSmallSize
 
     /**
      * The default size of an icon when used inside an icon button of size DefaultButtonSize.
      * Use [iconSizeFor] to easily determine the icon size.
      */
-    val DefaultIconSize = 26.dp
+    val DefaultIconSize = IconButtonTokens.IconDefaultSize
 
     /**
      * The size of an icon when used inside an icon button with size [LargeButtonSize].
      * Use [iconSizeFor] to easily determine the icon size.
      */
-    val LargeIconSize = 30.dp
+    val LargeIconSize = IconButtonTokens.IconLargeSize
 
     /**
      * The recommended background size of an extra small, compact button.
      * It is recommended to apply this size using Modifier.touchTargetAwareSize.
      */
-    val ExtraSmallButtonSize = 32.dp
+    val ExtraSmallButtonSize = IconButtonTokens.ContainerExtraSmallSize
 
     /**
      * The recommended size for a small button.
      * It is recommended to apply this size using Modifier.touchTargetAwareSize.
      */
-    val SmallButtonSize = 48.dp
+    val SmallButtonSize = IconButtonTokens.ContainerSmallSize
 
     /**
      * The default size applied for buttons.
      * It is recommended to apply this size using Modifier.touchTargetAwareSize.
      */
-    val DefaultButtonSize = 52.dp
+    val DefaultButtonSize = IconButtonTokens.ContainerDefaultSize
 
     /**
      * The recommended size for a large button.
      * It is recommended to apply this size using Modifier.touchTargetAwareSize.
      */
-    val LargeButtonSize = 60.dp
+    val LargeButtonSize = IconButtonTokens.ContainerLargeSize
 }
 
 /**
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SelectionControls.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SelectionControls.kt
index a3ef21c..0fe17a3 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SelectionControls.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SelectionControls.kt
@@ -230,13 +230,26 @@
 
 /**
  * Represents the content colors used in [Checkbox] in different states.
+ *
+ * @param checkedBoxColor The box color of [Checkbox] when enabled and checked.
+ * @param checkedCheckmarkColor The check mark color of [Checkbox] when enabled
+ * and checked.
+ * @param uncheckedBoxColor The box color of [Checkbox] when enabled and unchecked.
+ * @param uncheckedCheckmarkColor The check mark color of [Checkbox] when enabled
+ * and unchecked.
+ * @param disabledCheckedBoxColor The box color of [Checkbox] when disabled and checked.
+ * @param disabledCheckedCheckmarkColor The check mark color of [Checkbox] when disabled
+ * and checked.
+ * @param disabledUncheckedBoxColor The box color of [Checkbox] when disabled and unchecked.
+ * @param disabledUncheckedCheckmarkColor The check mark color of [Checkbox] when disabled
+ * and unchecked.
  */
 @Immutable
-class CheckboxColors internal constructor(
+class CheckboxColors(
     val checkedBoxColor: Color,
     val checkedCheckmarkColor: Color,
-    val uncheckedCheckmarkColor: Color,
     val uncheckedBoxColor: Color,
+    val uncheckedCheckmarkColor: Color,
     val disabledCheckedBoxColor: Color,
     val disabledCheckedCheckmarkColor: Color,
     val disabledUncheckedBoxColor: Color,
@@ -311,9 +324,29 @@
 
 /**
  * Represents the content colors used in [Switch] in different states.
+ *
+ * @param checkedThumbColor The thumb color of [Switch] when enabled and checked.
+ * @param checkedThumbIconColor The thumb icon color of [Switch] when enabled and checked.
+ * @param checkedTrackColor The track color of [Switch] when enabled and checked.
+ * @param checkedTrackBorderColor The track border color of [Switch] when enabled and checked.
+ * @param uncheckedThumbColor The thumb color of [Switch] when enabled and unchecked.
+ * @param uncheckedThumbIconColor The thumb icon color of [Switch] when enabled and unchecked.
+ * @param uncheckedTrackColor The track color of [Switch] when enabled and unchecked.
+ * @param uncheckedTrackBorderColor The track border color of [Switch] when enabled and unchecked.
+ * @param disabledCheckedThumbColor The thumb color of [Switch] when disabled and checked.
+ * @param disabledCheckedThumbIconColor The thumb icon color of [Switch] when disabled and checked.
+ * @param disabledCheckedTrackColor The track color of [Switch] when disabled and checked.
+ * @param disabledCheckedTrackBorderColor The track border color of [Switch] when disabled
+ * and checked.
+ * @param disabledUncheckedThumbColor The thumb color of [Switch] when disabled and unchecked.
+ * @param disabledUncheckedThumbIconColor The thumb icon color of [Switch] when disabled
+ * and unchecked.
+ * @param disabledUncheckedTrackColor The track color of [Switch] when disabled and unchecked.
+ * @param disabledUncheckedTrackBorderColor The track border color of [Switch] when disabled
+ * and unchecked.
  */
 @Immutable
-class SwitchColors internal constructor(
+class SwitchColors(
     val checkedThumbColor: Color,
     val checkedThumbIconColor: Color,
     val checkedTrackColor: Color,
@@ -426,9 +459,14 @@
 
 /**
  * Represents the content colors used in [RadioButton] in different states.
+ *
+ * @param selectedColor The color of the radio button when enabled and selected.
+ * @param unselectedColor The color of the radio button when enabled and unselected.
+ * @param disabledSelectedColor The color of the radio button when disabled and selected.
+ * @param disabledUnselectedColor The color of the radio button when disabled and unselected.
  */
 @Immutable
-class RadioButtonColors internal constructor(
+class RadioButtonColors(
     val selectedColor: Color,
     val unselectedColor: Color,
     val disabledSelectedColor: Color,
@@ -508,37 +546,37 @@
      * @param checkedThumbColor The thumb color of this [Switch] when enabled and checked.
      * @param checkedThumbIconColor The thumb icon color of this [Switch] when enabled and checked.
      * @param checkedTrackColor The track color of this [Switch] when enabled and checked.
-     * @param checkedTrackStrokeColor The track border color of this [Switch] when enabled and checked.
+     * @param checkedTrackBorderColor The track border color of this [Switch] when enabled and checked.
      * @param uncheckedThumbColor The thumb color of this [Switch] when enabled and unchecked.
      * @param uncheckedThumbIconColor The thumb icon color of this [Switch] when enabled and checked.
      * @param uncheckedTrackColor The track color of this [Switch] when enabled and unchecked.
-     * @param uncheckedTrackStrokeColor The track border color of this [Switch] when enabled and unchecked.
+     * @param uncheckedTrackBorderColor The track border color of this [Switch] when enabled and unchecked.
      */
     @Composable
     fun colors(
         checkedThumbColor: Color = MaterialTheme.colorScheme.onPrimary,
         checkedThumbIconColor: Color = MaterialTheme.colorScheme.primary,
         checkedTrackColor: Color = MaterialTheme.colorScheme.primaryDim,
-        checkedTrackStrokeColor: Color = MaterialTheme.colorScheme.primaryDim,
+        checkedTrackBorderColor: Color = MaterialTheme.colorScheme.primaryDim,
         uncheckedThumbColor: Color = MaterialTheme.colorScheme.outline,
         uncheckedThumbIconColor: Color = MaterialTheme.colorScheme.background,
         uncheckedTrackColor: Color = MaterialTheme.colorScheme.surface,
-        uncheckedTrackStrokeColor: Color = MaterialTheme.colorScheme.outline
+        uncheckedTrackBorderColor: Color = MaterialTheme.colorScheme.outline
     ): SwitchColors = SwitchColors(
         checkedThumbColor = checkedThumbColor,
         checkedThumbIconColor = checkedThumbIconColor,
         checkedTrackColor = checkedTrackColor,
-        checkedTrackBorderColor = checkedTrackStrokeColor,
+        checkedTrackBorderColor = checkedTrackBorderColor,
         uncheckedThumbColor = uncheckedThumbColor,
         uncheckedThumbIconColor = uncheckedThumbIconColor,
         uncheckedTrackColor = uncheckedTrackColor,
-        uncheckedTrackBorderColor = uncheckedTrackStrokeColor,
+        uncheckedTrackBorderColor = uncheckedTrackBorderColor,
         disabledCheckedThumbColor = checkedThumbColor.toDisabledColor(),
         disabledCheckedThumbIconColor = checkedThumbIconColor.toDisabledColor(),
         disabledCheckedTrackColor = checkedTrackColor.toDisabledColor(
             disabledAlpha = DisabledContainerAlpha
         ),
-        disabledCheckedTrackBorderColor = checkedTrackStrokeColor.toDisabledColor(
+        disabledCheckedTrackBorderColor = checkedTrackBorderColor.toDisabledColor(
             disabledAlpha = DisabledBorderAlpha
         ),
         disabledUncheckedThumbColor = uncheckedThumbColor.toDisabledColor(),
@@ -546,7 +584,7 @@
         disabledUncheckedTrackColor = uncheckedTrackColor.toDisabledColor(
             disabledAlpha = DisabledContainerAlpha
         ),
-        disabledUncheckedTrackBorderColor = uncheckedTrackStrokeColor.toDisabledColor(
+        disabledUncheckedTrackBorderColor = uncheckedTrackBorderColor.toDisabledColor(
             disabledAlpha = DisabledBorderAlpha
         )
     )
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledIconButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledIconButtonTokens.kt
new file mode 100644
index 0000000..82324a5
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledIconButtonTokens.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+// VERSION: v0_12
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.wear.compose.material3.tokens
+
+internal object FilledIconButtonTokens {
+  val ContainerColor = ColorSchemeKeyTokens.Primary
+  val ContentColor = ColorSchemeKeyTokens.OnPrimary
+  val DisabledContainerColor = ColorSchemeKeyTokens.OnSurface
+  val DisabledContainerOpacity = 0.12f
+  val DisabledContentColor = ColorSchemeKeyTokens.OnSurface
+  val DisabledContentOpacity = 0.38f
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledTonalIconButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledTonalIconButtonTokens.kt
new file mode 100644
index 0000000..e2c42f5
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledTonalIconButtonTokens.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+// VERSION: v0_12
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.wear.compose.material3.tokens
+
+internal object FilledTonalIconButtonTokens {
+  val ContainerColor = ColorSchemeKeyTokens.Surface
+  val ContentColor = ColorSchemeKeyTokens.OnSurfaceVariant
+  val DisabledContainerColor = ColorSchemeKeyTokens.OnSurface
+  val DisabledContainerOpacity = 0.12f
+  val DisabledContentColor = ColorSchemeKeyTokens.OnSurface
+  val DisabledContentOpacity = 0.38f
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/IconButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/IconButtonTokens.kt
new file mode 100644
index 0000000..2eb7943
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/IconButtonTokens.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+// VERSION: v0_13
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.wear.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object IconButtonTokens {
+  val ContainerDefaultSize = 52.0.dp
+  val ContainerExtraSmallSize = 32.0.dp
+  val ContainerLargeSize = 60.0.dp
+  val ContainerShape = ShapeKeyTokens.CornerFull
+  val ContainerSmallSize = 48.0.dp
+  val ContentColor = ColorSchemeKeyTokens.OnBackground
+  val DisabledContentColor = ColorSchemeKeyTokens.OnSurface
+  val DisabledContentOpacity = 0.38f
+  val IconDefaultSize = 26.0.dp
+  val IconLargeSize = 30.0.dp
+  val IconSmallSize = 24.0.dp
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/IconToggleButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/IconToggleButtonTokens.kt
new file mode 100644
index 0000000..b6ae374
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/IconToggleButtonTokens.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+// VERSION: v0_16
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.wear.compose.material3.tokens
+
+internal object IconToggleButtonTokens {
+    val CheckedContainerColor = ColorSchemeKeyTokens.Primary
+    val CheckedContentColor = ColorSchemeKeyTokens.OnPrimary
+    val DisabledCheckedContainerColor = ColorSchemeKeyTokens.Primary
+    val DisabledCheckedContainerOpacity = 0.38f
+    val DisabledCheckedContentColor = ColorSchemeKeyTokens.OnPrimary
+    val DisabledCheckedContentOpacity = 0.38f
+    val DisabledUncheckedContainerColor = ColorSchemeKeyTokens.Surface
+    val DisabledUncheckedContainerOpacity = 0.38f
+    val DisabledUncheckedContentColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val DisabledUncheckedContentOpacity = 0.38f
+    val UncheckedContainerColor = ColorSchemeKeyTokens.Surface
+    val UncheckedContentColor = ColorSchemeKeyTokens.OnSurfaceVariant
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/OutlinedIconButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/OutlinedIconButtonTokens.kt
new file mode 100644
index 0000000..e32f5b5
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/OutlinedIconButtonTokens.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// VERSION: v0_12
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.wear.compose.material3.tokens
+
+internal object OutlinedIconButtonTokens {
+  val ContentColor = ColorSchemeKeyTokens.Primary
+  val DisabledContentColor = ColorSchemeKeyTokens.OnSurface
+  val DisabledContentOpacity = 0.38f
+}
diff --git a/wear/compose/compose-ui-tooling/build.gradle b/wear/compose/compose-ui-tooling/build.gradle
index 67408bd..8c4de3f 100644
--- a/wear/compose/compose-ui-tooling/build.gradle
+++ b/wear/compose/compose-ui-tooling/build.gradle
@@ -28,7 +28,7 @@
 
     implementation(libs.kotlinStdlibCommon)
     implementation(project(":compose:ui:ui-tooling-preview"))
-    implementation(project(":wear:wear-tooling-preview"))
+    implementation("androidx.wear:wear-tooling-preview:1.0.0-alpha01")
 
     samples(project(":wear:compose:compose-material-samples"))
 }
diff --git a/wear/compose/integration-tests/demos/lint-baseline.xml b/wear/compose/integration-tests/demos/lint-baseline.xml
index 2d4d6df..6223fbbe 100644
--- a/wear/compose/integration-tests/demos/lint-baseline.xml
+++ b/wear/compose/integration-tests/demos/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-beta01)" variant="all" version="8.2.0-beta01">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="WearStandaloneAppFlag"
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
index c6f8d33..a13d6d5 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
@@ -41,6 +41,8 @@
 import androidx.wear.compose.foundation.samples.SwipeToRevealWithExpandables
 import androidx.wear.compose.integration.demos.common.ComposableDemo
 import androidx.wear.compose.integration.demos.common.DemoCategory
+import androidx.wear.compose.material.samples.SwipeToRevealCardSample
+import androidx.wear.compose.material.samples.SwipeToRevealChipSample
 
 // Declare the swipe to dismiss demos so that we can use this variable as the background composable
 // for the SwipeToDismissDemo itself.
@@ -167,6 +169,15 @@
                 },
                 ComposableDemo("Swipe To Reveal - Undo") {
                     SwipeToRevealWithDifferentUndo()
+                },
+                ComposableDemo("S2R + EdgeSwipeToDismiss") { params ->
+                    SwipeToRevealWithEdgeSwipeToDismiss(params.navigateBack)
+                },
+                ComposableDemo("Material S2R Chip") {
+                    SwipeToRevealChipSample()
+                },
+                ComposableDemo("Material S2R Card") {
+                    SwipeToRevealCardSample()
                 }
             )
         )
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt
index a8029d3..e37c1fc 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.compose.integration.demos
 
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -43,12 +44,15 @@
 import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
 import androidx.wear.compose.foundation.RevealActionType
 import androidx.wear.compose.foundation.RevealValue
+import androidx.wear.compose.foundation.SwipeToDismissBox
 import androidx.wear.compose.foundation.createAnchors
+import androidx.wear.compose.foundation.edgeSwipeToDismiss
 import androidx.wear.compose.foundation.expandableItem
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.rememberExpandableState
 import androidx.wear.compose.foundation.rememberExpandableStateMapping
 import androidx.wear.compose.foundation.rememberRevealState
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
 import androidx.wear.compose.material.AppCard
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
@@ -240,6 +244,45 @@
     }
 }
 
+@OptIn(ExperimentalWearMaterialApi::class, ExperimentalWearFoundationApi::class)
+@Composable
+fun SwipeToRevealWithEdgeSwipeToDismiss(
+    navigateBack: () -> Unit
+) {
+    val swipeToDismissBoxState = rememberSwipeToDismissBoxState()
+    SwipeToDismissBox(
+        state = swipeToDismissBoxState,
+        onDismissed = navigateBack
+    ) {
+        ScalingLazyColumn(
+            contentPadding = PaddingValues(0.dp)
+        ) {
+            repeat(5) {
+                item {
+                    SwipeToRevealChip(
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .edgeSwipeToDismiss(swipeToDismissBoxState),
+                        primaryAction = SwipeToRevealDefaults.primaryAction(
+                            icon = { Icon(SwipeToRevealDefaults.Delete, "Delete") },
+                            label = { Text("Delete") }),
+                        revealState = rememberRevealState()
+                    ) {
+                        Chip(
+                            onClick = { /*TODO*/ },
+                            colors = ChipDefaults.secondaryChipColors(),
+                            modifier = Modifier.fillMaxWidth(),
+                            label = {
+                                Text("S2R Chip with defaults")
+                            }
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
+
 @OptIn(ExperimentalWearFoundationApi::class, ExperimentalWearMaterialApi::class)
 @Composable
 private fun SwipeToRevealChipExpandable(
diff --git a/wear/compose/integration-tests/macrobenchmark-target/build.gradle b/wear/compose/integration-tests/macrobenchmark-target/build.gradle
index af518ce..5ed6e31 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -44,7 +44,7 @@
     implementation(project(":compose:runtime:runtime"))
     implementation(project(":compose:ui:ui-tooling"))
     implementation(project(":compose:material:material-icons-core"))
-    implementation("androidx.activity:activity-compose:1.3.1")
+    implementation(project(":activity:activity-compose"))
     implementation(project(":profileinstaller:profileinstaller"))
     implementation project(path: ':wear:compose:compose-foundation')
     implementation project(path: ':wear:compose:compose-material')
diff --git a/wear/compose/integration-tests/macrobenchmark/build.gradle b/wear/compose/integration-tests/macrobenchmark/build.gradle
index 61090b4..1b3a97c 100644
--- a/wear/compose/integration-tests/macrobenchmark/build.gradle
+++ b/wear/compose/integration-tests/macrobenchmark/build.gradle
@@ -23,7 +23,6 @@
 android {
     defaultConfig {
         minSdkVersion 29
-        testInstrumentationRunnerArguments["androidx.benchmark.fullTracing.enable"] = "true"
     }
     namespace "androidx.wear.compose.integration.macrobenchmark"
     targetProjectPath = ":wear:compose:integration-tests:macrobenchmark-target"
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
index c9871fe..25d3741 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
@@ -40,8 +40,6 @@
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
-    private lateinit var device: UiDevice
-
     @Before
     fun setUp() {
         val instrumentation = InstrumentationRegistry.getInstrumentation()
@@ -64,8 +62,8 @@
             val swipeToDismissBox = device.findObject(By.desc(CONTENT_DESCRIPTION))
             // Setting a gesture margin is important otherwise gesture nav is triggered.
             swipeToDismissBox.setGestureMargin(device.displayWidth / 5)
-            repeat(10) {
-                swipeToDismissBox.swipe(Direction.RIGHT, 0.75f)
+            repeat(3) {
+                swipeToDismissBox.swipe(Direction.RIGHT, 0.75f, SWIPE_SPEED)
                 device.waitForIdle()
             }
         }
@@ -80,4 +78,7 @@
         @JvmStatic
         fun parameters() = createCompilationParams()
     }
+
+    private lateinit var device: UiDevice
+    private val SWIPE_SPEED = 500
 }
diff --git a/wear/compose/integration-tests/navigation/build.gradle b/wear/compose/integration-tests/navigation/build.gradle
index 5bd009f..6d3c77c 100644
--- a/wear/compose/integration-tests/navigation/build.gradle
+++ b/wear/compose/integration-tests/navigation/build.gradle
@@ -58,5 +58,5 @@
     // but it doesn't work in androidx.
     // See aosp/1804059
     androidTestImplementation "androidx.lifecycle:lifecycle-common-java8:2.4.0"
-    androidTestImplementation(project(":annotation:annotation"))
+    androidTestImplementation libs.androidx.annotation
 }
\ No newline at end of file
diff --git a/wear/tiles/tiles-material/lint-baseline.xml b/wear/tiles/tiles-material/lint-baseline.xml
index 9c080eb..046d30e 100644
--- a/wear/tiles/tiles-material/lint-baseline.xml
+++ b/wear/tiles/tiles-material/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="BanThreadSleep"
@@ -39,7 +39,7 @@
 
     <issue
         id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
+        message="Failed to read `level` from `@androidx.wear.tiles.TilesExperimental` -- assuming `ERROR`. This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
         errorLine1="                .setVariant(variant)"
         errorLine2="                 ~~~~~~~~~~">
         <location
@@ -48,7 +48,7 @@
 
     <issue
         id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
+        message="Failed to read `level` from `@androidx.wear.tiles.TilesExperimental` -- assuming `ERROR`. This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
         errorLine1="                androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM,"
         errorLine2="                                                          ~~~~~~~~~~~~~~~~~~">
         <location
@@ -57,7 +57,7 @@
 
     <issue
         id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
+        message="Failed to read `level` from `@androidx.wear.tiles.TilesExperimental` -- assuming `ERROR`. This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
         errorLine1="                androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM,"
         errorLine2="                                                          ~~~~~~~~~~~~~~~~~~">
         <location
@@ -66,7 +66,7 @@
 
     <issue
         id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
+        message="Failed to read `level` from `@androidx.wear.tiles.TilesExperimental` -- assuming `ERROR`. This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
         errorLine1="                androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM,"
         errorLine2="                                                          ~~~~~~~~~~~~~~~~~~">
         <location
@@ -75,7 +75,7 @@
 
     <issue
         id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
+        message="Failed to read `level` from `@androidx.wear.tiles.TilesExperimental` -- assuming `ERROR`. This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
         errorLine1="                androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM,"
         errorLine2="                                                          ~~~~~~~~~~~~~~~~~~">
         <location
@@ -84,7 +84,7 @@
 
     <issue
         id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
+        message="Failed to read `level` from `@androidx.wear.tiles.TilesExperimental` -- assuming `ERROR`. This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
         errorLine1="                androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM,"
         errorLine2="                                                          ~~~~~~~~~~~~~~~~~~">
         <location
@@ -93,7 +93,7 @@
 
     <issue
         id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
+        message="Failed to read `level` from `@androidx.wear.tiles.TilesExperimental` -- assuming `ERROR`. This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
         errorLine1="                androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM,"
         errorLine2="                                                          ~~~~~~~~~~~~~~~~~~">
         <location
@@ -102,7 +102,7 @@
 
     <issue
         id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
+        message="Failed to read `level` from `@androidx.wear.tiles.TilesExperimental` -- assuming `ERROR`. This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
         errorLine1="                androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM,"
         errorLine2="                                                          ~~~~~~~~~~~~~~~~~~">
         <location
@@ -111,7 +111,7 @@
 
     <issue
         id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
+        message="Failed to read `level` from `@androidx.wear.tiles.TilesExperimental` -- assuming `ERROR`. This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
         errorLine1="                androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM,"
         errorLine2="                                                          ~~~~~~~~~~~~~~~~~~">
         <location
@@ -120,7 +120,7 @@
 
     <issue
         id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
+        message="Failed to read `level` from `@androidx.wear.tiles.TilesExperimental` -- assuming `ERROR`. This declaration is opt-in and its usage should be marked with `@androidx.wear.tiles.TilesExperimental` or `@OptIn(markerClass = androidx.wear.tiles.TilesExperimental.class)`"
         errorLine1="                androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM,"
         errorLine2="                                                          ~~~~~~~~~~~~~~~~~~">
         <location
diff --git a/wear/tiles/tiles-tooling-preview/api/current.txt b/wear/tiles/tiles-tooling-preview/api/current.txt
index e2c0e4e..f1331ab 100644
--- a/wear/tiles/tiles-tooling-preview/api/current.txt
+++ b/wear/tiles/tiles-tooling-preview/api/current.txt
@@ -19,11 +19,12 @@
   }
 
   public final class TilePreviewData {
-    ctor public TilePreviewData(optional kotlin.jvm.functions.Function2<? super androidx.wear.tiles.RequestBuilders.ResourcesRequest,? super android.content.Context,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest, kotlin.jvm.functions.Function2<? super androidx.wear.tiles.RequestBuilders.TileRequest,? super android.content.Context,androidx.wear.tiles.TileBuilders.Tile> onTileRequest);
-    method public kotlin.jvm.functions.Function2<androidx.wear.tiles.RequestBuilders.TileRequest,android.content.Context,androidx.wear.tiles.TileBuilders.Tile> getOnTileRequest();
-    method public kotlin.jvm.functions.Function2<androidx.wear.tiles.RequestBuilders.ResourcesRequest,android.content.Context,androidx.wear.protolayout.ResourceBuilders.Resources> getOnTileResourceRequest();
-    property public final kotlin.jvm.functions.Function2<androidx.wear.tiles.RequestBuilders.TileRequest,android.content.Context,androidx.wear.tiles.TileBuilders.Tile> onTileRequest;
-    property public final kotlin.jvm.functions.Function2<androidx.wear.tiles.RequestBuilders.ResourcesRequest,android.content.Context,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest;
+    ctor public TilePreviewData(optional kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest, kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest);
+    ctor public TilePreviewData(kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest);
+    method public kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> getOnTileRequest();
+    method public kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> getOnTileResourceRequest();
+    property public final kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest;
+    property public final kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest;
   }
 
   public final class TilePreviewHelper {
diff --git a/wear/tiles/tiles-tooling-preview/api/restricted_current.txt b/wear/tiles/tiles-tooling-preview/api/restricted_current.txt
index e2c0e4e..f1331ab 100644
--- a/wear/tiles/tiles-tooling-preview/api/restricted_current.txt
+++ b/wear/tiles/tiles-tooling-preview/api/restricted_current.txt
@@ -19,11 +19,12 @@
   }
 
   public final class TilePreviewData {
-    ctor public TilePreviewData(optional kotlin.jvm.functions.Function2<? super androidx.wear.tiles.RequestBuilders.ResourcesRequest,? super android.content.Context,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest, kotlin.jvm.functions.Function2<? super androidx.wear.tiles.RequestBuilders.TileRequest,? super android.content.Context,androidx.wear.tiles.TileBuilders.Tile> onTileRequest);
-    method public kotlin.jvm.functions.Function2<androidx.wear.tiles.RequestBuilders.TileRequest,android.content.Context,androidx.wear.tiles.TileBuilders.Tile> getOnTileRequest();
-    method public kotlin.jvm.functions.Function2<androidx.wear.tiles.RequestBuilders.ResourcesRequest,android.content.Context,androidx.wear.protolayout.ResourceBuilders.Resources> getOnTileResourceRequest();
-    property public final kotlin.jvm.functions.Function2<androidx.wear.tiles.RequestBuilders.TileRequest,android.content.Context,androidx.wear.tiles.TileBuilders.Tile> onTileRequest;
-    property public final kotlin.jvm.functions.Function2<androidx.wear.tiles.RequestBuilders.ResourcesRequest,android.content.Context,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest;
+    ctor public TilePreviewData(optional kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest, kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest);
+    ctor public TilePreviewData(kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest);
+    method public kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> getOnTileRequest();
+    method public kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> getOnTileResourceRequest();
+    property public final kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest;
+    property public final kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest;
   }
 
   public final class TilePreviewHelper {
diff --git a/wear/tiles/tiles-tooling-preview/build.gradle b/wear/tiles/tiles-tooling-preview/build.gradle
index 83e8392..d7d5e61 100644
--- a/wear/tiles/tiles-tooling-preview/build.gradle
+++ b/wear/tiles/tiles-tooling-preview/build.gradle
@@ -28,7 +28,7 @@
     implementation(project(":wear:protolayout:protolayout-proto"))
     implementation(project(":wear:tiles:tiles"))
 
-    api(project(":wear:wear-tooling-preview"))
+    api("androidx.wear:wear-tooling-preview:1.0.0-alpha01")
     api("androidx.annotation:annotation:1.6.0")
 }
 
diff --git a/wear/tiles/tiles-tooling-preview/src/main/java/androidx/wear/tiles/tooling/preview/TilePreview.kt b/wear/tiles/tiles-tooling-preview/src/main/java/androidx/wear/tiles/tooling/preview/TilePreview.kt
index 73aee32..84e1282 100644
--- a/wear/tiles/tiles-tooling-preview/src/main/java/androidx/wear/tiles/tooling/preview/TilePreview.kt
+++ b/wear/tiles/tiles-tooling-preview/src/main/java/androidx/wear/tiles/tooling/preview/TilePreview.kt
@@ -16,13 +16,42 @@
 
 package androidx.wear.tiles.tooling.preview
 
+import android.content.Context
 import androidx.annotation.FloatRange
 import androidx.wear.tooling.preview.devices.WearDevice
 import androidx.wear.tooling.preview.devices.WearDevices
 
 /**
- * The annotation that marks Tile preview components (functions that return [TilePreviewData]) that
- * should have a visual preview in the Android Studio preview panel.
+ * The annotation that marks Tile preview components that should have a visual preview in the
+ * Android Studio preview panel. Tile preview components are methods that take an optional [Context]
+ * parameter and return a [TilePreviewData]. Methods annotated with [TilePreview] must be top level
+ * declarations or in a top level class with a default constructor.
+ *
+ * For example:
+ * ```kotlin
+ * @TilePreview
+ * fun myTilePreview(): TilePreviewData {
+ *     return TilePreviewData { request -> myTile(request) }
+ * }
+ * ```
+ * or:
+ * ```kotlin
+ * @TilePreview
+ * fun myTilePreview(context: Context): TilePreviewData {
+ *     return TilePreviewData { request -> myTile(request, context) }
+ * }
+ * ```
+ *
+ * Because of the way previews are rendered within Android Studio, they are lightweight and don't
+ * require the whole Android framework to render them. However, this comes with the following
+ * limitations:
+ * * No network access
+ * * No file access
+ * * Some [Context] APIs may not be fully available, such as launching activities or retrieving
+ * services
+ *
+ * For more information, see
+ * https://developer.android.com/jetpack/compose/tooling/previews#preview-limitations
  *
  * The annotation contains a number of parameters that allow to define the way the Tile will be
  * rendered within the preview. The passed parameters are only read by Studio when rendering the
diff --git a/wear/tiles/tiles-tooling-preview/src/main/java/androidx/wear/tiles/tooling/preview/TilePreviewData.kt b/wear/tiles/tiles-tooling-preview/src/main/java/androidx/wear/tiles/tooling/preview/TilePreviewData.kt
index 10f2910..cd36965 100644
--- a/wear/tiles/tiles-tooling-preview/src/main/java/androidx/wear/tiles/tooling/preview/TilePreviewData.kt
+++ b/wear/tiles/tiles-tooling-preview/src/main/java/androidx/wear/tiles/tooling/preview/TilePreviewData.kt
@@ -16,7 +16,6 @@
 
 package androidx.wear.tiles.tooling.preview
 
-import android.content.Context
 import androidx.wear.protolayout.ResourceBuilders.Resources
 import androidx.wear.tiles.RequestBuilders.ResourcesRequest
 import androidx.wear.tiles.RequestBuilders.TileRequest
@@ -39,10 +38,10 @@
  *
  * @see [TilePreviewHelper.singleTimelineEntryTileBuilder]
  */
-class TilePreviewData(
-    val onTileResourceRequest: (ResourcesRequest, Context) -> Resources =
-        { _, _ -> defaultResources },
-    val onTileRequest: (TileRequest, Context) -> TileBuilders.Tile,
+class TilePreviewData
+@JvmOverloads constructor(
+    val onTileResourceRequest: (ResourcesRequest) -> Resources = { defaultResources },
+    val onTileRequest: (TileRequest) -> TileBuilders.Tile,
 ) {
     override fun toString(): String {
         return "TilePreviewData(onTileResourceRequest=$onTileResourceRequest," +
diff --git a/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.java b/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.java
new file mode 100644
index 0000000..7ffe0ac
--- /dev/null
+++ b/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.java
@@ -0,0 +1,115 @@
+/*
+ * 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.tiles.tooling;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+import static androidx.wear.tiles.tooling.preview.TilePreviewHelper.singleTimelineEntryTileBuilder;
+
+import android.content.Context;
+
+import androidx.wear.protolayout.LayoutElementBuilders.FontStyle;
+import androidx.wear.protolayout.LayoutElementBuilders.Layout;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.LayoutElementBuilders.Text;
+import androidx.wear.protolayout.ResourceBuilders.Resources;
+import androidx.wear.protolayout.TimelineBuilders.Timeline;
+import androidx.wear.protolayout.TimelineBuilders.TimelineEntry;
+import androidx.wear.tiles.TileBuilders.Tile;
+import androidx.wear.tiles.tooling.preview.TilePreview;
+import androidx.wear.tiles.tooling.preview.TilePreviewData;
+
+public class TestTilePreviews {
+    private static final String RESOURCES_VERSION = "1";
+    private static final Resources RESOURCES = new Resources.Builder().setVersion(
+            RESOURCES_VERSION).build();
+
+    private static LayoutElement layoutElement() {
+        return new Text.Builder()
+                .setText("Hello world!")
+                .setFontStyle(new FontStyle.Builder()
+                        .setColor(argb(0xFF000000))
+                        .build())
+                .build();
+    }
+
+    private static Layout layout() {
+        return new Layout.Builder()
+                .setRoot(layoutElement())
+                .build();
+    }
+
+    private static Tile tile() {
+        return new Tile.Builder()
+                .setResourcesVersion(RESOURCES_VERSION)
+                .setTileTimeline(new Timeline.Builder()
+                        .addTimelineEntry(new TimelineEntry.Builder()
+                                .setLayout(layout())
+                                .build())
+                        .build())
+                .build();
+    }
+
+    /** Declaration of a static tile preview method */
+    @TilePreview
+    public static TilePreviewData tilePreview() {
+        return new TilePreviewData((request) -> RESOURCES, (request) -> tile());
+    }
+
+    @TilePreview
+    static TilePreviewData tileLayoutPreview() {
+        return new TilePreviewData((request) -> singleTimelineEntryTileBuilder(layout()).build());
+    }
+
+    @TilePreview
+    static TilePreviewData tileLayoutElementPreview() {
+        return new TilePreviewData((request) ->
+                singleTimelineEntryTileBuilder(layoutElement()).build());
+    }
+
+    @TilePreview
+    private static TilePreviewData tilePreviewWithPrivateVisibility() {
+        return new TilePreviewData((request) -> tile());
+    }
+
+    static int duplicateFunctionName(int x) {
+        return x;
+    }
+
+    @TilePreview
+    static TilePreviewData duplicateFunctionName() {
+        return new TilePreviewData((request) -> tile());
+    }
+
+    @TilePreview
+    static TilePreviewData tilePreviewWithContextParameter(Context context) {
+        return new TilePreviewData((request) -> tile());
+    }
+
+    @TilePreview
+    static void tilePreviewWithWrongReturnType() {
+    }
+
+    @TilePreview
+    static TilePreviewData tilePreviewWithNonContextParameter(int i) {
+        return new TilePreviewData((request) -> tile());
+    }
+
+    @TilePreview
+    TilePreviewData nonStaticMethod() {
+        return new TilePreviewData((request) -> tile());
+    }
+}
diff --git a/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.kt b/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.kt
index 56bdd72..7ed3346 100644
--- a/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.kt
+++ b/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.tiles.tooling
 
+import android.content.Context
 import androidx.wear.protolayout.ColorBuilders.argb
 import androidx.wear.protolayout.LayoutElementBuilders
 import androidx.wear.protolayout.ResourceBuilders
@@ -51,25 +52,41 @@
     ).build()
 
 @TilePreview
-fun TilePreview() = TilePreviewData(
-    onTileResourceRequest = { _, _ -> resources },
-    onTileRequest = { _, _ -> tile() },
+fun tilePreview() = TilePreviewData(
+    onTileResourceRequest = { resources },
+    onTileRequest = { tile() },
 )
 
 @TilePreview
-fun TileLayoutPreview() = TilePreviewData { _, _ ->
+fun tileLayoutPreview() = TilePreviewData {
     singleTimelineEntryTileBuilder(layout()).build()
 }
 
 @TilePreview
-fun TileLayoutElementPreview() = TilePreviewData { _, _ ->
+fun tileLayoutElementPreview() = TilePreviewData {
     singleTimelineEntryTileBuilder(layoutElement()).build()
 }
 
 @TilePreview
-private fun TilePreviewWithPrivateVisibility() = TilePreviewData { _, _ -> tile() }
+private fun tilePreviewWithPrivateVisibility() = TilePreviewData { tile() }
 
 fun duplicateFunctionName(x: Int) = x
 
 @TilePreview
-fun duplicateFunctionName() = TilePreviewData { _, _ -> tile() }
+fun duplicateFunctionName() = TilePreviewData { tile() }
+
+@TilePreview
+fun tilePreviewWithContextParameter(@Suppress("UNUSED_PARAMETER") context: Context) =
+    TilePreviewData { tile() }
+
+@TilePreview
+fun tilePreviewWithWrongReturnType() = Unit
+
+@TilePreview
+fun tilePreviewWithNonContextParameter(@Suppress("UNUSED_PARAMETER") i: Int) =
+    TilePreviewData { tile() }
+
+class SomeClass {
+    @TilePreview
+    fun nonStaticMethod() = TilePreviewData { tile() }
+}
diff --git a/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TileServiceViewAdapterTest.kt b/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TileServiceViewAdapterTest.kt
index 32605b1..eeb74fc 100644
--- a/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TileServiceViewAdapterTest.kt
+++ b/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TileServiceViewAdapterTest.kt
@@ -27,10 +27,16 @@
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 
-private const val TEST_TILE_PREVIEWS_FILE = "androidx.wear.tiles.tooling.TestTilePreviewsKt"
+private const val TEST_TILE_PREVIEWS_KOTLIN_FILE = "androidx.wear.tiles.tooling.TestTilePreviewsKt"
+private const val TEST_TILE_PREVIEWS_JAVA_FILE = "androidx.wear.tiles.tooling.TestTilePreviews"
 
-class TileServiceViewAdapterTest {
+@RunWith(Parameterized::class)
+class TileServiceViewAdapterTest(
+    private val testFile: String,
+) {
     @Suppress("DEPRECATION")
     @get:Rule
     val activityTestRule = androidx.test.rule.ActivityTestRule(TestActivity::class.java)
@@ -54,70 +60,99 @@
 
     @Test
     fun testTilePreview() {
-        initAndInflate("$TEST_TILE_PREVIEWS_FILE.TilePreview")
+        initAndInflate("$testFile.tilePreview")
 
-        activityTestRule.runOnUiThread {
-            val textView =
-                (tileServiceViewAdapter.getChildAt(0) as ViewGroup)
-                    .getChildAt(0) as TextView
-            assertNotNull(textView)
-            assertEquals("Hello world!", textView.text.toString())
-        }
+        assertThatTileHasInflatedSuccessfully()
     }
 
     @Test
     fun testTileLayoutPreview() {
-        initAndInflate("$TEST_TILE_PREVIEWS_FILE.TileLayoutPreview")
+        initAndInflate("$testFile.tileLayoutPreview")
 
-        activityTestRule.runOnUiThread {
-            val textView =
-                (tileServiceViewAdapter.getChildAt(0) as ViewGroup)
-                    .getChildAt(0) as TextView
-            assertNotNull(textView)
-            assertEquals("Hello world!", textView.text.toString())
-        }
+        assertThatTileHasInflatedSuccessfully()
     }
 
     @Test
     fun testTileLayoutElementPreview() {
-        initAndInflate("$TEST_TILE_PREVIEWS_FILE.TileLayoutElementPreview")
+        initAndInflate("$testFile.tileLayoutElementPreview")
 
-        activityTestRule.runOnUiThread {
-            val textView =
-                ((tileServiceViewAdapter.getChildAt(0) as ViewGroup)
-                    .getChildAt(0) as FrameLayout).getChildAt(0) as TextView
-            assertNotNull(textView)
-            assertEquals("Hello world!", textView.text.toString())
-        }
+        assertThatTileHasInflatedSuccessfully()
     }
 
     @Test
     fun testTilePreviewDeclaredWithPrivateMethod() {
-        initAndInflate("$TEST_TILE_PREVIEWS_FILE.TilePreviewWithPrivateVisibility")
+        initAndInflate("$testFile.tilePreviewWithPrivateVisibility")
 
-        activityTestRule.runOnUiThread {
-            val textView =
-                (tileServiceViewAdapter.getChildAt(0) as ViewGroup)
-                    .getChildAt(0) as TextView
-            assertNotNull(textView)
-            assertEquals("Hello world!", textView.text.toString())
-        }
+        assertThatTileHasInflatedSuccessfully()
     }
 
     @Test
     fun testTilePreviewThatHasSharedFunctionName() {
-        initAndInflate("$TEST_TILE_PREVIEWS_FILE.duplicateFunctionName")
+        initAndInflate("$testFile.duplicateFunctionName")
 
+        assertThatTileHasInflatedSuccessfully()
+    }
+
+    @Test
+    fun testTilePreviewWithContextParameter() {
+        initAndInflate("$testFile.tilePreviewWithContextParameter")
+
+        assertThatTileHasInflatedSuccessfully()
+    }
+
+    @Test
+    fun testTileWithWrongReturnTypeIsNotInflated() {
+        initAndInflate("$testFile.tilePreviewWithWrongReturnType")
+
+        assertThatTileHasNotInflated()
+    }
+
+    @Test
+    fun testTilePreviewWithNonContextParameterIsNotInflated() {
+        initAndInflate("$testFile.tilePreviewWithNonContextParameter")
+
+        assertThatTileHasNotInflated()
+    }
+
+    @Test
+    fun testNonStaticPreviewMethodWithDefaultConstructor() {
+        if (testFile == TEST_TILE_PREVIEWS_KOTLIN_FILE) {
+            initAndInflate("androidx.wear.tiles.tooling.SomeClass.nonStaticMethod")
+        } else {
+            initAndInflate("$testFile.nonStaticMethod")
+        }
+
+        assertThatTileHasInflatedSuccessfully()
+    }
+
+    private fun assertThatTileHasInflatedSuccessfully() {
         activityTestRule.runOnUiThread {
-            val textView =
-                (tileServiceViewAdapter.getChildAt(0) as ViewGroup)
-                    .getChildAt(0) as TextView
+            val textView = when (
+                val child = (tileServiceViewAdapter.getChildAt(0) as ViewGroup).getChildAt(0)
+            ) {
+                is TextView -> child
+                // layout elements are wrapped with a FrameLayout
+                else -> (child as? FrameLayout)?.getChildAt(0) as? TextView
+            }
             assertNotNull(textView)
-            assertEquals("Hello world!", textView.text.toString())
+            assertEquals("Hello world!", textView?.text.toString())
+        }
+    }
+
+    private fun assertThatTileHasNotInflated() {
+        activityTestRule.runOnUiThread {
+            assertEquals(0, tileServiceViewAdapter.childCount)
         }
     }
 
     companion object {
+        @Parameterized.Parameters
+        @JvmStatic
+        fun parameters() = listOf(
+            TEST_TILE_PREVIEWS_KOTLIN_FILE,
+            TEST_TILE_PREVIEWS_JAVA_FILE,
+        )
+
         class TestActivity : Activity() {
             override fun onCreate(savedInstanceState: Bundle?) {
                 super.onCreate(savedInstanceState)
diff --git a/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt b/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt
index e8a1a9c..d9e6909 100644
--- a/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt
+++ b/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt
@@ -32,6 +32,7 @@
 import androidx.wear.tiles.timeline.TilesTimelineCache
 import androidx.wear.tiles.tooling.preview.TilePreviewData
 import java.lang.reflect.Method
+import java.lang.reflect.Modifier
 import kotlin.math.roundToInt
 
 private const val TOOLS_NS_URI = "http://schemas.android.com/tools"
@@ -77,7 +78,7 @@
     }
 
     internal fun init(tilePreviewMethodFqn: String) {
-        val tilePreview = getTilePreview(tilePreviewMethodFqn)
+        val tilePreview = getTilePreview(tilePreviewMethodFqn) ?: return
         lateinit var tileRenderer: TileRenderer
         tileRenderer = TileRenderer(context, executor) { newState ->
             tileRenderer.previewTile(tilePreview, newState)
@@ -98,7 +99,7 @@
             .setDeviceConfiguration(deviceParams)
             .build()
 
-        val tile = tilePreview.onTileRequest(tileRequest, context).also { tile ->
+        val tile = tilePreview.onTileRequest(tileRequest).also { tile ->
             tile.state?.let { setState(it.keyToValueMapping) }
         }
         val layout = tile.tileTimeline?.getCurrentLayout() ?: return
@@ -107,7 +108,7 @@
             .setDeviceConfiguration(deviceParams)
             .setVersion(tile.resourcesVersion)
             .build()
-        val resources = tilePreview.onTileResourceRequest(resourcesRequest, context)
+        val resources = tilePreview.onTileResourceRequest(resourcesRequest)
 
         val inflateFuture = inflateAsync(layout, resources, this@TileServiceViewAdapter)
         inflateFuture.addListener({
@@ -116,20 +117,36 @@
             }
         }, executor)
     }
-}
 
-@SuppressLint("BanUncheckedReflection")
-internal fun getTilePreview(tilePreviewMethodFqn: String): TilePreviewData {
-    val className = tilePreviewMethodFqn.substringBeforeLast('.')
-    val methodName = tilePreviewMethodFqn.substringAfterLast('.')
+    @SuppressLint("BanUncheckedReflection")
+    internal fun getTilePreview(tilePreviewMethodFqn: String): TilePreviewData? {
+        val className = tilePreviewMethodFqn.substringBeforeLast('.')
+        val methodName = tilePreviewMethodFqn.substringAfterLast('.')
 
-    val method = Class.forName(className).declaredMethods.first {
-        it.name == methodName && it.parameterCount == 0
-    }.apply {
-        isAccessible = true
+        val methods = Class.forName(className).declaredMethods.filter { it.name == methodName }
+        methods.firstOrNull {
+            it.parameterCount == 1 && it.parameters.first().type == Context::class.java
+        }?.let { methodWithContextParameter ->
+            return invokeTilePreviewMethod(methodWithContextParameter, context)
+        }
+
+        return methods.firstOrNull {
+            it.name == methodName && it.parameterCount == 0
+        }?.let { methodWithoutContextParameter ->
+            return invokeTilePreviewMethod(methodWithoutContextParameter)
+        }
     }
 
-    return method.invoke(null) as TilePreviewData
+    @SuppressLint("BanUncheckedReflection")
+    private fun invokeTilePreviewMethod(method: Method, vararg args: Any?): TilePreviewData? {
+        method.isAccessible = true
+        return if (Modifier.isStatic(method.modifiers)) {
+            method.invoke(null, *args) as? TilePreviewData
+        } else {
+            val instance = method.declaringClass.getConstructor().newInstance()
+            method.invoke(instance, *args) as? TilePreviewData
+        }
+    }
 }
 
 internal fun TimelineBuilders.Timeline?.getCurrentLayout(): LayoutElementBuilders.Layout? {
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceUpdateRequester.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceUpdateRequester.kt
index 13b3f52..bf7e252 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceUpdateRequester.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceUpdateRequester.kt
@@ -57,6 +57,9 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public const val UPDATE_REQUEST_RECEIVER_PACKAGE = "com.google.android.wearable.app"
 
+        /** An override to [UPDATE_REQUEST_RECEIVER_PACKAGE] for tests. */
+        internal var overrideUpdateRequestsReceiverPackage: String? = null
+
         /**
          * Creates a [ComplicationDataSourceUpdateRequester].
          *
@@ -100,9 +103,13 @@
     private val complicationDataSourceComponent: ComponentName
 ) : ComplicationDataSourceUpdateRequester {
 
+    private fun updateRequestReceiverPackage() =
+        ComplicationDataSourceUpdateRequester.overrideUpdateRequestsReceiverPackage
+            ?: ComplicationDataSourceUpdateRequester.UPDATE_REQUEST_RECEIVER_PACKAGE
+
     override fun requestUpdateAll() {
         val intent = Intent(ComplicationDataSourceUpdateRequester.ACTION_REQUEST_UPDATE_ALL)
-        intent.setPackage(ComplicationDataSourceUpdateRequester.UPDATE_REQUEST_RECEIVER_PACKAGE)
+        intent.setPackage(updateRequestReceiverPackage())
         intent.putExtra(
             ComplicationDataSourceUpdateRequester.EXTRA_PROVIDER_COMPONENT,
             complicationDataSourceComponent
@@ -117,7 +124,7 @@
 
     override fun requestUpdate(vararg complicationInstanceIds: Int) {
         val intent = Intent(ComplicationDataSourceUpdateRequester.ACTION_REQUEST_UPDATE)
-        intent.setPackage(ComplicationDataSourceUpdateRequester.UPDATE_REQUEST_RECEIVER_PACKAGE)
+        intent.setPackage(updateRequestReceiverPackage())
         intent.putExtra(
             ComplicationDataSourceUpdateRequester.EXTRA_PROVIDER_COMPONENT,
             complicationDataSourceComponent
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationTextTemplate.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationTextTemplate.java
index d920085..0b19935 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationTextTemplate.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationTextTemplate.java
@@ -41,12 +41,10 @@
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
-/**
- * Displays one or more ComplicationText objects in a template.
- *
- */
+/** Displays one or more ComplicationText objects in a template. */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @SuppressLint("BanParcelableUsage")
 public final class ComplicationTextTemplate implements Parcelable, TimeDependentText {
@@ -196,6 +194,17 @@
         return 0;
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+        return "ComplicationTextTemplate{"
+                + "surroundingText="
+                + mSurroundingText
+                + ", complicationTexts="
+                + Arrays.toString(mComplicationTexts)
+                + "}";
+    }
+
     /**
      * Builder for a ComplicationTextTemplate object that displays one or more {@link
      * ComplicationText} objects, within a format string if specified.
diff --git a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
index 561aa31..5258a99 100644
--- a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
+++ b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
@@ -80,6 +80,7 @@
 import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.complications.data.EmptyComplicationData
 import androidx.wear.watchface.complications.data.LongTextComplicationData
+import androidx.wear.watchface.complications.data.NoDataComplicationData
 import androidx.wear.watchface.complications.data.PlainComplicationText
 import androidx.wear.watchface.complications.data.ShortTextComplicationData
 import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
@@ -96,7 +97,6 @@
 import androidx.wear.watchface.style.WatchFaceLayer
 import androidx.wear.watchface.style.data.UserStyleWireFormat
 import com.google.common.truth.Truth.assertThat
-import java.lang.IllegalArgumentException
 import java.time.Instant
 import java.time.ZonedDateTime
 import java.util.concurrent.CountDownLatch
@@ -183,19 +183,19 @@
 private val placeholderWatchState = MutableWatchState().asWatchState()
 private val mockLeftCanvasComplication =
     CanvasComplicationDrawable(
-        ComplicationDrawable(),
+        ComplicationDrawable(ApplicationProvider.getApplicationContext()),
         placeholderWatchState,
         mockInvalidateCallback
     )
 private val mockRightCanvasComplication =
     CanvasComplicationDrawable(
-        ComplicationDrawable(),
+        ComplicationDrawable(ApplicationProvider.getApplicationContext()),
         placeholderWatchState,
         mockInvalidateCallback
     )
 private val mockBackgroundCanvasComplication =
     CanvasComplicationDrawable(
-        ComplicationDrawable(),
+        ComplicationDrawable(ApplicationProvider.getApplicationContext()),
         placeholderWatchState,
         mockInvalidateCallback
     )
@@ -901,7 +901,7 @@
     public fun fixedComplicationDataSource() {
         val mockLeftCanvasComplication =
             CanvasComplicationDrawable(
-                ComplicationDrawable(),
+                ComplicationDrawable(ApplicationProvider.getApplicationContext()),
                 placeholderWatchState,
                 mockInvalidateCallback
             )
@@ -1231,6 +1231,7 @@
                 .isEqualTo(Rect(120, 160, 160, 240))
             assertThat(editorSession.complicationsDataSourceInfo.value[LEFT_COMPLICATION_ID]!!.name)
                 .isEqualTo("DataSource1")
+            assertThat(leftComplication.complicationData.value).isEqualTo(NoDataComplicationData())
 
             /**
              * Invoke [TestComplicationHelperActivity] which will change the complication data
@@ -1247,6 +1248,10 @@
                 chosenComplicationDataSource.complicationDataSourceInfo
             )
 
+            // This should set the interactive complication to empty, preventing briefly seeing the
+            // old complication.
+            assertThat(leftComplication.complicationData.value).isEqualTo(EmptyComplicationData())
+
             // This should update the preview data to point to the updated DataSource3 data.
             val previewComplication =
                 editorSession.complicationsPreviewData.value[LEFT_COMPLICATION_ID]
@@ -1942,33 +1947,62 @@
     @Test
     @Suppress("Deprecation") // userStyleSettings
     public fun doNotCommit() {
+        ComplicationDataSourceChooserContract.useTestComplicationHelperActivity = true
+        TestComplicationHelperActivity.resultIntent =
+            CompletableDeferred(
+                Intent().apply {
+                    putExtra(
+                        ComplicationDataSourceChooserIntent.EXTRA_PROVIDER_INFO,
+                        ComplicationDataSourceInfo(
+                                "TestDataSource3App",
+                                "TestDataSource3",
+                                Icon.createWithBitmap(
+                                    Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+                                ),
+                                ComplicationType.LONG_TEXT,
+                                dataSource3
+                            )
+                            .toWireComplicationProviderInfo()
+                    )
+                }
+            )
         val scenario =
             createOnWatchFaceEditingTestActivity(
                 listOf(colorStyleSetting, watchHandStyleSetting),
-                emptyList(),
+                listOf(leftComplication, rightComplication),
                 previewScreenshotParams =
                     PreviewScreenshotParams(RenderParameters.DEFAULT_INTERACTIVE, Instant.EPOCH)
             )
+        lateinit var activity: OnWatchFaceEditingTestActivity
+        scenario.onActivity { activity = it }
+
         val editorObserver = TestEditorObserver()
         val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
-        scenario.onActivity { activity ->
-            assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
-                .isEqualTo(redStyleOption.id.value)
-            assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
-                .isEqualTo(classicStyleOption.id.value)
 
-            // Select [blueStyleOption] and [gothicStyleOption].
-            val mutableUserStyle = activity.editorSession.userStyle.value.toMutableUserStyle()
-            for (userStyleSetting in activity.editorSession.userStyleSchema.userStyleSettings) {
-                mutableUserStyle[userStyleSetting] = userStyleSetting.options.last()
-            }
-            activity.editorSession.userStyle.value = mutableUserStyle.toUserStyle()
-
-            // This should cause the style on the to be reverted back to the initial style.
-            activity.editorSession.commitChangesOnClose = false
-            activity.editorSession.close()
-            activity.finish()
+        assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
+            .isEqualTo(redStyleOption.id.value)
+        assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
+            .isEqualTo(classicStyleOption.id.value)
+        // Select [blueStyleOption] and [gothicStyleOption].
+        val mutableUserStyle = activity.editorSession.userStyle.value.toMutableUserStyle()
+        for (userStyleSetting in activity.editorSession.userStyleSchema.userStyleSettings) {
+            mutableUserStyle[userStyleSetting] = userStyleSetting.options.last()
         }
+        activity.editorSession.userStyle.value = mutableUserStyle.toUserStyle()
+
+        assertThat(leftComplication.complicationData.value).isEqualTo(NoDataComplicationData())
+        // Select another complication.
+        runBlocking {
+            activity.editorSession.openComplicationDataSourceChooser(LEFT_COMPLICATION_ID)
+        }
+        // This should set the interactive complication to empty, preventing briefly seeing the
+        // old complication.
+        assertThat(leftComplication.complicationData.value).isEqualTo(EmptyComplicationData())
+
+        // This should cause the style on the to be reverted back to the initial style.
+        activity.editorSession.commitChangesOnClose = false
+        activity.editorSession.close()
+        activity.finish()
 
         val result =
             editorObserver
@@ -1981,12 +2015,13 @@
         assertFalse(result.shouldCommitChanges)
         assertNull(result.previewImage)
 
-        // The original style should be applied to the watch face however because
-        // commitChangesOnClose is false.
         assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
             .isEqualTo(redStyleOption.id.value)
         assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
             .isEqualTo(classicStyleOption.id.value)
+        // The original complication data and style should be applied to the watch face however
+        // because commitChangesOnClose is false.
+        assertThat(leftComplication.complicationData.value).isEqualTo(NoDataComplicationData())
 
         EditorService.globalEditorService.unregisterObserver(observerId)
     }
diff --git a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index acfcd72..8c92894 100644
--- a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -570,6 +570,8 @@
 
             try {
                 deferredComplicationPreviewDataAvailable.await()
+                val previousDataSourceInfo: ComplicationDataSourceInfo? =
+                    complicationsDataSourceInfo.value[complicationSlotId]
 
                 // Emit an updated complicationsDataSourceInfoMap.
                 complicationsDataSourceInfo.value =
@@ -589,6 +591,11 @@
                     HashMap(complicationsPreviewData.value).apply {
                         this[complicationSlotId] = previewData ?: EmptyComplicationData()
                     }
+                onComplicationUpdated(
+                    complicationSlotId,
+                    from = previousDataSourceInfo,
+                    to = complicationDataSourceChooserResult.dataSourceInfo,
+                )
 
                 return ChosenComplicationDataSource(
                     complicationSlotId,
@@ -779,6 +786,12 @@
     protected open val showComplicationDeniedDialogIntent: Intent? = null
 
     protected open val showComplicationRationaleDialogIntent: Intent? = null
+
+    protected open fun onComplicationUpdated(
+        complicationSlotId: Int,
+        from: ComplicationDataSourceInfo?,
+        to: ComplicationDataSourceInfo?,
+    ) {}
 }
 
 /**
@@ -936,6 +949,11 @@
         if (!commitChangesOnClose && this::previousWatchFaceUserStyle.isInitialized) {
             userStyle.value = previousWatchFaceUserStyle
         }
+        if (this::editorDelegate.isInitialized) {
+            editorDelegate.complicationSlotsManager.unfreezeAllSlotsForEdit(
+                clearData = commitChangesOnClose
+            )
+        }
 
         if (this::fetchComplicationsDataJob.isInitialized) {
             // Wait until the fetchComplicationsDataJob has finished and released the
@@ -995,6 +1013,18 @@
         requireNotClosed()
         return editorDelegate.complicationSlotsManager.getComplicationSlotAt(x, y)?.id
     }
+
+    override fun onComplicationUpdated(
+        complicationSlotId: Int,
+        from: ComplicationDataSourceInfo?,
+        to: ComplicationDataSourceInfo?,
+    ) {
+        editorDelegate.complicationSlotsManager.freezeSlotForEdit(
+            complicationSlotId,
+            from = from,
+            to = to,
+        )
+    }
 }
 
 @RequiresApi(27)
diff --git a/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/AsyncListenableWatchFaceServiceTest.kt b/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/AsyncListenableWatchFaceServiceTest.kt
index b12573c..77b9215 100644
--- a/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/AsyncListenableWatchFaceServiceTest.kt
+++ b/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/AsyncListenableWatchFaceServiceTest.kt
@@ -102,6 +102,7 @@
     private var surfaceHolderOverride: SurfaceHolder,
 ) : ListenableWatchFaceRuntimeService() {
     lateinit var lastResourceOnlyWatchFacePackageName: String
+    val lastResourceOnlyWatchFacePackageNameLatch = CountDownLatch(1)
 
     init {
         attachBaseContext(testContext)
@@ -117,6 +118,7 @@
         resourceOnlyWatchFacePackageName: String
     ): ListenableFuture<WatchFace> {
         lastResourceOnlyWatchFacePackageName = resourceOnlyWatchFacePackageName
+        lastResourceOnlyWatchFacePackageNameLatch.countDown()
 
         val future = SettableFuture.create<WatchFace>()
         // Post a task to resolve the future.
@@ -210,9 +212,12 @@
 
         val client = awaitWithTimeout(deferredClient)
 
-        // To avoid a race condition, we need to wait for the watchface to be fully created, which
-        // this does.
-        client.complicationSlotsState
+        Assert.assertTrue(
+            service.lastResourceOnlyWatchFacePackageNameLatch.await(
+                TIME_OUT_MILLIS,
+                TimeUnit.MILLISECONDS
+            )
+        )
 
         assertThat(service.lastResourceOnlyWatchFacePackageName).isEqualTo("com.example.wf")
 
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index 33b865a..4a563e0 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -722,6 +722,15 @@
         mutableUserStyle.value = newUserStyle
     }
 
+    /** Sets the user style, and returns a restoration function. */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun updateUserStyleForScreenshot(newUserStyle: UserStyle): AutoCloseable {
+        val originalStyle = userStyle.value
+        updateUserStyle(newUserStyle)
+        // Avoid overwriting a change made by someone else.
+        return AutoCloseable { mutableUserStyle.compareAndSet(newUserStyle, originalStyle) }
+    }
+
     @Suppress("Deprecation") // userStyleSettings
     internal fun validateUserStyle(userStyle: UserStyle) {
         for ((key, value) in userStyle) {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index c36b80e..b89aaba 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -28,8 +28,10 @@
 import androidx.annotation.Px
 import androidx.annotation.RestrictTo
 import androidx.annotation.UiThread
+import androidx.annotation.VisibleForTesting
 import androidx.annotation.WorkerThread
 import androidx.wear.watchface.RenderParameters.HighlightedElement
+import androidx.wear.watchface.complications.ComplicationDataSourceInfo
 import androidx.wear.watchface.complications.ComplicationSlotBounds
 import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
 import androidx.wear.watchface.complications.data.ComplicationData
@@ -243,8 +245,8 @@
     public const val ROUND_RECT: Int = 0
 
     /**
-     * For a full screen image complication slot drawn behind the watch face. Note you can only
-     * have a single background complication slot.
+     * For a full screen image complication slot drawn behind the watch face. Note you can only have
+     * a single background complication slot.
      */
     public const val BACKGROUND: Int = 1
 
@@ -342,21 +344,6 @@
  * expanded by [ComplicationSlotBounds.perComplicationTypeMargins]. Expanded bounds can overlap so
  * the [ComplicationSlot] with the lowest id that intersects the coordinates, if any, is selected.
  *
- * @param accessibilityTraversalIndex Used to sort Complications when generating accessibility
- *   content description labels.
- * @param bounds The complication slot's [ComplicationSlotBounds].
- * @param supportedTypes The list of [ComplicationType]s accepted by this complication slot, must be
- *   non-empty. During complication data source selection, each item in this list is compared in
- *   turn with entries from a data source's data source's supported types. The first matching entry
- *   from `supportedTypes` is chosen. If there are no matches then that data source is not eligible
- *   to be selected in this slot.
- * @param defaultPolicy The [DefaultComplicationDataSourcePolicy] which controls the initial
- *   complication data source when the watch face is first installed.
- * @param defaultDataSourceType The default [ComplicationType] for the default complication data
- *   source.
- * @param configExtras Extras to be merged into the Intent sent when invoking the complication data
- *   source chooser activity. This features is intended for OEM watch faces where they have elements
- *   that behave like a complication but are in fact entirely watch face specific.
  * @property id The Watch Face's ID for the complication slot.
  * @property boundsType The [ComplicationSlotBoundsTypeIntDef] of the complication slot.
  * @property canvasComplicationFactory The [CanvasComplicationFactory] used to generate a
@@ -373,6 +360,25 @@
  *   complication slot.
  */
 public class ComplicationSlot
+/**
+ * Constructs a [ComplicationSlot].
+ *
+ * @param accessibilityTraversalIndex Used to sort Complications when generating accessibility
+ *   content description labels.
+ * @param bounds The complication slot's [ComplicationSlotBounds].
+ * @param supportedTypes The list of [ComplicationType]s accepted by this complication slot, must be
+ *   non-empty. During complication data source selection, each item in this list is compared in
+ *   turn with entries from a data source's data source's supported types. The first matching entry
+ *   from `supportedTypes` is chosen. If there are no matches then that data source is not eligible
+ *   to be selected in this slot.
+ * @param defaultPolicy The [DefaultComplicationDataSourcePolicy] which controls the initial
+ *   complication data source when the watch face is first installed.
+ * @param defaultDataSourceType The default [ComplicationType] for the default complication data
+ *   source.
+ * @param configExtras Extras to be merged into the Intent sent when invoking the complication data
+ *   source chooser activity. This features is intended for OEM watch faces where they have elements
+ *   that behave like a complication but are in fact entirely watch face specific.
+ */
 @ComplicationExperimental
 internal constructor(
     public val id: Int,
@@ -391,8 +397,7 @@
     screenReaderNameResourceId: Int?,
     // TODO(b/230364881): This should really be public but some metalava bug is preventing
     // @ComplicationExperimental from working on the getter so it's currently hidden.
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public val boundingArc: BoundingArc?
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public val boundingArc: BoundingArc?
 ) {
     /**
      * The [ComplicationSlotsManager] this is attached to. Only set after the
@@ -430,7 +435,8 @@
 
     private var lastComplicationUpdate = Instant.EPOCH
 
-    private class ComplicationDataHistoryEntry(
+    @VisibleForTesting
+    internal class ComplicationDataHistoryEntry(
         val complicationData: ComplicationData,
         val time: Instant
     )
@@ -439,7 +445,8 @@
      * There doesn't seem to be a convenient ring buffer in the standard library so implement our
      * own one.
      */
-    private class RingBuffer(val size: Int) : Iterable<ComplicationDataHistoryEntry> {
+    @VisibleForTesting
+    internal class RingBuffer(val size: Int) : Iterable<ComplicationDataHistoryEntry> {
         private val entries = arrayOfNulls<ComplicationDataHistoryEntry>(size)
         private var readIndex = 0
         private var writeIndex = 0
@@ -467,9 +474,11 @@
 
     /**
      * In userdebug builds maintain a history of the last [MAX_COMPLICATION_HISTORY_ENTRIES]-1
-     * complications, which is logged in dumpsys to help debug complication issues.
+     * complications sent by the system, which is logged in dumpsys to help debug complication
+     * issues.
      */
-    private val complicationHistory =
+    @VisibleForTesting
+    internal val complicationHistory =
         if (Build.TYPE.equals("userdebug")) {
             RingBuffer(MAX_COMPLICATION_HISTORY_ENTRIES)
         } else {
@@ -666,8 +675,11 @@
             )
     }
 
+    /** Builder for constructing [ComplicationSlot]s. */
+    @OptIn(ComplicationExperimental::class)
+    public class Builder
     /**
-     * Builder for constructing [ComplicationSlot]s.
+     * Constructs a [Builder].
      *
      * @param id The watch face's ID for this complication. Can be any integer but should be unique
      *   within the watch face.
@@ -683,8 +695,6 @@
      * @param complicationTapFilter The [ComplicationTapFilter] used to perform hit testing for this
      *   complication.
      */
-    @OptIn(ComplicationExperimental::class)
-    public class Builder
     internal constructor(
         private val id: Int,
         private val canvasComplicationFactory: CanvasComplicationFactory,
@@ -995,11 +1005,42 @@
     internal var dataDirty = true
 
     /**
+     * The data set by [setComplicationData] (and then selected by
+     * [selectComplicationDataForInstant]). Exposed by [complicationData] unless
+     * [frozenDataSourceForEdit] is set.
+     */
+    private var selectedData: ComplicationData = NoDataComplicationData()
+
+    private data class FrozenDataSourceForEdit(
+        val from: ComplicationDataSourceInfo?,
+        val to: ComplicationDataSourceInfo?,
+    )
+
+    /**
+     * Marks the slot frozen, so [complicationData] only returns [EmptyComplicationData].
+     *
+     * This reduces the chances of the slot showing the previous complication momentarily when the
+     * user finishes editing.
+     *
+     * Memorizing from/to edited data source because we need to avoid clearing the complication data
+     * when the data source is the same, because the platform doesn't re-fetch complications when
+     * only updating configuration.
+     */
+    private var frozenDataSourceForEdit: FrozenDataSourceForEdit? = null
+
+    /**
      * The [androidx.wear.watchface.complications.data.ComplicationData] associated with the
      * [ComplicationSlot]. This defaults to [NoDataComplicationData].
+     *
+     * If the slot is frozen for edit, this is set to [EmptyComplicationData].
      */
-    public val complicationData: StateFlow<ComplicationData> =
-        MutableStateFlow(NoDataComplicationData())
+    // Can be described as:
+    //   selectedData.combine(frozenDataSourceForEdit) { data, frozenDataSource ->
+    //     if (frozenDataSource == null) data else EmptyComplicationData()
+    //   }
+    // but some flows depend on this StateFlow updating immediately after selectedData was changed,
+    // and Flow.combine() doesn't ensure that.
+    public val complicationData: StateFlow<ComplicationData> = MutableStateFlow(selectedData)
 
     /**
      * The complication data sent by the system. This may contain a timeline out of which
@@ -1010,18 +1051,59 @@
 
     /**
      * Sets the current [ComplicationData] and if it's a timeline, the correct override for
-     * [instant] is chosen.
+     * [instant] is chosen. Any images associated with the complication are loaded asynchronously
+     * and the complication history is updated.
      */
-    internal fun setComplicationData(
-        complicationData: ComplicationData,
-        loadDrawablesAsynchronous: Boolean,
-        instant: Instant
-    ) {
-        lastComplicationUpdate = instant
+    internal fun setComplicationData(complicationData: ComplicationData, instant: Instant) {
         complicationHistory?.push(ComplicationDataHistoryEntry(complicationData, instant))
-        timelineComplicationData = complicationData
-        timelineEntries = complicationData.asWireComplicationData().timelineEntries?.toList()
-        selectComplicationDataForInstant(instant, loadDrawablesAsynchronous, true)
+        setTimelineData(complicationData, instant)
+        selectComplicationDataForInstant(instant, forceUpdate = true)
+    }
+
+    /**
+     * Sets the current [ComplicationData] and if it's a timeline, the correct override for
+     * [instant] is chosen. Any images are loaded synchronously. The complication history is not
+     * updated.
+     *
+     * Returns a restoration function.
+     */
+    internal fun setComplicationDataForScreenshot(
+        complicationData: ComplicationData,
+        instant: Instant
+    ): AutoCloseable {
+        val originalComplicationData = timelineComplicationData
+        val originalInstant = lastComplicationUpdate
+        val restore = AutoCloseable {
+            // Avoid overwriting a change made by someone else, can still race.
+            if (timelineComplicationData !== complicationData) return@AutoCloseable
+            setTimelineData(originalComplicationData, originalInstant)
+            selectComplicationDataForInstant(
+                originalInstant,
+                forceUpdate = true,
+                forScreenshot = false,
+            )
+        }
+
+        try {
+            setTimelineData(complicationData, instant)
+            selectComplicationDataForInstant(instant, forceUpdate = true, forScreenshot = true)
+        } catch (e: Throwable) {
+            // Cleanup on failure.
+            restore.close()
+            throw e
+        }
+        return restore
+    }
+
+    private fun setTimelineData(data: ComplicationData, instant: Instant) {
+        lastComplicationUpdate = instant
+        timelineComplicationData = data
+        timelineEntries = data.asWireComplicationData().timelineEntries?.toList()
+    }
+
+    private fun loadData(data: ComplicationData, loadDrawablesAsynchronous: Boolean = false) {
+        renderer.loadData(data, loadDrawablesAsynchronous = loadDrawablesAsynchronous)
+        (complicationData as MutableStateFlow<ComplicationData>).value = data
     }
 
     /**
@@ -1030,8 +1112,8 @@
      */
     internal fun selectComplicationDataForInstant(
         instant: Instant,
-        loadDrawablesAsynchronous: Boolean,
-        forceUpdate: Boolean
+        forceUpdate: Boolean,
+        forScreenshot: Boolean = false
     ) {
         var previousShortest = Long.MAX_VALUE
         val time = instant.epochSecond
@@ -1061,14 +1143,48 @@
             best = screenLockedFallback // This is NoDataComplicationData.
         }
 
-        if (!forceUpdate && complicationData.value == best) return
-        renderer.loadData(best, loadDrawablesAsynchronous)
-        (complicationData as MutableStateFlow).value = best
+        if (!forceUpdate && selectedData == best) return
+
+        val frozen = frozenDataSourceForEdit != null
+        if (!frozen || forScreenshot) {
+            loadData(best, loadDrawablesAsynchronous = !forScreenshot)
+        } else {
+            // Restoring frozen slot to empty in case it was changed for screenshot.
+            loadData(EmptyComplicationData())
+        }
+        selectedData = best
 
         // forceUpdate is used for screenshots, don't set the dirty flag for those.
-        if (!forceUpdate) {
-            dataDirty = true
+        if (!forceUpdate) dataDirty = true
+    }
+
+    /** Sets [frozenDataSourceForEdit]. */
+    internal fun freezeForEdit(
+        from: ComplicationDataSourceInfo?,
+        to: ComplicationDataSourceInfo?,
+    ) {
+        val previous = frozenDataSourceForEdit
+        // Keeping the original "from" of the first edit.
+        frozenDataSourceForEdit = FrozenDataSourceForEdit(from = previous?.from ?: from, to = to)
+        // If this is the first freeze, render EmptyComplicationData.
+        if (previous == null) loadData(EmptyComplicationData())
+    }
+
+    /** Unsets [frozenDataSourceForEdit]. */
+    internal fun unfreezeForEdit(clearData: Boolean) {
+        val frozenDataSourceForEdit = frozenDataSourceForEdit ?: return
+        // Clearing the previously selected data if needed.
+        if (
+            clearData &&
+                frozenDataSourceForEdit.from?.componentName !=
+                    frozenDataSourceForEdit.to?.componentName
+        ) {
+            setComplicationData(EmptyComplicationData(), Instant.now())
         }
+        this.frozenDataSourceForEdit = null
+        // Re-load current/new data immediately
+        // (especially in case of new data skipped loading in selectComplicationDataForInstant).
+        loadData(selectedData)
     }
 
     /**
@@ -1160,7 +1276,8 @@
 
         if (isHeadless) {
             timelineComplicationData = EmptyComplicationData()
-            (complicationData as MutableStateFlow).value = EmptyComplicationData()
+            selectedData = EmptyComplicationData()
+            (complicationData as MutableStateFlow<ComplicationData>).value = EmptyComplicationData()
         }
     }
 
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
index d9cdbd9..d90b9d3 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
@@ -28,6 +28,7 @@
 import androidx.annotation.UiThread
 import androidx.annotation.VisibleForTesting
 import androidx.annotation.WorkerThread
+import androidx.wear.watchface.complications.ComplicationDataSourceInfo
 import androidx.wear.watchface.complications.ComplicationSlotBounds
 import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.data.ComplicationExperimental
@@ -327,28 +328,40 @@
             return
         }
         complication.dataDirty = complication.dataDirty || (complication.renderer.getData() != data)
-        complication.setComplicationData(data, true, instant)
+        complication.setComplicationData(data, instant)
     }
 
     /**
-     * For use by screen shot code which will reset the data afterwards, hence dirty bit not set.
+     * Sets complication data, returning a restoration function.
+     *
+     * As this is used for screen shots, dirty bit (used for content description) is not set.
      */
     @UiThread
-    internal fun setComplicationDataUpdateSync(
-        complicationSlotId: Int,
-        data: ComplicationData,
-        instant: Instant
-    ) {
-        val complication = complicationSlots[complicationSlotId]
-        if (complication == null) {
-            Log.e(
-                TAG,
-                "setComplicationDataUpdateSync failed due to invalid complicationSlotId=" +
-                    "$complicationSlotId with data=$data"
-            )
-            return
+    internal fun setComplicationDataForScreenshot(
+        slotIdToData: Map<Int, ComplicationData>,
+        instant: Instant,
+    ): AutoCloseable {
+        val restores = mutableListOf<AutoCloseable>()
+        val restore = AutoCloseable { restores.forEach(AutoCloseable::close) }
+        try {
+            for ((id, data) in slotIdToData) {
+                val slot = complicationSlots[id]
+                if (slot == null) {
+                    Log.e(
+                        TAG,
+                        "setComplicationDataForScreenshot failed due to invalid " +
+                            "complicationSlotId=$id with data=$data"
+                    )
+                    continue
+                }
+                restores.add(slot.setComplicationDataForScreenshot(data, instant))
+            }
+        } catch (e: Throwable) {
+            // Cleanup changes on failure.
+            restore.close()
+            throw e
         }
-        complication.setComplicationData(data, false, instant)
+        return restore
     }
 
     /**
@@ -358,11 +371,7 @@
     @UiThread
     internal fun selectComplicationDataForInstant(instant: Instant) {
         for ((_, complication) in complicationSlots) {
-            complication.selectComplicationDataForInstant(
-                instant,
-                loadDrawablesAsynchronous = true,
-                forceUpdate = false
-            )
+            complication.selectComplicationDataForInstant(instant, forceUpdate = false)
         }
 
         // selectComplicationDataForInstant may have changed the complication, if so we need to
@@ -372,6 +381,22 @@
         }
     }
 
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun freezeSlotForEdit(
+        slotId: Int,
+        from: ComplicationDataSourceInfo?,
+        to: ComplicationDataSourceInfo?,
+    ) {
+        complicationSlots[slotId]?.freezeForEdit(from = from, to = to)
+    }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun unfreezeAllSlotsForEdit(clearData: Boolean) {
+        for (slot in complicationSlots.values) {
+            slot.unfreezeForEdit(clearData)
+        }
+    }
+
     /**
      * Returns the id of the complication slot at coordinates x, y or `null` if there isn't one.
      * Initially checks slots without margins (should be no overlaps) then then if there was no hit
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index b2e238e..270373f 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -252,8 +252,9 @@
                     }
                     .apply { setContext(context) }
 
-            val engine = watchFaceService.createHeadlessEngine(componentName)
-                as WatchFaceService.EngineWrapper
+            val engine =
+                watchFaceService.createHeadlessEngine(componentName)
+                    as WatchFaceService.EngineWrapper
             val headlessWatchFaceImpl = engine.createHeadlessInstance(params)
             return engine.deferredWatchFaceImpl.await().WFEditorDelegate(headlessWatchFaceImpl)
         }
@@ -472,10 +473,7 @@
         }
     }
 
-    /**
-     * The [OverlayStyle]. This feature is unimplemented on any platform, and will be
-     * removed.
-     */
+    /** The [OverlayStyle]. This feature is unimplemented on any platform, and will be removed. */
     @Deprecated("OverlayStyle will be removed in a future release.")
     @Suppress("Deprecation")
     public var overlayStyle: OverlayStyle = OverlayStyle()
@@ -655,8 +653,7 @@
     private val tapListener = watchface.tapListener
     internal var complicationDeniedDialogIntent = watchface.complicationDeniedDialogIntent
     internal var complicationRationaleDialogIntent = watchface.complicationRationaleDialogIntent
-    @Suppress("Deprecation")
-    internal var overlayStyle = watchface.overlayStyle
+    @Suppress("Deprecation") internal var overlayStyle = watchface.overlayStyle
 
     private var mockTime = MockTime(1.0, 0, Long.MAX_VALUE)
 
@@ -668,30 +665,31 @@
     internal var nextDrawTimeMillis: Long = 0
 
     internal val componentName = watchFaceHostApi.getComponentName()
+    private val displayManager: DisplayManager
+    private val displayListener: DisplayManager.DisplayListener
 
     init {
         val context = watchFaceHostApi.getContext()
-        val displayManager =
-                context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+         displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+         displayListener = object : DisplayManager.DisplayListener {
+            override fun onDisplayAdded(displayId: Int) {}
+
+            override fun onDisplayChanged(displayId: Int) {
+                val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)!!
+                if (display.state == Display.STATE_OFF && watchState.isVisible.value == false) {
+                    // We want to avoid a glimpse of a stale time when transitioning from
+                    // hidden to visible, so we render two black frames to clear the buffers
+                    // when the display has been turned off and the watch is not visible.
+                    renderer.renderBlackFrame()
+                    renderer.renderBlackFrame()
+                }
+            }
+
+            override fun onDisplayRemoved(displayId: Int) {}
+        }
         displayManager.registerDisplayListener(
-                object : DisplayManager.DisplayListener {
-                    override fun onDisplayAdded(displayId: Int) {}
-
-                    override fun onDisplayChanged(displayId: Int) {
-                        val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)!!
-                        if (display.state == Display.STATE_OFF &&
-                                watchState.isVisible.value == false) {
-                            // We want to avoid a glimpse of a stale time when transitioning from
-                            // hidden to visible, so we render two black frames to clear the buffers
-                            // when the display has been turned off and the watch is not visible.
-                            renderer.renderBlackFrame()
-                            renderer.renderBlackFrame()
-                        }
-                    }
-
-                    override fun onDisplayRemoved(displayId: Int) {}
-                },
-                watchFaceHostApi.getUiThreadHandler()
+            displayListener,
+            watchFaceHostApi.getUiThreadHandler()
         )
     }
 
@@ -893,8 +891,11 @@
             get() = watchFaceHostApi.getComplicationRationaleIntent()
 
         override var editorObscuresWatchFace: Boolean
-            get() = InteractiveInstanceManager
-                .getCurrentInteractiveInstance()?.engine?.editorObscuresWatchFace ?: false
+            get() =
+                InteractiveInstanceManager.getCurrentInteractiveInstance()
+                    ?.engine
+                    ?.editorObscuresWatchFace
+                    ?: false
             set(value) {
                 InteractiveInstanceManager.getCurrentInteractiveInstance()?.engine?.let {
                     it.editorObscuresWatchFace = value
@@ -907,37 +908,14 @@
             slotIdToComplicationData: Map<Int, ComplicationData>?
         ): Bitmap =
             TraceEvent("WFEditorDelegate.takeScreenshot").use {
-                val oldComplicationData =
-                    complicationSlotsManager.complicationSlots.values.associateBy(
-                        { it.id },
-                        { it.renderer.getData() }
-                    )
-
-                slotIdToComplicationData?.let {
-                    for ((id, complicationData) in it) {
-                        complicationSlotsManager.setComplicationDataUpdateSync(
-                            id,
-                            complicationData,
-                            instant
+                slotIdToComplicationData
+                    ?.let { complicationSlotsManager.setComplicationDataForScreenshot(it, instant) }
+                    .use {
+                        renderer.takeScreenshot(
+                            ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")),
+                            renderParameters
                         )
                     }
-                }
-                val screenShot =
-                    renderer.takeScreenshot(
-                        ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")),
-                        renderParameters
-                    )
-                slotIdToComplicationData?.let {
-                    val now = getNow()
-                    for ((id, complicationData) in oldComplicationData) {
-                        complicationSlotsManager.setComplicationDataUpdateSync(
-                            id,
-                            complicationData,
-                            now
-                        )
-                    }
-                }
-                return screenShot
             }
 
         override fun setComplicationSlotConfigExtrasChangeCallback(
@@ -965,6 +943,7 @@
             WatchFace.unregisterEditorDelegate(componentName)
         }
         unregisterReceivers()
+        displayManager.unregisterDisplayListener(displayListener)
     }
 
     @UiThread
@@ -994,8 +973,11 @@
         // Separate calls are issued to deliver the state of isAmbient and isVisible, so during init
         // we might not yet know the state of both (which is required by the shouldAnimate logic).
         // If the editor is obscuring the watch face, there's no need to schedule a frame.
-        if (!watchState.isAmbient.hasValue() || !watchState.isVisible.hasValue() ||
-            editorObscuresWatchFace) {
+        if (
+            !watchState.isAmbient.hasValue() ||
+                !watchState.isVisible.hasValue() ||
+                editorObscuresWatchFace
+        ) {
             return
         }
 
@@ -1177,57 +1159,26 @@
     @RequiresApi(27)
     internal fun renderWatchFaceToBitmap(params: WatchFaceRenderParams): Bundle =
         TraceEvent("WatchFaceImpl.renderWatchFaceToBitmap").use {
-            val oldStyle = currentUserStyleRepository.userStyle.value
             val instant = Instant.ofEpochMilli(params.calendarTimeMillis)
-
-            params.userStyle?.let {
-                currentUserStyleRepository.updateUserStyle(
-                    UserStyle(UserStyleData(it), currentUserStyleRepository.schema)
-                )
-            }
-
-            val oldComplicationData =
-                complicationSlotsManager.complicationSlots.values.associateBy(
-                    { it.id },
-                    { it.renderer.getData() }
-                )
-
-            params.idAndComplicationDatumWireFormats?.let {
-                for (idAndData in it) {
-                    complicationSlotsManager.setComplicationDataUpdateSync(
-                        idAndData.id,
-                        idAndData.complicationData.toApiComplicationData(),
+            val bitmap =
+                setForScreenshot(
+                        params.userStyle?.let {
+                            UserStyle(UserStyleData(it), currentUserStyleRepository.schema)
+                        },
+                        params.idAndComplicationDatumWireFormats?.let { idAndData ->
+                            idAndData.associate {
+                                it.id to it.complicationData.toApiComplicationData()
+                            }
+                        },
                         instant
                     )
-                }
-            }
-
-            val bitmap =
-                renderer.takeScreenshot(
-                    ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")),
-                    RenderParameters(params.renderParametersWireFormat)
-                )
-
-            // No point in restoring the old style and complication if this is headless.
-            if (!watchState.isHeadless) {
-                // Restore previous style & complicationSlots if required.
-                if (params.userStyle != null) {
-                    currentUserStyleRepository.updateUserStyle(oldStyle)
-                }
-
-                if (params.idAndComplicationDatumWireFormats != null) {
-                    val now = getNow()
-                    for ((id, complicationData) in oldComplicationData) {
-                        complicationSlotsManager.setComplicationDataUpdateSync(
-                            id,
-                            complicationData,
-                            now
+                    .use {
+                        renderer.takeScreenshot(
+                            ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")),
+                            RenderParameters(params.renderParametersWireFormat)
                         )
                     }
-                }
-            }
-
-            return SharedMemoryImage.ashmemWriteImageBundle(bitmap)
+            SharedMemoryImage.ashmemWriteImageBundle(bitmap)
         }
 
     @UiThread
@@ -1254,100 +1205,94 @@
     @RequiresApi(27)
     internal fun renderComplicationToBitmap(params: ComplicationRenderParams): Bundle? =
         TraceEvent("WatchFaceImpl.renderComplicationToBitmap").use {
-            val zonedDateTime =
-                ZonedDateTime.ofInstant(
-                    Instant.ofEpochMilli(params.calendarTimeMillis),
-                    ZoneId.of("UTC")
+            val slot = complicationSlotsManager[params.complicationSlotId] ?: return@use null
+            setForScreenshot(
+                    params.userStyle?.let {
+                        UserStyle(UserStyleData(it), currentUserStyleRepository.schema)
+                    },
+                    params.complicationData?.let {
+                        mapOf(params.complicationSlotId to it.toApiComplicationData())
+                    },
+                    Instant.ofEpochMilli(params.calendarTimeMillis)
                 )
-            return complicationSlotsManager[params.complicationSlotId]?.let {
-                val oldStyle = currentUserStyleRepository.userStyle.value
-                val instant = Instant.ofEpochMilli(params.calendarTimeMillis)
-
-                val newStyle = params.userStyle
-                if (newStyle != null) {
-                    currentUserStyleRepository.updateUserStyle(
-                        UserStyle(UserStyleData(newStyle), currentUserStyleRepository.schema)
-                    )
-                }
-
-                // Compute the bounds of the complication based on the display rather than
-                // the headless renderer (which may be smaller).
-                val bounds = it.computeBounds(
-                    Rect(
-                    0,
-                    0,
-                        Resources.getSystem().displayMetrics.widthPixels,
-                        Resources.getSystem().displayMetrics.heightPixels
-                    )
-                )
-
-                var prevData: ComplicationData? = null
-                val screenshotComplicationData = params.complicationData
-                if (screenshotComplicationData != null) {
-                    prevData = it.renderer.getData()
-                    complicationSlotsManager.setComplicationDataUpdateSync(
-                        params.complicationSlotId,
-                        screenshotComplicationData.toApiComplicationData(),
-                        instant
-                    )
-                }
-
-                val complicationBitmap: Bitmap
-                val picture = Picture()
-                if (Build.VERSION.SDK_INT >= 28) {
-                    it.renderer.render(
-                        picture.beginRecording(bounds.width(), bounds.height()),
-                        Rect(0, 0, bounds.width(), bounds.height()),
-                        zonedDateTime,
-                        RenderParameters(params.renderParametersWireFormat),
-                        params.complicationSlotId
-                    )
-                    picture.endRecording()
-                    complicationBitmap = Api28CreateBitmapHelper.createBitmap(
-                        picture,
-                        bounds.width(),
-                        bounds.height(),
-                        Bitmap.Config.ARGB_8888
-                    )
-                } else {
-                    complicationBitmap =
-                        Bitmap.createBitmap(
-                            bounds.width(),
-                            bounds.height(),
-                            Bitmap.Config.ARGB_8888
+                .use {
+                    val zonedDateTime =
+                        ZonedDateTime.ofInstant(
+                            Instant.ofEpochMilli(params.calendarTimeMillis),
+                            ZoneId.of("UTC")
                         )
-                    it.renderer.render(
-                        Canvas(complicationBitmap),
-                        Rect(0, 0, bounds.width(), bounds.height()),
-                        zonedDateTime,
-                        RenderParameters(params.renderParametersWireFormat),
-                        params.complicationSlotId
-                    )
-                }
+                    // Compute the bounds of the complication based on the display rather than
+                    // the headless renderer (which may be smaller).
+                    val bounds =
+                        slot.computeBounds(
+                            Rect(
+                                0,
+                                0,
+                                Resources.getSystem().displayMetrics.widthPixels,
+                                Resources.getSystem().displayMetrics.heightPixels
+                            )
+                        )
 
-                // No point in restoring the old style and complication if this is headless.
-                if (!watchState.isHeadless) {
-                    // Restore previous ComplicationData & style if required.
-                    if (prevData != null) {
-                        val now = getNow()
-                        complicationSlotsManager.setComplicationDataUpdateSync(
-                            params.complicationSlotId,
-                            prevData,
-                            now
+                    val complicationBitmap: Bitmap
+                    val picture = Picture()
+                    if (Build.VERSION.SDK_INT >= 28) {
+                        slot.renderer.render(
+                            picture.beginRecording(bounds.width(), bounds.height()),
+                            Rect(0, 0, bounds.width(), bounds.height()),
+                            zonedDateTime,
+                            RenderParameters(params.renderParametersWireFormat),
+                            params.complicationSlotId
+                        )
+                        picture.endRecording()
+                        complicationBitmap =
+                            Api28CreateBitmapHelper.createBitmap(
+                                picture,
+                                bounds.width(),
+                                bounds.height(),
+                                Bitmap.Config.ARGB_8888
+                            )
+                    } else {
+                        complicationBitmap =
+                            Bitmap.createBitmap(
+                                bounds.width(),
+                                bounds.height(),
+                                Bitmap.Config.ARGB_8888
+                            )
+                        slot.renderer.render(
+                            Canvas(complicationBitmap),
+                            Rect(0, 0, bounds.width(), bounds.height()),
+                            zonedDateTime,
+                            RenderParameters(params.renderParametersWireFormat),
+                            params.complicationSlotId
                         )
                     }
-
-                    if (newStyle != null) {
-                        currentUserStyleRepository.updateUserStyle(oldStyle)
-                    }
+                    val bundle = SharedMemoryImage.ashmemWriteImageBundle(complicationBitmap)
+                    complicationBitmap.recycle()
+                    bundle
                 }
-
-                val bundle = SharedMemoryImage.ashmemWriteImageBundle(complicationBitmap)
-                complicationBitmap.recycle()
-                bundle
-            }
         }
 
+    /** Sets the user style and complication data, and returns a restoration function. */
+    internal fun setForScreenshot(
+        userStyle: UserStyle?,
+        complicationIdToData: Map<Int, ComplicationData>?,
+        instant: Instant,
+    ): AutoCloseable {
+        val restoreUserStyle =
+            userStyle?.let { currentUserStyleRepository.updateUserStyleForScreenshot(it) }
+        try {
+            val restoreComplications =
+                complicationIdToData?.let {
+                    complicationSlotsManager.setComplicationDataForScreenshot(it, instant)
+                }
+            return AutoCloseable { restoreUserStyle?.use { restoreComplications?.close() } }
+        } catch (e: Throwable) {
+            // Cleanup on failure.
+            restoreUserStyle?.close()
+            throw e
+        }
+    }
+
     @UiThread
     internal fun dump(writer: IndentingPrintWriter) {
         writer.println("WatchFaceImpl ($componentName): ")
@@ -1364,6 +1309,16 @@
         )
         writer.println("currentUserStyleRepository.schema=${currentUserStyleRepository.schema}")
         writer.println("editorObscuresWatchFace=$editorObscuresWatchFace")
+        writer.println("additionalContentDescriptionLabels:")
+        writer.increaseIndent()
+        for (label in renderer.additionalContentDescriptionLabels) {
+            if (Build.TYPE.equals("userdebug")) {
+                writer.println("${label.first}: ${label.second}")
+            } else {
+                writer.println("${label.first}: Redacted")
+            }
+        }
+        writer.decreaseIndent()
         overlayStyle.dump(writer)
         watchState.dump(writer)
         complicationSlotsManager.dump(writer)
@@ -1404,52 +1359,27 @@
         return RemoteWatchFaceView(view, host, watchFaceHostApi.getUiThreadCoroutineScope()) {
             surfaceHolder,
             params ->
-            val oldStyle = watchFaceImpl.currentUserStyleRepository.userStyle.value
             val instant = Instant.ofEpochMilli(params.calendarTimeMillis)
-
-            params.userStyle?.let {
-                watchFaceImpl.currentUserStyleRepository.updateUserStyle(
-                    UserStyle(UserStyleData(it), watchFaceImpl.currentUserStyleRepository.schema)
+            watchFaceImpl
+                .setForScreenshot(
+                    params.userStyle?.let {
+                        UserStyle(
+                            UserStyleData(it),
+                            watchFaceImpl.currentUserStyleRepository.schema
+                        )
+                    },
+                    params.idAndComplicationDatumWireFormats?.associate {
+                        it.id to it.complicationData.toApiComplicationData()
+                    },
+                    instant
                 )
-            }
-
-            val oldComplicationData =
-                watchFaceImpl.complicationSlotsManager.complicationSlots.values.associateBy(
-                    { it.id },
-                    { it.renderer.getData() }
-                )
-
-            params.idAndComplicationDatumWireFormats?.let {
-                for (idAndData in it) {
-                    watchFaceImpl.complicationSlotsManager.setComplicationDataUpdateSync(
-                        idAndData.id,
-                        idAndData.complicationData.toApiComplicationData(),
-                        instant
+                .use {
+                    watchFaceImpl.renderer.renderScreenshotToSurface(
+                        ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")),
+                        RenderParameters(params.renderParametersWireFormat),
+                        surfaceHolder
                     )
                 }
-            }
-
-            watchFaceImpl.renderer.renderScreenshotToSurface(
-                ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")),
-                RenderParameters(params.renderParametersWireFormat),
-                surfaceHolder
-            )
-
-            // Restore previous style & complicationSlots if required.
-            if (params.userStyle != null) {
-                watchFaceImpl.currentUserStyleRepository.updateUserStyle(oldStyle)
-            }
-
-            if (params.idAndComplicationDatumWireFormats != null) {
-                val now = watchFaceImpl.getNow()
-                for ((id, complicationData) in oldComplicationData) {
-                    watchFaceImpl.complicationSlotsManager.setComplicationDataUpdateSync(
-                        id,
-                        complicationData,
-                        now
-                    )
-                }
-            }
         }
     }
 }
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 3a6c4ec..e2caf60 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -57,6 +57,7 @@
 import androidx.wear.watchface.complications.ComplicationSlotBounds
 import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
 import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.data.ComplicationDisplayPolicies
 import androidx.wear.watchface.complications.data.ComplicationExperimental
 import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
@@ -70,6 +71,7 @@
 import androidx.wear.watchface.complications.data.ShortTextComplicationData
 import androidx.wear.watchface.complications.data.TimeDifferenceComplicationText
 import androidx.wear.watchface.complications.data.TimeDifferenceStyle
+import androidx.wear.watchface.complications.data.toApiComplicationData
 import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
 import androidx.wear.watchface.control.HeadlessWatchFaceImpl
@@ -142,6 +144,7 @@
 import org.mockito.kotlin.verifyNoMoreInteractions
 import org.robolectric.Shadows.shadowOf
 import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowBuild
 
 private const val INTERACTIVE_UPDATE_RATE_MS = 16L
 private const val LEFT_COMPLICATION_ID = 1000
@@ -728,6 +731,7 @@
 
     @Before
     public fun setUp() {
+        ShadowBuild.setType("userdebug")
         Assume.assumeTrue("This test suite assumes API 26", Build.VERSION.SDK_INT >= 26)
 
         `when`(handler.getLooper()).thenReturn(Looper.myLooper())
@@ -758,9 +762,11 @@
                  (TODO: b/264994539) - Explicitly releasing the mSurfaceControl field,
                  accessed via reflection. Remove when a proper fix is found
                 */
-                val mSurfaceControlObject: Field = WatchFaceService.EngineWrapper::class
-                    .java.superclass // android.service.wallpaper.WallpaperService$Engine
-                    .getDeclaredField("mSurfaceControl")
+                val mSurfaceControlObject: Field =
+                    WatchFaceService.EngineWrapper::class
+                        .java
+                        .superclass // android.service.wallpaper.WallpaperService$Engine
+                        .getDeclaredField("mSurfaceControl")
                 mSurfaceControlObject.isAccessible = true
                 (mSurfaceControlObject.get(engineWrapper) as SurfaceControl).release()
             }
@@ -1317,11 +1323,7 @@
                 null
             )
         verify(tapListener)
-            .onTapEvent(
-                TapType.UP,
-                TapEvent(10, 200, Instant.ofEpochMilli(looperTimeMillis)),
-                null
-            )
+            .onTapEvent(TapType.UP, TapEvent(10, 200, Instant.ofEpochMilli(looperTimeMillis)), null)
     }
 
     @Test
@@ -2858,6 +2860,95 @@
 
     @Test
     @Config(sdk = [Build.VERSION_CODES.O_MR1])
+    public fun updateComplicationData_appendsToHistory() {
+        initWallpaperInteractiveWatchFaceInstance(complicationSlots = listOf(leftComplication))
+        // Validate that the history is initially empty.
+        assertThat(leftComplication.complicationHistory!!).isEmpty()
+        val longTextComplication =
+            WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+                .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT"))
+                .build()
+
+        interactiveWatchFaceInstance.updateComplicationData(
+            listOf(IdAndComplicationDataWireFormat(LEFT_COMPLICATION_ID, longTextComplication))
+        )
+
+        assertThat(leftComplication.complicationHistory.map { it.complicationData })
+            .containsExactly(longTextComplication.toApiComplicationData())
+    }
+
+    @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
+    public fun setComplicationDataUpdateForScreenshot_restoresAndDoesNotChangeHistoryOrDirtyFlag() {
+        // Arrange
+        val firstTimelineData: ComplicationData =
+            WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+                .setLongText(WireComplicationText.plainText("first timeline data"))
+                .build()
+                .also {
+                    it.timelineStartEpochSecond = 1000L
+                    it.timelineEndEpochSecond = 2000L
+                }
+                .toApiComplicationData()
+        val secondTimelineData: ComplicationData =
+            WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+                .setLongText(WireComplicationText.plainText("second timeline data"))
+                .build()
+                .also {
+                    it.timelineStartEpochSecond = 2000L
+                    it.timelineEndEpochSecond = 3000L
+                }
+                .toApiComplicationData()
+        val wrapperTimelineData: ComplicationData =
+            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                .build()
+                .also {
+                    it.timelineStartEpochSecond = 0L
+                    it.timelineEndEpochSecond = 1000L
+                    it.setTimelineEntryCollection(
+                        listOf(
+                            firstTimelineData.asWireComplicationData(),
+                            secondTimelineData.asWireComplicationData(),
+                        )
+                    )
+                }
+                .toApiComplicationData()
+        val screenshotData: ComplicationData =
+            WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+                .setLongText(WireComplicationText.plainText("screenshot"))
+                .build()
+                .toApiComplicationData()
+        initWallpaperInteractiveWatchFaceInstance(complicationSlots = listOf(leftComplication))
+        complicationSlotsManager.onComplicationDataUpdate(
+            leftComplication.id,
+            wrapperTimelineData,
+            Instant.ofEpochSecond(1000)
+        )
+        leftComplication.dataDirty = false
+
+        // Act
+        val actualScreenshotData =
+            complicationSlotsManager
+                .setComplicationDataForScreenshot(
+                    mapOf(LEFT_COMPLICATION_ID to screenshotData),
+                    Instant.ofEpochSecond(4000) // Also restored.
+                )
+                .use { leftComplication.complicationData.value }
+
+        // Assert
+        assertThat(actualScreenshotData).isEqualTo(screenshotData)
+        assertThat(leftComplication.complicationData.value).isEqualTo(firstTimelineData)
+        // History and dirty flag unchanged for screenshots
+        assertThat(leftComplication.complicationHistory!!.map { it.complicationData })
+            .containsExactly(wrapperTimelineData)
+        assertThat(leftComplication.dataDirty).isFalse()
+        // Timeline preserved
+        complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(2000L))
+        assertThat(leftComplication.complicationData.value).isEqualTo(secondTimelineData)
+    }
+
+    @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun complicationCache() {
         val complicationCache = HashMap<String, ByteArray>()
         val instanceParams =
diff --git a/webkit/integration-tests/testapp/lint-baseline.xml b/webkit/integration-tests/testapp/lint-baseline.xml
index f050d14..f1e21ab 100644
--- a/webkit/integration-tests/testapp/lint-baseline.xml
+++ b/webkit/integration-tests/testapp/lint-baseline.xml
@@ -1,23 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WebViewFeature.DOCUMENT_START_SCRIPT can only be accessed from within the same library group (referenced groupId=`androidx.webkit` from groupId=`androidx.webkit.integration-tests`)"
-        errorLine1="        if (!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {"
-        errorLine2="                                                              ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WebViewCompat.addDocumentStartJavaScript can only be called from within the same library group (referenced groupId=`androidx.webkit` from groupId=`androidx.webkit.integration-tests`)"
-        errorLine1="        WebViewCompat.addDocumentStartJavaScript(webView, jsCode, allowedOriginRules);"
-        errorLine2="                      ~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java"/>
-    </issue>
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="RestrictedApiAndroidX"
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java
index dc199bd..e5b08aa 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java
@@ -28,6 +28,7 @@
 import android.webkit.WebViewClient;
 import android.widget.Button;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.appcompat.app.AppCompatActivity;
@@ -37,15 +38,14 @@
 import androidx.webkit.WebViewCompat;
 import androidx.webkit.WebViewFeature;
 
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 
 /**
- * An {@link Activity} to exercise {@link WebViewCompat#addDocumentStartJavaScript(WebView, String,
- * Set)} related functionality.
+ * An {@link Activity} to exercise
+ * {@link WebViewCompat#addDocumentStartJavaScript(WebView, String, java.util.Set)} related
+ * functionality.
  */
-// TODO(swestphal): Remove the @SuppressLint after addDocumentStartJavaScript is unhidden.
-@SuppressLint("RestrictedApi")
 public class DocumentStartJavaScriptActivity extends AppCompatActivity {
     private final Uri mExampleUri = new Uri.Builder()
                                             .scheme("https")
@@ -54,7 +54,6 @@
                                             .appendPath("example")
                                             .appendPath("assets")
                                             .build();
-    private Button mReplyProxyButton;
 
     private static class MyWebViewClient extends WebViewClient {
         private final WebViewAssetLoader mAssetLoader;
@@ -88,9 +87,10 @@
         }
 
         @Override
-        public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
-                boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
-            if (message.getData().equals("initialization")) {
+        public void onPostMessage(@NonNull WebView view, WebMessageCompat message,
+                @NonNull Uri sourceOrigin,
+                boolean isMainFrame, @NonNull JavaScriptReplyProxy replyProxy) {
+            if ("initialization".equals(message.getData())) {
                 mReplyProxy = replyProxy;
             }
         }
@@ -117,16 +117,17 @@
                         .addPathHandler(mExampleUri.getPath() + "/", new AssetsPathHandler(this))
                         .build();
 
-        mReplyProxyButton = findViewById(R.id.button_reply_proxy);
+        Button replyProxyButton = findViewById(R.id.button_reply_proxy);
 
         WebView webView = findViewById(R.id.webview);
         webView.setWebViewClient(new MyWebViewClient(assetLoader));
         webView.getSettings().setJavaScriptEnabled(true);
 
-        HashSet<String> allowedOriginRules = new HashSet<>(Arrays.asList("https://example.com"));
+        HashSet<String> allowedOriginRules = new HashSet<>(
+                Collections.singletonList("https://example.com"));
         // Add WebMessageListeners.
         WebViewCompat.addWebMessageListener(webView, "replyObject", allowedOriginRules,
-                new ReplyMessageListener(mReplyProxyButton));
+                new ReplyMessageListener(replyProxyButton));
         final String jsCode = "replyObject.onmessage = function(event) {"
                 + "    document.getElementById('result').innerHTML = event.data;"
                 + "};"
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index 748c00e..b608615 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -67,6 +67,10 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void showInterstitial(boolean);
   }
 
+  public interface ScriptHandler {
+    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DOCUMENT_START_SCRIPT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public void remove();
+  }
+
   public abstract class ServiceWorkerClientCompat {
     ctor public ServiceWorkerClientCompat();
     method @WorkerThread public abstract android.webkit.WebResourceResponse? shouldInterceptRequest(android.webkit.WebResourceRequest);
@@ -223,6 +227,7 @@
   }
 
   public class WebViewCompat {
+    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DOCUMENT_START_SCRIPT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ScriptHandler addDocumentStartJavaScript(android.webkit.WebView, String, java.util.Set<java.lang.String!>);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void addWebMessageListener(android.webkit.WebView, String, java.util.Set<java.lang.String!>, androidx.webkit.WebViewCompat.WebMessageListener);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebMessagePortCompat![] createWebMessageChannel(android.webkit.WebView);
     method public static android.content.pm.PackageInfo? getCurrentWebViewPackage(android.content.Context);
@@ -257,6 +262,7 @@
     field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
     field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
     field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
+    field public static final String DOCUMENT_START_SCRIPT = "DOCUMENT_START_SCRIPT";
     field public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
     field public static final String FORCE_DARK = "FORCE_DARK";
     field public static final String FORCE_DARK_STRATEGY = "FORCE_DARK_STRATEGY";
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index 748c00e..b608615 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -67,6 +67,10 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void showInterstitial(boolean);
   }
 
+  public interface ScriptHandler {
+    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DOCUMENT_START_SCRIPT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public void remove();
+  }
+
   public abstract class ServiceWorkerClientCompat {
     ctor public ServiceWorkerClientCompat();
     method @WorkerThread public abstract android.webkit.WebResourceResponse? shouldInterceptRequest(android.webkit.WebResourceRequest);
@@ -223,6 +227,7 @@
   }
 
   public class WebViewCompat {
+    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DOCUMENT_START_SCRIPT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ScriptHandler addDocumentStartJavaScript(android.webkit.WebView, String, java.util.Set<java.lang.String!>);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void addWebMessageListener(android.webkit.WebView, String, java.util.Set<java.lang.String!>, androidx.webkit.WebViewCompat.WebMessageListener);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebMessagePortCompat![] createWebMessageChannel(android.webkit.WebView);
     method public static android.content.pm.PackageInfo? getCurrentWebViewPackage(android.content.Context);
@@ -257,6 +262,7 @@
     field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
     field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
     field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
+    field public static final String DOCUMENT_START_SCRIPT = "DOCUMENT_START_SCRIPT";
     field public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
     field public static final String FORCE_DARK = "FORCE_DARK";
     field public static final String FORCE_DARK_STRATEGY = "FORCE_DARK_STRATEGY";
diff --git a/webkit/webkit/lint.xml b/webkit/webkit/lint.xml
index dd56e16..e992b01 100644
--- a/webkit/webkit/lint.xml
+++ b/webkit/webkit/lint.xml
@@ -9,7 +9,8 @@
         <ignore path="**/src/test/**" />
         <ignore path="**/src/androidTest/**" />
         <!-- Required for Kotlin multi-platform tests. -->
-        <ignore path="**/src/androidAndroidTest/**" />
+        <ignore path="**/src/androidInstrumentedTest/**" />
+        <ignore path="**/src/androidUnitTest/**" />
         <ignore path="**/src/jvmTest/**" />
         <ignore path="**/src/commonTest/**" />
         <!-- Required for AppSearch icing tests. -->
diff --git a/webkit/webkit/src/main/java/androidx/webkit/ScriptHandler.java b/webkit/webkit/src/main/java/androidx/webkit/ScriptHandler.java
index 3f45801..e69adc7 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/ScriptHandler.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/ScriptHandler.java
@@ -17,29 +17,19 @@
 package androidx.webkit;
 
 import androidx.annotation.RequiresFeature;
-import androidx.annotation.RestrictTo;
 
 /**
- * This class represents the return result from {@link WebViewCompat#addDocumentStartJavaScript(
- * android.webkit.WebView, String, Set)}. Call {@link ScriptHandler#remove()} when the
+ * This interface represents the return result from {@link WebViewCompat#addDocumentStartJavaScript(
+ * android.webkit.WebView, String, java.util.Set)}. Call {@link ScriptHandler#remove()} when the
  * corresponding JavaScript script should be removed.
  *
- * @see WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView, String, Set)
- *
- * TODO(swestphal): unhide when ready.
+ * @see WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView, String, java.util.Set)
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class ScriptHandler {
+public interface ScriptHandler {
     /**
      * Removes the corresponding script, it will take effect from next page load.
      */
     @RequiresFeature(name = WebViewFeature.DOCUMENT_START_SCRIPT,
             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
-    public abstract void remove();
-
-    /**
-     * This class cannot be created by applications.
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    public ScriptHandler() {}
+    void remove();
 }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index a7c2377..34472aa 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -782,7 +782,7 @@
      * origin matches {@code allowedOriginRules} when the document begins to load.
      *
      * <p>Note that the script will run before any of the page's JavaScript code and the DOM tree
-     * might not be ready at this moment. It will block the loadng of the page until it's finished,
+     * might not be ready at this moment. It will block the loading of the page until it's finished,
      * so should be kept as short as possible.
      *
      * <p>The injected object from {@link #addWebMessageListener(WebView, String, Set,
@@ -809,13 +809,10 @@
      * @throws IllegalArgumentException If one of the {@code allowedOriginRules} is invalid.
      * @see #addWebMessageListener(WebView, String, Set, WebMessageListener)
      * @see ScriptHandler
-     * @deprecated unreleased API will be removed in 1.9.0
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @RequiresFeature(
             name = WebViewFeature.DOCUMENT_START_SCRIPT,
             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
-    @Deprecated
     public static @NonNull ScriptHandler addDocumentStartJavaScript(
             @NonNull WebView webview,
             @NonNull String script,
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index 2a982dc..87f4b9e 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -472,10 +472,7 @@
      * Feature for {@link #isFeatureSupported(String)}.
      * This feature covers {@link WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,
      * String, Set)}.
-     *
-     * TODO(swestphal): unhide when ready.
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public static final String DOCUMENT_START_SCRIPT = "DOCUMENT_START_SCRIPT";
 
     /**
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java b/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java
index e882b7b..87e7eed 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java
@@ -27,14 +27,24 @@
 /**
  * Internal implementation of {@link androidx.webkit.ScriptHandler}.
  */
-public class ScriptHandlerImpl extends ScriptHandler {
-    private ScriptHandlerBoundaryInterface mBoundaryInterface;
+public class ScriptHandlerImpl implements ScriptHandler {
+    private final ScriptHandlerBoundaryInterface mBoundaryInterface;
 
     private ScriptHandlerImpl(@NonNull ScriptHandlerBoundaryInterface boundaryInterface) {
         mBoundaryInterface = boundaryInterface;
     }
 
     /**
+     * Removes the corresponding script from WebView.
+     */
+    @Override
+    public void remove() {
+        // If this method is called, the feature must exist, so no need to check feature
+        // DOCUMENT_START_JAVASCRIPT.
+        mBoundaryInterface.remove();
+    }
+
+    /**
      * Create an AndroidX ScriptHandler from the given InvocationHandler.
      */
     public static @NonNull ScriptHandlerImpl toScriptHandler(
@@ -44,14 +54,4 @@
                         ScriptHandlerBoundaryInterface.class, invocationHandler);
         return new ScriptHandlerImpl(boundaryInterface);
     }
-
-    /**
-     * Removes the corresponding script from WebView.
-     */
-    @Override
-    public void remove() {
-        // If this method is called, the feature must exist, so no need to check feature
-        // DOCUMENT_START_JAVASCRIPT.
-        mBoundaryInterface.remove();
-    }
 }
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
index 676d286..74163e6 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
@@ -18,12 +18,15 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import android.app.Activity;
 import android.app.ActivityOptions;
+import android.content.Context;
 import android.os.IBinder;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.window.extensions.area.WindowAreaComponent;
+import androidx.window.extensions.core.util.function.Consumer;
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
 import androidx.window.extensions.embedding.ActivityStack;
 import androidx.window.extensions.embedding.SplitAttributes;
@@ -75,10 +78,13 @@
      * block.
      * The added APIs for Vendor API level 2 are:
      * <ul>
+     *     <li>{@link WindowAreaComponent#addRearDisplayStatusListener(Consumer)}</li>
+     *     <li>{@link WindowAreaComponent#startRearDisplaySession(Activity, Consumer)}</li>
      *     <li>{@link androidx.window.extensions.embedding.SplitPlaceholderRule.Builder#setFinishPrimaryWithPlaceholder(int)}</li>
      *     <li>{@link androidx.window.extensions.embedding.SplitAttributes}</li>
      *     <li>{@link ActivityEmbeddingComponent#setSplitAttributesCalculator(
      *      androidx.window.extensions.core.util.function.Function)}</li>
+     *     <li>{@link WindowLayoutComponent#addWindowLayoutInfoListener(Context, Consumer)}</li>
      * </ul>
      */
     @RestrictTo(LIBRARY_GROUP)
@@ -99,7 +105,11 @@
      *     <li>{@link ActivityEmbeddingComponent#updateSplitAttributes(IBinder, SplitAttributes)}
      *     </li>
      *     <li>{@link ActivityEmbeddingComponent#finishActivityStacks(Set)}</li>
-     *     <li>{@link androidx.window.extensions.area.WindowAreaComponent} APIs</li>
+     *     <li>{@link WindowAreaComponent#addRearDisplayPresentationStatusListener(Consumer)}</li>
+     *     <li>{@link WindowAreaComponent#startRearDisplayPresentationSession(Activity, Consumer)}
+     *     </li>
+     *     <li>{@link WindowAreaComponent#getRearDisplayMetrics()}</li>
+     *     <li>{@link WindowAreaComponent#getRearDisplayPresentation()}</li>
      * </ul>
      * </p>
      */
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
index f9d8303..55425fa 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import androidx.startup.Initializer
+import androidx.window.WindowSdkExtensions
 import androidx.window.demo.R
 import androidx.window.demo.embedding.SplitAttributesToggleMainActivity.Companion.PREFIX_FULLSCREEN_TOGGLE
 import androidx.window.demo.embedding.SplitAttributesToggleMainActivity.Companion.PREFIX_PLACEHOLDER
@@ -55,7 +56,7 @@
 
     override fun create(context: Context): RuleController {
         SplitController.getInstance(context).apply {
-            if (isSplitAttributesCalculatorSupported()) {
+            if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
                 setSplitAttributesCalculator(::sampleSplitAttributesCalculator)
             }
         }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
index faf792e..aaa751e 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
@@ -39,6 +39,7 @@
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.core.util.Consumer;
+import androidx.window.WindowSdkExtensions;
 import androidx.window.demo.R;
 import androidx.window.demo.databinding.ActivitySplitActivityLayoutBinding;
 import androidx.window.embedding.ActivityEmbeddingController;
@@ -111,8 +112,7 @@
             }
             startActivity(new Intent(this, SplitActivityE.class), bundle);
         });
-        if (!ActivityEmbeddingOptions.isSetLaunchingActivityStackSupported(
-                ActivityOptions.makeBasic())) {
+        if (WindowSdkExtensions.getInstance().getExtensionVersion() < 3) {
             mViewBinding.setLaunchingEInActivityStack.setEnabled(false);
         }
         mViewBinding.launchF.setOnClickListener((View v) ->
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt
index 1021af3..d74e64d 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt
@@ -28,6 +28,7 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.ExperimentalWindowApi
 import androidx.window.demo.R
 import androidx.window.demo.databinding.ActivitySplitAttributesTogglePrimaryActivityBinding
@@ -70,7 +71,7 @@
 
         activityEmbeddingController = ActivityEmbeddingController.getInstance(this)
 
-        if (!splitController.isSplitAttributesCalculatorSupported()) {
+        if (WindowSdkExtensions.getInstance().extensionVersion < 2) {
             placeholderFoldingAwareAttrsRadioButton.isEnabled = false
             viewBinding.placeholderUseCustomizedSplitAttributes.isEnabled = false
             splitRuleFoldingAwareAttrsRadioButton.isEnabled = false
@@ -212,11 +213,13 @@
 
     private suspend fun updateWarningMessages() {
         val warningMessages = StringBuilder().apply {
-            if (!splitController.isSplitAttributesCalculatorSupported()) {
+            val apiLevel = WindowSdkExtensions.getInstance().extensionVersion
+
+            if (apiLevel < 2) {
                 append(resources.getString(R.string.split_attributes_calculator_not_supported))
                 append("\n")
             }
-            if (!activityEmbeddingController.isFinishingActivityStacksSupported()) {
+            if (apiLevel < 3) {
                 append("Finishing secondary activities is not supported on this device!\n")
             }
             if (viewBinding.finishSecondaryActivitiesButton.isEnabled &&
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt
index 097da84..e7c62cd 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt
@@ -24,6 +24,7 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.ExperimentalWindowApi
 import androidx.window.demo.R
 import androidx.window.embedding.ActivityStack
@@ -43,8 +44,7 @@
 
         viewBinding.rootSplitActivityLayout.setBackgroundColor(Color.parseColor("#e8f5e9"))
 
-        val isRuntimeApiSupported = activityEmbeddingController
-            .isFinishingActivityStacksSupported()
+        val isRuntimeApiSupported = WindowSdkExtensions.getInstance().extensionVersion >= 3
 
         secondaryActivityIntent = Intent(
             this,
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt
index eac978a..01104cb 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt
@@ -24,6 +24,7 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.ExperimentalWindowApi
 import androidx.window.demo.R
 import androidx.window.demo.databinding.ActivitySplitAttributesToggleSecondaryActivityBinding
@@ -63,10 +64,8 @@
         activityEmbeddingController = ActivityEmbeddingController.getInstance(this)
         val splitAttributesCustomizationEnabled = demoActivityEmbeddingController
             .splitAttributesCustomizationEnabled.get()
-        splitAttributesUpdatesSupported =
-            splitController.isInvalidatingTopVisibleSplitAttributesSupported() &&
-                splitController.isUpdatingSplitAttributesSupported() &&
-                !splitAttributesCustomizationEnabled
+        splitAttributesUpdatesSupported = WindowSdkExtensions.getInstance().extensionVersion >= 3 &&
+            !splitAttributesCustomizationEnabled
 
         fullscreenToggleButton.apply {
             // Disable the toggle fullscreen feature if the device doesn't support runtime
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
index 16aad4f..fcb0ddb 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
@@ -28,6 +28,7 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.WindowSdkExtensions
 import androidx.window.demo.R
 import androidx.window.demo.databinding.ActivitySplitDeviceStateLayoutBinding
 import androidx.window.embedding.EmbeddingRule
@@ -63,6 +64,8 @@
     /** The last selected split rule id. */
     private var lastCheckedRuleId = 0
 
+    private val isCallbackSupported = WindowSdkExtensions.getInstance().extensionVersion >= 2
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         viewBinding = ActivitySplitDeviceStateLayoutBinding.inflate(layoutInflater)
@@ -103,7 +106,6 @@
         viewBinding.swapPrimarySecondaryPositionCheckBox.setOnCheckedChangeListener(this)
         viewBinding.launchActivityToSide.setOnClickListener(this)
 
-        val isCallbackSupported = splitController.isSplitAttributesCalculatorSupported()
         if (!isCallbackSupported) {
             // Disable the radioButtons that use SplitAttributesCalculator
             viewBinding.showFullscreenInPortraitRadioButton.isEnabled = false
@@ -295,7 +297,7 @@
             .setSplitType(SPLIT_TYPE_EXPAND)
             .build()
         var suggestToFinishItself = false
-        val isCallbackSupported = splitController.isSplitAttributesCalculatorSupported()
+
         // Traverse SplitInfos from the end because last SplitInfo has the highest z-order.
         for (info in newSplitInfos.reversed()) {
             if (info.contains(this@SplitDeviceStateActivityBase)) {
diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt
index 78aca27..758b6d0 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt
@@ -159,10 +159,6 @@
         TODO("Not yet implemented")
     }
 
-    override fun isSplitAttributesCalculatorSupported(): Boolean {
-        TODO("Not yet implemented")
-    }
-
     override fun getActivityStack(activity: Activity): ActivityStack? {
         TODO("Not yet implemented")
     }
@@ -178,10 +174,6 @@
         TODO("Not yet implemented")
     }
 
-    override fun isFinishActivityStacksSupported(): Boolean {
-        TODO("Not yet implemented")
-    }
-
     override fun invalidateTopVisibleSplitAttributes() {
         TODO("Not yet implemented")
     }
@@ -190,10 +182,6 @@
         TODO("Not yet implemented")
     }
 
-    override fun areSplitAttributesUpdatesSupported(): Boolean {
-        TODO("Not yet implemented")
-    }
-
     private fun validateRules(rules: Set<EmbeddingRule>) {
         val tags = HashSet<String>()
         rules.forEach { rule ->
diff --git a/window/window/api/1.2.0-beta03.txt b/window/window/api/1.2.0-beta03.txt
index 1930a35..4241d96 100644
--- a/window/window/api/1.2.0-beta03.txt
+++ b/window/window/api/1.2.0-beta03.txt
@@ -1,6 +1,11 @@
 // Signature format: 4.0
 package androidx.window {
 
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface RequiresWindowSdkExtension {
+    method public abstract int version();
+    property public abstract int version;
+  }
+
   public final class WindowProperties {
     field public static final androidx.window.WindowProperties INSTANCE;
     field public static final String PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE = "android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE";
@@ -10,6 +15,17 @@
     field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
   }
 
+  public abstract class WindowSdkExtensions {
+    method @IntRange(from=0L) public int getExtensionVersion();
+    method public static final androidx.window.WindowSdkExtensions getInstance();
+    property @IntRange(from=0L) public int extensionVersion;
+    field public static final androidx.window.WindowSdkExtensions.Companion Companion;
+  }
+
+  public static final class WindowSdkExtensions.Companion {
+    method public androidx.window.WindowSdkExtensions getInstance();
+  }
+
 }
 
 package androidx.window.area {
@@ -107,11 +123,10 @@
 package androidx.window.embedding {
 
   public final class ActivityEmbeddingController {
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
     method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
     method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
     method public boolean isActivityEmbedded(android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public boolean isFinishingActivityStacksSupported();
     field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion;
   }
 
@@ -120,9 +135,8 @@
   }
 
   public final class ActivityEmbeddingOptions {
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public static boolean isSetLaunchingActivityStackSupported(android.app.ActivityOptions);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.app.Activity activity);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
   }
 
   public final class ActivityFilter {
@@ -245,16 +259,13 @@
   }
 
   public final class SplitController {
-    method public void clearSplitAttributesCalculator();
+    method @androidx.window.RequiresWindowSdkExtension(version=2) public void clearSplitAttributesCalculator();
     method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
     method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public boolean isInvalidatingTopVisibleSplitAttributesSupported();
-    method public boolean isSplitAttributesCalculatorSupported();
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public boolean isUpdatingSplitAttributesSupported();
-    method public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
+    method @androidx.window.RequiresWindowSdkExtension(version=2) public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
     method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.embedding.SplitInfo>> splitInfoList(android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
     property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus;
     field public static final androidx.window.embedding.SplitController.Companion Companion;
   }
diff --git a/window/window/api/current.ignore b/window/window/api/current.ignore
deleted file mode 100644
index 2b9500cb..0000000
--- a/window/window/api/current.ignore
+++ /dev/null
@@ -1,7 +0,0 @@
-// Baseline format: 1.0
-AddedMethod: androidx.window.embedding.SplitAttributesCalculatorParams#areDefaultConstraintsSatisfied():
-    Added method androidx.window.embedding.SplitAttributesCalculatorParams.areDefaultConstraintsSatisfied()
-
-
-RemovedMethod: androidx.window.embedding.SplitAttributesCalculatorParams#getAreDefaultConstraintsSatisfied():
-    Removed method androidx.window.embedding.SplitAttributesCalculatorParams.getAreDefaultConstraintsSatisfied()
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 1930a35..4241d96 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -1,6 +1,11 @@
 // Signature format: 4.0
 package androidx.window {
 
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface RequiresWindowSdkExtension {
+    method public abstract int version();
+    property public abstract int version;
+  }
+
   public final class WindowProperties {
     field public static final androidx.window.WindowProperties INSTANCE;
     field public static final String PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE = "android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE";
@@ -10,6 +15,17 @@
     field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
   }
 
+  public abstract class WindowSdkExtensions {
+    method @IntRange(from=0L) public int getExtensionVersion();
+    method public static final androidx.window.WindowSdkExtensions getInstance();
+    property @IntRange(from=0L) public int extensionVersion;
+    field public static final androidx.window.WindowSdkExtensions.Companion Companion;
+  }
+
+  public static final class WindowSdkExtensions.Companion {
+    method public androidx.window.WindowSdkExtensions getInstance();
+  }
+
 }
 
 package androidx.window.area {
@@ -107,11 +123,10 @@
 package androidx.window.embedding {
 
   public final class ActivityEmbeddingController {
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
     method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
     method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
     method public boolean isActivityEmbedded(android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public boolean isFinishingActivityStacksSupported();
     field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion;
   }
 
@@ -120,9 +135,8 @@
   }
 
   public final class ActivityEmbeddingOptions {
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public static boolean isSetLaunchingActivityStackSupported(android.app.ActivityOptions);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.app.Activity activity);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
   }
 
   public final class ActivityFilter {
@@ -245,16 +259,13 @@
   }
 
   public final class SplitController {
-    method public void clearSplitAttributesCalculator();
+    method @androidx.window.RequiresWindowSdkExtension(version=2) public void clearSplitAttributesCalculator();
     method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
     method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public boolean isInvalidatingTopVisibleSplitAttributesSupported();
-    method public boolean isSplitAttributesCalculatorSupported();
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public boolean isUpdatingSplitAttributesSupported();
-    method public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
+    method @androidx.window.RequiresWindowSdkExtension(version=2) public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
     method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.embedding.SplitInfo>> splitInfoList(android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
     property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus;
     field public static final androidx.window.embedding.SplitController.Companion Companion;
   }
diff --git a/window/window/api/restricted_1.2.0-beta03.txt b/window/window/api/restricted_1.2.0-beta03.txt
index 1930a35..4241d96 100644
--- a/window/window/api/restricted_1.2.0-beta03.txt
+++ b/window/window/api/restricted_1.2.0-beta03.txt
@@ -1,6 +1,11 @@
 // Signature format: 4.0
 package androidx.window {
 
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface RequiresWindowSdkExtension {
+    method public abstract int version();
+    property public abstract int version;
+  }
+
   public final class WindowProperties {
     field public static final androidx.window.WindowProperties INSTANCE;
     field public static final String PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE = "android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE";
@@ -10,6 +15,17 @@
     field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
   }
 
+  public abstract class WindowSdkExtensions {
+    method @IntRange(from=0L) public int getExtensionVersion();
+    method public static final androidx.window.WindowSdkExtensions getInstance();
+    property @IntRange(from=0L) public int extensionVersion;
+    field public static final androidx.window.WindowSdkExtensions.Companion Companion;
+  }
+
+  public static final class WindowSdkExtensions.Companion {
+    method public androidx.window.WindowSdkExtensions getInstance();
+  }
+
 }
 
 package androidx.window.area {
@@ -107,11 +123,10 @@
 package androidx.window.embedding {
 
   public final class ActivityEmbeddingController {
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
     method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
     method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
     method public boolean isActivityEmbedded(android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public boolean isFinishingActivityStacksSupported();
     field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion;
   }
 
@@ -120,9 +135,8 @@
   }
 
   public final class ActivityEmbeddingOptions {
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public static boolean isSetLaunchingActivityStackSupported(android.app.ActivityOptions);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.app.Activity activity);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
   }
 
   public final class ActivityFilter {
@@ -245,16 +259,13 @@
   }
 
   public final class SplitController {
-    method public void clearSplitAttributesCalculator();
+    method @androidx.window.RequiresWindowSdkExtension(version=2) public void clearSplitAttributesCalculator();
     method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
     method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public boolean isInvalidatingTopVisibleSplitAttributesSupported();
-    method public boolean isSplitAttributesCalculatorSupported();
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public boolean isUpdatingSplitAttributesSupported();
-    method public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
+    method @androidx.window.RequiresWindowSdkExtension(version=2) public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
     method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.embedding.SplitInfo>> splitInfoList(android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
     property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus;
     field public static final androidx.window.embedding.SplitController.Companion Companion;
   }
diff --git a/window/window/api/restricted_current.ignore b/window/window/api/restricted_current.ignore
deleted file mode 100644
index 2b9500cb..0000000
--- a/window/window/api/restricted_current.ignore
+++ /dev/null
@@ -1,7 +0,0 @@
-// Baseline format: 1.0
-AddedMethod: androidx.window.embedding.SplitAttributesCalculatorParams#areDefaultConstraintsSatisfied():
-    Added method androidx.window.embedding.SplitAttributesCalculatorParams.areDefaultConstraintsSatisfied()
-
-
-RemovedMethod: androidx.window.embedding.SplitAttributesCalculatorParams#getAreDefaultConstraintsSatisfied():
-    Removed method androidx.window.embedding.SplitAttributesCalculatorParams.getAreDefaultConstraintsSatisfied()
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 1930a35..4241d96 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -1,6 +1,11 @@
 // Signature format: 4.0
 package androidx.window {
 
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface RequiresWindowSdkExtension {
+    method public abstract int version();
+    property public abstract int version;
+  }
+
   public final class WindowProperties {
     field public static final androidx.window.WindowProperties INSTANCE;
     field public static final String PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE = "android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE";
@@ -10,6 +15,17 @@
     field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
   }
 
+  public abstract class WindowSdkExtensions {
+    method @IntRange(from=0L) public int getExtensionVersion();
+    method public static final androidx.window.WindowSdkExtensions getInstance();
+    property @IntRange(from=0L) public int extensionVersion;
+    field public static final androidx.window.WindowSdkExtensions.Companion Companion;
+  }
+
+  public static final class WindowSdkExtensions.Companion {
+    method public androidx.window.WindowSdkExtensions getInstance();
+  }
+
 }
 
 package androidx.window.area {
@@ -107,11 +123,10 @@
 package androidx.window.embedding {
 
   public final class ActivityEmbeddingController {
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
     method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
     method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
     method public boolean isActivityEmbedded(android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public boolean isFinishingActivityStacksSupported();
     field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion;
   }
 
@@ -120,9 +135,8 @@
   }
 
   public final class ActivityEmbeddingOptions {
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public static boolean isSetLaunchingActivityStackSupported(android.app.ActivityOptions);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.app.Activity activity);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
   }
 
   public final class ActivityFilter {
@@ -245,16 +259,13 @@
   }
 
   public final class SplitController {
-    method public void clearSplitAttributesCalculator();
+    method @androidx.window.RequiresWindowSdkExtension(version=2) public void clearSplitAttributesCalculator();
     method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
     method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public boolean isInvalidatingTopVisibleSplitAttributesSupported();
-    method public boolean isSplitAttributesCalculatorSupported();
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public boolean isUpdatingSplitAttributesSupported();
-    method public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
+    method @androidx.window.RequiresWindowSdkExtension(version=2) public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
     method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.embedding.SplitInfo>> splitInfoList(android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
+    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
     property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus;
     field public static final androidx.window.embedding.SplitController.Companion Companion;
   }
diff --git a/window/window/lint-baseline.xml b/window/window/lint-baseline.xml
index 848181a..d1c7f8d 100644
--- a/window/window/lint-baseline.xml
+++ b/window/window/lint-baseline.xml
@@ -1,32 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WindowExtensions.VENDOR_API_LEVEL_3 can only be accessed from within the same library group (referenced groupId=`androidx.window.extensions` from groupId=`androidx.window`)"
-        errorLine1="    return ExtensionsUtil.safeVendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_3"
-        errorLine2="                                                                 ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WindowExtensions.VENDOR_API_LEVEL_1 can only be accessed from within the same library group (referenced groupId=`androidx.window.extensions` from groupId=`androidx.window`)"
-        errorLine1="            WindowExtensions.VENDOR_API_LEVEL_1 -> api1Impl.translateCompat(splitInfo)"
-        errorLine2="                             ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/window/embedding/EmbeddingAdapter.kt"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WindowExtensions.VENDOR_API_LEVEL_2 can only be accessed from within the same library group (referenced groupId=`androidx.window.extensions` from groupId=`androidx.window`)"
-        errorLine1="            WindowExtensions.VENDOR_API_LEVEL_2 -> api2Impl.translateCompat(splitInfo)"
-        errorLine2="                             ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/window/embedding/EmbeddingAdapter.kt"/>
-    </issue>
+<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
 
     <issue
         id="RestrictedApiAndroidX"
@@ -67,82 +40,10 @@
     <issue
         id="RestrictedApiAndroidX"
         message="WindowExtensions.VENDOR_API_LEVEL_2 can only be accessed from within the same library group (referenced groupId=`androidx.window.extensions` from groupId=`androidx.window`)"
-        errorLine1="        if (vendorApiLevel &lt; WindowExtensions.VENDOR_API_LEVEL_2) {"
-        errorLine2="                                              ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/window/embedding/EmbeddingAdapter.kt"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WindowExtensions.VENDOR_API_LEVEL_2 can only be accessed from within the same library group (referenced groupId=`androidx.window.extensions` from groupId=`androidx.window`)"
-        errorLine1="        require(vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2)"
-        errorLine2="                                                   ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/window/embedding/EmbeddingAdapter.kt"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WindowExtensions.VENDOR_API_LEVEL_2 can only be accessed from within the same library group (referenced groupId=`androidx.window.extensions` from groupId=`androidx.window`)"
-        errorLine1="        require(vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2)"
-        errorLine2="                                                   ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/window/embedding/EmbeddingAdapter.kt"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WindowExtensions.VENDOR_API_LEVEL_2 can only be accessed from within the same library group (referenced groupId=`androidx.window.extensions` from groupId=`androidx.window`)"
-        errorLine1="        if (vendorApiLevel &lt; WindowExtensions.VENDOR_API_LEVEL_2) {"
-        errorLine2="                                              ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/window/embedding/EmbeddingAdapter.kt"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WindowExtensions.VENDOR_API_LEVEL_2 can only be accessed from within the same library group (referenced groupId=`androidx.window.extensions` from groupId=`androidx.window`)"
-        errorLine1="        if (vendorApiLevel &lt; WindowExtensions.VENDOR_API_LEVEL_2) {"
-        errorLine2="                                              ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/window/embedding/EmbeddingAdapter.kt"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WindowExtensions.VENDOR_API_LEVEL_2 can only be accessed from within the same library group (referenced groupId=`androidx.window.extensions` from groupId=`androidx.window`)"
         errorLine1="        if (ExtensionsUtil.safeVendorApiLevel &lt; VENDOR_API_LEVEL_2) {"
         errorLine2="                                                ~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/window/embedding/EmbeddingCompat.kt"/>
     </issue>
 
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WindowExtensions.VENDOR_API_LEVEL_2 can only be accessed from within the same library group (referenced groupId=`androidx.window.extensions` from groupId=`androidx.window`)"
-        errorLine1="        ExtensionsUtil.safeVendorApiLevel >= VENDOR_API_LEVEL_2"
-        errorLine2="                                             ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/window/embedding/EmbeddingCompat.kt"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WindowExtensions.VENDOR_API_LEVEL_3 can only be accessed from within the same library group (referenced groupId=`androidx.window.extensions` from groupId=`androidx.window`)"
-        errorLine1="        ExtensionsUtil.safeVendorApiLevel >= VENDOR_API_LEVEL_3"
-        errorLine2="                                             ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/window/embedding/EmbeddingCompat.kt"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="WindowExtensions.VENDOR_API_LEVEL_3 can only be accessed from within the same library group (referenced groupId=`androidx.window.extensions` from groupId=`androidx.window`)"
-        errorLine1="        ExtensionsUtil.safeVendorApiLevel >= VENDOR_API_LEVEL_3"
-        errorLine2="                                             ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/window/embedding/EmbeddingCompat.kt"/>
-    </issue>
-
 </issues>
diff --git a/window/window/samples/src/main/java/androidx.window.samples/WindowSdkExtensionsSamples.kt b/window/window/samples/src/main/java/androidx.window.samples/WindowSdkExtensionsSamples.kt
new file mode 100644
index 0000000..f4049d1
--- /dev/null
+++ b/window/window/samples/src/main/java/androidx.window.samples/WindowSdkExtensionsSamples.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.window.samples
+
+import androidx.annotation.Sampled
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.embedding.SplitAttributes
+import androidx.window.embedding.SplitAttributesCalculatorParams
+import androidx.window.embedding.SplitController
+import androidx.window.samples.embedding.context
+
+@Sampled
+fun checkWindowSdkExtensionsVersion() {
+    // For example, SplitController#setSplitAttributesCalculator requires extension version 2.
+    // Callers must check the current extension version before invoking the API, or exception will
+    // be thrown.
+    if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
+        SplitController.getInstance(context).setSplitAttributesCalculator(splitAttributesCalculator)
+    }
+}
+
+val splitAttributesCalculator = { _: SplitAttributesCalculatorParams ->
+    SplitAttributes.Builder().build()
+}
+
+@Sampled
+fun annotateRequiresWindowSdkExtension() {
+    // Given that there's an API required Window SDK Extension version 3
+    @RequiresWindowSdkExtension(3)
+    fun coolFeature() {}
+
+    // Developers can use @RequiresWindowSdkExtension to annotate their own functions to document
+    // the required minimum API level.
+    @RequiresWindowSdkExtension(3)
+    fun useCoolFeatureNoCheck() {
+        coolFeature()
+    }
+
+    // Then users know they should wrap the function with version check
+    if (WindowSdkExtensions.getInstance().extensionVersion >= 3) {
+        useCoolFeatureNoCheck()
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/RequiresWindowSdkExtension.kt b/window/window/src/main/java/androidx/window/RequiresWindowSdkExtension.kt
new file mode 100644
index 0000000..d112c94
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/RequiresWindowSdkExtension.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.window
+
+import androidx.annotation.IntRange
+
+// TODO(b/292738295): Provide lint checks for RequiresWindowSdkExtension
+/**
+ * Denotes that the annotated element must only be used if
+ * [WindowSdkExtensions.extensionVersion] is greater than or equal to the given [version].
+ * Please see code sample linked below for usages.
+ *
+ * Calling the API that requires a higher level than the device's current level may lead to
+ * exceptions or unexpected results.
+ *
+ * @param version the minimum required [WindowSdkExtensions] version of the denoted target
+ *
+ * @sample androidx.window.samples.annotateRequiresWindowSdkExtension
+ */
+@MustBeDocumented
+@Retention(value = AnnotationRetention.BINARY)
+@Target(
+    AnnotationTarget.CLASS,
+    AnnotationTarget.FUNCTION,
+    AnnotationTarget.PROPERTY_GETTER,
+    AnnotationTarget.PROPERTY_SETTER,
+    AnnotationTarget.CONSTRUCTOR,
+    AnnotationTarget.FIELD,
+    AnnotationTarget.PROPERTY,
+)
+annotation class RequiresWindowSdkExtension(
+    /** The minimum required [WindowSdkExtensions] version of the denoted target */
+    @IntRange(from = 1)
+    val version: Int
+)
diff --git a/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt b/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt
new file mode 100644
index 0000000..26f0a6d
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.window
+
+import androidx.annotation.IntRange
+import androidx.window.core.ExtensionsUtil
+
+/**
+ * This class provides information about the extension SDK versions for window features present on
+ * this device. Use [extensionVersion] to get the version of the extension. The extension version
+ * advances as the platform evolves and new APIs are added, so is suitable to use for determining
+ * API availability at runtime.
+ *
+ * Window Manager Jetpack APIs that require window SDK extensions support are denoted with
+ * [RequiresWindowSdkExtension]. The caller must check whether the device's extension version is
+ * greater than or equal to the minimum level reported in [RequiresWindowSdkExtension].
+ *
+ * @sample androidx.window.samples.checkWindowSdkExtensionsVersion
+ */
+abstract class WindowSdkExtensions internal constructor() {
+
+    /**
+     * Reports the device's extension version
+     *
+     * When Window SDK Extensions is not present on the device, the extension version will be 0.
+     */
+    @get: IntRange(from = 0)
+    open val extensionVersion: Int = ExtensionsUtil.safeVendorApiLevel
+
+    /**
+     * Checks the [extensionVersion] and throws [UnsupportedOperationException] if the minimum
+     * [version] is not satisfied.
+     *
+     * @param version The minimum required extension version of the targeting API.
+     * @throws UnsupportedOperationException if the minimum [version] is not satisfied.
+     */
+    internal fun requireExtensionVersion(@IntRange(from = 1) version: Int) {
+        if (extensionVersion < version) {
+            throw UnsupportedOperationException("This API requires extension version " +
+                "$version, but the device is on $extensionVersion")
+        }
+    }
+
+    companion object {
+        /** Returns a [WindowSdkExtensions] instance. */
+        @JvmStatic
+        fun getInstance(): WindowSdkExtensions {
+            return decorator.decorate(object : WindowSdkExtensions() {})
+        }
+
+        private var decorator: WindowSdkExtensionsDecorator = EmptyDecoratorWindowSdk
+
+        internal fun overrideDecorator(overridingDecorator: WindowSdkExtensionsDecorator) {
+            decorator = overridingDecorator
+        }
+
+        internal fun reset() {
+            decorator = EmptyDecoratorWindowSdk
+        }
+    }
+}
+
+internal interface WindowSdkExtensionsDecorator {
+    /** Returns a [WindowSdkExtensions] instance. */
+    fun decorate(windowSdkExtensions: WindowSdkExtensions): WindowSdkExtensions
+}
+
+private object EmptyDecoratorWindowSdk : WindowSdkExtensionsDecorator {
+    override fun decorate(windowSdkExtensions: WindowSdkExtensions): WindowSdkExtensions =
+        windowSdkExtensions
+}
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
index ae2500c..d671726 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
@@ -21,6 +21,7 @@
 import android.os.Build
 import android.util.Log
 import androidx.annotation.RestrictTo
+import androidx.window.WindowSdkExtensions
 import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
 import androidx.window.area.utils.DeviceUtils
 import androidx.window.core.BuildConfig
@@ -41,6 +42,9 @@
     /**
      * [Flow] of the list of current [WindowAreaInfo]s that are currently available to be interacted
      * with.
+     *
+     * If [WindowSdkExtensions.extensionVersion] is less than 2,  the flow will return
+     * empty [WindowAreaInfo] list flow.
      */
     val windowAreaInfos: Flow<List<WindowAreaInfo>>
 
diff --git a/window/window/src/main/java/androidx/window/core/ExtensionsUtil.kt b/window/window/src/main/java/androidx/window/core/ExtensionsUtil.kt
index d12062c..ec92f1e 100644
--- a/window/window/src/main/java/androidx/window/core/ExtensionsUtil.kt
+++ b/window/window/src/main/java/androidx/window/core/ExtensionsUtil.kt
@@ -17,20 +17,19 @@
 package androidx.window.core
 
 import android.util.Log
-import androidx.annotation.VisibleForTesting
+import androidx.annotation.IntRange
 import androidx.window.core.VerificationMode.LOG
 import androidx.window.extensions.WindowExtensionsProvider
 
 internal object ExtensionsUtil {
 
     private val TAG = ExtensionsUtil::class.simpleName
-    private var overrideVendorApiLevel: Int? = null
 
+    @get:IntRange(from = 0)
     val safeVendorApiLevel: Int
         get() {
             return try {
-                overrideVendorApiLevel
-                    ?: WindowExtensionsProvider.getWindowExtensions().vendorApiLevel
+                WindowExtensionsProvider.getWindowExtensions().vendorApiLevel
             } catch (e: NoClassDefFoundError) {
                 if (BuildConfig.verificationMode == LOG) {
                     Log.d(TAG, "Embedding extension version not found")
@@ -43,14 +42,4 @@
                 0
             }
         }
-
-    @VisibleForTesting
-    internal fun setOverrideVendorApiLevel(apiLevel: Int) {
-        overrideVendorApiLevel = apiLevel
-    }
-
-    @VisibleForTesting
-    internal fun resetOverrideVendorApiLevel() {
-        overrideVendorApiLevel = null
-    }
 }
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
index 8adffae..bbbd045 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
@@ -20,9 +20,13 @@
 import android.app.ActivityOptions
 import android.content.Context
 import android.os.IBinder
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.ExperimentalWindowApi
 
-/** The controller that allows checking the current [Activity] embedding status. */
+/**
+ * The controller that allows checking the current [Activity] embedding status.
+ */
 class ActivityEmbeddingController internal constructor(private val backend: EmbeddingBackend) {
     /**
      * Checks if the [activity] is embedded and its presentation may be customized by the host
@@ -41,7 +45,7 @@
      *
      * @param activity The [Activity] to check.
      * @return the [ActivityStack] that this [activity] is part of, or `null` if there is no such
-     *   [ActivityStack].
+     * [ActivityStack].
      */
     @ExperimentalWindowApi
     fun getActivityStack(activity: Activity): ActivityStack? =
@@ -53,6 +57,7 @@
      * @param options The [android.app.ActivityOptions] to be updated.
      * @param token The token of the [ActivityStack] to be set.
      */
+    @RequiresWindowSdkExtension(3)
     internal fun setLaunchingActivityStack(
         options: ActivityOptions,
         token: IBinder
@@ -73,28 +78,20 @@
      * will be expanded to fill the parent task container. This is useful to expand the primary
      * container as the sample linked below shows.
      *
-     * **Note** that it's caller's responsibility to check whether this API is supported by calling
-     * [isFinishingActivityStacksSupported]. If not, an alternative approach to finishing all
-     * containers above a particular activity can be to launch it again with flag
-     * [android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP].
+     * **Note** that it's caller's responsibility to check whether this API is supported by checking
+     * [WindowSdkExtensions.extensionVersion] is greater than or equal to 3. If not, an alternative
+     * approach to finishing all containers above a particular activity can be to launch it again
+     * with flag [android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP].
      *
      * @param activityStacks The set of [ActivityStack] to be finished.
-     * @throws UnsupportedOperationException if this device doesn't support this API and
-     * [isFinishingActivityStacksSupported] returns `false`.
+     * @throws UnsupportedOperationException if extension version is less than 3.
      * @sample androidx.window.samples.embedding.expandPrimaryContainer
      */
     @ExperimentalWindowApi
-    fun finishActivityStacks(activityStacks: Set<ActivityStack>) =
+    @RequiresWindowSdkExtension(3)
+    fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
         backend.finishActivityStacks(activityStacks)
-
-    /**
-     * Checks whether [finishActivityStacks] is supported.
-     *
-     * @return `true` if [finishActivityStacks] is supported on the device, `false` otherwise.
-     */
-    @ExperimentalWindowApi
-    fun isFinishingActivityStacksSupported(): Boolean =
-        backend.isFinishActivityStacksSupported()
+    }
 
     companion object {
         /**
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt
index 190ffa3..8f5b66b 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt
@@ -20,34 +20,29 @@
 import android.app.Activity
 import android.app.ActivityOptions
 import android.content.Context
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.ExperimentalWindowApi
-import androidx.window.core.ExtensionsUtil
-import androidx.window.extensions.WindowExtensions
 
 /**
  * Sets the launching [ActivityStack] to the given [android.app.ActivityOptions].
  *
  * If the device doesn't support setting launching, [UnsupportedOperationException] will be thrown.
- * @see isSetLaunchingActivityStackSupported
  *
  * @param context The [android.content.Context] that is going to be used for launching
  * activity with this [android.app.ActivityOptions], which is usually be the [android.app.Activity]
  * of the app that hosts the task.
  * @param activityStack The target [ActivityStack] for launching.
- * @throws UnsupportedOperationException if this device doesn't support this API.
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less than 3.
  */
 @ExperimentalWindowApi
+@RequiresWindowSdkExtension(3)
 fun ActivityOptions.setLaunchingActivityStack(
     context: Context,
     activityStack: ActivityStack
 ): ActivityOptions = let {
-    if (!isSetLaunchingActivityStackSupported()) {
-        throw UnsupportedOperationException("#setLaunchingActivityStack is not " +
-            "supported on the device.")
-    } else {
-        ActivityEmbeddingController.getInstance(context)
-            .setLaunchingActivityStack(this, activityStack.token)
-    }
+    ActivityEmbeddingController.getInstance(context)
+        .setLaunchingActivityStack(this, activityStack.token)
 }
 
 /**
@@ -57,13 +52,12 @@
  *
  * If the device doesn't support setting launching or no available [ActivityStack]
  * can be found from the given [activity], [UnsupportedOperationException] will be thrown.
- * @see isSetLaunchingActivityStackSupported
  *
  * @param activity The existing [android.app.Activity] on the target [ActivityStack].
- * @throws UnsupportedOperationException if this device doesn't support this API or no
- * available [ActivityStack] can be found.
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less than 3.
  */
 @ExperimentalWindowApi
+@RequiresWindowSdkExtension(3)
 fun ActivityOptions.setLaunchingActivityStack(activity: Activity): ActivityOptions {
     val activityStack =
         ActivityEmbeddingController.getInstance(activity).getActivityStack(activity)
@@ -74,12 +68,3 @@
             "The given activity may not be embedded.")
     }
 }
-
-/**
- * Return `true` if the [setLaunchingActivityStack] APIs is supported and can be used
- * to set the launching [ActivityStack]. Otherwise, return `false`.
- */
-@ExperimentalWindowApi
-fun ActivityOptions.isSetLaunchingActivityStackSupported(): Boolean {
-    return ExtensionsUtil.safeVendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_3
-}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
index ce56396..2462f83 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
@@ -24,7 +24,7 @@
 import android.util.LayoutDirection
 import android.util.Pair as AndroidPair
 import android.view.WindowMetrics
-import androidx.window.core.ExtensionsUtil
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.PredicateAdapter
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.BOTTOM_TO_TOP
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LEFT_TO_RIGHT
@@ -36,7 +36,6 @@
 import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
 import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_HINGE
 import androidx.window.embedding.SplitAttributes.SplitType.Companion.ratio
-import androidx.window.extensions.WindowExtensions
 import androidx.window.extensions.core.util.function.Function
 import androidx.window.extensions.core.util.function.Predicate
 import androidx.window.extensions.embedding.ActivityRule as OEMActivityRule
@@ -63,7 +62,8 @@
 internal class EmbeddingAdapter(
     private val predicateAdapter: PredicateAdapter
 ) {
-    private val vendorApiLevel = ExtensionsUtil.safeVendorApiLevel
+    private val vendorApiLevel
+        get() = WindowSdkExtensions.getInstance().extensionVersion
     private val api1Impl = VendorApiLevel1Impl(predicateAdapter)
     private val api2Impl = VendorApiLevel2Impl()
 
@@ -73,8 +73,8 @@
 
     private fun translate(splitInfo: OEMSplitInfo): SplitInfo {
         return when (vendorApiLevel) {
-            WindowExtensions.VENDOR_API_LEVEL_1 -> api1Impl.translateCompat(splitInfo)
-            WindowExtensions.VENDOR_API_LEVEL_2 -> api2Impl.translateCompat(splitInfo)
+            1 -> api1Impl.translateCompat(splitInfo)
+            2 -> api2Impl.translateCompat(splitInfo)
             else -> {
                 val primaryActivityStack = splitInfo.primaryActivityStack
                 val secondaryActivityStack = splitInfo.secondaryActivityStack
@@ -152,7 +152,7 @@
         rule: SplitPairRule,
         predicateClass: Class<*>
     ): OEMSplitPairRule {
-        if (vendorApiLevel < WindowExtensions.VENDOR_API_LEVEL_2) {
+        if (vendorApiLevel < 2) {
             return api1Impl.translateSplitPairRuleCompat(context, rule, predicateClass)
         } else {
             val activitiesPairPredicate =
@@ -194,7 +194,7 @@
     }
 
     fun translateSplitAttributes(splitAttributes: SplitAttributes): OEMSplitAttributes {
-        require(vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2)
+        require(vendorApiLevel >= 2)
         // To workaround the "unused" error in ktlint. It is necessary to translate SplitAttributes
         // from WM Jetpack version to WM extension version.
         return androidx.window.extensions.embedding.SplitAttributes.Builder()
@@ -215,7 +215,7 @@
     }
 
     private fun translateSplitType(splitType: SplitType): OEMSplitType {
-        require(vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2)
+        require(vendorApiLevel >= 2)
         return when (splitType) {
             SPLIT_TYPE_HINGE -> OEMSplitType.HingeSplitType(
                 translateSplitType(SPLIT_TYPE_EQUAL)
@@ -238,7 +238,7 @@
         rule: SplitPlaceholderRule,
         predicateClass: Class<*>
     ): OEMSplitPlaceholderRule {
-        if (vendorApiLevel < WindowExtensions.VENDOR_API_LEVEL_2) {
+        if (vendorApiLevel < 2) {
             return api1Impl.translateSplitPlaceholderRuleCompat(
                 context,
                 rule,
@@ -285,7 +285,7 @@
         rule: ActivityRule,
         predicateClass: Class<*>
     ): OEMActivityRule {
-        if (vendorApiLevel < WindowExtensions.VENDOR_API_LEVEL_2) {
+        if (vendorApiLevel < 2) {
             return api1Impl.translateActivityRuleCompat(rule, predicateClass)
         } else {
             val activityPredicate = Predicate<Activity> { activity ->
@@ -317,6 +317,7 @@
         }.toSet()
     }
 
+    /** Provides backward compatibility for Window extensions with API level 2 */
     private inner class VendorApiLevel2Impl {
         fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo {
             val primaryActivityStack = splitInfo.primaryActivityStack
@@ -342,15 +343,13 @@
     }
 
     /**
-     * Provides backward compatibility for Window extensions with
-     * [WindowExtensions.VENDOR_API_LEVEL_1]
-     * @see WindowExtensions.getVendorApiLevel
+     * Provides backward compatibility for [WindowSdkExtensions] version 1
      */
     // Suppress deprecation because this object is to provide backward compatibility.
     @Suppress("DEPRECATION")
     private inner class VendorApiLevel1Impl(val predicateAdapter: PredicateAdapter) {
         /**
-         * Obtains [SplitAttributes] from [OEMSplitInfo] with [WindowExtensions.VENDOR_API_LEVEL_1]
+         * Obtains [SplitAttributes] from [OEMSplitInfo] with [WindowSdkExtensions] version 1
          */
         fun getSplitAttributesCompat(splitInfo: OEMSplitInfo): SplitAttributes =
             SplitAttributes.Builder()
@@ -472,9 +471,8 @@
             }
 
         /**
-         * Returns `true` if `attrs` is compatible with [WindowExtensions.VENDOR_API_LEVEL_1] and
-         * doesn't use the new features introduced in [WindowExtensions.VENDOR_API_LEVEL_2] or
-         * higher.
+         * Returns `true` if `attrs` is compatible with vendor API level 1 and
+         * doesn't use the new features introduced in vendor API level 2 or higher.
          */
         private fun isSplitAttributesSupported(attrs: SplitAttributes) =
             attrs.splitType.value in 0.0..1.0 && attrs.splitType.value != 1.0f &&
@@ -519,12 +517,12 @@
     internal companion object {
         /**
          * The default token of [SplitInfo], which provides compatibility for device prior to
-         * [WindowExtensions.VENDOR_API_LEVEL_3]
+         * vendor API level 3
          */
         val INVALID_SPLIT_INFO_TOKEN = Binder()
         /**
          * The default token of [ActivityStack], which provides compatibility for device prior to
-         * [WindowExtensions.VENDOR_API_LEVEL_3]
+         * vendor API level 3
          */
         val INVALID_ACTIVITY_STACK_TOKEN = Binder()
     }
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
index 22b6333..c1b09d0 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
@@ -22,6 +22,7 @@
 import android.os.IBinder
 import androidx.annotation.RestrictTo
 import androidx.core.util.Consumer
+import androidx.window.RequiresWindowSdkExtension
 import java.util.concurrent.Executor
 
 /**
@@ -50,28 +51,28 @@
 
     fun isActivityEmbedded(activity: Activity): Boolean
 
+    @RequiresWindowSdkExtension(2)
     fun setSplitAttributesCalculator(
         calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
     )
 
+    @RequiresWindowSdkExtension(2)
     fun clearSplitAttributesCalculator()
 
-    fun isSplitAttributesCalculatorSupported(): Boolean
-
     fun getActivityStack(activity: Activity): ActivityStack?
 
+    @RequiresWindowSdkExtension(3)
     fun setLaunchingActivityStack(options: ActivityOptions, token: IBinder): ActivityOptions
 
+    @RequiresWindowSdkExtension(3)
     fun finishActivityStacks(activityStacks: Set<ActivityStack>)
 
-    fun isFinishActivityStacksSupported(): Boolean
-
+    @RequiresWindowSdkExtension(3)
     fun invalidateTopVisibleSplitAttributes()
 
+    @RequiresWindowSdkExtension(3)
     fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes)
 
-    fun areSplitAttributesUpdatesSupported(): Boolean
-
     companion object {
 
         private var decorator: (EmbeddingBackend) -> EmbeddingBackend =
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
index 2c543f3..a4b7166 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
@@ -21,6 +21,8 @@
 import android.content.Context
 import android.os.IBinder
 import android.util.Log
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.BuildConfig
 import androidx.window.core.ConsumerAdapter
 import androidx.window.core.ExtensionsUtil
@@ -28,7 +30,6 @@
 import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
 import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_AVAILABLE
 import androidx.window.extensions.WindowExtensions.VENDOR_API_LEVEL_2
-import androidx.window.extensions.WindowExtensions.VENDOR_API_LEVEL_3
 import androidx.window.extensions.WindowExtensionsProvider
 import androidx.window.extensions.core.util.function.Consumer
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent
@@ -39,7 +40,7 @@
  * Adapter implementation for different historical versions of activity embedding OEM interface in
  * [ActivityEmbeddingComponent]. Only supports the single current version in this implementation.
  */
-internal class EmbeddingCompat constructor(
+internal class EmbeddingCompat(
     private val embeddingExtension: ActivityEmbeddingComponent,
     private val adapter: EmbeddingAdapter,
     private val consumerAdapter: ConsumerAdapter,
@@ -92,70 +93,59 @@
         return embeddingExtension.isActivityEmbedded(activity)
     }
 
+    @RequiresWindowSdkExtension(2)
     override fun setSplitAttributesCalculator(
         calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
     ) {
-        if (!isSplitAttributesCalculatorSupported()) {
-            throw UnsupportedOperationException("#setSplitAttributesCalculator is not supported " +
-                "on the device.")
-        }
+        WindowSdkExtensions.getInstance().requireExtensionVersion(2)
+
         embeddingExtension.setSplitAttributesCalculator(
             adapter.translateSplitAttributesCalculator(calculator)
         )
     }
 
+    @RequiresWindowSdkExtension(2)
     override fun clearSplitAttributesCalculator() {
-        if (!isSplitAttributesCalculatorSupported()) {
-            throw UnsupportedOperationException("#clearSplitAttributesCalculator is not " +
-                "supported on the device.")
-        }
+        WindowSdkExtensions.getInstance().requireExtensionVersion(2)
+
         embeddingExtension.clearSplitAttributesCalculator()
     }
 
-    override fun isSplitAttributesCalculatorSupported(): Boolean =
-        ExtensionsUtil.safeVendorApiLevel >= VENDOR_API_LEVEL_2
-
+    @RequiresWindowSdkExtension(3)
     override fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
-        if (!isFinishActivityStacksSupported()) {
-            throw UnsupportedOperationException("#finishActivityStacks is not " +
-                "supported on the device.")
-        }
+        WindowSdkExtensions.getInstance().requireExtensionVersion(3)
+
         val stackTokens = activityStacks.mapTo(mutableSetOf()) { it.token }
         embeddingExtension.finishActivityStacks(stackTokens)
     }
 
-    override fun isFinishActivityStacksSupported(): Boolean =
-        ExtensionsUtil.safeVendorApiLevel >= VENDOR_API_LEVEL_3
-
+    @RequiresWindowSdkExtension(3)
     override fun invalidateTopVisibleSplitAttributes() {
-        if (!areSplitAttributesUpdatesSupported()) {
-            throw UnsupportedOperationException("#invalidateTopVisibleSplitAttributes is not " +
-                "supported on the device.")
-        }
+        WindowSdkExtensions.getInstance().requireExtensionVersion(3)
+
         embeddingExtension.invalidateTopVisibleSplitAttributes()
     }
 
+    @RequiresWindowSdkExtension(3)
     override fun updateSplitAttributes(
         splitInfo: SplitInfo,
         splitAttributes: SplitAttributes
     ) {
-        if (!areSplitAttributesUpdatesSupported()) {
-            throw UnsupportedOperationException("#updateSplitAttributes is not supported on the " +
-                "device.")
-        }
+        WindowSdkExtensions.getInstance().requireExtensionVersion(3)
+
         embeddingExtension.updateSplitAttributes(
             splitInfo.token,
             adapter.translateSplitAttributes(splitAttributes)
         )
     }
 
-    override fun areSplitAttributesUpdatesSupported(): Boolean =
-        ExtensionsUtil.safeVendorApiLevel >= VENDOR_API_LEVEL_3
-
+    @RequiresWindowSdkExtension(3)
     override fun setLaunchingActivityStack(
         options: ActivityOptions,
         token: IBinder
     ): ActivityOptions {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(3)
+
         return embeddingExtension.setLaunchingActivityStack(options, token)
     }
 
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
index ccfba1d..e176a02 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
@@ -19,6 +19,7 @@
 import android.app.Activity
 import android.app.ActivityOptions
 import android.os.IBinder
+import androidx.window.RequiresWindowSdkExtension
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent
 
 /**
@@ -37,23 +38,23 @@
 
     fun isActivityEmbedded(activity: Activity): Boolean
 
+    @RequiresWindowSdkExtension(2)
     fun setSplitAttributesCalculator(
         calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
     )
 
+    @RequiresWindowSdkExtension(2)
     fun clearSplitAttributesCalculator()
 
-    fun isSplitAttributesCalculatorSupported(): Boolean
-
+    @RequiresWindowSdkExtension(3)
     fun setLaunchingActivityStack(options: ActivityOptions, token: IBinder): ActivityOptions
 
+    @RequiresWindowSdkExtension(3)
     fun finishActivityStacks(activityStacks: Set<ActivityStack>)
 
-    fun isFinishActivityStacksSupported(): Boolean
-
+    @RequiresWindowSdkExtension(3)
     fun invalidateTopVisibleSplitAttributes()
 
+    @RequiresWindowSdkExtension(3)
     fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes)
-
-    fun areSplitAttributesUpdatesSupported(): Boolean
 }
diff --git a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
index c7f4c3b..a53a98c 100644
--- a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
@@ -29,6 +29,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.collection.ArraySet
 import androidx.core.util.Consumer
+import androidx.window.RequiresWindowSdkExtension
 import androidx.window.WindowProperties
 import androidx.window.core.BuildConfig
 import androidx.window.core.ConsumerAdapter
@@ -336,6 +337,7 @@
         return embeddingExtension?.isActivityEmbedded(activity) ?: false
     }
 
+    @RequiresWindowSdkExtension(2)
     override fun setSplitAttributesCalculator(
         calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
     ) {
@@ -344,15 +346,13 @@
         }
     }
 
+    @RequiresWindowSdkExtension(2)
     override fun clearSplitAttributesCalculator() {
         globalLock.withLock {
             embeddingExtension?.clearSplitAttributesCalculator()
         }
     }
 
-    override fun isSplitAttributesCalculatorSupported(): Boolean =
-        embeddingExtension?.isSplitAttributesCalculatorSupported() ?: false
-
     override fun getActivityStack(activity: Activity): ActivityStack? {
         globalLock.withLock {
             val lastInfo: List<SplitInfo> = splitInfoEmbeddingCallback.lastInfo ?: return null
@@ -371,22 +371,23 @@
         }
     }
 
+    @RequiresWindowSdkExtension(3)
     override fun setLaunchingActivityStack(
         options: ActivityOptions,
         token: IBinder
     ): ActivityOptions = embeddingExtension?.setLaunchingActivityStack(options, token) ?: options
 
+    @RequiresWindowSdkExtension(3)
     override fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
         embeddingExtension?.finishActivityStacks(activityStacks)
     }
 
-    override fun isFinishActivityStacksSupported(): Boolean =
-        embeddingExtension?.isFinishActivityStacksSupported() ?: false
-
+    @RequiresWindowSdkExtension(3)
     override fun invalidateTopVisibleSplitAttributes() {
         embeddingExtension?.invalidateTopVisibleSplitAttributes()
     }
 
+    @RequiresWindowSdkExtension(3)
     override fun updateSplitAttributes(
         splitInfo: SplitInfo,
         splitAttributes: SplitAttributes
@@ -394,8 +395,6 @@
         embeddingExtension?.updateSplitAttributes(splitInfo, splitAttributes)
     }
 
-    override fun areSplitAttributesUpdatesSupported(): Boolean =
-        embeddingExtension?.areSplitAttributesUpdatesSupported() ?: false
     @RequiresApi(31)
     private object Api31Impl {
         @DoNotInline
diff --git a/window/window/src/main/java/androidx/window/embedding/RuleController.kt b/window/window/src/main/java/androidx/window/embedding/RuleController.kt
index 192f3ec..8943726 100644
--- a/window/window/src/main/java/androidx/window/embedding/RuleController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/RuleController.kt
@@ -27,6 +27,7 @@
  * - [setRules]
  * - [parseRules]
  * - [clearRules]
+ * - [getRules]
  *
  * **Note** that this class is recommended to be configured in [androidx.startup.Initializer] or
  * [android.app.Application.onCreate], so that the rules are applied early in the application
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
index 3e22b1a..ac627fc 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
@@ -21,6 +21,7 @@
 import androidx.annotation.IntRange
 import androidx.annotation.RestrictTo
 import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.SpecificationComputer.Companion.startSpecification
 import androidx.window.core.VerificationMode
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
@@ -307,7 +308,8 @@
              * <img width="70%" height="70%" src="/images/guide/topics/large-screens/activity-embedding/reference-docs/a_to_a_b_ttb.png" alt="Activity A starts activity B to the bottom."/>
              *
              * If the horizontal layout direction is not supported on the
-             * device, layout direction falls back to `LOCALE`.
+             * device that [WindowSdkExtensions.extensionVersion] is less than 2, layout direction
+             * falls back to `LOCALE`.
              *
              * See also [layoutDirection].
              */
@@ -323,7 +325,8 @@
              * <img width="70%" height="70%" src="/images/guide/topics/large-screens/activity-embedding/reference-docs/a_to_a_b_btt.png" alt="Activity A starts activity B to the top."/>
              *
              * If the horizontal layout direction is not supported on the
-             * device, layout direction falls back to `LOCALE`.
+             * device that [WindowSdkExtensions.extensionVersion] is less than 2, layout direction
+             * falls back to `LOCALE`.
              *
              * See also [layoutDirection].
              */
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitController.kt b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
index e478ff7..063bc0c 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
@@ -19,7 +19,9 @@
 import android.app.Activity
 import android.content.Context
 import androidx.core.util.Consumer
+import androidx.window.RequiresWindowSdkExtension
 import androidx.window.WindowProperties
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.ExperimentalWindowApi
 import androidx.window.layout.WindowMetrics
 import kotlinx.coroutines.channels.awaitClose
@@ -27,17 +29,17 @@
 import kotlinx.coroutines.flow.callbackFlow
 
 /**
-* The controller class that gets information about the currently active activity
-* splits and provides interaction points to customize the splits and form new
-* splits.
-*
-* A split is a pair of containers that host activities in the same or different
-* processes, combined under the same parent window of the hosting task.
-*
-* A pair of activities can be put into a split by providing a static or runtime
-* split rule and then launching the activities in the same task using
-* [Activity.startActivity()][android.app.Activity.startActivity].
-*/
+ * The controller class that gets information about the currently active activity
+ * splits and provides interaction points to customize the splits and form new
+ * splits.
+ *
+ * A split is a pair of containers that host activities in the same or different
+ * processes, combined under the same parent window of the hosting task.
+ *
+ * A pair of activities can be put into a split by providing a static or runtime
+ * split rule and then launching the activities in the same task using
+ * [Activity.startActivity()][android.app.Activity.startActivity].
+ */
 class SplitController internal constructor(private val embeddingBackend: EmbeddingBackend) {
 
     /**
@@ -86,8 +88,8 @@
     /**
      * Sets or replaces the previously registered [SplitAttributes] calculator.
      *
-     * **Note** that it's callers' responsibility to check if this API is supported by calling
-     * [isSplitAttributesCalculatorSupported] before using the this API. It is suggested to always
+     * **Note** that it's callers' responsibility to check if this API is supported by checking
+     * [WindowSdkExtensions.extensionVersion] before using the this API. It is suggested to always
      * set meaningful [SplitRule.defaultSplitAttributes] in case this API is not supported on some
      * devices.
      *
@@ -124,9 +126,10 @@
      * @sample androidx.window.samples.embedding.splitAttributesCalculatorSample
      * @param calculator the function to calculate [SplitAttributes] based on the
      * [SplitAttributesCalculatorParams]. It will replace the previously set if it exists.
-     * @throws UnsupportedOperationException if [isSplitAttributesCalculatorSupported] reports
-     * `false`
+     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]
+     *                                       is less than 2.
      */
+    @RequiresWindowSdkExtension(2)
     fun setSplitAttributesCalculator(
         calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
     ) {
@@ -135,19 +138,17 @@
 
     /**
      * Clears the callback previously set by [setSplitAttributesCalculator].
-     * The caller **must** make sure [isSplitAttributesCalculatorSupported] before invoking.
+     * The caller **must** make sure if [WindowSdkExtensions.extensionVersion] is greater than
+     * or equal to 2.
      *
-     * @throws UnsupportedOperationException if [isSplitAttributesCalculatorSupported] reports
-     * `false`
+     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]
+     *                                       is less than 2.
      */
+    @RequiresWindowSdkExtension(2)
     fun clearSplitAttributesCalculator() {
         embeddingBackend.clearSplitAttributesCalculator()
     }
 
-    /** Returns whether [setSplitAttributesCalculator] is supported or not. */
-    fun isSplitAttributesCalculatorSupported(): Boolean =
-        embeddingBackend.isSplitAttributesCalculatorSupported()
-
     /**
      * Triggers a [SplitAttributes] update callback for the current topmost and visible split layout
      * if there is one. This method can be used when a change to the split presentation originates
@@ -160,23 +161,14 @@
      *
      * The call will be ignored if there is no visible split.
      *
-     * @throws UnsupportedOperationException if the device doesn't support this API.
+     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]
+     *                                       is less than 3.
      */
     @ExperimentalWindowApi
-    fun invalidateTopVisibleSplitAttributes() =
+    @RequiresWindowSdkExtension(3)
+    fun invalidateTopVisibleSplitAttributes() {
         embeddingBackend.invalidateTopVisibleSplitAttributes()
-
-    /**
-     * Checks whether [invalidateTopVisibleSplitAttributes] is supported on the device.
-     *
-     * Invoking these APIs if the feature is not supported would trigger an
-     * [UnsupportedOperationException].
-     * @return `true` if the runtime APIs to update [SplitAttributes] are supported and can be
-     * called safely, `false` otherwise.
-     */
-    @ExperimentalWindowApi
-    fun isInvalidatingTopVisibleSplitAttributesSupported(): Boolean =
-        embeddingBackend.areSplitAttributesUpdatesSupported()
+    }
 
     /**
      * Updates the [SplitAttributes] of a split pair. This is an alternative to using
@@ -198,23 +190,14 @@
      *
      * @param splitInfo the split pair to update
      * @param splitAttributes the [SplitAttributes] to be applied
-     * @throws UnsupportedOperationException if this device doesn't support this API
+     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]
+     *                                       is less than 3.
      */
     @ExperimentalWindowApi
-    fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes) =
+    @RequiresWindowSdkExtension(3)
+    fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes) {
         embeddingBackend.updateSplitAttributes(splitInfo, splitAttributes)
-
-    /**
-     * Checks whether [updateSplitAttributes] is supported on the device.
-     *
-     * Invoking these APIs if the feature is not supported would trigger an
-     * [UnsupportedOperationException].
-     * @return `true` if the runtime APIs to update [SplitAttributes] are supported and can be
-     * called safely, `false` otherwise.
-     */
-    @ExperimentalWindowApi
-    fun isUpdatingSplitAttributesSupported(): Boolean =
-        embeddingBackend.areSplitAttributesUpdatesSupported()
+    }
 
     /**
      * A class to determine if activity splits with Activity Embedding are currently available.
diff --git a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
index 60eb71e..06ca64f 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
@@ -23,6 +23,7 @@
 import androidx.annotation.RestrictTo
 import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
 import androidx.annotation.UiContext
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.ConsumerAdapter
 import androidx.window.layout.adapter.WindowBackend
 import androidx.window.layout.adapter.extensions.ExtensionWindowBackend
@@ -53,6 +54,9 @@
      * Obtaining a [WindowInfoTracker] through [WindowInfoTracker.getOrCreate] guarantees having a
      * default implementation for this method.
      *
+     * If the passed [context] is not an [Activity] and [WindowSdkExtensions.extensionVersion]
+     * is less than 2, the flow will return empty [WindowLayoutInfo] list flow.
+     *
      * @param context a [UiContext] such as an [Activity], an [InputMethodService], or an instance
      * created via [Context.createWindowContext] that listens to configuration changes.
      * @see WindowLayoutInfo
diff --git a/window/window/src/test/java/androidx/window/StubWindowSdkExtensions.kt b/window/window/src/test/java/androidx/window/StubWindowSdkExtensions.kt
new file mode 100644
index 0000000..3ffe46f
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/StubWindowSdkExtensions.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.window
+
+import androidx.annotation.IntRange
+
+internal class StubWindowSdkExtensions : WindowSdkExtensions() {
+    override val extensionVersion: Int
+        get() = overrideVersion
+
+    private var overrideVersion: Int = 0
+
+    internal fun overrideExtensionVersion(@IntRange(from = 0) version: Int) {
+        overrideVersion = version
+    }
+}
diff --git a/window/window/src/test/java/androidx/window/WindowSdkExtensionsRule.kt b/window/window/src/test/java/androidx/window/WindowSdkExtensionsRule.kt
new file mode 100644
index 0000000..a2826e2
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/WindowSdkExtensionsRule.kt
@@ -0,0 +1,67 @@
+/**
+*
+* 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.window
+
+import androidx.annotation.IntRange
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Test rule for overriding [WindowSdkExtensions] properties.
+ *
+ * This should mainly be used for validating the behavior with a simplified version of
+ * [WindowSdkExtensions] in unit tests.
+ * For on-device Android tests, it's highly suggested to respect
+ * the device's [WindowSdkExtensions.extensionVersion]. Overriding the real device's is error-prone,
+ * and may lead to unexpected behavior.
+ */
+class WindowSdkExtensionsRule : TestRule {
+
+    private val mStubWindowSdkExtensions = StubWindowSdkExtensions()
+
+    override fun apply(
+        @Suppress("InvalidNullabilityOverride") // JUnit missing annotations
+        base: Statement,
+        @Suppress("InvalidNullabilityOverride") // JUnit missing annotations
+        description: Description
+    ): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                WindowSdkExtensions.overrideDecorator(object : WindowSdkExtensionsDecorator {
+                    override fun decorate(windowSdkExtensions: WindowSdkExtensions):
+                        WindowSdkExtensions = mStubWindowSdkExtensions
+                })
+                try {
+                    base.evaluate()
+                } finally {
+                    WindowSdkExtensions.reset()
+                }
+            }
+        }
+    }
+
+    /**
+     * Overrides [WindowSdkExtensions.extensionVersion] for testing.
+     *
+     * @param version The extension version to override
+     */
+    fun overrideExtensionVersion(@IntRange(from = 0) version: Int) {
+        mStubWindowSdkExtensions.overrideExtensionVersion(version)
+    }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingControllerTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingControllerTest.kt
index c4cf0b5..6d950f6 100644
--- a/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingControllerTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingControllerTest.kt
@@ -19,6 +19,7 @@
 import android.app.Activity
 import android.content.Context
 import android.os.Binder
+import androidx.window.core.ExperimentalWindowApi
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -61,7 +62,7 @@
     }
 
     @Test
-    @OptIn(androidx.window.core.ExperimentalWindowApi::class)
+    @OptIn(ExperimentalWindowApi::class)
     fun testGetActivityStack() {
         val activityStack = ActivityStack(listOf(), true, Binder())
         whenever(mockEmbeddingBackend.getActivityStack(mockActivity)).thenReturn(activityStack)
@@ -70,19 +71,7 @@
     }
 
     @Test
-    @OptIn(androidx.window.core.ExperimentalWindowApi::class)
-    fun testIsFinishingActivityStacksSupported() {
-        whenever(mockEmbeddingBackend.isFinishActivityStacksSupported()).thenReturn(true)
-
-        assertTrue(activityEmbeddingController.isFinishingActivityStacksSupported())
-
-        whenever(mockEmbeddingBackend.isFinishActivityStacksSupported()).thenReturn(false)
-
-        assertFalse(activityEmbeddingController.isFinishingActivityStacksSupported())
-    }
-
-    @Test
-    @OptIn(androidx.window.core.ExperimentalWindowApi::class)
+    @OptIn(ExperimentalWindowApi::class)
     fun testFinishActivityStacks() {
         val activityStacks: Set<ActivityStack> = mock()
         activityEmbeddingController.finishActivityStacks(activityStacks)
@@ -91,7 +80,7 @@
     }
 
     @Test
-    @OptIn(androidx.window.core.ExperimentalWindowApi::class)
+    @OptIn(ExperimentalWindowApi::class)
     fun testGetInstance() {
         EmbeddingBackend.overrideDecorator(object : EmbeddingBackendDecorator {
             override fun decorate(embeddingBackend: EmbeddingBackend): EmbeddingBackend =
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingOptionsTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingOptionsTest.kt
index d9c2127..aea6821 100644
--- a/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingOptionsTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingOptionsTest.kt
@@ -19,18 +19,12 @@
 import android.app.Activity
 import android.app.ActivityOptions
 import android.content.Context
-import androidx.window.core.ExtensionsUtil
-import androidx.window.extensions.WindowExtensions
+import androidx.window.core.ExperimentalWindowApi
 import org.junit.After
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertThrows
-import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
-import org.mockito.kotlin.any
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -38,9 +32,8 @@
  * The unit tests for activity embedding extension functions to [ActivityOptions]
  *
  * @see [ActivityOptions.setLaunchingActivityStack]
- * @see [ActivityOptions.isSetLaunchingActivityStackSupported]
  */
-@OptIn(androidx.window.core.ExperimentalWindowApi::class)
+@OptIn(ExperimentalWindowApi::class)
 class ActivityEmbeddingOptionsTest {
 
     private lateinit var mockEmbeddingBackend: EmbeddingBackend
@@ -64,15 +57,11 @@
             override fun decorate(embeddingBackend: EmbeddingBackend): EmbeddingBackend =
                 mockEmbeddingBackend
         })
-
-        // ActivityEmbeddingOptions is only supported since level 3
-        ExtensionsUtil.setOverrideVendorApiLevel(WindowExtensions.VENDOR_API_LEVEL_3)
     }
 
     @After
     fun tearDown() {
         EmbeddingBackend.reset()
-        ExtensionsUtil.resetOverrideVendorApiLevel()
     }
 
     @Test
@@ -90,25 +79,4 @@
         verify(mockEmbeddingBackend).setLaunchingActivityStack(
             mockActivityOptions, mockActivityStack.token)
     }
-
-    @Test
-    fun testSetLaunchingActivityStack_unsupportedApiLevel() {
-        ExtensionsUtil.setOverrideVendorApiLevel(WindowExtensions.VENDOR_API_LEVEL_2)
-
-        assertThrows(UnsupportedOperationException::class.java) {
-            mockActivityOptions.setLaunchingActivityStack(mockActivity, mockActivityStack)
-        }
-        verify(mockEmbeddingBackend, never()).setLaunchingActivityStack(any(), any())
-    }
-
-    @Test
-    fun testIsSetLaunchingActivityStackSupported() {
-        ExtensionsUtil.setOverrideVendorApiLevel(WindowExtensions.VENDOR_API_LEVEL_2)
-
-        assertFalse(mockActivityOptions.isSetLaunchingActivityStackSupported())
-
-        ExtensionsUtil.setOverrideVendorApiLevel(WindowExtensions.VENDOR_API_LEVEL_3)
-
-        assertTrue(mockActivityOptions.isSetLaunchingActivityStackSupported())
-    }
 }
diff --git a/window/window/src/test/java/androidx/window/embedding/RequiresWindowSdkExtensionTests.kt b/window/window/src/test/java/androidx/window/embedding/RequiresWindowSdkExtensionTests.kt
new file mode 100644
index 0000000..7b0b830
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/RequiresWindowSdkExtensionTests.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.window.embedding
+
+import android.app.ActivityOptions
+import android.content.Context
+import android.os.Binder
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.WindowSdkExtensionsRule
+import androidx.window.core.ConsumerAdapter
+import androidx.window.core.PredicateAdapter
+import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_ACTIVITY_STACK_TOKEN
+import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_SPLIT_INFO_TOKEN
+import androidx.window.extensions.core.util.function.Function
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.SplitAttributes as OemSplitAttributes
+import androidx.window.extensions.embedding.SplitAttributesCalculatorParams as OemSplitAttributesCalculatorParams
+import org.junit.After
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/**
+ * Verifies the behavior of [RequiresWindowSdkExtension]
+ * - If the [WindowSdkExtensions.extensionVersion] is greater than or equal to the minimum required
+ *   version denoted in [RequiresWindowSdkExtension.version], the denoted API must be called
+ *   successfully
+ * - Otherwise, [UnsupportedOperationException] must be thrown.
+ */
+@RequiresApi(Build.VERSION_CODES.M) // To call ActivityOptions.makeBasic()
+class RequiresWindowSdkExtensionTests {
+
+    @get:Rule
+    val testRule = WindowSdkExtensionsRule()
+
+    @Mock
+    private lateinit var embeddingExtension: ActivityEmbeddingComponent
+    @Mock
+    private lateinit var classLoader: ClassLoader
+    @Mock
+    private lateinit var applicationContext: Context
+    @Mock
+    private lateinit var activityOptions: ActivityOptions
+
+    private lateinit var mockAnnotations: AutoCloseable
+    private lateinit var embeddingCompat: EmbeddingCompat
+
+    @Before
+    fun setUp() {
+        mockAnnotations = MockitoAnnotations.openMocks(this)
+        embeddingCompat = EmbeddingCompat(
+            embeddingExtension,
+            EmbeddingAdapter(PredicateAdapter(classLoader)),
+            ConsumerAdapter(classLoader),
+            applicationContext
+        )
+
+        doReturn(activityOptions).whenever(embeddingExtension).setLaunchingActivityStack(
+            activityOptions,
+            INVALID_ACTIVITY_STACK_TOKEN
+        )
+    }
+
+    @After
+    fun tearDown() {
+        mockAnnotations.close()
+    }
+
+    @Test
+    fun testVendorApiLevel1() {
+        testRule.overrideExtensionVersion(1)
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
+        }
+        verify(embeddingExtension, never()).setSplitAttributesCalculator(any())
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.clearSplitAttributesCalculator()
+        }
+        verify(embeddingExtension, never()).clearSplitAttributesCalculator()
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.setLaunchingActivityStack(activityOptions, Binder())
+        }
+        verify(embeddingExtension, never()).setLaunchingActivityStack(any(), any())
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.finishActivityStacks(emptySet())
+        }
+        verify(embeddingExtension, never()).finishActivityStacks(any())
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.updateSplitAttributes(TEST_SPLIT_INFO, TEST_SPLIT_ATTRIBUTES)
+        }
+        verify(embeddingExtension, never()).updateSplitAttributes(any(), any())
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.invalidateTopVisibleSplitAttributes()
+        }
+        verify(embeddingExtension, never()).invalidateTopVisibleSplitAttributes()
+    }
+
+    @Test
+    fun testVendorApiLevel2() {
+        testRule.overrideExtensionVersion(2)
+
+        embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
+        verify(embeddingExtension).setSplitAttributesCalculator(
+            any<Function<OemSplitAttributesCalculatorParams, OemSplitAttributes>>()
+        )
+
+        embeddingCompat.clearSplitAttributesCalculator()
+        verify(embeddingExtension).clearSplitAttributesCalculator()
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.setLaunchingActivityStack(activityOptions, INVALID_ACTIVITY_STACK_TOKEN)
+        }
+        verify(embeddingExtension, never()).setLaunchingActivityStack(any(), any())
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.finishActivityStacks(emptySet())
+        }
+        verify(embeddingExtension, never()).finishActivityStacks(any())
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.updateSplitAttributes(TEST_SPLIT_INFO, TEST_SPLIT_ATTRIBUTES)
+        }
+        verify(embeddingExtension, never()).updateSplitAttributes(any(), any())
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.invalidateTopVisibleSplitAttributes()
+        }
+        verify(embeddingExtension, never()).invalidateTopVisibleSplitAttributes()
+    }
+
+    @Test
+    fun testVendorApiLevel3() {
+        testRule.overrideExtensionVersion(3)
+
+        embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
+        verify(embeddingExtension).setSplitAttributesCalculator(
+            any<Function<OemSplitAttributesCalculatorParams, OemSplitAttributes>>()
+        )
+
+        embeddingCompat.clearSplitAttributesCalculator()
+        verify(embeddingExtension).clearSplitAttributesCalculator()
+
+        embeddingCompat.setLaunchingActivityStack(activityOptions, INVALID_ACTIVITY_STACK_TOKEN)
+
+        verify(embeddingExtension).setLaunchingActivityStack(
+            activityOptions,
+            INVALID_ACTIVITY_STACK_TOKEN
+        )
+
+        embeddingCompat.finishActivityStacks(emptySet())
+        verify(embeddingExtension).finishActivityStacks(emptySet())
+
+        embeddingCompat.updateSplitAttributes(TEST_SPLIT_INFO, TEST_SPLIT_ATTRIBUTES)
+        verify(embeddingExtension).updateSplitAttributes(
+            INVALID_SPLIT_INFO_TOKEN,
+            OemSplitAttributes.Builder().build()
+        )
+
+        embeddingCompat.invalidateTopVisibleSplitAttributes()
+        verify(embeddingExtension).invalidateTopVisibleSplitAttributes()
+    }
+
+    companion object {
+        private val TEST_SPLIT_INFO = SplitInfo(
+            ActivityStack(emptyList(), isEmpty = true, INVALID_ACTIVITY_STACK_TOKEN),
+            ActivityStack(emptyList(), isEmpty = true, INVALID_ACTIVITY_STACK_TOKEN),
+            SplitAttributes.Builder().build(),
+            INVALID_SPLIT_INFO_TOKEN,
+        )
+
+        private val TEST_SPLIT_ATTRIBUTES = SplitAttributes.Builder().build()
+    }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt
index 2188e0e..390c681 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt
@@ -26,7 +26,6 @@
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
 import org.junit.Test
 import org.mockito.kotlin.any
 import org.mockito.kotlin.doAnswer
@@ -78,9 +77,6 @@
     fun test_splitAttributesCalculator_delegates() {
         val mockCalculator = mock<(SplitAttributesCalculatorParams) -> SplitAttributes>()
 
-        whenever(mockBackend.isSplitAttributesCalculatorSupported()).thenReturn(true)
-        assertTrue(splitController.isSplitAttributesCalculatorSupported())
-
         splitController.setSplitAttributesCalculator(mockCalculator)
         verify(mockBackend).setSplitAttributesCalculator(mockCalculator)
 
@@ -90,9 +86,6 @@
 
     @Test
     fun test_updateSplitAttribute_delegates() {
-        whenever(mockBackend.areSplitAttributesUpdatesSupported()).thenReturn(true)
-        assertTrue(splitController.isUpdatingSplitAttributesSupported())
-
         val mockSplitAttributes = SplitAttributes()
         val mockSplitInfo = SplitInfo(
             ActivityStack(emptyList(), true, mock()),
@@ -106,9 +99,6 @@
 
     @Test
     fun test_invalidateTopVisibleSplitAttributes_delegates() {
-        whenever(mockBackend.areSplitAttributesUpdatesSupported()).thenReturn(true)
-        assertTrue(splitController.isInvalidatingTopVisibleSplitAttributesSupported())
-
         splitController.invalidateTopVisibleSplitAttributes()
         verify(mockBackend).invalidateTopVisibleSplitAttributes()
     }
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index c0384de..bdf570fa 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -14,13 +14,6 @@
  * limitations under the License.
  */
 
-buildscript {
-    // TODO: Remove this when this test app no longer depends on 1.0.0 of vectordrawable-animated.
-    // vectordrawable and vectordrawable-animated were accidentally using the same package name
-    // which is no longer valid in namespaced resource world.
-    project.ext["android.uniquePackageNames"] = false
-}
-
 plugins {
     id("AndroidXPlugin")
     id("com.android.application")
diff --git a/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/UserInitiatedTaskRequestTest.kt b/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/UserInitiatedTaskRequestTest.kt
index bc758ac..83b7cbb 100644
--- a/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/UserInitiatedTaskRequestTest.kt
+++ b/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/UserInitiatedTaskRequestTest.kt
@@ -23,10 +23,12 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotEquals
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -36,7 +38,7 @@
 
     @Test
     fun testDefaultNetworkConstraints() {
-        val request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java)
+        val request = UserInitiatedTaskRequest(MyTask::class.java)
         val networkRequest = NetworkRequest.Builder()
                                 .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                                 .build()
@@ -50,7 +52,7 @@
 
     @Test
     fun testCustomNetworkConstraints() {
-        val request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java,
+        val request = UserInitiatedTaskRequest(MyTask::class.java,
             _constraints = Constraints(NetworkRequest.Builder()
                 .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
                 .build()
@@ -70,16 +72,16 @@
     @Test
     fun testTags() {
         val taskClassName = "androidx.work.datatransfer.UserInitiatedTaskRequestTest\$MyTask"
-        var request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java)
+        var request = UserInitiatedTaskRequest(MyTask::class.java)
         assertEquals(1, request.tags.size)
         assertEquals(taskClassName, request.tags.get(0))
 
-        request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java,
+        request = UserInitiatedTaskRequest(MyTask::class.java,
                                            _tags = mutableListOf("test"))
         assertEquals(2, request.tags.size)
         assertTrue(request.tags.contains("test"))
 
-        request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java,
+        request = UserInitiatedTaskRequest(MyTask::class.java,
                                            _tags = mutableListOf("test", "test2"))
         assertEquals(3, request.tags.size)
         assertTrue(request.tags.contains(taskClassName))
@@ -89,29 +91,52 @@
 
     @Test
     fun testDefaultTransferInfo() {
-        val request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java)
+        val request = UserInitiatedTaskRequest(MyTask::class.java)
         assertNull(request.transferInfo)
     }
 
     @Test
     fun testCustomTransferInfo() {
-        var request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java,
+        var request = UserInitiatedTaskRequest(MyTask::class.java,
             _transferInfo = TransferInfo(estimatedDownloadBytes = 1000L))
         val transferInfo = TransferInfo(0L, 1000L)
         assertEquals(request.transferInfo, transferInfo)
 
-        request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java,
+        request = UserInitiatedTaskRequest(MyTask::class.java,
             _transferInfo = TransferInfo(estimatedUploadBytes = 1000L))
         val transferInfo2 = TransferInfo(1000L, 0L)
         assertEquals(request.transferInfo, transferInfo2)
         assertNotEquals(request.transferInfo, transferInfo)
 
-        request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java,
+        request = UserInitiatedTaskRequest(MyTask::class.java,
             _transferInfo = TransferInfo(2000L, 20L))
         val transferInfo3 = TransferInfo(2000L, 20L)
         assertEquals(request.transferInfo, transferInfo3)
     }
 
+    @Test
+    fun testDefaultFallbackPolicy(): Unit = runBlocking {
+        // Default policy FALLBACK_NONE should allow enqueue
+        val request = UserInitiatedTaskRequest(MyTask::class.java)
+        request.enqueue(ApplicationProvider.getApplicationContext())
+    }
+
+    @Test
+    fun testCustomFallbackPolicy(): Unit = runBlocking {
+        val request = UserInitiatedTaskRequest(MyTask::class.java,
+            fallbackPolicy = UserInitiatedTaskRequest.FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE)
+        try {
+            request.enqueue(ApplicationProvider.getApplicationContext())
+            fail("Expected enqueue to fail without setting a foreground service")
+        } catch (_: IllegalArgumentException) {
+            // expected
+        }
+
+        request.setForegroundService(MyFgs::class.java,
+            UserInitiatedTaskRequest.ForegroundServiceOnTaskFinishPolicy.FOREGROUND_SERVICE_DETACH)
+        request.enqueue(ApplicationProvider.getApplicationContext())
+    }
+
     private class MyTask : UserInitiatedTask(
         "test_task",
         ApplicationProvider.getApplicationContext()
diff --git a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTaskRequest.kt b/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTaskRequest.kt
index 62aac55..3578843 100644
--- a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTaskRequest.kt
+++ b/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTaskRequest.kt
@@ -25,20 +25,9 @@
 class UserInitiatedTaskRequest constructor(
     private val task: Class<out UserInitiatedTask>,
     /**
-     * The foreground service which will be used as a fallback solution on Android 14- devices.
-     *
-     * <p>
-     * Upon scheduling the task request, the library will call [Context.startForegroundService] with
-     * [ACTION_UIT_SCHEDULE] on the given service here.
-     * The app needs to call [android.app.Service.startForeground] within a certain amount of time,
-     * otherwise it will crash with a [android.app.ForegroundServiceDidNotStartInTimeException].
+     * [FallbackPolicy] indicating what the library should do on Android 14- devices.
      */
-    private val service: Class<out AbstractUitService>,
-    /**
-     * [ForegroundServiceOnTaskFinishPolicy] indicating what should occur when the task is finished.
-     */
-    private val onTaskFinishPolicy: ForegroundServiceOnTaskFinishPolicy =
-        ForegroundServiceOnTaskFinishPolicy.FOREGROUND_SERVICE_STOP_FOREGROUND,
+    private val fallbackPolicy: FallbackPolicy = FallbackPolicy.FALLBACK_NONE,
     /**
      * [Constraints] required for this task to run.
      * The default value assumes a requirement of any internet.
@@ -71,16 +60,56 @@
     val tags: List<String>
         get() = _tags
 
+    /**
+     * The foreground service which will be used as a fallback solution on Android 14- devices.
+     * This is only used if [FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE] is set.
+     */
+    var service: Class<out AbstractUitService>? = null
+
+    /**
+     * [ForegroundServiceOnTaskFinishPolicy] indicating what should occur when the task is finished.
+     */
+    var onTaskFinishPolicy: ForegroundServiceOnTaskFinishPolicy =
+        ForegroundServiceOnTaskFinishPolicy.FOREGROUND_SERVICE_STOP_FOREGROUND
+
     init {
         // Update the list of tags to include the UserInitiatedTask class name if available
         _tags += task.name
     }
 
+    /**
+     * Set the [AbstractUitService] service to fallback to on Android 14- devices along with
+     * a [ForegroundServiceOnTaskFinishPolicy] policy which defines what will happen when the
+     * task is finished.
+     *
+     * This is only used if [FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE] is set. If this method
+     * is not called and [FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE] is set, an exception will
+     * be thrown when the task is enqueued.
+     *
+     * Upon scheduling the task request, the library will call [Context.startForegroundService] with
+     * [ACTION_UIT_SCHEDULE] on the given service here.
+     * The app needs to call [android.app.Service.startForeground] within a certain amount of time,
+     * otherwise it will crash with a [android.app.ForegroundServiceDidNotStartInTimeException].
+     */
+    fun setForegroundService(
+        service: Class<out AbstractUitService>,
+        onTaskFinishPolicy: ForegroundServiceOnTaskFinishPolicy =
+            ForegroundServiceOnTaskFinishPolicy.FOREGROUND_SERVICE_STOP_FOREGROUND
+    ) {
+        this.service = service
+        this.onTaskFinishPolicy = onTaskFinishPolicy
+    }
+
     internal fun getTaskState(): TaskState {
         return TaskState.TASK_STATE_INVALID // TODO: update impl
     }
 
     suspend fun enqueue(@Suppress("UNUSED_PARAMETER") context: Context) {
+        if (this.fallbackPolicy == FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE &&
+            this.service == null) {
+            throw IllegalArgumentException("FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE is set," +
+                " but a foreground service has not been set via setForegroundService().")
+        }
         // TODO: update impl
     }
 
@@ -93,6 +122,26 @@
             "androidx.work.datatransfer.UserInitiatedTaskRequest.SCHEDULE"
     }
 
+    enum class FallbackPolicy {
+        /**
+         * Indicates to the library that it should not do anything on Android 14- devices. The
+         * developer will perform the data transfer task on previous versions of Android with their
+         * own logic.
+         *
+         * **This is the default policy.**
+         */
+        FALLBACK_NONE,
+
+        /**
+         * Indicates that the developer will provide an implementation of [AbstractUitService] which
+         * will be used by the library to perform the data transfer work on Android 14- devices.
+         *
+         * _The foreground service fallback will act as a best effort to perform the data transfer
+         * work on Android 14- devices._
+         */
+        FALLBACK_TO_FOREGROUND_SERVICE,
+    }
+
     enum class ForegroundServiceOnTaskFinishPolicy {
         /**
          * This indicates that the foreground service should be stopped when the job is done.
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkRequest.kt b/work/work-runtime/src/main/java/androidx/work/WorkRequest.kt
index d2bc7a7..fa2c304 100644
--- a/work/work-runtime/src/main/java/androidx/work/WorkRequest.kt
+++ b/work/work-runtime/src/main/java/androidx/work/WorkRequest.kt
@@ -87,7 +87,7 @@
         /**
          * Sets the backoff policy and backoff delay for the work.  The default values are
          * [BackoffPolicy.EXPONENTIAL] and
-         * {@value WorkRequest#DEFAULT_BACKOFF_DELAY_MILLIS}, respectively.  `backoffDelay`
+         * [WorkRequest#DEFAULT_BACKOFF_DELAY_MILLIS], respectively.  `backoffDelay`
          * will be clamped between [WorkRequest.MIN_BACKOFF_MILLIS] and
          * [WorkRequest.MAX_BACKOFF_MILLIS].
          *
@@ -110,7 +110,7 @@
         /**
          * Sets the backoff policy and backoff delay for the work.  The default values are
          * [BackoffPolicy.EXPONENTIAL] and
-         * {@value WorkRequest#DEFAULT_BACKOFF_DELAY_MILLIS}, respectively.  `duration` will
+         * [WorkRequest#DEFAULT_BACKOFF_DELAY_MILLIS], respectively.  `duration` will
          * be clamped between [WorkRequest.MIN_BACKOFF_MILLIS] and
          * [WorkRequest.MAX_BACKOFF_MILLIS].
          *